From b451ceba5628b15e9fe9f4cb95db3a18fb719b6e Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Fri, 28 Aug 2026 11:35:57 -0600 Subject: [PATCH 01/17] fix(code-intel): preserve equal store migration rows --- go/internal/codeintel/store_migration_rows.go | 64 +++- go/internal/codeintel/store_migration_test.go | 290 ++++++++++++++++++ 2 files changed, 338 insertions(+), 16 deletions(-) diff --git a/go/internal/codeintel/store_migration_rows.go b/go/internal/codeintel/store_migration_rows.go index ea5ad451..3aaba11f 100644 --- a/go/internal/codeintel/store_migration_rows.go +++ b/go/internal/codeintel/store_migration_rows.go @@ -158,6 +158,7 @@ func mergeMigrationTable( sourceTable, destinationTable, spec, + columns, ) if err != nil { return StoreMigrationTable{}, err @@ -203,13 +204,20 @@ func validateMigrationTableKeys( sourceTable string, destinationTable string, spec migrationTableSpec, + columns []migrationColumn, ) error { - err := validateMigrationKeyUniqueness(ctx, destination, sourceTable, spec) + err := validateMigrationKeyConsistency(ctx, destination, sourceTable, spec, columns) if err != nil { return fmt.Errorf("validate source %s: %w", spec.name, err) } - err = validateMigrationKeyUniqueness(ctx, destination, destinationTable, spec) + err = validateMigrationKeyConsistency( + ctx, + destination, + destinationTable, + spec, + columns, + ) if err != nil { return fmt.Errorf("validate destination %s: %w", spec.name, err) } @@ -320,34 +328,49 @@ func finishMigrationTable( }, nil } -func validateMigrationKeyUniqueness( +func validateMigrationKeyConsistency( ctx context.Context, database migrationQueryer, table string, spec migrationTableSpec, + columns []migrationColumn, ) error { keys := quoteMigrationIdentifiers(spec.keyColumns) + variantChecks := make([]string, 0, len(columns)) + + for _, column := range migrationColumnIdentifiers(columns) { + variantChecks = append( + variantChecks, + fmt.Sprintf( + "(COUNT(DISTINCT %s) > 1 OR (COUNT(%s) > 0 AND COUNT(%s) < COUNT(*)))", + column, + column, + column, + ), + ) + } - // #nosec G201 -- table and keys come from the validated schema inventory. + // #nosec G201 -- table, keys, and columns come from the validated schema inventory. query := fmt.Sprintf( - "SELECT COUNT(*) FROM (SELECT %s FROM %s GROUP BY %s HAVING COUNT(*) > 1)", + "SELECT COUNT(*) FROM (SELECT %s FROM %s GROUP BY %s HAVING %s)", strings.Join(keys, ", "), table, strings.Join(keys, ", "), + strings.Join(variantChecks, " OR "), ) - var duplicates int64 + var conflictingKeys int64 - err := database.QueryRowContext(ctx, query).Scan(&duplicates) + err := database.QueryRowContext(ctx, query).Scan(&conflictingKeys) if err != nil { - return fmt.Errorf("query duplicate migration keys: %w", err) + return fmt.Errorf("query conflicting migration key variants: %w", err) } - if duplicates != 0 { + if conflictingKeys != 0 { return fmt.Errorf( - "%w: table has %d duplicate migration keys for %s", + "%w: table has %d conflicting row variants for migration key %s", errStoreMigrationIntegrity, - duplicates, + conflictingKeys, strings.Join(spec.keyColumns, ","), ) } @@ -368,14 +391,23 @@ func migrationDuplicateCounts( // #nosec G201 -- identifiers come from the validated schema inventory. query := fmt.Sprintf( - `SELECT COUNT(*), - COALESCE(SUM(CASE WHEN %s THEN 0 ELSE 1 END), 0) - FROM %s AS source - JOIN %s AS destination ON %s`, + `SELECT + COALESCE(SUM(CASE WHEN EXISTS ( + SELECT 1 FROM %s AS destination WHERE %s + ) THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN EXISTS ( + SELECT 1 FROM %s AS destination WHERE %s + ) AND NOT EXISTS ( + SELECT 1 FROM %s AS destination WHERE %s + ) THEN 1 ELSE 0 END), 0) + FROM %s AS source`, + destinationTable, equal, - sourceTable, destinationTable, join, + destinationTable, + equal, + sourceTable, ) var matchedRows, conflictRows int64 diff --git a/go/internal/codeintel/store_migration_test.go b/go/internal/codeintel/store_migration_test.go index 94f0d54b..adfdce0c 100644 --- a/go/internal/codeintel/store_migration_test.go +++ b/go/internal/codeintel/store_migration_test.go @@ -5,6 +5,7 @@ package codeintel import ( "context" + "database/sql" "encoding/json" "os" "path/filepath" @@ -118,6 +119,131 @@ func TestMigrateStoreRejectsUnequalDuplicateRows(t *testing.T) { } } +func TestMigrateStorePreservesEqualLogicalKeyDuplicates(t *testing.T) { + tests := []struct { + name string + sourceCopies int + destinationCopies int + wantImported int64 + wantMatched int64 + wantDestinationRows int64 + }{ + { + name: "source duplicates are imported", + sourceCopies: 3, + destinationCopies: 0, + wantImported: 3, + wantMatched: 0, + wantDestinationRows: 3, + }, + { + name: "destination duplicates match once per source row", + sourceCopies: 2, + destinationCopies: 3, + wantImported: 0, + wantMatched: 2, + wantDestinationRows: 3, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + repositoryRoot := t.TempDir() + sourcePath := DefaultDBPath(repositoryRoot) + destinationPath := filepath.Join( + t.TempDir(), + ".coding-ethos", + "code-intel.duckdb", + ) + + insertMigrationLogicalKeyRows(t, ctx, sourcePath, test.sourceCopies) + insertMigrationLogicalKeyRows(t, ctx, destinationPath, test.destinationCopies) + recordTestMigrationIdentity(t, ctx, destinationPath, repositoryRoot) + + result, err := MigrateStore(ctx, StoreMigrationOptions{ + RepositoryRoot: repositoryRoot, + SourcePath: sourcePath, + DestinationPath: destinationPath, + }) + if err != nil { + t.Fatalf("migrate equal logical-key duplicates: %v", err) + } + + for _, tableName := range []string{ + "lsh_bands", + "code_intel_fts", + "code_intel_search_terms", + } { + evidence := migrationTableEvidence(t, result.Manifest.Tables, tableName) + if evidence.SourceRows != int64(test.sourceCopies) || + evidence.ImportedRows != test.wantImported || + evidence.MatchedRows != test.wantMatched || + evidence.DestinationRows != test.wantDestinationRows { + t.Fatalf("unexpected %s evidence: %#v", tableName, evidence) + } + + assertMigrationTableRowCount( + t, + ctx, + destinationPath, + tableName, + test.wantDestinationRows, + ) + } + }) + } +} + +func TestMigrateStoreRejectsConflictingLogicalKeyVariants(t *testing.T) { + for _, tableName := range []string{"lsh_bands", "code_intel_fts"} { + for _, conflictLocation := range []string{"source", "destination"} { + t.Run(tableName+"/"+conflictLocation, func(t *testing.T) { + ctx := context.Background() + repositoryRoot := t.TempDir() + sourcePath := DefaultDBPath(repositoryRoot) + destinationPath := filepath.Join( + t.TempDir(), + ".coding-ethos", + "code-intel.duckdb", + ) + + initializeMigrationStore(t, ctx, sourcePath) + initializeMigrationStore(t, ctx, destinationPath) + if conflictLocation == "source" { + insertMigrationLogicalKeyConflict(t, ctx, sourcePath, tableName) + } else { + insertMigrationLogicalKeyConflict(t, ctx, destinationPath, tableName) + } + recordTestMigrationIdentity(t, ctx, destinationPath, repositoryRoot) + + _, err := MigrateStore(ctx, StoreMigrationOptions{ + RepositoryRoot: repositoryRoot, + SourcePath: sourcePath, + DestinationPath: destinationPath, + }) + if err == nil || + !strings.Contains(err.Error(), "conflicting row variants") || + !strings.Contains(err.Error(), tableName) { + t.Fatalf("expected %s conflict rejection, got %v", tableName, err) + } + + assertMigrationTableRowCount( + t, + ctx, + map[string]string{ + "source": sourcePath, + "destination": destinationPath, + }[conflictLocation], + tableName, + 2, + ) + assertNoMigrationManifest(t, destinationPath) + }) + } + } +} + func TestMigrateStoreRejectsRepositoryIdentityMismatch(t *testing.T) { ctx := context.Background() repositoryRoot := t.TempDir() @@ -226,6 +352,170 @@ func insertMigrationCodeFile( closeMigrationTestStore(t, store) } +func initializeMigrationStore(t *testing.T, ctx context.Context, databasePath string) { + t.Helper() + closeMigrationTestStore(t, openMigrationTestStore(t, ctx, databasePath)) +} + +func insertMigrationLogicalKeyRows( + t *testing.T, + ctx context.Context, + databasePath string, + copies int, +) { + t.Helper() + store := openMigrationTestStore(t, ctx, databasePath) + + insertMigrationLogicalKeySupport(t, ctx, store.Database()) + for range copies { + _, err := store.Database().ExecContext( + ctx, + `INSERT INTO lsh_bands( + band_hash, band_index, chunk_id, path, symbol_name + ) VALUES ('band-shared', 1, 'chunk-shared', 'pkg/shared.go', 'Shared')`, + ) + if err != nil { + t.Fatalf("insert equal LSH migration row: %v", err) + } + + _, err = store.Database().ExecContext( + ctx, + `INSERT INTO code_intel_fts( + fts_id, kind, record_id, trace_id, policy_id, skill_id, + path, message, search_text + ) VALUES ( + 'fts-shared', 'finding', 'record-shared', 'trace-shared', + 'policy-shared', 'skill-shared', 'pkg/shared.go', + 'shared message', 'shared search text' + )`, + ) + if err != nil { + t.Fatalf("insert equal FTS migration row: %v", err) + } + + _, err = store.Database().ExecContext( + ctx, + `INSERT INTO code_intel_search_terms(term, fts_id) + VALUES ('shared', 'fts-shared')`, + ) + if err != nil { + t.Fatalf("insert equal search-term migration row: %v", err) + } + } + + closeMigrationTestStore(t, store) +} + +func insertMigrationLogicalKeyConflict( + t *testing.T, + ctx context.Context, + databasePath string, + tableName string, +) { + t.Helper() + store := openMigrationTestStore(t, ctx, databasePath) + insertMigrationLogicalKeySupport(t, ctx, store.Database()) + + var err error + switch tableName { + case "lsh_bands": + _, err = store.Database().ExecContext( + ctx, + `INSERT INTO lsh_bands( + band_hash, band_index, chunk_id, path, symbol_name + ) VALUES + ('band-a', 1, 'chunk-shared', 'pkg/shared.go', 'Shared'), + ('band-b', 1, 'chunk-shared', 'pkg/shared.go', 'Shared')`, + ) + case "code_intel_fts": + _, err = store.Database().ExecContext( + ctx, + `INSERT INTO code_intel_fts( + fts_id, kind, record_id, trace_id, path, message, search_text + ) VALUES + ('fts-shared', 'finding', 'record-shared', 'trace-shared', + 'pkg/shared.go', NULL, 'shared search text'), + ('fts-shared', 'finding', 'record-shared', 'trace-shared', + 'pkg/shared.go', 'different message', 'shared search text')`, + ) + default: + t.Fatalf("unsupported conflicting logical-key table %s", tableName) + } + if err != nil { + t.Fatalf("insert %s conflict fixture: %v", tableName, err) + } + + closeMigrationTestStore(t, store) +} + +func insertMigrationLogicalKeySupport( + t *testing.T, + ctx context.Context, + database *sql.DB, +) { + t.Helper() + + _, err := database.ExecContext( + ctx, + `INSERT OR IGNORE INTO code_files( + path, language, content_hash, size_bytes, line_count, indexed_at_utc + ) VALUES ('pkg/shared.go', 'go', 'shared-hash', 10, 1, '2026-08-23T00:00:00Z')`, + ) + if err != nil { + t.Fatalf("insert logical-key code file fixture: %v", err) + } + + _, err = database.ExecContext( + ctx, + `INSERT OR IGNORE INTO code_chunks( + chunk_id, path, language, node_kind, symbol_name, + start_byte, end_byte, start_line, end_line, + content_hash, search_text, raw_text + ) VALUES ( + 'chunk-shared', 'pkg/shared.go', 'go', 'function', 'Shared', + 0, 10, 1, 1, 'chunk-hash', 'shared search text', 'func Shared() {}' + )`, + ) + if err != nil { + t.Fatalf("insert logical-key code chunk fixture: %v", err) + } +} + +func assertMigrationTableRowCount( + t *testing.T, + ctx context.Context, + databasePath string, + tableName string, + want int64, +) { + t.Helper() + store, err := OpenReadOnly(ctx, databasePath) + if err != nil { + t.Fatalf("open migration store for row count: %v", err) + } + defer store.Close() + + query := "SELECT COUNT(*) FROM " + quoteMigrationIdentifier(tableName) + var got int64 + if err = store.Database().QueryRowContext(ctx, query).Scan(&got); err != nil { + t.Fatalf("count migration rows in %s: %v", tableName, err) + } + if got != want { + t.Fatalf("row count in %s = %d, want %d", tableName, got, want) + } +} + +func assertNoMigrationManifest(t *testing.T, destinationPath string) { + t.Helper() + manifests, err := filepath.Glob(destinationPath + ".migration-*.json") + if err != nil { + t.Fatalf("glob migration manifests: %v", err) + } + if len(manifests) != 0 { + t.Fatalf("failed migration wrote manifests: %#v", manifests) + } +} + func recordTestMigrationIdentity( t *testing.T, ctx context.Context, From dec5a772c5e66a5cf49f3d7a716efd6eec77434c Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Fri, 28 Aug 2026 17:13:19 -0600 Subject: [PATCH 02/17] fix(code-intel): materialize lifecycle outcomes --- go/internal/agentproxy/transform_test.go | 14 +- .../automatic_remediation_outcomes_test.go | 191 ++++++++++++++++++ go/internal/codeintel/ingest.go | 60 ++++++ go/internal/codeintel/write.go | 3 + go/internal/evidence/evidence.go | 5 + go/internal/hooks/remediation_correlation.go | 11 + .../hooks/remediation_correlation_test.go | 62 +++++- go/internal/hooks/trace_test.go | 5 +- 8 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 go/internal/codeintel/automatic_remediation_outcomes_test.go diff --git a/go/internal/agentproxy/transform_test.go b/go/internal/agentproxy/transform_test.go index c72f2ca3..20db6cde 100644 --- a/go/internal/agentproxy/transform_test.go +++ b/go/internal/agentproxy/transform_test.go @@ -213,14 +213,18 @@ func TestSavedOutputNoticeRecognitionAndAllowPath(t *testing.T) { func TestToolOutputCompressionPreservesHeadTailAndRecordsSavings(t *testing.T) { t.Parallel() + verboseLine := strings.Repeat( + "verbose compiler progress chunk with repeated metadata ", + 16, + ) input := strings.Join([]string{ "$ go test ./...", "package alpha", - "line 03: verbose compiler progress chunk with repeated metadata", - "line 04: verbose compiler progress chunk with repeated metadata", - "line 05: verbose compiler progress chunk with repeated metadata", - "line 06: verbose compiler progress chunk with repeated metadata", - "line 07: verbose compiler progress chunk with repeated metadata", + "line 03: " + verboseLine, + "line 04: " + verboseLine, + "line 05: " + verboseLine, + "line 06: " + verboseLine, + "line 07: " + verboseLine, "line 08", "FAIL", }, "\n") + "\n" diff --git a/go/internal/codeintel/automatic_remediation_outcomes_test.go b/go/internal/codeintel/automatic_remediation_outcomes_test.go new file mode 100644 index 00000000..30aa5ffb --- /dev/null +++ b/go/internal/codeintel/automatic_remediation_outcomes_test.go @@ -0,0 +1,191 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel_test + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "blackcat.ca/coding-ethos/go/internal/codeintel" + "blackcat.ca/coding-ethos/go/internal/evidence" +) + +func TestHookTraceMaterializesTerminalAutomaticRemediationOutcomes(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store := openTestStore(t, ctx) + ingester := codeintel.NewTraceIngester(store) + + for _, traceID := range []string{"trace-source-fixed", "trace-source-abandoned"} { + err := ingester.IngestHookTrace( + ctx, + hookTracePayloadWithIDs( + t, + traceID, + "tracking-"+traceID, + "2026-01-01T00:01:00Z", + ), + ) + if err != nil { + t.Fatalf("ingest source trace %q: %v", traceID, err) + } + } + + followup := hookTraceWithRemediationEvents(t, []evidence.RemediationEvent{ + { + ID: "event-fixed", + RemediationID: "remediation-fixed", + FindingID: "finding-fixed", + SourceTraceID: "trace-source-fixed", + TraceID: "trace-followup", + Event: "fixed", + PolicyID: "policy.fixed", + SkillID: "skill-fixed", + Path: "pkg/fixed.go", + Provider: "claude", + Tool: "Edit", + SchemaVersion: evidence.SchemaVersion, + }, + { + ID: "event-abandoned", + RemediationID: "remediation-abandoned", + FindingID: "finding-abandoned", + SourceTraceID: "trace-source-abandoned", + TraceID: "trace-followup", + Event: "abandoned", + PolicyID: "policy.abandoned", + SkillID: "skill-abandoned", + File: "pkg/abandoned.go", + SchemaVersion: evidence.SchemaVersion, + }, + { + ID: "event-attempted", + RemediationID: "remediation-attempted", + SourceTraceID: "trace-source-fixed", + TraceID: "trace-followup", + Event: "attempted", + SchemaVersion: evidence.SchemaVersion, + }, + { + ID: "event-repeated", + RemediationID: "remediation-repeated", + SourceTraceID: "trace-source-fixed", + TraceID: "trace-followup", + Event: "repeated", + SchemaVersion: evidence.SchemaVersion, + }, + }) + + err := ingester.IngestHookTrace(ctx, followup) + if err != nil { + t.Fatalf("ingest follow-up trace: %v", err) + } + assertAutomaticRemediationOutcomes(t, ctx, store) + + err = store.RecordRemediationOutcome(ctx, codeintel.RemediationOutcome{ + ID: "explicit-outcome", + SourceTraceID: "trace-source-fixed", + FollowupTraceID: "trace-followup", + PolicyID: "policy.explicit", + Outcome: "repeated", + }) + if err != nil { + t.Fatalf("record explicit outcome: %v", err) + } + + err = ingester.IngestHookTrace(ctx, followup) + if err != nil { + t.Fatalf("reingest follow-up trace: %v", err) + } + assertAutomaticRemediationOutcomes(t, ctx, store) + + err = ingester.IngestHookTrace(ctx, hookTraceWithRemediationEvents(t, nil)) + if err != nil { + t.Fatalf("reingest follow-up without outcomes: %v", err) + } + + outcomes, err := store.RemediationOutcomes( + ctx, + codeintel.RemediationOutcomeQuery{Limit: 10}, + ) + if err != nil { + t.Fatalf("query outcomes after cleanup: %v", err) + } + if len(outcomes) != 1 || outcomes[0].ID != "explicit-outcome" { + t.Fatalf("outcomes after cleanup = %#v, want explicit outcome only", outcomes) + } +} + +func hookTraceWithRemediationEvents( + t *testing.T, + events []evidence.RemediationEvent, +) []byte { + t.Helper() + + payload := map[string]any{} + err := json.Unmarshal( + hookTracePayloadWithIDs( + t, + "trace-followup", + "tracking-trace-followup", + "2026-01-01T00:02:00Z", + ), + &payload, + ) + if err != nil { + t.Fatalf("decode follow-up trace fixture: %v", err) + } + + payload["status"] = "allowed" + payload["remediation_events"] = events + + return mustJSON(t, payload) +} + +func assertAutomaticRemediationOutcomes( + t *testing.T, + ctx context.Context, + store *codeintel.Store, +) { + t.Helper() + + outcomes, err := store.RemediationOutcomes( + ctx, + codeintel.RemediationOutcomeQuery{Limit: 10}, + ) + if err != nil { + t.Fatalf("query automatic outcomes: %v", err) + } + + automatic := map[string]codeintel.RemediationOutcome{} + for _, outcome := range outcomes { + if strings.HasPrefix(outcome.ID, "automatic-remediation-outcome:") { + automatic[outcome.Outcome] = outcome + } + } + if len(automatic) != 2 { + t.Fatalf("automatic outcomes = %#v, want fixed and abandoned", automatic) + } + + fixed := automatic["fixed"] + if fixed.SourceTraceID != "trace-source-fixed" || + fixed.FollowupTraceID != "trace-followup" || + fixed.Provider != "claude" || + fixed.Tool != "Edit" || + fixed.Path != "pkg/fixed.go" { + t.Fatalf("fixed outcome = %#v", fixed) + } + + abandoned := automatic["abandoned"] + if abandoned.SourceTraceID != "trace-source-abandoned" || + abandoned.FollowupTraceID != "trace-followup" || + abandoned.Provider != "codex" || + abandoned.Tool != "Bash" || + abandoned.File != "pkg/abandoned.go" { + t.Fatalf("abandoned outcome = %#v", abandoned) + } +} diff --git a/go/internal/codeintel/ingest.go b/go/internal/codeintel/ingest.go index 5ae93ebe..d329edab 100644 --- a/go/internal/codeintel/ingest.go +++ b/go/internal/codeintel/ingest.go @@ -14,6 +14,12 @@ import ( "blackcat.ca/coding-ethos/go/internal/evidence" ) +const ( + automaticRemediationOutcomeIDPrefix = "automatic-remediation-outcome" + automaticRemediationOutcomeFixed = "fixed" + automaticRemediationOutcomeAbandoned = "abandoned" +) + type IngestSummary struct { FilesScanned int `json:"files_scanned"` FilesIngested int `json:"files_ingested"` @@ -86,6 +92,7 @@ func insertTraceRows( insertFindings, insertRemediations, insertRemediationEvents, + insertAutomaticRemediationOutcomes, insertHookAnalytics, insertTraceDeleteIntents, } { @@ -98,6 +105,59 @@ func insertTraceRows( return nil } +func insertAutomaticRemediationOutcomes( + ctx context.Context, + transaction *sql.Tx, + trace Trace, +) error { + for _, event := range trace.RemediationEvents { + outcome := strings.ToLower(strings.TrimSpace(event.Event)) + if outcome != automaticRemediationOutcomeFixed && + outcome != automaticRemediationOutcomeAbandoned { + continue + } + + if strings.TrimSpace(event.SourceTraceID) == "" { + continue + } + + remediationOutcome := normalizeRemediationOutcome(RemediationOutcome{ + ID: stableID( + automaticRemediationOutcomeIDPrefix, + event.ID, + event.RemediationID, + event.FindingID, + event.SourceTraceID, + trace.ID, + outcome, + ), + RemediationID: event.RemediationID, + FindingID: event.FindingID, + SourceTraceID: event.SourceTraceID, + FollowupTraceID: trace.ID, + PolicyID: event.PolicyID, + SkillID: event.SkillID, + File: event.File, + Path: event.Path, + Provider: firstNonEmpty(event.Provider, trace.Provider), + Tool: firstNonEmpty(event.Tool, trace.Tool), + Outcome: outcome, + RecordedAtUTC: trace.RecordedAtUTC, + }) + + err := insertRemediationOutcome(ctx, transaction, remediationOutcome) + if err != nil { + return fmt.Errorf( + "insert automatic remediation outcome %q: %w", + remediationOutcome.ID, + err, + ) + } + } + + return nil +} + func insertTraceDeleteIntents( ctx context.Context, transaction *sql.Tx, diff --git a/go/internal/codeintel/write.go b/go/internal/codeintel/write.go index 37cab753..d90fbd88 100644 --- a/go/internal/codeintel/write.go +++ b/go/internal/codeintel/write.go @@ -29,6 +29,9 @@ const ( func deleteTraceRows(ctx context.Context, transaction *sql.Tx, traceID string) error { for _, statement := range []string{ "DELETE FROM code_intel_fts WHERE trace_id = ?", + `DELETE FROM remediation_outcomes + WHERE followup_trace_id = ? + AND outcome_id LIKE 'automatic-remediation-outcome:%'`, "DELETE FROM code_delete_intents WHERE trace_id = ?", "DELETE FROM hook_targets WHERE trace_id = ?", "DELETE FROM hook_decisions WHERE trace_id = ?", diff --git a/go/internal/evidence/evidence.go b/go/internal/evidence/evidence.go index a1728c5c..587c017b 100644 --- a/go/internal/evidence/evidence.go +++ b/go/internal/evidence/evidence.go @@ -66,10 +66,15 @@ type RemediationEvent struct { ID string `json:"id"` RemediationID string `json:"remediation_id,omitempty"` FindingID string `json:"finding_id,omitempty"` + SourceTraceID string `json:"source_trace_id,omitempty"` TraceID string `json:"trace_id,omitempty"` Event string `json:"event"` PolicyID string `json:"policy_id,omitempty"` SkillID string `json:"skill_id,omitempty"` + File string `json:"file,omitempty"` + Path string `json:"path,omitempty"` + Provider string `json:"provider,omitempty"` + Tool string `json:"tool,omitempty"` SearchText string `json:"search_text,omitempty"` SchemaVersion int `json:"schema_version"` } diff --git a/go/internal/hooks/remediation_correlation.go b/go/internal/hooks/remediation_correlation.go index 521dc17e..836f965d 100644 --- a/go/internal/hooks/remediation_correlation.go +++ b/go/internal/hooks/remediation_correlation.go @@ -21,6 +21,9 @@ type remediationReference struct { SourceTraceID string `json:"source_trace_id"` PolicyID string `json:"policy_id,omitempty"` SkillID string `json:"skill_id,omitempty"` + File string `json:"file,omitempty"` + Path string `json:"path,omitempty"` + Provider string `json:"provider,omitempty"` Tool string `json:"tool,omitempty"` } @@ -214,6 +217,9 @@ func remediationReferences( SourceTraceID: traceID, PolicyID: remediation.PolicyID, SkillID: remediation.SkillID, + File: remediation.File, + Path: remediation.Path, + Provider: event.Provider(), Tool: event.ToolName, }) } @@ -294,10 +300,15 @@ func remediationOutcomeEvent( result := evidence.RemediationEvent{ RemediationID: reference.RemediationID, FindingID: reference.FindingID, + SourceTraceID: reference.SourceTraceID, TraceID: traceID, Event: outcome, PolicyID: reference.PolicyID, SkillID: reference.SkillID, + File: reference.File, + Path: reference.Path, + Provider: reference.Provider, + Tool: reference.Tool, SearchText: strings.TrimSpace(strings.Join([]string{ reference.PolicyID, reference.SkillID, diff --git a/go/internal/hooks/remediation_correlation_test.go b/go/internal/hooks/remediation_correlation_test.go index 7a1810cc..1426bab4 100644 --- a/go/internal/hooks/remediation_correlation_test.go +++ b/go/internal/hooks/remediation_correlation_test.go @@ -46,6 +46,7 @@ func TestRemediationCorrelationRecordsRepeatedAttemptedAndFixedEvents(t *testing repeated, "repeated", "trace-repeated", + "trace-suggested", ) attempted := applyRemediationCorrelation( @@ -61,6 +62,7 @@ func TestRemediationCorrelationRecordsRepeatedAttemptedAndFixedEvents(t *testing attempted, "attempted", "trace-attempted", + "trace-repeated", ) fixed := applyRemediationCorrelation( @@ -77,7 +79,13 @@ func TestRemediationCorrelationRecordsRepeatedAttemptedAndFixedEvents(t *testing nil, &record, ) - assertRemediationCorrelationEvent(t, fixed, "fixed", "trace-fixed") + assertRemediationCorrelationEvent( + t, + fixed, + "fixed", + "trace-fixed", + "trace-repeated", + ) if len(record.PendingRemediations) != 0 || len(record.AttemptedRemediations) != 0 { @@ -137,7 +145,54 @@ func TestRemediationCorrelationDoesNotCrossSessions(t *testing.T) { sourceSession, "attempted", "trace-source-attempt", + "trace-source-session", + ) +} + +func TestRemediationCorrelationRecordsAbandonedEventAtSessionEnd(t *testing.T) { + record := lifecycleSessionRecord{} + remediations := []agentmsg.Remediation{{ + ID: "remediation-abandoned", + PolicyID: "policy.abandoned", + SkillID: "skill-abandoned", + File: "pkg/app.go", + }} + findings := []evidence.Finding{{ID: "finding-abandoned"}} + + applyRemediationCorrelation( + Event{ + HookEventName: eventPreToolUse, + ProviderHint: providerCodex, + ToolName: toolBash, + }, + Result{Status: statusBlocked}, + "trace-abandoned-source", + remediations, + findings, + &record, ) + + abandoned := applyRemediationCorrelation( + Event{HookEventName: eventSessionEnd}, + Result{Status: statusAllowed}, + "trace-session-end", + nil, + nil, + &record, + ) + assertRemediationCorrelationEvent( + t, + abandoned, + "abandoned", + "trace-session-end", + "trace-abandoned-source", + ) + + if abandoned[0].Provider != providerCodex || + abandoned[0].Tool != toolBash || + abandoned[0].File != "pkg/app.go" { + t.Fatalf("abandoned remediation context = %#v", abandoned[0]) + } } func assertRemediationCorrelationEvent( @@ -145,6 +200,7 @@ func assertRemediationCorrelationEvent( events []evidence.RemediationEvent, wantEvent string, wantTraceID string, + wantSourceTraceID string, ) { t.Helper() @@ -154,12 +210,14 @@ func assertRemediationCorrelationEvent( event := events[0] if event.Event != wantEvent || event.TraceID != wantTraceID || + event.SourceTraceID != wantSourceTraceID || event.RemediationID == "" || event.FindingID == "" || event.ID == "" { t.Fatalf( - "remediation event = %#v, want event=%q trace=%q with identities", + "remediation event = %#v, want event=%q trace=%q source=%q with identities", event, wantEvent, wantTraceID, + wantSourceTraceID, ) } } diff --git a/go/internal/hooks/trace_test.go b/go/internal/hooks/trace_test.go index c84520da..e6bbb51e 100644 --- a/go/internal/hooks/trace_test.go +++ b/go/internal/hooks/trace_test.go @@ -296,10 +296,12 @@ func TestWriteAgentHookTraceCorrelatesAllowedRetryWithPriorRemediation( }}, } - err := WriteAgentHookTrace(t.TempDir(), event, blocked) + sourceRunDir := t.TempDir() + err := WriteAgentHookTrace(sourceRunDir, event, blocked) if err != nil { t.Fatalf("write blocked hook trace: %v", err) } + sourceTrace, _ := readTracePayload(t, sourceRunDir) event.ToolInput = map[string]any{ "command": "cerun -- git commit -m test", @@ -321,6 +323,7 @@ func TestWriteAgentHookTraceCorrelatesAllowedRetryWithPriorRemediation( if outcome["event"] != "attempted" || outcome["remediation_id"] == "" || outcome["finding_id"] == "" || + outcome["source_trace_id"] != sourceTrace["trace_id"] || outcome["trace_id"] != trace["trace_id"] { t.Fatalf("allowed retry lacks correlated remediation outcome: %#v", outcome) } From 6fad0902c46ff0d365dafafcf8cc69c57e84626f Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Fri, 28 Aug 2026 18:03:52 -0600 Subject: [PATCH 03/17] feat(code-intel): measure token economy --- README.md | 4 + docs/CODE_INTEL.md | 61 ++ docs/TOKEN_ECONOMY.md | 164 ++++ go/cmd/coding-ethos-run/args.go | 26 + go/cmd/coding-ethos-run/main_test.go | 62 ++ go/internal/codeintelcli/main.go | 1 + go/internal/codeintelcli/token_economy.go | 337 ++++++++ .../codeintelcli/token_economy_test.go | 200 +++++ go/internal/tokeneconomy/artifact.go | 258 ++++++ .../tokeneconomy/benchmark_manifest.go | 786 ++++++++++++++++++ go/internal/tokeneconomy/benchmark_runner.go | 783 +++++++++++++++++ go/internal/tokeneconomy/benchmark_source.go | 403 +++++++++ go/internal/tokeneconomy/benchmark_test.go | 489 +++++++++++ .../tokeneconomy/benchmark_validation.go | 327 ++++++++ go/internal/tokeneconomy/historical.go | 600 +++++++++++++ go/internal/tokeneconomy/ledger.go | 403 +++++++++ go/internal/tokeneconomy/ledger_test.go | 117 +++ go/internal/tokeneconomy/mechanisms.go | 94 +++ go/internal/tokeneconomy/report.go | 500 +++++++++++ go/internal/tokeneconomy/report_test.go | 622 ++++++++++++++ go/internal/tokeneconomy/store.go | 600 +++++++++++++ go/internal/tokeneconomy/store_test.go | 138 +++ go/internal/tokeneconomy/types.go | 246 ++++++ 23 files changed, 7221 insertions(+) create mode 100644 docs/TOKEN_ECONOMY.md create mode 100644 go/internal/codeintelcli/token_economy.go create mode 100644 go/internal/codeintelcli/token_economy_test.go create mode 100644 go/internal/tokeneconomy/artifact.go create mode 100644 go/internal/tokeneconomy/benchmark_manifest.go create mode 100644 go/internal/tokeneconomy/benchmark_runner.go create mode 100644 go/internal/tokeneconomy/benchmark_source.go create mode 100644 go/internal/tokeneconomy/benchmark_test.go create mode 100644 go/internal/tokeneconomy/benchmark_validation.go create mode 100644 go/internal/tokeneconomy/historical.go create mode 100644 go/internal/tokeneconomy/ledger.go create mode 100644 go/internal/tokeneconomy/ledger_test.go create mode 100644 go/internal/tokeneconomy/mechanisms.go create mode 100644 go/internal/tokeneconomy/report.go create mode 100644 go/internal/tokeneconomy/report_test.go create mode 100644 go/internal/tokeneconomy/store.go create mode 100644 go/internal/tokeneconomy/store_test.go create mode 100644 go/internal/tokeneconomy/types.go diff --git a/README.md b/README.md index 9ca6d0c1..e2d19c31 100644 --- a/README.md +++ b/README.md @@ -474,6 +474,10 @@ bin/coding-ethos-run code-intel proxy-sessions --provider codex bin/coding-ethos-run code-intel proxy-events --session-id sess-1 bin/coding-ethos-run code-intel session-snapshot --session-id sess-1 --format toon bin/coding-ethos-run code-intel context-advice --session-id sess-1 --format toon +bin/coding-ethos-run code-intel token-economy ledger --provider codex --path /absolute/path/to/rollout.jsonl +bin/coding-ethos-run code-intel token-economy benchmark validate --manifest /private/protocol/frozen.yaml +bin/coding-ethos-run code-intel token-economy benchmark run --manifest /private/protocol/frozen.yaml --state-root /private/evidence --approved-max-runs 30 +bin/coding-ethos-run code-intel token-economy report --historical --db /private/lane-1/code-intel.duckdb --db /private/lane-2/code-intel.duckdb --from 2026-08-01T00:00:00Z --to 2026-09-01T00:00:00Z --output-prefix /private/reports/history bin/coding-ethos-run code-intel repeated-edits --path pkg/app.py bin/coding-ethos-run code-intel remediation-outcomes --outcome repeated bin/coding-ethos-run code-intel remediation-effectiveness --policy-id python.unused_imports diff --git a/docs/CODE_INTEL.md b/docs/CODE_INTEL.md index 641bfe6a..95722bf6 100644 --- a/docs/CODE_INTEL.md +++ b/docs/CODE_INTEL.md @@ -228,6 +228,66 @@ its before/after file hashes must match. When `--destination` is omitted, the command resolves it through `CODE_ETHOS_STATE_ROOT`; when `--source` is omitted, the repository-local store is used. +## Token Economy Evidence + +`code-intel token-economy` measures context reduction without treating an +enabled-only trace as a causal savings result. Its evidence lives in the +separate private store `.coding-ethos/token-economy.duckdb`, so benchmark +records do not change the code-intel migration inventory. + +Inspect a provider-native ledger without retaining prompt or response bodies: + +```bash +bin/coding-ethos-run code-intel token-economy ledger \ + --provider codex --path /absolute/path/to/rollout.jsonl +``` + +Codex ledgers use the final cumulative provider total and treat cached input as +a subset. Claude ledgers deduplicate message IDs, exclude synthetic messages, +and sum input, cache-creation, cache-read, and output tokens. A ledger that +changes while being read, contains conflicting identities, or has decreasing +cumulative usage fails closed. + +Write an observational historical report from existing proxy transforms: + +```bash +bin/coding-ethos-run code-intel token-economy report \ + --historical \ + --db /private/lane-1/code-intel.duckdb \ + --db /private/lane-2/code-intel.duckdb \ + --from 2026-08-01T00:00:00Z \ + --to 2026-09-01T00:00:00Z \ + --output-prefix /private/reports/token-economy-history +``` + +Historical inputs are explicit and `--db` is repeatable. The UTC interval is +half-open: `--from` is inclusive and `--to` is exclusive. Sources are +canonicalized and ordered before aggregation. The command rejects repeated +source files and event IDs found in more than one source, hashes every source +before and after the read, and records the ordered paths and both hashes in the +report. + +Write a controlled report after benchmark runs have been recorded: + +```bash +bin/coding-ethos-run code-intel token-economy report \ + --experiment-id EXPERIMENT \ + --output-prefix /private/reports/token-economy-EXPERIMENT +``` + +Both report modes create JSON, Markdown, and a detached SHA-256 receipt with +mode `0600`; existing paths are never overwritten. Historical reports always +set `causal=false`. Controlled reports require randomized and isolated full, +static, and off arms, complete provider ledgers and validation receipts, at +least ten tasks, a savings interval excluding zero, and acceptance-rate +noninferiority before returning `causal_savings`. Controlled intervals spend +alpha across predeclared complete-block checkpoints to preserve 95% family-wise +confidence during adaptive collection. + +The complete frozen-manifest, three-arm runner, adaptive collection, and +failure-accounting protocol is documented in +[`TOKEN_ECONOMY.md`](TOKEN_ECONOMY.md). + The first implementation should create `.coding-ethos/code-intel.duckdb` with tables for: @@ -293,6 +353,7 @@ bin/coding-ethos-run code-intel proxy-sessions --provider codex bin/coding-ethos-run code-intel proxy-events --session-id sess-1 bin/coding-ethos-run code-intel session-snapshot --session-id sess-1 --format toon bin/coding-ethos-run code-intel context-advice --format toon +bin/coding-ethos-run code-intel token-economy report --historical --db /private/code-intel.duckdb --from 2026-08-01T00:00:00Z --to 2026-09-01T00:00:00Z --output-prefix /private/reports/history bin/coding-ethos-run code-intel remediation-outcomes --outcome repeated bin/coding-ethos-run code-intel remediation-effectiveness --policy-id python.unused_imports bin/coding-ethos-run code-intel skill-health --format toon diff --git a/docs/TOKEN_ECONOMY.md b/docs/TOKEN_ECONOMY.md new file mode 100644 index 00000000..c78d7ea7 --- /dev/null +++ b/docs/TOKEN_ECONOMY.md @@ -0,0 +1,164 @@ +# Measuring Coding Ethos Token Economy + +Coding Ethos uses two deliberately separate evidence tiers: + +1. A historical report measures gross context removed by recorded proxy + transforms. It is observational and never claims provider-billed savings. +2. A randomized three-arm benchmark compares provider-native token ledgers for + `full`, `static`, and `off` runs that share the same source, prompt, model, + reasoning effort, and validators. + +`full` receives the frozen global context plus the manifest's dynamic Coding +Ethos configuration. `static` receives exactly the same global context without +the dynamic configuration or state environment. `off` receives neither, while +retaining governance files that belong to the frozen task repository itself. + +## Report observational history + +Historical reporting reads one or more existing code-intel stores. Every +source is named explicitly with a repeatable `--db`; no ambient database is +selected. The time interval is also explicit and half-open: `--from` is +inclusive and `--to` is exclusive. + +```bash +bin/coding-ethos-run code-intel token-economy report \ + --historical \ + --db /private/lane-1/code-intel.duckdb \ + --db /private/lane-2/code-intel.duckdb \ + --from 2026-08-01T00:00:00Z \ + --to 2026-09-01T00:00:00Z \ + --output-prefix /private/reports/token-economy-history +``` + +The report canonicalizes and sorts source paths before aggregation, counts an +event ID at most once, and fails if the same event ID appears in distinct +sources. Each source is hashed before it is opened and after it is closed; a +changed source aborts report creation. The JSON and Markdown artifacts record +the normalized UTC window, ordered source identities, before/after hashes, and +the deterministic aggregation contract. Historical results always remain +observational and set `causal=false`. + +## Freeze and validate a protocol + +Start with an absolute-path YAML draft. Hash and version fields may be blank in +the draft because `freeze` recomputes them from the named files, Git archives, +executables, arguments, and provider `--version` output. The output is +create-new and mode `0600`. + +```yaml +schema_version: 1 +experiment_id: ethos-economy-2026-08 +created_at_utc: "2026-08-28T18:00:00Z" +randomization_seed: "publish-this-before-running" +replicates: 2 +analysis_block_checkpoints: [10, 20] +static_context: + path: /private/protocol/coding-ethos-AGENTS.md + sha256: "" +provider: + kind: codex + executable: /absolute/path/to/codex + executable_sha256: "" + runtime_version: "" + model: your-approved-model-id + reasoning_effort: high + auth_file: /private/codex-home/auth.json +full_config_overrides: + - 'features.code_intel=true' +tasks: + - task_id: real-fix-01 + kind: real + repository_path: /absolute/path/to/frozen-source-repository + commit: 0123456789abcdef0123456789abcdef01234567 + source_archive_sha256: "" + prompt_path: /private/protocol/prompts/real-fix-01.txt + prompt_sha256: "" + validator_sha256: "" + agent_timeout: 45m + validation_timeout: 20m + allowed_paths: + - go/internal/example + validators: + - [/absolute/path/to/go, test, ./internal/example, -count=1] + - task_id: diagnostic-01 + kind: diagnostic + repository_path: /absolute/path/to/frozen-diagnostic-repository + commit: fedcba9876543210fedcba9876543210fedcba98 + source_archive_sha256: "" + prompt_path: /private/protocol/prompts/diagnostic-01.txt + prompt_sha256: "" + validator_sha256: "" + agent_timeout: 30m + validation_timeout: 10m + allowed_paths: + - pkg/diagnostic + validators: + - [/absolute/path/to/pytest, -q, pkg/diagnostic] +``` + +The task list above is abbreviated; a causal protocol needs at least ten +distinct tasks. The corpus must include both real and diagnostic tasks. A manifest can contain +at most 20 tasks, two replicates, and 120 provider runs. Validators are argv +arrays executed directly; Coding Ethos does not interpolate them through a +shell. Analysis checkpoints count complete task-replicate three-arm blocks, +must increase, and must end at `tasks * replicates`. Replicate-one blocks are +scheduled before replicate-two blocks so the first ten blocks cover ten +distinct tasks. Their intervals use Bonferroni-adjusted alpha so family-wise +confidence remains 95% across every declared look. The auth file must be a +private regular file and is copied only into an ephemeral provider home for +each run. + +```bash +bin/coding-ethos-run code-intel token-economy benchmark freeze \ + --draft /private/protocol/draft.yaml \ + --output /private/protocol/frozen.yaml + +bin/coding-ethos-run code-intel token-economy benchmark validate \ + --manifest /private/protocol/frozen.yaml +``` + +Both commands inspect identities and invoke only the provider's `--version` +mode. They do not start a model run. + +## Run controlled blocks + +`run` is the only model-backed command. It requires an explicit absolute state +root and an operator-approved cap that is a multiple of three. Scheduling is +deterministic and randomized in complete `full`/`static`/`off` blocks. Each run +uses a new no-remote Git workspace and an isolated provider home. A terminal +failure, timeout, missing ledger, validator failure, or out-of-scope edit is +recorded as evidence; it is not silently replaced. Model-generated commands +run in the workspace-write sandbox with outbound network access disabled. + +```bash +bin/coding-ethos-run code-intel token-economy benchmark run \ + --manifest /private/protocol/frozen.yaml \ + --state-root /private/evidence \ + --approved-max-runs 30 +``` + +The command is resumable and immutable. Re-running it skips already recorded +run IDs. For adaptive collection, report only when a predeclared checkpoint has +that many complete task-replicate three-arm blocks, and authorize another multiple of three +only when the result remains imprecise. At least ten complete tasks are required +for a causal claim. Stop at a supported conclusion or the manifest maximum; the +runner never launches additional blocks on its own. + +```bash +bin/coding-ethos-run code-intel token-economy report \ + --state-root /private/evidence \ + --experiment-id ethos-economy-2026-08 \ + --output-prefix /private/reports/ethos-economy-2026-08-look-01 +``` + +The primary comparison is `full` versus `off`; `full` versus `static` and +`static` versus `off` separate dynamic and static mechanisms. A causal savings +conclusion requires complete randomized arm evidence, at least ten distinct +tasks, a savings interval excluding zero, no added severe governance +violations, and acceptance-rate noninferiority within five percentage points. +Otherwise the report explicitly returns inconclusive, no detectable +difference, regression, or quality tradeoff. + +Codex execution and Claude ledger ingestion are supported. The controlled +runner is Codex-only in schema version 1; a future Claude runner can implement +the same frozen protocol without changing the provider-neutral evidence store. diff --git a/go/cmd/coding-ethos-run/args.go b/go/cmd/coding-ethos-run/args.go index 2de2f665..522f52f3 100644 --- a/go/cmd/coding-ethos-run/args.go +++ b/go/cmd/coding-ethos-run/args.go @@ -15,6 +15,8 @@ const ( golangciLintFormatTool = "golangci-lint-format" golangciLintTool = "golangci-lint" injectedRootArgCount = 2 + reportCommand = "report" + tokenEconomyCommand = "token-economy" ) func runnerArgs(argv []string) []string { @@ -34,6 +36,10 @@ func runnerArgs(argv []string) []string { } func codeIntelArgs(root, stateRoot string, args []string) []string { + if len(args) > 0 && args[0] == tokenEconomyCommand { + return tokenEconomyArgs(root, stateRoot, args) + } + args = withoutFlags(args, "--state-root") if len(args) == 0 { return args @@ -57,6 +63,26 @@ func codeIntelArgs(root, stateRoot string, args []string) []string { return next } +//nolint:wsl_v5 // Nested injection keeps the emitted argv sequence adjacent. +func tokenEconomyArgs(root, stateRoot string, args []string) []string { + if len(args) < 2 || args[1] != reportCommand { + return append([]string(nil), args...) + } + + explicitStateRoot := flagValue(args[2:], "--state-root", "") + next := []string{args[0], args[1]} + if !hasFlag(args[2:], "--root") { + next = append(next, "--root", root) + } + if explicitStateRoot == "" && !sameCleanPath(root, stateRoot) { + next = append(next, "--state-root", stateRoot) + } + + next = append(next, args[2:]...) + + return next +} + func outputArgs(root string, args []string) []string { if len(args) == 0 || hasFlag(args, "--root") { return args diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index 7f03d787..b1e01aed 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -228,6 +228,68 @@ func TestCodeIntelArgsBindPrivateStateDatabase(t *testing.T) { } } +func TestCodeIntelArgsBindTokenEconomyStateRootAfterNestedCommand(t *testing.T) { + t.Parallel() + + args := codeIntelArgs( + "/repo", + "/private/state", + []string{"token-economy", "report", "--historical", "--output-prefix", "/tmp/report"}, + ) + + want := []string{ + "token-economy", + "report", + "--root", + "/repo", + "--state-root", + "/private/state", + "--historical", + "--output-prefix", + "/tmp/report", + } + if !slices.Equal(args, want) { + t.Fatalf("codeIntelArgs() = %#v, want %#v", args, want) + } +} + +func TestCodeIntelArgsDoNotRewriteExplicitBenchmarkContract(t *testing.T) { + t.Parallel() + + input := []string{ + "token-economy", + "benchmark", + "run", + "--manifest", + "/private/benchmark.yaml", + "--state-root", + "/private/evidence", + "--approved-max-runs", + "3", + } + args := codeIntelArgs("/repo", "/ambient/state", input) + if !slices.Equal(args, input) { + t.Fatalf("codeIntelArgs() = %#v, want explicit contract %#v", args, input) + } +} + +func TestCodeIntelArgsDoNotAddReportFlagsToLedger(t *testing.T) { + t.Parallel() + + input := []string{ + "token-economy", + "ledger", + "--provider", + "codex", + "--path", + "/private/rollout.jsonl", + } + args := codeIntelArgs("/repo", "/ambient/state", input) + if !slices.Equal(args, input) { + t.Fatalf("codeIntelArgs() = %#v, want ledger contract %#v", args, input) + } +} + func TestAgentHooksArgsInjectCapabilityEthosRootWithoutHookCommand(t *testing.T) { t.Parallel() diff --git a/go/internal/codeintelcli/main.go b/go/internal/codeintelcli/main.go index b387df2b..c2bf24a7 100644 --- a/go/internal/codeintelcli/main.go +++ b/go/internal/codeintelcli/main.go @@ -105,6 +105,7 @@ func commandHandlers() map[string]codeIntelCommand { "skill-health": printSkillHealth, "stats": printStats, "surprises": printSurprises, + tokenEconomyCommand: tokenEconomy, "upsert-vector": upsertVector, "vector-stats": printVectorStats, "workspace": workspace, diff --git a/go/internal/codeintelcli/token_economy.go b/go/internal/codeintelcli/token_economy.go new file mode 100644 index 00000000..08abf8dd --- /dev/null +++ b/go/internal/codeintelcli/token_economy.go @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:wsl_v5 // CLI validation and evidence stages stay visibly separated. +package codeintelcli + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "blackcat.ca/coding-ethos/go/internal/codeintel" + "blackcat.ca/coding-ethos/go/internal/tokeneconomy" +) + +const ( + reportKindKey = "kind" + tokenEconomyCommand = "token-economy" + tokenEconomyReportCommand = "report" +) + +var ( + errTokenEconomySubcommand = errors.New("token-economy subcommand is required") + errUnknownTokenEconomy = errors.New("unknown token-economy subcommand") + errBenchmarkSubcommand = errors.New( + "token-economy benchmark subcommand is required", + ) + errUnknownBenchmark = errors.New("unknown token-economy benchmark subcommand") + errReportOutputPrefix = errors.New( + "token-economy report --output-prefix is required", + ) + errTokenEconomyCohort = errors.New( + "choose exactly one token-economy cohort: --historical or --experiment-id", + ) + errHistoricalReportInputs = errors.New( + "historical token-economy reports require repeatable --db and explicit --from/--to", + ) + errControlledReportInputs = errors.New( + "--db, --from, and --to are valid only with --historical", + ) +) + +type tokenEconomyDatabaseFlags []string + +type tokenEconomyReportOptions struct { + root string + stateRoot string + tokenDB string + experimentID string + outputPrefix string + fromUTC string + toUTC string + historicalDatabases []string + historical bool +} + +func (values *tokenEconomyDatabaseFlags) String() string { + return strings.Join(*values, ",") +} + +func (values *tokenEconomyDatabaseFlags) Set(value string) error { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("%w: --db value is empty", errHistoricalReportInputs) + } + + *values = append(*values, value) + + return nil +} + +func tokenEconomy(ctx context.Context, args []string) error { + if len(args) == 0 { + return errTokenEconomySubcommand + } + + switch args[0] { + case "benchmark": + return tokenEconomyBenchmark(ctx, args[1:]) + case "ledger": + return inspectTokenEconomyLedger(args[1:]) + case tokenEconomyReportCommand: + return writeTokenEconomyReport(ctx, args[1:]) + default: + return fmt.Errorf("%w: %q", errUnknownTokenEconomy, args[0]) + } +} + +func tokenEconomyBenchmark(ctx context.Context, args []string) error { + if len(args) == 0 { + return errBenchmarkSubcommand + } + + switch args[0] { + case "freeze": + return freezeTokenEconomyBenchmark(ctx, args[1:]) + case "run": + return runTokenEconomyBenchmark(ctx, args[1:]) + case "validate": + return validateTokenEconomyBenchmark(ctx, args[1:]) + default: + return fmt.Errorf("%w: %q", errUnknownBenchmark, args[0]) + } +} + +func freezeTokenEconomyBenchmark(ctx context.Context, args []string) error { + flags := flag.NewFlagSet("token-economy benchmark freeze", flag.ExitOnError) + draftPath := flags.String("draft", "", "Absolute benchmark draft YAML path") + outputPath := flags.String("output", "", "Create-new frozen benchmark YAML path") + + err := parseCommandFlags(flags, args, "token-economy benchmark freeze") + if err != nil { + return err + } + + prepared, err := tokeneconomy.FreezeBenchmarkManifest(ctx, *draftPath, *outputPath) + if err != nil { + return fmt.Errorf("freeze token-economy benchmark: %w", err) + } + + return encodeJSON(os.Stdout, prepared) +} + +func validateTokenEconomyBenchmark(ctx context.Context, args []string) error { + flags := flag.NewFlagSet("token-economy benchmark validate", flag.ExitOnError) + manifestPath := flags.String("manifest", "", "Absolute frozen benchmark manifest path") + + err := parseCommandFlags(flags, args, "token-economy benchmark validate") + if err != nil { + return err + } + + prepared, err := tokeneconomy.LoadBenchmarkManifest(ctx, *manifestPath) + if err != nil { + return fmt.Errorf("validate token-economy benchmark: %w", err) + } + + return encodeJSON(os.Stdout, prepared) +} + +func runTokenEconomyBenchmark(ctx context.Context, args []string) error { + flags := flag.NewFlagSet("token-economy benchmark run", flag.ExitOnError) + manifestPath := flags.String("manifest", "", "Absolute frozen benchmark manifest path") + stateRoot := flags.String("state-root", "", "Absolute private benchmark state root") + approvedRuns := flags.Int( + "approved-max-runs", + 0, + "Explicit maximum provider runs for this invocation (multiple of three)", + ) + + err := parseCommandFlags(flags, args, "token-economy benchmark run") + if err != nil { + return err + } + + prepared, err := tokeneconomy.LoadBenchmarkManifest(ctx, *manifestPath) + if err != nil { + return fmt.Errorf("validate token-economy benchmark: %w", err) + } + + options := tokeneconomy.BenchmarkRunOptions{ + StateRoot: strings.TrimSpace(*stateRoot), + ApprovedMaxRuns: *approvedRuns, + } + summary, err := tokeneconomy.RunBenchmark(ctx, prepared, options) + if err != nil { + return fmt.Errorf("run token-economy benchmark: %w", err) + } + + return encodeJSON(os.Stdout, summary) +} + +func inspectTokenEconomyLedger(args []string) error { + flags := flag.NewFlagSet("token-economy ledger", flag.ExitOnError) + provider := flags.String("provider", "", "Provider ledger format: codex or claude") + path := flags.String("path", "", "Absolute provider-native JSONL ledger path") + + err := parseCommandFlags(flags, args, "token-economy ledger") + if err != nil { + return err + } + + ledger, err := tokeneconomy.ParseLedger( + tokeneconomy.Provider(strings.TrimSpace(*provider)), + *path, + ) + if err != nil { + return fmt.Errorf("inspect provider token ledger: %w", err) + } + + return encodeJSON(os.Stdout, ledger) +} + +func writeTokenEconomyReport(ctx context.Context, args []string) error { + options, err := parseTokenEconomyReportOptions(args) + if err != nil { + return err + } + + report, err := buildTokenEconomyReport(ctx, options) + if err != nil { + return fmt.Errorf("build token-economy report: %w", err) + } + + artifacts, err := tokeneconomy.WriteReportArtifacts(report, options.outputPrefix) + if err != nil { + return fmt.Errorf("write token-economy report: %w", err) + } + + return encodeJSON(os.Stdout, map[string]any{ + "artifacts": artifacts, + "causal": report.Causal, + "cohort": report.Cohort, + "conclusion": report.Conclusion, + reportKindKey: report.Kind, + }) +} + +func parseTokenEconomyReportOptions(args []string) (tokenEconomyReportOptions, error) { + flags := flag.NewFlagSet("token-economy "+tokenEconomyReportCommand, flag.ExitOnError) + root := flags.String("root", ".", "Repository root owning Coding Ethos evidence") + stateRoot := flags.String("state-root", "", "Private Coding Ethos state root") + historicalDatabases := tokenEconomyDatabaseFlags{} + flags.Var( + &historicalDatabases, + "db", + "Historical code-intel DuckDB path; repeat for every source", + ) + fromUTC := flags.String("from", "", "Historical window start, inclusive RFC3339") + toUTC := flags.String("to", "", "Historical window end, exclusive RFC3339") + tokenDB := flags.String("token-db", "", "Token-economy DuckDB path") + historical := flags.Bool("historical", false, "Report observational proxy transforms") + experimentID := flags.String("experiment-id", "", "Report one controlled experiment") + outputPrefix := flags.String( + "output-prefix", + "", + "Create-new report path without extension", + ) + + err := parseCommandFlags(flags, args, "token-economy "+tokenEconomyReportCommand) + if err != nil { + return tokenEconomyReportOptions{}, err + } + if *historical == (strings.TrimSpace(*experimentID) != "") { + return tokenEconomyReportOptions{}, errTokenEconomyCohort + } + if strings.TrimSpace(*outputPrefix) == "" { + return tokenEconomyReportOptions{}, errReportOutputPrefix + } + if *historical { + if len(historicalDatabases) == 0 || strings.TrimSpace(*fromUTC) == "" || + strings.TrimSpace(*toUTC) == "" || strings.TrimSpace(*tokenDB) != "" { + return tokenEconomyReportOptions{}, errHistoricalReportInputs + } + } else { + if len(historicalDatabases) != 0 || strings.TrimSpace(*fromUTC) != "" || + strings.TrimSpace(*toUTC) != "" { + return tokenEconomyReportOptions{}, errControlledReportInputs + } + } + + return tokenEconomyReportOptions{ + root: *root, + stateRoot: *stateRoot, + tokenDB: *tokenDB, + experimentID: *experimentID, + outputPrefix: *outputPrefix, + fromUTC: *fromUTC, + toUTC: *toUTC, + historicalDatabases: historicalDatabases, + historical: *historical, + }, nil +} + +func buildTokenEconomyReport( + ctx context.Context, + options tokenEconomyReportOptions, +) (tokeneconomy.Report, error) { + if options.historical { + report, err := tokeneconomy.HistoricalReport( + ctx, + tokeneconomy.HistoricalReportOptions{ + DatabasePaths: options.historicalDatabases, + FromUTC: options.fromUTC, + ToUTC: options.toUTC, + }, + time.Now(), + ) + if err != nil { + return tokeneconomy.Report{}, fmt.Errorf( + "query historical token-economy report: %w", + err, + ) + } + + return report, nil + } + + evidenceRoot := resolvedTokenEconomyStateRoot(options.root, options.stateRoot) + path := strings.TrimSpace(options.tokenDB) + if path == "" { + path = tokeneconomy.DefaultDBPath(evidenceRoot) + } + + return controlledTokenEconomyReport(ctx, path, options.experimentID) +} + +func controlledTokenEconomyReport( + ctx context.Context, + path string, + experimentID string, +) (tokeneconomy.Report, error) { + store, err := tokeneconomy.Open(ctx, filepath.Clean(path)) + if err != nil { + return tokeneconomy.Report{}, fmt.Errorf("open token-economy store: %w", err) + } + defer store.Close() + + report, err := store.ExperimentReport(ctx, strings.TrimSpace(experimentID), time.Now()) + if err != nil { + return tokeneconomy.Report{}, fmt.Errorf("query token-economy report: %w", err) + } + + return report, nil +} + +func resolvedTokenEconomyStateRoot(root, stateRoot string) string { + if strings.TrimSpace(stateRoot) != "" { + return filepath.Clean(stateRoot) + } + + return codeintel.ResolveStateRoot(filepath.Clean(root)) +} diff --git a/go/internal/codeintelcli/token_economy_test.go b/go/internal/codeintelcli/token_economy_test.go new file mode 100644 index 00000000..1bcebd7c --- /dev/null +++ b/go/internal/codeintelcli/token_economy_test.go @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintelcli + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "blackcat.ca/coding-ethos/go/internal/codeintel" + "blackcat.ca/coding-ethos/go/internal/tokeneconomy" +) + +func TestTokenEconomyLedgerPrintsSanitizedProviderUsage(t *testing.T) { + path := filepath.Join(t.TempDir(), "rollout.jsonl") + payload := strings.Join([]string{ + `{"type":"session_meta","payload":{"id":"session-1"}}`, + `{"type":"turn_context","payload":{"model":"gpt-test"}}`, + `{"type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15}}}}`, + }, "\n") + if err := os.WriteFile(path, []byte(payload+"\n"), 0o600); err != nil { + t.Fatalf("write ledger: %v", err) + } + + var runErr error + output := captureStdout(t, func() { + runErr = run(context.Background(), []string{ + "token-economy", + "ledger", + "--provider", + "codex", + "--path", + path, + }) + }) + if runErr != nil { + t.Fatalf("token-economy ledger: %v", runErr) + } + for _, expected := range []string{ + `"session_id": "session-1"`, + `"model": "gpt-test"`, + `"total_tokens": 15`, + } { + if !strings.Contains(output, expected) { + t.Fatalf("ledger output missing %s:\n%s", expected, output) + } + } +} + +func TestTokenEconomyReportRequiresExactlyOneCohort(t *testing.T) { + err := runCapturingStdout(t, context.Background(), []string{ + "token-economy", + "report", + "--output-prefix", + filepath.Join(t.TempDir(), "report"), + }) + if err == nil || !strings.Contains(err.Error(), "choose exactly one") { + t.Fatalf("expected cohort validation failure, got %v", err) + } +} + +func TestTokenEconomyHistoricalReportRequiresSourcesAndWindow(t *testing.T) { + err := runCapturingStdout(t, context.Background(), []string{ + "token-economy", + "report", + "--historical", + "--output-prefix", + filepath.Join(t.TempDir(), "report"), + }) + if err == nil || !strings.Contains(err.Error(), "repeatable --db") { + t.Fatalf("expected explicit historical input failure, got %v", err) + } +} + +func TestTokenEconomyHistoricalReportAcceptsRepeatableDatabases(t *testing.T) { + ctx := context.Background() + first := createCLIHistoricalStore( + t, + ctx, + "codex", + "session-codex", + "event-codex", + 100, + 50, + ) + second := createCLIHistoricalStore( + t, + ctx, + "claude", + "session-claude", + "event-claude", + 200, + 80, + ) + prefix := filepath.Join(t.TempDir(), "historical") + + err := runCapturingStdout(t, ctx, []string{ + "token-economy", + "report", + "--historical", + "--db", second, + "--db", first, + "--from", "2026-08-01T00:00:00Z", + "--to", "2026-09-01T00:00:00Z", + "--output-prefix", prefix, + }) + if err != nil { + t.Fatalf("write historical report from repeated databases: %v", err) + } + + payload, err := os.ReadFile(prefix + ".json") + if err != nil { + t.Fatalf("read historical report artifact: %v", err) + } + var report tokeneconomy.Report + if err = json.Unmarshal(payload, &report); err != nil { + t.Fatalf("decode historical report artifact: %v", err) + } + if report.Historical == nil || len(report.Historical.Sources) != 2 || + report.Historical.RawContextTokens != 300 || + report.Historical.DeliveredContextTokens != 130 || + report.Historical.ProxySessions != 2 { + t.Fatalf("unexpected repeated-database report: %#v", report) + } +} + +func TestTokenEconomyBenchmarkValidateIsReadOnlyDispatch(t *testing.T) { + err := runCapturingStdout(t, context.Background(), []string{ + "token-economy", + "benchmark", + "validate", + "--manifest", + filepath.Join(t.TempDir(), "missing.yaml"), + }) + if err == nil || !strings.Contains(err.Error(), "validate token-economy benchmark") { + t.Fatalf("expected benchmark validation failure, got %v", err) + } +} + +func createCLIHistoricalStore( + t *testing.T, + ctx context.Context, + provider string, + sessionID string, + eventID string, + inputTokens int64, + outputTokens int64, +) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "code-intel.duckdb") + store, err := codeintel.Open(ctx, path) + if err != nil { + t.Fatalf("open CLI historical store: %v", err) + } + + statements := []struct { + query string + args []any + }{ + { + query: `INSERT INTO proxy_sessions( + session_id, provider, started_at_utc, last_seen_utc, raw_json + ) VALUES (?, ?, '2026-08-10T00:00:00Z', '2026-08-10T00:00:00Z', '{}')`, + args: []any{sessionID, provider}, + }, + { + query: `INSERT INTO proxy_events( + event_id, session_id, event_kind, provider, recorded_at_utc, + output_tokens, metadata_json, raw_json + ) VALUES (?, ?, 'file_read', ?, '2026-08-10T00:00:00Z', ?, '{}', '{}')`, + args: []any{eventID, sessionID, provider, outputTokens}, + }, + { + query: `INSERT INTO proxy_transforms( + event_id, ordinal, name, input_tokens, output_tokens + ) VALUES (?, 0, 'compact', ?, ?)`, + args: []any{eventID, inputTokens, outputTokens}, + }, + } + for _, statement := range statements { + if _, err = store.Database().ExecContext( + ctx, + statement.query, + statement.args..., + ); err != nil { + _ = store.Close() + t.Fatalf("populate CLI historical store: %v", err) + } + } + if err = store.Close(); err != nil { + t.Fatalf("close CLI historical store: %v", err) + } + + return path +} diff --git a/go/internal/tokeneconomy/artifact.go b/go/internal/tokeneconomy/artifact.go new file mode 100644 index 00000000..5734b620 --- /dev/null +++ b/go/internal/tokeneconomy/artifact.go @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:cyclop,funlen,gocyclo,lll,noinlineerr,wsl_v5 // Artifact cleanup remains visible. +package tokeneconomy + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +var errArtifact = errors.New("token-economy artifact error") + +// ArtifactSet identifies the verified report files created by one write. +type ArtifactSet struct { + JSONPath string `json:"json_path"` + MarkdownPath string `json:"markdown_path"` + ReceiptPath string `json:"receipt_path"` + JSONSHA256 string `json:"json_sha256"` + MarkdownSHA256 string `json:"markdown_sha256"` +} + +// WriteReportArtifacts writes create-new JSON, Markdown, and checksum files. +func WriteReportArtifacts(report Report, outputPrefix string) (ArtifactSet, error) { + prefix, err := filepath.Abs(strings.TrimSpace(outputPrefix)) + if err != nil { + return ArtifactSet{}, fmt.Errorf("resolve token-economy output prefix: %w", err) + } + if strings.TrimSpace(outputPrefix) == "" { + return ArtifactSet{}, fmt.Errorf("%w: output prefix is required", errArtifact) + } + + artifacts := ArtifactSet{ + JSONPath: prefix + ".json", + MarkdownPath: prefix + ".md", + ReceiptPath: prefix + ".sha256", + } + for _, path := range []string{ + artifacts.JSONPath, + artifacts.MarkdownPath, + artifacts.ReceiptPath, + } { + if _, statErr := os.Stat(path); statErr == nil { + return ArtifactSet{}, fmt.Errorf("%w: path already exists: %s", errArtifact, path) + } else if !errors.Is(statErr, os.ErrNotExist) { + return ArtifactSet{}, fmt.Errorf( + "inspect token-economy artifact %s: %w", + path, + statErr, + ) + } + } + + err = os.MkdirAll(filepath.Dir(prefix), storeDirMode) + if err != nil { + return ArtifactSet{}, fmt.Errorf("create token-economy artifact directory: %w", err) + } + + jsonPayload, err := json.MarshalIndent(report, "", " ") + if err != nil { + return ArtifactSet{}, fmt.Errorf("encode token-economy JSON report: %w", err) + } + jsonPayload = append(jsonPayload, '\n') + markdownPayload := []byte(formatReportMarkdown(report)) + + created := []string{} + cleanup := func() { + for _, path := range created { + _ = os.Remove(path) + } + } + + err = writeExclusiveArtifact(artifacts.JSONPath, jsonPayload) + if err != nil { + return ArtifactSet{}, err + } + created = append(created, artifacts.JSONPath) + + err = writeExclusiveArtifact(artifacts.MarkdownPath, markdownPayload) + if err != nil { + cleanup() + + return ArtifactSet{}, err + } + created = append(created, artifacts.MarkdownPath) + + artifacts.JSONSHA256, err = ledgerFileSHA256(artifacts.JSONPath) + if err != nil { + cleanup() + + return ArtifactSet{}, err + } + artifacts.MarkdownSHA256, err = ledgerFileSHA256(artifacts.MarkdownPath) + if err != nil { + cleanup() + + return ArtifactSet{}, err + } + + receipt := artifacts.JSONSHA256 + " " + filepath.Base(artifacts.JSONPath) + "\n" + + artifacts.MarkdownSHA256 + " " + filepath.Base(artifacts.MarkdownPath) + "\n" + err = writeExclusiveArtifact(artifacts.ReceiptPath, []byte(receipt)) + if err != nil { + cleanup() + + return ArtifactSet{}, err + } + + return artifacts, nil +} + +func writeExclusiveArtifact(path string, payload []byte) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, storeFileMode) + if err != nil { + return fmt.Errorf("create token-economy artifact %s: %w", path, err) + } + + _, writeErr := file.Write(payload) + syncErr := file.Sync() + closeErr := file.Close() + if err = errors.Join(writeErr, syncErr, closeErr); err != nil { + return fmt.Errorf("write token-economy artifact %s: %w", path, err) + } + + return nil +} + +func formatReportMarkdown(report Report) string { + var output strings.Builder + + output.WriteString("# Coding Ethos Token Economy Report\n\n") + writeMarkdownField(&output, "Cohort", report.Cohort) + writeMarkdownField(&output, "Generated", report.GeneratedAtUTC) + writeMarkdownField(&output, "Conclusion", string(report.Conclusion)) + writeMarkdownField(&output, "Causal", strconv.FormatBool(report.Causal)) + + if report.Historical != nil { + output.WriteString("\n## Observational context reduction\n\n") + writeMarkdownField(&output, "Window start (inclusive)", report.Historical.FromUTC) + writeMarkdownField(&output, "Window end (exclusive)", report.Historical.ToUTC) + writeMarkdownField( + &output, + "Raw estimated tokens", + strconv.FormatInt(report.Historical.RawContextTokens, 10), + ) + writeMarkdownField( + &output, + "Delivered estimated tokens", + strconv.FormatInt(report.Historical.DeliveredContextTokens, 10), + ) + writeMarkdownField( + &output, + "Avoided estimated tokens", + strconv.FormatInt(report.Historical.AvoidedContextTokens, 10), + ) + writeMarkdownField( + &output, + "Gross reduction", + fmt.Sprintf("%.2f%%", report.Historical.GrossReductionPercent), + ) + + output.WriteString("\n### Verified sources\n\n") + for _, source := range report.Historical.Sources { + output.WriteString("- `") + output.WriteString(strings.ReplaceAll(source.Path, "`", "\\`")) + output.WriteString("`: `") + output.WriteString(source.SHA256After) + output.WriteString("` (unchanged)\n") + } + } + + if len(report.Comparisons) > 0 { + output.WriteString("\n## Arm comparisons\n\n") + output.WriteString( + "| Treatment | Control | Tasks | Savings | Adjusted interval | Confidence | Quality |\n", + ) + output.WriteString("|---|---|---:|---:|---:|---:|---|\n") + for _, comparison := range report.Comparisons { + writeComparisonMarkdownRow(&output, comparison) + } + } + + output.WriteString("\n## Coverage\n\n") + writeMarkdownField(&output, "Tasks", strconv.Itoa(report.Coverage.TaskCount)) + writeMarkdownField( + &output, + "Complete three-arm tasks", + strconv.Itoa(report.Coverage.CompleteTaskCount), + ) + writeMarkdownField( + &output, + "Complete task-replicate blocks", + strconv.Itoa(report.Coverage.CompleteBlockCount), + ) + writeMarkdownField( + &output, + "Partial task-replicate blocks", + strconv.Itoa(report.Coverage.PartialBlockCount), + ) + writeMarkdownField(&output, "Runs", strconv.Itoa(report.Coverage.RunCount)) + writeMarkdownField( + &output, + "Accepted runs", + strconv.Itoa(report.Coverage.AcceptedRunCount), + ) + for _, reason := range report.Coverage.Reasons { + output.WriteString("- ") + output.WriteString(strings.ReplaceAll(reason, "\n", " ")) + output.WriteByte('\n') + } + + return output.String() +} + +func writeMarkdownField(output *strings.Builder, label, value string) { + output.WriteString("- ") + output.WriteString(label) + output.WriteString(": ") + output.WriteString(strings.ReplaceAll(value, "\n", " ")) + output.WriteByte('\n') +} + +func writeComparisonMarkdownRow(output *strings.Builder, comparison Comparison) { + output.WriteString("| ") + output.WriteString(string(comparison.TreatmentArm)) + output.WriteString(" | ") + output.WriteString(string(comparison.ControlArm)) + output.WriteString(" | ") + output.WriteString(strconv.Itoa(comparison.TaskCount)) + output.WriteString(" | ") + output.WriteString(strconv.FormatFloat(comparison.SavingsPercent, 'f', 2, 64)) + output.WriteString("% | ") + output.WriteString( + strconv.FormatFloat(comparison.SavingsPercentInterval.Lower, 'f', 2, 64), + ) + output.WriteString("% to ") + output.WriteString( + strconv.FormatFloat(comparison.SavingsPercentInterval.Upper, 'f', 2, 64), + ) + output.WriteString("% | ") + output.WriteString(strconv.FormatFloat(comparison.ConfidenceLevelPercent, 'f', 2, 64)) + output.WriteString("% | ") + output.WriteString(qualityLabel(comparison)) + output.WriteString(" |\n") +} + +func qualityLabel(comparison Comparison) string { + if comparison.QualityNonInferior { + return "non-inferior" + } + + return "not established" +} diff --git a/go/internal/tokeneconomy/benchmark_manifest.go b/go/internal/tokeneconomy/benchmark_manifest.go new file mode 100644 index 00000000..bb5171c4 --- /dev/null +++ b/go/internal/tokeneconomy/benchmark_manifest.go @@ -0,0 +1,786 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:cyclop,funlen,gocyclo,govet,lll,mnd,noinlineerr,tagalign,wsl_v5 // Protocol retains review order. +package tokeneconomy + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "slices" + "strings" + "time" + + "go.yaml.in/yaml/v3" + + "blackcat.ca/coding-ethos/go/internal/safeexec" +) + +const ( + // BenchmarkManifestVersion is the accepted controlled-experiment contract. + BenchmarkManifestVersion = 1 + maximumBenchmarkTasks = 20 + maximumBenchmarkReplicates = 2 + maximumBenchmarkRuns = 120 + maximumStaticContextBytes = 128 << 10 + benchmarkTaskDiagnostic = "diagnostic" + benchmarkTaskReal = "real" +) + +var ( + errInvalidBenchmarkManifest = errors.New("invalid token-economy benchmark manifest") + benchmarkIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) + benchmarkCommitPattern = regexp.MustCompile(`^[a-f0-9]{40}$`) + hexSHA256Pattern = regexp.MustCompile(`^[a-f0-9]{64}$`) +) + +// BenchmarkManifest is the versioned YAML experiment contract. +type BenchmarkManifest struct { + ExperimentID string `yaml:"experiment_id" json:"experiment_id"` + CreatedAtUTC string `yaml:"created_at_utc" json:"created_at_utc"` + RandomizationSeed string `yaml:"randomization_seed" json:"randomization_seed"` + StaticContext BenchmarkStaticContext `yaml:"static_context" json:"static_context"` + Provider BenchmarkProvider `yaml:"provider" json:"provider"` + Tasks []BenchmarkTask `yaml:"tasks" json:"tasks"` + FullConfigOverrides []string `yaml:"full_config_overrides" json:"full_config_overrides"` + SchemaVersion int `yaml:"schema_version" json:"schema_version"` + Replicates int `yaml:"replicates" json:"replicates"` + BlockCheckpoints []int `yaml:"analysis_block_checkpoints" json:"analysis_block_checkpoints"` +} + +// BenchmarkStaticContext is the exact global AGENTS.md content shared by the +// full and static arms. +type BenchmarkStaticContext struct { + Path string `yaml:"path" json:"path"` + SHA256 string `yaml:"sha256" json:"sha256"` +} + +// BenchmarkProvider freezes the executable and model used by every arm. +type BenchmarkProvider struct { + Kind Provider `yaml:"kind" json:"kind"` + Executable string `yaml:"executable" json:"executable"` + ExecutableSHA256 string `yaml:"executable_sha256" json:"executable_sha256"` + RuntimeVersion string `yaml:"runtime_version" json:"runtime_version"` + Model string `yaml:"model" json:"model"` + ReasoningEffort string `yaml:"reasoning_effort" json:"reasoning_effort"` + AuthFile string `yaml:"auth_file" json:"auth_file"` +} + +// BenchmarkTask freezes one source revision, prompt, scope, and validator. +type BenchmarkTask struct { + TaskID string `yaml:"task_id" json:"task_id"` + Kind string `yaml:"kind" json:"kind"` + RepositoryPath string `yaml:"repository_path" json:"repository_path"` + Commit string `yaml:"commit" json:"commit"` + SourceArchiveSHA256 string `yaml:"source_archive_sha256" json:"source_archive_sha256"` + PromptPath string `yaml:"prompt_path" json:"prompt_path"` + PromptSHA256 string `yaml:"prompt_sha256" json:"prompt_sha256"` + ValidatorSHA256 string `yaml:"validator_sha256" json:"validator_sha256"` + AgentTimeout string `yaml:"agent_timeout" json:"agent_timeout"` + ValidationTimeout string `yaml:"validation_timeout" json:"validation_timeout"` + AllowedPaths []string `yaml:"allowed_paths" json:"allowed_paths"` + Validators [][]string `yaml:"validators" json:"validators"` +} + +// PreparedBenchmark is a validated manifest plus computed protocol identity. +type PreparedBenchmark struct { + Manifest BenchmarkManifest `json:"manifest"` + ManifestPath string `json:"manifest_path"` + ManifestSHA256 string `json:"manifest_sha256"` + ProtocolSHA256 string `json:"protocol_sha256"` + TotalRuns int `json:"total_runs"` +} + +// LoadBenchmarkManifest decodes and validates a benchmark without launching an +// agent or changing a repository. +func LoadBenchmarkManifest( + ctx context.Context, + path string, +) (PreparedBenchmark, error) { + canonical, payload, err := readBenchmarkManifest(path) + if err != nil { + return PreparedBenchmark{}, err + } + + manifest, err := decodeBenchmarkManifest(payload, "manifest") + if err != nil { + return PreparedBenchmark{}, err + } + + if err = validateBenchmarkManifest(ctx, manifest); err != nil { + return PreparedBenchmark{}, err + } + + manifestDigest := sha256.Sum256(payload) + protocolPayload, err := json.Marshal(manifest) + if err != nil { + return PreparedBenchmark{}, fmt.Errorf("encode benchmark protocol: %w", err) + } + protocolDigest := sha256.Sum256(protocolPayload) + + return PreparedBenchmark{ + Manifest: manifest, + ManifestPath: canonical, + ManifestSHA256: hex.EncodeToString(manifestDigest[:]), + ProtocolSHA256: hex.EncodeToString(protocolDigest[:]), + TotalRuns: len(manifest.Tasks) * manifest.Replicates * 3, + }, nil +} + +// FreezeBenchmarkManifest computes every external identity in a draft and +// creates a validated immutable manifest without launching an agent. +func FreezeBenchmarkManifest( + ctx context.Context, + draftPath string, + outputPath string, +) (PreparedBenchmark, error) { + _, payload, err := readBenchmarkManifest(draftPath) + if err != nil { + return PreparedBenchmark{}, err + } + + manifest, err := decodeBenchmarkManifest(payload, "draft") + if err != nil { + return PreparedBenchmark{}, err + } + + if !filepath.IsAbs(outputPath) { + return PreparedBenchmark{}, fmt.Errorf( + "%w: frozen manifest output path must be absolute", + errInvalidBenchmarkManifest, + ) + } + outputPath = filepath.Clean(outputPath) + if err = fingerprintBenchmarkManifest(ctx, &manifest); err != nil { + return PreparedBenchmark{}, err + } + if err = validateBenchmarkManifest(ctx, manifest); err != nil { + return PreparedBenchmark{}, err + } + + frozen, err := yaml.Marshal(manifest) + if err != nil { + return PreparedBenchmark{}, fmt.Errorf("encode frozen benchmark manifest: %w", err) + } + if err = os.MkdirAll(filepath.Dir(outputPath), storeDirMode); err != nil { + return PreparedBenchmark{}, fmt.Errorf("create frozen manifest directory: %w", err) + } + if err = writeExclusiveArtifact(outputPath, frozen); err != nil { + return PreparedBenchmark{}, err + } + + prepared, err := LoadBenchmarkManifest(ctx, outputPath) + if err != nil { + _ = os.Remove(outputPath) + + return PreparedBenchmark{}, fmt.Errorf("verify frozen benchmark manifest: %w", err) + } + + return prepared, nil +} + +func decodeBenchmarkManifest(payload []byte, label string) (BenchmarkManifest, error) { + var manifest BenchmarkManifest + decoder := yaml.NewDecoder(bytes.NewReader(payload)) + decoder.KnownFields(true) + err := decoder.Decode(&manifest) + if err != nil { + return BenchmarkManifest{}, fmt.Errorf("decode benchmark %s: %w", label, err) + } + + var trailing any + err = decoder.Decode(&trailing) + if !errors.Is(err, io.EOF) { + if err == nil { + return BenchmarkManifest{}, fmt.Errorf( + "%w: benchmark %s contains multiple YAML documents", + errInvalidBenchmarkManifest, + label, + ) + } + + return BenchmarkManifest{}, fmt.Errorf("decode benchmark %s trailer: %w", label, err) + } + + return manifest, nil +} + +func fingerprintBenchmarkManifest( + ctx context.Context, + manifest *BenchmarkManifest, +) error { + if !filepath.IsAbs(manifest.Provider.Executable) { + return fmt.Errorf( + "%w: provider executable path must be absolute", + errInvalidBenchmarkManifest, + ) + } + + providerDigest, err := ledgerFileSHA256(manifest.Provider.Executable) + if err != nil { + return fmt.Errorf("hash provider executable: %w", err) + } + manifest.Provider.ExecutableSHA256 = providerDigest + + version, err := safeexec.CommandContext(ctx, manifest.Provider.Executable, "--version"). + Output() + if err != nil { + return fmt.Errorf("query provider runtime version: %w", err) + } + manifest.Provider.RuntimeVersion = strings.TrimSpace(string(version)) + + staticDigest, err := ledgerFileSHA256(manifest.StaticContext.Path) + if err != nil { + return fmt.Errorf("hash static context: %w", err) + } + manifest.StaticContext.SHA256 = staticDigest + + for index := range manifest.Tasks { + task := &manifest.Tasks[index] + task.SourceArchiveSHA256, err = benchmarkSourceArchiveSHA256( + ctx, + task.RepositoryPath, + task.Commit, + ) + if err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + task.PromptSHA256, err = ledgerFileSHA256(task.PromptPath) + if err != nil { + return fmt.Errorf("task %q prompt: %w", task.TaskID, err) + } + task.ValidatorSHA256, err = benchmarkValidatorSHA256(task.Validators) + if err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + } + + return nil +} + +func readBenchmarkManifest(path string) (string, []byte, error) { + path = strings.TrimSpace(path) + if !filepath.IsAbs(path) { + return "", nil, fmt.Errorf( + "%w: manifest path must be absolute", + errInvalidBenchmarkManifest, + ) + } + + canonical := filepath.Clean(path) + payload, err := os.ReadFile(canonical) + if err != nil { + return "", nil, fmt.Errorf("read benchmark manifest: %w", err) + } + + return canonical, payload, nil +} + +func validateBenchmarkManifest(ctx context.Context, manifest BenchmarkManifest) error { + if manifest.SchemaVersion != BenchmarkManifestVersion { + return fmt.Errorf( + "%w: schema_version is %d, expected %d", + errInvalidBenchmarkManifest, + manifest.SchemaVersion, + BenchmarkManifestVersion, + ) + } + if !benchmarkIDPattern.MatchString(manifest.ExperimentID) { + return fmt.Errorf("%w: invalid experiment_id", errInvalidBenchmarkManifest) + } + if strings.TrimSpace(manifest.RandomizationSeed) == "" { + return fmt.Errorf("%w: randomization_seed is required", errInvalidBenchmarkManifest) + } + if _, err := time.Parse(time.RFC3339Nano, manifest.CreatedAtUTC); err != nil { + return fmt.Errorf( + "%w: created_at_utc must be RFC3339: %w", + errInvalidBenchmarkManifest, + err, + ) + } + if manifest.Replicates < 1 || manifest.Replicates > maximumBenchmarkReplicates { + return fmt.Errorf( + "%w: replicates must be between 1 and %d", + errInvalidBenchmarkManifest, + maximumBenchmarkReplicates, + ) + } + if len(manifest.Tasks) == 0 || len(manifest.Tasks) > maximumBenchmarkTasks { + return fmt.Errorf( + "%w: tasks must contain between 1 and %d entries", + errInvalidBenchmarkManifest, + maximumBenchmarkTasks, + ) + } + if len(manifest.Tasks)*manifest.Replicates*3 > maximumBenchmarkRuns { + return fmt.Errorf( + "%w: run count exceeds %d", + errInvalidBenchmarkManifest, + maximumBenchmarkRuns, + ) + } + if err := validateAnalysisBlockCheckpoints( + manifest.BlockCheckpoints, + len(manifest.Tasks)*manifest.Replicates, + ); err != nil { + return err + } + + if err := validateBenchmarkProvider(ctx, manifest.Provider); err != nil { + return err + } + if err := validateStaticContext(manifest.StaticContext); err != nil { + return err + } + if err := validateFullConfigOverrides(manifest.FullConfigOverrides); err != nil { + return err + } + + taskIDs := map[string]struct{}{} + taskKinds := map[string]struct{}{} + for _, task := range manifest.Tasks { + if _, found := taskIDs[task.TaskID]; found { + return fmt.Errorf( + "%w: duplicate task_id %q", + errInvalidBenchmarkManifest, + task.TaskID, + ) + } + taskIDs[task.TaskID] = struct{}{} + taskKinds[task.Kind] = struct{}{} + + if err := validateBenchmarkTask(ctx, task); err != nil { + return err + } + } + if _, found := taskKinds[benchmarkTaskReal]; !found { + return fmt.Errorf( + "%w: mixed corpus requires at least one real task", + errInvalidBenchmarkManifest, + ) + } + if _, found := taskKinds[benchmarkTaskDiagnostic]; !found { + return fmt.Errorf( + "%w: mixed corpus requires at least one diagnostic task", + errInvalidBenchmarkManifest, + ) + } + + return nil +} + +func validateAnalysisBlockCheckpoints(checkpoints []int, blockCount int) error { + if len(checkpoints) == 0 { + return fmt.Errorf( + "%w: analysis_block_checkpoints is required", + errInvalidBenchmarkManifest, + ) + } + + previous := 0 + for _, checkpoint := range checkpoints { + if checkpoint <= previous || checkpoint > blockCount { + return fmt.Errorf( + "%w: analysis block checkpoints must increase and not exceed %d", + errInvalidBenchmarkManifest, + blockCount, + ) + } + previous = checkpoint + } + if checkpoints[len(checkpoints)-1] != blockCount { + return fmt.Errorf( + "%w: final analysis block checkpoint must equal block count %d", + errInvalidBenchmarkManifest, + blockCount, + ) + } + + return nil +} + +func validateBenchmarkProvider(ctx context.Context, provider BenchmarkProvider) error { + if provider.Kind != ProviderCodex { + return fmt.Errorf( + "%w: benchmark runner currently requires provider codex", + errInvalidBenchmarkManifest, + ) + } + if strings.TrimSpace(provider.Model) == "" || + strings.TrimSpace(provider.ReasoningEffort) == "" { + return fmt.Errorf( + "%w: provider model and reasoning_effort are required", + errInvalidBenchmarkManifest, + ) + } + if err := validateExpectedFileHash( + provider.Executable, + provider.ExecutableSHA256, + "provider executable", + ); err != nil { + return err + } + if !filepath.IsAbs(provider.AuthFile) { + return fmt.Errorf( + "%w: provider auth_file must be absolute", + errInvalidBenchmarkManifest, + ) + } + + info, err := os.Stat(provider.AuthFile) + if err != nil { + return fmt.Errorf("stat provider auth_file: %w", err) + } + if !info.Mode().IsRegular() || info.Mode().Perm()&0o077 != 0 { + return fmt.Errorf( + "%w: provider auth_file must be a private regular file", + errInvalidBenchmarkManifest, + ) + } + + command := safeexec.CommandContext(ctx, provider.Executable, "--version") + output, err := command.Output() + if err != nil { + return fmt.Errorf("query provider runtime version: %w", err) + } + actualVersion := strings.TrimSpace(string(output)) + if actualVersion != strings.TrimSpace(provider.RuntimeVersion) { + return fmt.Errorf( + "%w: provider runtime version is %q, expected %q", + errInvalidBenchmarkManifest, + actualVersion, + provider.RuntimeVersion, + ) + } + + return nil +} + +func validateStaticContext(context BenchmarkStaticContext) error { + if err := validateExpectedFileHash( + context.Path, + context.SHA256, + "static context", + ); err != nil { + return err + } + + info, err := os.Stat(context.Path) + if err != nil { + return fmt.Errorf("stat static context: %w", err) + } + if info.Size() > maximumStaticContextBytes { + return fmt.Errorf( + "%w: static context exceeds %d bytes", + errInvalidBenchmarkManifest, + maximumStaticContextBytes, + ) + } + + return nil +} + +func validateFullConfigOverrides(overrides []string) error { + if len(overrides) == 0 { + return fmt.Errorf( + "%w: full_config_overrides is required", + errInvalidBenchmarkManifest, + ) + } + + forbidden := []string{ + "approval_policy=", + "model=", + "model_reasoning_effort=", + "project_doc_max_bytes=", + "sandbox_mode=", + "sandbox_workspace_write.network_access=", + } + for _, override := range overrides { + cleaned := strings.ReplaceAll(strings.TrimSpace(override), " ", "") + if cleaned == "" || !strings.Contains(cleaned, "=") { + return fmt.Errorf("%w: invalid full config override", errInvalidBenchmarkManifest) + } + for _, prefix := range forbidden { + if strings.HasPrefix(cleaned, prefix) { + return fmt.Errorf( + "%w: full config override changes controlled setting %q", + errInvalidBenchmarkManifest, + override, + ) + } + } + } + + return nil +} + +func validateBenchmarkTask(ctx context.Context, task BenchmarkTask) error { + if !benchmarkIDPattern.MatchString(task.TaskID) { + return fmt.Errorf("%w: invalid task_id %q", errInvalidBenchmarkManifest, task.TaskID) + } + if task.Kind != benchmarkTaskReal && task.Kind != benchmarkTaskDiagnostic { + return fmt.Errorf( + "%w: task %q kind must be real or diagnostic", + errInvalidBenchmarkManifest, + task.TaskID, + ) + } + if !filepath.IsAbs(task.RepositoryPath) || !filepath.IsAbs(task.PromptPath) { + return fmt.Errorf( + "%w: task %q paths must be absolute", + errInvalidBenchmarkManifest, + task.TaskID, + ) + } + if !benchmarkCommitPattern.MatchString(task.Commit) { + return fmt.Errorf( + "%w: task %q commit must be a full SHA-1", + errInvalidBenchmarkManifest, + task.TaskID, + ) + } + if err := validateExpectedFileHash( + task.PromptPath, + task.PromptSHA256, + "task prompt", + ); err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + if !hexSHA256Pattern.MatchString(task.SourceArchiveSHA256) || + !hexSHA256Pattern.MatchString(task.ValidatorSHA256) { + return fmt.Errorf( + "%w: task %q expected hashes are invalid", + errInvalidBenchmarkManifest, + task.TaskID, + ) + } + if err := validateTaskTimeout( + task.AgentTimeout, + 4*time.Hour, + "agent_timeout", + ); err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + if err := validateTaskTimeout( + task.ValidationTimeout, + time.Hour, + "validation_timeout", + ); err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + if err := validateAllowedPaths(task.AllowedPaths); err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + if len(task.Validators) == 0 { + return fmt.Errorf( + "%w: task %q validators are required", + errInvalidBenchmarkManifest, + task.TaskID, + ) + } + + actualSource, err := benchmarkSourceArchiveSHA256( + ctx, + task.RepositoryPath, + task.Commit, + ) + if err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + if actualSource != task.SourceArchiveSHA256 { + return fmt.Errorf( + "%w: task %q source archive hash is %s, expected %s", + errInvalidBenchmarkManifest, + task.TaskID, + actualSource, + task.SourceArchiveSHA256, + ) + } + + actualValidator, err := benchmarkValidatorSHA256(task.Validators) + if err != nil { + return fmt.Errorf("task %q: %w", task.TaskID, err) + } + if actualValidator != task.ValidatorSHA256 { + return fmt.Errorf( + "%w: task %q validator hash is %s, expected %s", + errInvalidBenchmarkManifest, + task.TaskID, + actualValidator, + task.ValidatorSHA256, + ) + } + + return nil +} + +func validateTaskTimeout( + value string, + maximum time.Duration, + field string, +) error { + duration, err := time.ParseDuration(strings.TrimSpace(value)) + if err != nil { + return fmt.Errorf("%w: %s is invalid: %w", errInvalidBenchmarkManifest, field, err) + } + if duration <= 0 || duration > maximum { + return fmt.Errorf( + "%w: %s must be positive and no greater than %s", + errInvalidBenchmarkManifest, + field, + maximum, + ) + } + + return nil +} + +func validateAllowedPaths(paths []string) error { + if len(paths) == 0 { + return fmt.Errorf("%w: allowed_paths is required", errInvalidBenchmarkManifest) + } + + for _, path := range paths { + cleaned := filepath.ToSlash(filepath.Clean(strings.TrimSpace(path))) + if cleaned == "." || filepath.IsAbs(path) || strings.HasPrefix(cleaned, "../") || + cleaned == ".git" || strings.HasPrefix(cleaned, ".git/") { + return fmt.Errorf("%w: unsafe allowed path %q", errInvalidBenchmarkManifest, path) + } + } + + return nil +} + +func validateExpectedFileHash(path, expected, label string) error { + if !filepath.IsAbs(path) { + return fmt.Errorf("%w: %s path must be absolute", errInvalidBenchmarkManifest, label) + } + if !hexSHA256Pattern.MatchString(expected) { + return fmt.Errorf("%w: %s SHA-256 is invalid", errInvalidBenchmarkManifest, label) + } + + actual, err := ledgerFileSHA256(filepath.Clean(path)) + if err != nil { + return fmt.Errorf("hash %s: %w", label, err) + } + if actual != expected { + return fmt.Errorf( + "%w: %s hash is %s, expected %s", + errInvalidBenchmarkManifest, + label, + actual, + expected, + ) + } + + return nil +} + +func benchmarkSourceArchiveSHA256( + ctx context.Context, + repositoryPath string, + commit string, +) (string, error) { + git, err := exec.LookPath("git") + if err != nil { + return "", fmt.Errorf("resolve git for benchmark source: %w", err) + } + + resolved, err := safeexec.CommandContext( + ctx, + git, + "-C", + repositoryPath, + "rev-parse", + commit+"^{commit}", + ).Output() + if err != nil { + return "", fmt.Errorf("resolve benchmark source commit: %w", err) + } + if strings.TrimSpace(string(resolved)) != commit { + return "", fmt.Errorf( + "%w: benchmark commit did not resolve exactly", + errInvalidBenchmarkManifest, + ) + } + + command := safeexec.CommandContext( + ctx, + git, + "-C", + repositoryPath, + "archive", + "--format=tar", + commit, + ) + stdout, err := command.StdoutPipe() + if err != nil { + return "", fmt.Errorf("open benchmark source archive: %w", err) + } + if err = command.Start(); err != nil { + return "", fmt.Errorf("start benchmark source archive: %w", err) + } + + digest := sha256.New() + _, copyErr := io.Copy(digest, stdout) + waitErr := command.Wait() + if err = errors.Join(copyErr, waitErr); err != nil { + return "", fmt.Errorf("hash benchmark source archive: %w", err) + } + + return hex.EncodeToString(digest.Sum(nil)), nil +} + +func benchmarkValidatorSHA256(validators [][]string) (string, error) { + type validatorIdentity struct { + Arguments []string `json:"arguments"` + Executable string `json:"executable"` + ExecutableSHA256 string `json:"executable_sha256"` + } + + identities := make([]validatorIdentity, 0, len(validators)) + for _, validator := range validators { + if len(validator) == 0 || strings.TrimSpace(validator[0]) == "" { + return "", fmt.Errorf("%w: validator command is empty", errInvalidBenchmarkManifest) + } + + executable, err := exec.LookPath(validator[0]) + if err != nil { + return "", fmt.Errorf("resolve validator %q: %w", validator[0], err) + } + executable, err = filepath.EvalSymlinks(executable) + if err != nil { + return "", fmt.Errorf("resolve validator symlink %q: %w", validator[0], err) + } + + digest, err := ledgerFileSHA256(executable) + if err != nil { + return "", fmt.Errorf("hash validator %q: %w", validator[0], err) + } + + identities = append(identities, validatorIdentity{ + Arguments: slices.Clone(validator[1:]), + Executable: executable, + ExecutableSHA256: digest, + }) + } + + payload, err := json.Marshal(identities) + if err != nil { + return "", fmt.Errorf("encode validator identities: %w", err) + } + digest := sha256.Sum256(payload) + + return hex.EncodeToString(digest[:]), nil +} diff --git a/go/internal/tokeneconomy/benchmark_runner.go b/go/internal/tokeneconomy/benchmark_runner.go new file mode 100644 index 00000000..f5e1946e --- /dev/null +++ b/go/internal/tokeneconomy/benchmark_runner.go @@ -0,0 +1,783 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:cyclop,funlen,gocyclo,govet,lll,mnd,noinlineerr,wsl_v5 // Runner stages remain auditable. +package tokeneconomy + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "time" + + "blackcat.ca/coding-ethos/go/internal/safeexec" +) + +const ( + maximumBenchmarkPromptBytes = 1 << 20 + benchmarkProjectDocMaxBytes = 131_072 + benchmarkCompletedStatus = "completed" + codexConfigFlag = "--config" +) + +var errBenchmarkRun = errors.New("token-economy benchmark run error") + +// BenchmarkRunOptions are explicit operator controls for a model-backed run. +type BenchmarkRunOptions struct { + StateRoot string + ApprovedMaxRuns int +} + +// BenchmarkExecution summarizes one immutable scheduled run. +type BenchmarkExecution struct { + RunID string `json:"run_id"` + TaskID string `json:"task_id"` + Arm Arm `json:"arm"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + Accepted bool `json:"accepted"` +} + +// BenchmarkRunSummary reports the bounded work completed by one invocation. +type BenchmarkRunSummary struct { + ExperimentID string `json:"experiment_id"` + ManifestSHA256 string `json:"manifest_sha256"` + ProtocolSHA256 string `json:"protocol_sha256"` + ScheduledRuns int `json:"scheduled_runs"` + PreviouslyRecorded int `json:"previously_recorded"` + NewlyRecorded int `json:"newly_recorded"` + Executions []BenchmarkExecution `json:"executions"` +} + +type benchmarkRunSpec struct { + TaskID string + BlockID string + RunID string + Arm Arm + Replicate int +} + +// RunBenchmark executes pending three-arm blocks up to an explicit run cap. +func RunBenchmark( + ctx context.Context, + prepared PreparedBenchmark, + options BenchmarkRunOptions, +) (BenchmarkRunSummary, error) { + stateRoot, err := validateBenchmarkRunOptions(prepared, options) + if err != nil { + return BenchmarkRunSummary{}, err + } + + store, err := Open(ctx, DefaultDBPath(stateRoot)) + if err != nil { + return BenchmarkRunSummary{}, err + } + defer store.Close() + + err = registerPreparedBenchmark(ctx, store, prepared) + if err != nil { + return BenchmarkRunSummary{}, err + } + + schedule := benchmarkSchedule(prepared.Manifest) + summary := BenchmarkRunSummary{ + ExperimentID: prepared.Manifest.ExperimentID, + ManifestSHA256: prepared.ManifestSHA256, + ProtocolSHA256: prepared.ProtocolSHA256, + ScheduledRuns: len(schedule), + Executions: []BenchmarkExecution{}, + } + + remaining := options.ApprovedMaxRuns + for index := 0; index < len(schedule); index += 3 { + block := schedule[index : index+3] + pending, recorded, blockErr := pendingBenchmarkBlock(ctx, store, block) + if blockErr != nil { + return summary, blockErr + } + summary.PreviouslyRecorded += recorded + if len(pending) == 0 { + continue + } + if len(pending) > remaining { + break + } + + for _, spec := range pending { + run, execution, fatalErr := executeBenchmarkRun( + ctx, + prepared, + spec, + stateRoot, + ) + if fatalErr != nil { + return summary, fatalErr + } + + if err = store.RecordRun(ctx, run); err != nil { + return summary, err + } + summary.Executions = append(summary.Executions, execution) + summary.NewlyRecorded++ + remaining-- + } + } + + return summary, nil +} + +func validateBenchmarkRunOptions( + prepared PreparedBenchmark, + options BenchmarkRunOptions, +) (string, error) { + if options.ApprovedMaxRuns <= 0 || + options.ApprovedMaxRuns > maximumBenchmarkRuns || + options.ApprovedMaxRuns%3 != 0 { + return "", fmt.Errorf( + "%w: approved run count must be a positive multiple of 3 no greater than %d", + errBenchmarkRun, + maximumBenchmarkRuns, + ) + } + if options.ApprovedMaxRuns > prepared.TotalRuns { + return "", fmt.Errorf( + "%w: approved run count %d exceeds manifest schedule %d", + errBenchmarkRun, + options.ApprovedMaxRuns, + prepared.TotalRuns, + ) + } + if !filepath.IsAbs(options.StateRoot) { + return "", fmt.Errorf("%w: state root must be absolute", errBenchmarkRun) + } + + stateRoot := filepath.Clean(options.StateRoot) + err := os.MkdirAll(filepath.Join(stateRoot, ".coding-ethos"), storeDirMode) + if err != nil { + return "", fmt.Errorf("create benchmark state root: %w", err) + } + + return stateRoot, nil +} + +func registerPreparedBenchmark( + ctx context.Context, + store *Store, + prepared PreparedBenchmark, +) error { + manifest := prepared.Manifest + experiment := Experiment{ + ExperimentID: manifest.ExperimentID, + ManifestSHA256: prepared.ManifestSHA256, + ProtocolSHA256: prepared.ProtocolSHA256, + Provider: manifest.Provider.Kind, + Model: manifest.Provider.Model, + RuntimeVersion: manifest.Provider.RuntimeVersion, + CreatedAtUTC: manifest.CreatedAtUTC, + RandomizationSeed: manifest.RandomizationSeed, + Status: "registered", + AnalysisBlockCheckpoints: slices.Clone(manifest.BlockCheckpoints), + Randomized: true, + ArmIsolationVerified: true, + } + if err := store.RecordExperiment(ctx, experiment); err != nil { + return err + } + + for _, task := range manifest.Tasks { + record := Task{ + ExperimentID: manifest.ExperimentID, + TaskID: task.TaskID, + Kind: task.Kind, + SourceSHA256: task.SourceArchiveSHA256, + PromptSHA256: task.PromptSHA256, + ValidatorSHA256: task.ValidatorSHA256, + } + if err := store.RecordTask(ctx, record); err != nil { + return err + } + } + + return nil +} + +func benchmarkSchedule(manifest BenchmarkManifest) []benchmarkRunSpec { + type block struct { + TaskID string + ID string + Score string + Replicate int + } + + blocks := make([]block, 0, len(manifest.Tasks)*manifest.Replicates) + for replicate := 1; replicate <= manifest.Replicates; replicate++ { + layer := make([]block, 0, len(manifest.Tasks)) + for _, task := range manifest.Tasks { + id := fmt.Sprintf("%s:%d", task.TaskID, replicate) + layer = append(layer, block{ + TaskID: task.TaskID, + ID: id, + Score: benchmarkOrderScore(manifest.RandomizationSeed, "block", id), + Replicate: replicate, + }) + } + slices.SortFunc(layer, func(left, right block) int { + return strings.Compare(left.Score+left.ID, right.Score+right.ID) + }) + blocks = append(blocks, layer...) + } + + schedule := make([]benchmarkRunSpec, 0, len(blocks)*3) + for _, current := range blocks { + arms := []Arm{ArmFull, ArmStatic, ArmOff} + slices.SortFunc(arms, func(left, right Arm) int { + leftScore := benchmarkOrderScore( + manifest.RandomizationSeed, + current.ID, + string(left), + ) + rightScore := benchmarkOrderScore( + manifest.RandomizationSeed, + current.ID, + string(right), + ) + + return strings.Compare(leftScore+string(left), rightScore+string(right)) + }) + + for _, arm := range arms { + runDigest := sha256.Sum256([]byte( + manifest.ExperimentID + "\x00" + current.ID + "\x00" + string(arm), + )) + schedule = append(schedule, benchmarkRunSpec{ + TaskID: current.TaskID, + BlockID: current.ID, + RunID: "run-" + hex.EncodeToString(runDigest[:12]), + Arm: arm, + Replicate: current.Replicate, + }) + } + } + + return schedule +} + +func benchmarkOrderScore(seed, scope, value string) string { + digest := sha256.Sum256([]byte(seed + "\x00" + scope + "\x00" + value)) + + return hex.EncodeToString(digest[:]) +} + +func pendingBenchmarkBlock( + ctx context.Context, + store *Store, + block []benchmarkRunSpec, +) ([]benchmarkRunSpec, int, error) { + pending := []benchmarkRunSpec{} + recorded := 0 + for _, spec := range block { + found, err := store.RunRecorded(ctx, spec.RunID) + if err != nil { + return nil, 0, err + } + if found { + recorded++ + } else { + pending = append(pending, spec) + } + } + + return pending, recorded, nil +} + +func executeBenchmarkRun( + ctx context.Context, + prepared PreparedBenchmark, + spec benchmarkRunSpec, + stateRoot string, +) (Run, BenchmarkExecution, error) { + task, found := benchmarkTaskByID(prepared.Manifest.Tasks, spec.TaskID) + if !found { + return Run{}, BenchmarkExecution{}, fmt.Errorf( + "%w: scheduled task %q is missing", + errBenchmarkRun, + spec.TaskID, + ) + } + + err := revalidateBenchmarkInputs(prepared.Manifest, task) + if err != nil { + return Run{}, BenchmarkExecution{}, err + } + + runRoot := filepath.Join( + stateRoot, + ".coding-ethos", + "token-economy-runs", + prepared.Manifest.ExperimentID, + spec.RunID, + ) + workspace, err := prepareBenchmarkWorkspace( + ctx, + task, + filepath.Join(runRoot, "workspace"), + ) + if err != nil { + return Run{}, BenchmarkExecution{}, err + } + + provider := runCodexBenchmark( + ctx, + prepared.Manifest, + task, + spec, + runRoot, + workspace.Path, + ) + validation := validateBenchmarkWorkspace( + ctx, + task, + workspace, + runRoot, + provider.ExitCode == 0 && !provider.TimedOut && provider.EvidenceError == nil, + ) + + receiptSHA, receiptErr := writeBenchmarkValidationReceipt(runRoot, validation) + if receiptErr != nil { + return Run{}, BenchmarkExecution{}, receiptErr + } + + mechanisms := MechanismMetrics{} + if spec.Arm == ArmFull && provider.Ledger.SessionID != "" { + mechanisms, err = readRunMechanisms( + ctx, + filepath.Join(provider.FullStateRoot, ".coding-ethos", "code-intel.duckdb"), + provider.Ledger.SessionID, + ) + if err != nil { + provider.Errors = append(provider.Errors, err.Error()) + } + } + + status := benchmarkCompletedStatus + if provider.TimedOut { + status = "timed_out" + } else if provider.ExitCode != 0 || provider.EvidenceError != nil { + status = "failed" + } + failureReason := strings.Join(provider.Errors, "; ") + + run := Run{ + RunID: spec.RunID, + ExperimentID: prepared.Manifest.ExperimentID, + TaskID: spec.TaskID, + Arm: spec.Arm, + Provider: prepared.Manifest.Provider.Kind, + Model: prepared.Manifest.Provider.Model, + ProviderSessionID: provider.Ledger.SessionID, + LedgerSHA256: provider.Ledger.SourceSHA256, + ValidationReceiptSHA256: receiptSHA, + StartedAtUTC: provider.StartedAtUTC, + CompletedAtUTC: provider.CompletedAtUTC, + Status: status, + FailureReason: failureReason, + Replicate: spec.Replicate, + DurationMilliseconds: provider.Duration.Milliseconds(), + Accepted: validation.Accepted, + SevereGovernanceViolation: len(validation.OutOfScopePaths) > 0, + Usage: provider.Ledger.Usage, + UsageEvents: provider.Ledger.Events, + Mechanisms: mechanisms, + } + execution := BenchmarkExecution{ + RunID: spec.RunID, + TaskID: spec.TaskID, + Arm: spec.Arm, + Status: status, + Error: failureReason, + Accepted: validation.Accepted, + } + + return run, execution, nil +} + +func benchmarkTaskByID(tasks []BenchmarkTask, taskID string) (BenchmarkTask, bool) { + for _, task := range tasks { + if task.TaskID == taskID { + return task, true + } + } + + return BenchmarkTask{}, false +} + +func revalidateBenchmarkInputs(manifest BenchmarkManifest, task BenchmarkTask) error { + for _, input := range []struct { + path string + expected string + label string + }{ + {manifest.Provider.Executable, manifest.Provider.ExecutableSHA256, "provider executable"}, + {manifest.StaticContext.Path, manifest.StaticContext.SHA256, "static context"}, + {task.PromptPath, task.PromptSHA256, "task prompt"}, + } { + if err := validateExpectedFileHash( + input.path, + input.expected, + input.label, + ); err != nil { + return err + } + } + + validatorHash, err := benchmarkValidatorSHA256(task.Validators) + if err != nil { + return err + } + if validatorHash != task.ValidatorSHA256 { + return fmt.Errorf( + "%w: validator identity changed after manifest validation", + errBenchmarkRun, + ) + } + + return nil +} + +type codexBenchmarkOutcome struct { + Ledger Ledger + FullStateRoot string + StartedAtUTC string + CompletedAtUTC string + Errors []string + Duration time.Duration + EvidenceError error + ExitCode int + TimedOut bool +} + +func runCodexBenchmark( + ctx context.Context, + manifest BenchmarkManifest, + task BenchmarkTask, + spec benchmarkRunSpec, + runRoot string, + workspace string, +) codexBenchmarkOutcome { + started := time.Now().UTC() + outcome := codexBenchmarkOutcome{ + StartedAtUTC: started.Format(time.RFC3339Nano), + Errors: []string{}, + ExitCode: -1, + } + + agentTimeout, err := time.ParseDuration(task.AgentTimeout) + if err != nil { + outcome.Errors = append(outcome.Errors, err.Error()) + + return finishCodexOutcome(outcome, started) + } + + codexHome, err := os.MkdirTemp(runRoot, ".codex-home-") + if err != nil { + outcome.Errors = append( + outcome.Errors, + fmt.Sprintf("create isolated Codex home: %v", err), + ) + + return finishCodexOutcome(outcome, started) + } + defer func() { + if removeErr := os.RemoveAll(codexHome); removeErr != nil { + outcome.Errors = append( + outcome.Errors, + "remove isolated Codex home: "+removeErr.Error(), + ) + } + }() + + err = populateBenchmarkCodexHome(codexHome, manifest, spec.Arm) + if err != nil { + outcome.Errors = append(outcome.Errors, err.Error()) + + return finishCodexOutcome(outcome, started) + } + + prompt, err := os.ReadFile(task.PromptPath) + if err != nil { + outcome.Errors = append(outcome.Errors, fmt.Sprintf("read benchmark prompt: %v", err)) + + return finishCodexOutcome(outcome, started) + } + if len(prompt) > maximumBenchmarkPromptBytes { + outcome.Errors = append(outcome.Errors, "benchmark prompt exceeds size limit") + + return finishCodexOutcome(outcome, started) + } + + stdout, stderr, err := createAgentLogs(runRoot) + if err != nil { + outcome.Errors = append(outcome.Errors, err.Error()) + + return finishCodexOutcome(outcome, started) + } + + runContext, cancel := context.WithTimeout(ctx, agentTimeout) + defer cancel() + + arguments := benchmarkCodexArguments(manifest, spec.Arm, workspace) + command := safeexec.CommandContext( + runContext, + manifest.Provider.Executable, + arguments..., + ) + command.Dir = workspace + command.Env = benchmarkAgentEnvironment(codexHome, runRoot, workspace, spec.Arm) + command.Stdin = bytes.NewReader(prompt) + command.Stdout = stdout + command.Stderr = stderr + + runErr := command.Run() + closeErr := errors.Join(stdout.Close(), stderr.Close()) + if closeErr != nil { + outcome.Errors = append(outcome.Errors, "close agent logs: "+closeErr.Error()) + } + outcome.ExitCode = benchmarkProcessExitCode(runErr) + outcome.TimedOut = errors.Is(runContext.Err(), context.DeadlineExceeded) + if runErr != nil { + outcome.Errors = append(outcome.Errors, "provider process: "+runErr.Error()) + } + + ledgerPath, ledgerErr := findCodexSessionLedger(codexHome) + if ledgerErr == nil { + outcome.Ledger, ledgerErr = ParseLedger(ProviderCodex, ledgerPath) + } + if ledgerErr != nil { + outcome.EvidenceError = ledgerErr + outcome.Errors = append(outcome.Errors, "provider ledger: "+ledgerErr.Error()) + } + if spec.Arm == ArmFull { + outcome.FullStateRoot = filepath.Join(runRoot, "coding-ethos-state") + } + + return finishCodexOutcome(outcome, started) +} + +func finishCodexOutcome( + outcome codexBenchmarkOutcome, + started time.Time, +) codexBenchmarkOutcome { + completed := time.Now().UTC() + outcome.CompletedAtUTC = completed.Format(time.RFC3339Nano) + outcome.Duration = completed.Sub(started) + + return outcome +} + +func populateBenchmarkCodexHome( + codexHome string, + manifest BenchmarkManifest, + arm Arm, +) error { + auth, err := os.ReadFile(manifest.Provider.AuthFile) + if err != nil { + return fmt.Errorf("read provider auth file: %w", err) + } + // codexHome is created by os.MkdirTemp under the validated private run root. + if err = os.WriteFile( //nolint:gosec // The destination is not provider-controlled. + filepath.Join(codexHome, "auth.json"), + auth, + 0o600, + ); err != nil { + return fmt.Errorf("write isolated provider auth file: %w", err) + } + + if arm == ArmOff { + return nil + } + + staticContext, err := os.ReadFile(manifest.StaticContext.Path) + if err != nil { + return fmt.Errorf("read benchmark static context: %w", err) + } + if err = os.WriteFile( //nolint:gosec // codexHome is a validated private temp directory. + filepath.Join(codexHome, "AGENTS.md"), + staticContext, + 0o600, + ); err != nil { + return fmt.Errorf("write benchmark static context: %w", err) + } + + return nil +} + +func createAgentLogs(runRoot string) (*os.File, *os.File, error) { + stdout, err := os.OpenFile( + filepath.Join(runRoot, "agent.stdout.jsonl"), + os.O_CREATE|os.O_EXCL|os.O_WRONLY, + 0o600, + ) + if err != nil { + return nil, nil, fmt.Errorf("create agent stdout log: %w", err) + } + + stderr, err := os.OpenFile( + filepath.Join(runRoot, "agent.stderr.log"), + os.O_CREATE|os.O_EXCL|os.O_WRONLY, + 0o600, + ) + if err != nil { + _ = stdout.Close() + + return nil, nil, fmt.Errorf("create agent stderr log: %w", err) + } + + return stdout, stderr, nil +} + +func benchmarkCodexArguments( + manifest BenchmarkManifest, + arm Arm, + workspace string, +) []string { + arguments := []string{ + "exec", + "--json", + "--ignore-user-config", + "--ignore-rules", + "--strict-config", + "--model", + manifest.Provider.Model, + "--sandbox", + "workspace-write", + "--cd", + workspace, + codexConfigFlag, + "approval_policy=\"never\"", + codexConfigFlag, + fmt.Sprintf("model_reasoning_effort=%q", manifest.Provider.ReasoningEffort), + codexConfigFlag, + fmt.Sprintf("project_doc_max_bytes=%d", benchmarkProjectDocMaxBytes), + codexConfigFlag, + "sandbox_workspace_write.network_access=false", + } + if arm == ArmFull { + for _, override := range manifest.FullConfigOverrides { + arguments = append(arguments, codexConfigFlag, override) + } + } + + return append(arguments, "-") +} + +func benchmarkAgentEnvironment( + codexHome string, + runRoot string, + workspace string, + arm Arm, +) []string { + environment := scrubBenchmarkEnvironment(os.Environ()) + environment = append(environment, + "CODEX_HOME="+codexHome, + "GIT_CONFIG_GLOBAL="+os.DevNull, + "GIT_CONFIG_NOSYSTEM=1", + "GIT_TERMINAL_PROMPT=0", + "GH_PROMPT_DISABLED=1", + ) + if arm == ArmFull { + environment = append(environment, + "CODE_ETHOS_STATE_ROOT="+filepath.Join(runRoot, "coding-ethos-state"), + "CODE_ETHOS_CONSUMER_ROOT="+workspace, + ) + } + + return environment +} + +func scrubBenchmarkEnvironment(environment []string) []string { + result := make([]string, 0, len(environment)) + for _, entry := range environment { + key, _, found := strings.Cut(entry, "=") + if !found || !benchmarkAllowedEnvironmentKey(key) { + continue + } + + result = append(result, entry) + } + + return result +} + +func benchmarkAllowedEnvironmentKey(key string) bool { + upper := strings.ToUpper(strings.TrimSpace(key)) + if strings.HasPrefix(upper, "LC_") { + return true + } + + return slices.Contains([]string{ + "COLORTERM", + "LANG", + "LANGUAGE", + "PATH", + "SSL_CERT_DIR", + "SSL_CERT_FILE", + "TEMP", + "TERM", + "TMP", + "TMPDIR", + "TZ", + }, upper) +} + +func benchmarkProcessExitCode(err error) int { + if err == nil { + return 0 + } + + var exitError *exec.ExitError + if errors.As(err, &exitError) { + return exitError.ExitCode() + } + + return -1 +} + +func findCodexSessionLedger(codexHome string) (string, error) { + root := filepath.Join(codexHome, "sessions") + ledgers := []string{} + + err := filepath.WalkDir( + root, + func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".jsonl") { + ledgers = append(ledgers, path) + } + + return nil + }, + ) + if err != nil { + return "", fmt.Errorf("find Codex session ledger: %w", err) + } + if len(ledgers) != 1 { + return "", fmt.Errorf( + "%w: expected one Codex session ledger, found %d", + errBenchmarkRun, + len(ledgers), + ) + } + + return ledgers[0], nil +} diff --git a/go/internal/tokeneconomy/benchmark_source.go b/go/internal/tokeneconomy/benchmark_source.go new file mode 100644 index 00000000..ba03d9a7 --- /dev/null +++ b/go/internal/tokeneconomy/benchmark_source.go @@ -0,0 +1,403 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:cyclop,funlen,gocyclo,lll,mnd,noinlineerr,wsl_v5 // Archive stages remain reviewable. +package tokeneconomy + +import ( + "archive/tar" + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "blackcat.ca/coding-ethos/go/internal/safeexec" +) + +const ( + maximumArchiveFileBytes = 1 << 30 + maximumArchiveTotalBytes = 4 << 30 + workspaceDirMode = 0o700 +) + +var errBenchmarkSource = errors.New("token-economy benchmark source error") + +type preparedWorkspace struct { + Path string + BaselineCommit string +} + +func prepareBenchmarkWorkspace( + ctx context.Context, + task BenchmarkTask, + workspacePath string, +) (preparedWorkspace, error) { + if _, err := os.Stat(workspacePath); err == nil { + return preparedWorkspace{}, fmt.Errorf( + "%w: workspace already exists: %s", + errBenchmarkSource, + workspacePath, + ) + } else if !errors.Is(err, os.ErrNotExist) { + return preparedWorkspace{}, fmt.Errorf("inspect benchmark workspace: %w", err) + } + + err := os.MkdirAll(workspacePath, workspaceDirMode) + if err != nil { + return preparedWorkspace{}, fmt.Errorf("create benchmark workspace: %w", err) + } + + err = extractBenchmarkArchive(ctx, task, workspacePath) + if err != nil { + return preparedWorkspace{}, err + } + + git, err := exec.LookPath("git") + if err != nil { + return preparedWorkspace{}, fmt.Errorf("resolve git for benchmark workspace: %w", err) + } + + for _, arguments := range [][]string{ + {"init", "--quiet", "--initial-branch=benchmark"}, + {"add", "--all"}, + { + "-c", "user.name=Coding Ethos Benchmark", + "-c", "user.email=benchmark@invalid", + "-c", "commit.gpgSign=false", + "commit", "--quiet", "--no-gpg-sign", "-m", "frozen benchmark baseline", + }, + } { + if err = runBenchmarkGit(ctx, git, workspacePath, arguments...); err != nil { + return preparedWorkspace{}, err + } + } + + baseline, err := benchmarkGitOutput(ctx, git, workspacePath, "rev-parse", "HEAD") + if err != nil { + return preparedWorkspace{}, err + } + remotes, err := benchmarkGitOutput(ctx, git, workspacePath, "remote") + if err != nil { + return preparedWorkspace{}, err + } + if strings.TrimSpace(remotes) != "" { + return preparedWorkspace{}, fmt.Errorf( + "%w: workspace unexpectedly has a Git remote", + errBenchmarkSource, + ) + } + + return preparedWorkspace{ + Path: workspacePath, + BaselineCommit: strings.TrimSpace(baseline), + }, nil +} + +func extractBenchmarkArchive( + ctx context.Context, + task BenchmarkTask, + workspacePath string, +) error { + git, err := exec.LookPath("git") + if err != nil { + return fmt.Errorf("resolve git for benchmark archive: %w", err) + } + + command := safeexec.CommandContext( + ctx, + git, + "-C", + task.RepositoryPath, + "archive", + "--format=tar", + task.Commit, + ) + stdout, err := command.StdoutPipe() + if err != nil { + return fmt.Errorf("open benchmark archive stream: %w", err) + } + var stderr bytes.Buffer + command.Stderr = &stderr + + archive, err := os.CreateTemp(filepath.Dir(workspacePath), ".benchmark-source-*.tar") + if err != nil { + return fmt.Errorf("create benchmark archive staging file: %w", err) + } + archivePath := archive.Name() + defer func() { + _ = archive.Close() + _ = os.Remove(archivePath) + }() + + if err = command.Start(); err != nil { + return fmt.Errorf("start benchmark archive extraction: %w", err) + } + + digest := sha256.New() + written, copyErr := io.Copy( + io.MultiWriter(archive, digest), + io.LimitReader(stdout, maximumArchiveTotalBytes+1), + ) + oversized := written > maximumArchiveTotalBytes + var killErr error + if copyErr != nil || oversized { + closePipeErr := stdout.Close() + killErr = command.Process.Kill() + if errors.Is(killErr, os.ErrProcessDone) { + killErr = nil + } + copyErr = errors.Join(copyErr, closePipeErr) + } + waitErr := command.Wait() + if oversized { + return errors.Join( + fmt.Errorf("%w: source archive exceeds size limit", errBenchmarkSource), + copyErr, + killErr, + waitErr, + ) + } + if err = errors.Join(copyErr, killErr, waitErr); err != nil { + return fmt.Errorf( + "capture benchmark source archive: %w: %s", + err, + strings.TrimSpace(stderr.String()), + ) + } + actual := hex.EncodeToString(digest.Sum(nil)) + if actual != task.SourceArchiveSHA256 { + return fmt.Errorf( + "%w: source archive changed: got %s, expected %s", + errBenchmarkSource, + actual, + task.SourceArchiveSHA256, + ) + } + if _, err = archive.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("rewind benchmark source archive: %w", err) + } + if err = extractTarEntries(tar.NewReader(archive), workspacePath); err != nil { + return fmt.Errorf("extract benchmark source archive: %w", err) + } + + return nil +} + +func extractTarEntries(reader *tar.Reader, workspacePath string) error { + var total int64 + + for { + header, err := reader.Next() + if errors.Is(err, io.EOF) { + return nil + } + if err != nil { + return fmt.Errorf("read benchmark archive entry: %w", err) + } + if header.Size < 0 || header.Size > maximumArchiveFileBytes { + return fmt.Errorf( + "%w: archive entry %q has unsafe size", + errBenchmarkSource, + header.Name, + ) + } + total += header.Size + if total > maximumArchiveTotalBytes { + return fmt.Errorf("%w: archive exceeds extraction size limit", errBenchmarkSource) + } + + target, err := safeArchiveTarget(workspacePath, header.Name) + if err != nil { + return err + } + if err = ensureArchivePathSafe(workspacePath, target); err != nil { + return fmt.Errorf("benchmark archive path %q is unsafe: %w", header.Name, err) + } + if err = extractTarEntry(reader, header, workspacePath, target); err != nil { + return err + } + } +} + +func ensureArchivePathSafe(root, target string) error { + relative, err := filepath.Rel(root, target) + if err != nil { + return fmt.Errorf("resolve safe benchmark archive path: %w", err) + } + + current := root + for component := range strings.SplitSeq(relative, string(filepath.Separator)) { + if component == "" || component == "." { + continue + } + current = filepath.Join(current, component) + info, statErr := os.Lstat(current) + if errors.Is(statErr, os.ErrNotExist) { + return nil + } + if statErr != nil { + return fmt.Errorf("inspect benchmark archive path component: %w", statErr) + } + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("%w: path traverses an extracted symlink", errBenchmarkSource) + } + } + + return nil +} + +func safeArchiveTarget(root, name string) (string, error) { + if filepath.IsAbs(name) { + return "", fmt.Errorf( + "%w: archive contains absolute path %q", + errBenchmarkSource, + name, + ) + } + + target := filepath.Clean(filepath.Join(root, filepath.FromSlash(name))) + relative, err := filepath.Rel(root, target) + if err != nil { + return "", fmt.Errorf("resolve benchmark archive path %q: %w", name, err) + } + if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return "", fmt.Errorf( + "%w: archive path escapes workspace: %q", + errBenchmarkSource, + name, + ) + } + + return target, nil +} + +func extractTarEntry( + reader *tar.Reader, + header *tar.Header, + root string, + target string, +) error { + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, 0o755); err != nil { + return fmt.Errorf("create benchmark archive directory %q: %w", header.Name, err) + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return fmt.Errorf("create benchmark archive parent %q: %w", header.Name, err) + } + mode := os.FileMode(0o644) + if header.FileInfo().Mode()&0o111 != 0 { + mode = 0o755 + } + if err := writeArchiveFile(target, reader, mode); err != nil { + return fmt.Errorf("write benchmark archive file %q: %w", header.Name, err) + } + case tar.TypeSymlink: + if err := createSafeArchiveSymlink(root, target, header.Linkname); err != nil { + return fmt.Errorf("create benchmark archive symlink %q: %w", header.Name, err) + } + case tar.TypeXHeader, tar.TypeXGlobalHeader: + // Archive metadata carries no filesystem payload of its own. + return nil + default: + return fmt.Errorf( + "%w: archive entry %q has unsupported type %d", + errBenchmarkSource, + header.Name, + header.Typeflag, + ) + } + + return nil +} + +func writeArchiveFile(path string, reader io.Reader, mode os.FileMode) error { + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode) + if err != nil { + return fmt.Errorf("create benchmark archive file: %w", err) + } + + _, copyErr := io.Copy(file, reader) + closeErr := file.Close() + + return errors.Join(copyErr, closeErr) +} + +func createSafeArchiveSymlink(root, target, link string) error { + if filepath.IsAbs(link) { + return fmt.Errorf("%w: absolute symlink target is forbidden", errBenchmarkSource) + } + + resolved := filepath.Clean( + filepath.Join(filepath.Dir(target), filepath.FromSlash(link)), + ) + relative, err := filepath.Rel(root, resolved) + if err != nil { + return fmt.Errorf("resolve benchmark symlink target: %w", err) + } + if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { + return fmt.Errorf("%w: symlink target escapes the workspace", errBenchmarkSource) + } + if err = os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return fmt.Errorf("create benchmark symlink parent: %w", err) + } + + if err = os.Symlink(filepath.FromSlash(link), target); err != nil { + return fmt.Errorf("create benchmark symlink: %w", err) + } + + return nil +} + +func runBenchmarkGit(ctx context.Context, git, root string, args ...string) error { + command := safeexec.CommandContext(ctx, git, append([]string{"-C", root}, args...)...) + command.Env = benchmarkGitEnvironment() + + output, err := command.CombinedOutput() + if err != nil { + return fmt.Errorf( + "run benchmark Git command %q: %w: %s", + strings.Join(args, " "), + err, + strings.TrimSpace(string(output)), + ) + } + + return nil +} + +func benchmarkGitOutput( + ctx context.Context, + git string, + root string, + args ...string, +) (string, error) { + command := safeexec.CommandContext(ctx, git, append([]string{"-C", root}, args...)...) + command.Env = benchmarkGitEnvironment() + + output, err := command.Output() + if err != nil { + return "", fmt.Errorf("query benchmark Git state: %w", err) + } + + return string(output), nil +} + +func benchmarkGitEnvironment() []string { + environment := scrubBenchmarkEnvironment(os.Environ()) + environment = append(environment, + "GIT_CONFIG_GLOBAL="+os.DevNull, + "GIT_CONFIG_NOSYSTEM=1", + "GIT_TERMINAL_PROMPT=0", + ) + + return environment +} diff --git a/go/internal/tokeneconomy/benchmark_test.go b/go/internal/tokeneconomy/benchmark_test.go new file mode 100644 index 00000000..250c55c2 --- /dev/null +++ b/go/internal/tokeneconomy/benchmark_test.go @@ -0,0 +1,489 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package tokeneconomy + +import ( + "archive/tar" + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "testing" + "time" + + "go.yaml.in/yaml/v3" +) + +func TestExtractBenchmarkArchiveRejectsTraversalThroughSymlink(t *testing.T) { + t.Parallel() + + var payload bytes.Buffer + writer := tar.NewWriter(&payload) + if err := writer.WriteHeader(&tar.Header{ + Name: "link", + Linkname: "real", + Mode: 0o777, + Typeflag: tar.TypeSymlink, + }); err != nil { + t.Fatalf("write symlink header: %v", err) + } + content := []byte("escape\n") + if err := writer.WriteHeader(&tar.Header{ + Name: "link/result.txt", + Mode: 0o644, + Size: int64(len(content)), + Typeflag: tar.TypeReg, + }); err != nil { + t.Fatalf("write regular header: %v", err) + } + if _, err := writer.Write(content); err != nil { + t.Fatalf("write regular payload: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("close test archive: %v", err) + } + + root := t.TempDir() + err := extractTarEntries(tar.NewReader(bytes.NewReader(payload.Bytes())), root) + if err == nil || !strings.Contains(err.Error(), "traverses an extracted symlink") { + t.Fatalf("expected symlink traversal rejection, got %v", err) + } +} + +func TestLoadBenchmarkManifestRejectsUnknownAndDriftedInputs(t *testing.T) { + t.Parallel() + + fixture := newBenchmarkFixture(t) + prepared, err := LoadBenchmarkManifest(context.Background(), fixture.manifestPath) + if err != nil { + t.Fatalf("load benchmark manifest: %v", err) + } + if prepared.TotalRuns != 6 || prepared.Manifest.ExperimentID != fixture.experimentID { + t.Fatalf("unexpected prepared benchmark: %#v", prepared) + } + + unknownPath := filepath.Join(fixture.root, "unknown.yaml") + payload, err := os.ReadFile(fixture.manifestPath) + if err != nil { + t.Fatalf("read manifest: %v", err) + } + payload = append(payload, []byte("unknown_contract_field: true\n")...) + if err = os.WriteFile(unknownPath, payload, 0o600); err != nil { + t.Fatalf("write unknown-field manifest: %v", err) + } + if _, err = LoadBenchmarkManifest(context.Background(), unknownPath); err == nil || + !strings.Contains(err.Error(), "field unknown_contract_field not found") { + t.Fatalf("expected unknown field failure, got %v", err) + } + + multiplePath := filepath.Join(fixture.root, "multiple.yaml") + payload = append( + payload[:len(payload)-len("unknown_contract_field: true\n")], + []byte("---\n{}\n")...) + if err = os.WriteFile(multiplePath, payload, 0o600); err != nil { + t.Fatalf("write multiple-document manifest: %v", err) + } + if _, err = LoadBenchmarkManifest(context.Background(), multiplePath); err == nil || + !strings.Contains(err.Error(), "multiple YAML documents") { + t.Fatalf("expected multiple document failure, got %v", err) + } + + if err = os.WriteFile(fixture.promptPath, []byte("changed\n"), 0o600); err != nil { + t.Fatalf("drift prompt: %v", err) + } + if _, err = LoadBenchmarkManifest( + context.Background(), + fixture.manifestPath, + ); err == nil || + !strings.Contains(err.Error(), "task prompt hash") { + t.Fatalf("expected prompt drift failure, got %v", err) + } +} + +func TestFreezeBenchmarkManifestCreatesVerifiedManifestWithoutOverwrite(t *testing.T) { + t.Parallel() + + fixture := newBenchmarkFixture(t) + output := filepath.Join(fixture.root, "frozen", "benchmark.yaml") + prepared, err := FreezeBenchmarkManifest( + context.Background(), + fixture.manifestPath, + output, + ) + if err != nil { + t.Fatalf("freeze benchmark manifest: %v", err) + } + if prepared.ManifestPath != output || prepared.TotalRuns != 6 { + t.Fatalf("unexpected frozen manifest: %#v", prepared) + } + info, err := os.Stat(output) + if err != nil { + t.Fatalf("stat frozen manifest: %v", err) + } + if info.Mode().Perm() != storeFileMode { + t.Fatalf("frozen manifest mode = %o, want %o", info.Mode().Perm(), storeFileMode) + } + + if _, err = FreezeBenchmarkManifest( + context.Background(), + fixture.manifestPath, + output, + ); err == nil || !strings.Contains(err.Error(), "file exists") { + t.Fatalf("expected create-new failure, got %v", err) + } +} + +func TestBenchmarkScheduleIsDeterministicAndArmComplete(t *testing.T) { + t.Parallel() + + manifest := BenchmarkManifest{ + ExperimentID: "schedule-test", + RandomizationSeed: "fixed-seed", + Replicates: 2, + BlockCheckpoints: []int{2, 4}, + Tasks: []BenchmarkTask{ + {TaskID: "one"}, + {TaskID: "two"}, + }, + } + + first := benchmarkSchedule(manifest) + second := benchmarkSchedule(manifest) + if !slices.Equal(first, second) { + t.Fatalf("schedule is not deterministic:\n%#v\n%#v", first, second) + } + if len(first) != 12 { + t.Fatalf("schedule length = %d, want 12", len(first)) + } + for index, run := range first { + wantReplicate := index/(len(manifest.Tasks)*3) + 1 + if run.Replicate != wantReplicate { + t.Fatalf( + "schedule run %d replicate = %d, want layer %d", + index, + run.Replicate, + wantReplicate, + ) + } + } + for index := 0; index < len(first); index += 3 { + arms := []Arm{first[index].Arm, first[index+1].Arm, first[index+2].Arm} + slices.Sort(arms) + if !slices.Equal(arms, []Arm{ArmFull, ArmOff, ArmStatic}) { + t.Fatalf("block %d arms = %#v", index/3, arms) + } + } +} + +func TestValidateAnalysisBlockCheckpointsCoversEveryReplicate(t *testing.T) { + t.Parallel() + + if err := validateAnalysisBlockCheckpoints([]int{10, 20}, 20); err != nil { + t.Fatalf("valid analysis block checkpoints: %v", err) + } + if err := validateAnalysisBlockCheckpoints([]int{10}, 20); err == nil || + !strings.Contains(err.Error(), "must equal block count 20") { + t.Fatalf("expected incomplete checkpoint plan failure, got %v", err) + } +} + +func TestBenchmarkEnvironmentUsesBenignAllowlist(t *testing.T) { + t.Parallel() + + clean := scrubBenchmarkEnvironment([]string{ + "PATH=/usr/bin", + "LANG=C.UTF-8", + "LC_ALL=C.UTF-8", + "AWS_ACCESS_KEY_ID=must-not-leak", + "HTTP_PROXY=http://credential@proxy.invalid", + "HOME=/private/home", + "CODE_ETHOS_STATE_ROOT=/ambient/state", + }) + if !slices.Equal(clean, []string{ + "PATH=/usr/bin", + "LANG=C.UTF-8", + "LC_ALL=C.UTF-8", + }) { + t.Fatalf("scrubbed benchmark environment = %#v", clean) + } +} + +func TestBenchmarkCodexArgumentsDisableSandboxNetwork(t *testing.T) { + t.Parallel() + + arguments := benchmarkCodexArguments(BenchmarkManifest{ + Provider: BenchmarkProvider{ + Model: "fixed-model", + ReasoningEffort: "high", + }, + }, ArmOff, "/frozen/workspace") + if !slices.Contains(arguments, "sandbox_workspace_write.network_access=false") { + t.Fatalf("Codex benchmark arguments do not disable sandbox network: %#v", arguments) + } +} + +func TestRunBenchmarkUsesIsolatedArmsAndResumesWithoutReplacement(t *testing.T) { + fixture := newBenchmarkFixture(t) + prepared, err := LoadBenchmarkManifest(context.Background(), fixture.manifestPath) + if err != nil { + t.Fatalf("load benchmark manifest: %v", err) + } + + stateRoot := filepath.Join(fixture.root, "private-state") + options := BenchmarkRunOptions{StateRoot: stateRoot, ApprovedMaxRuns: 6} + first, err := RunBenchmark(context.Background(), prepared, options) + if err != nil { + t.Fatalf("run fake benchmark: %v", err) + } + if first.NewlyRecorded != 6 || first.PreviouslyRecorded != 0 { + t.Fatalf("unexpected first run summary: %#v", first) + } + + store, err := Open(context.Background(), DefaultDBPath(stateRoot)) + if err != nil { + t.Fatalf("open benchmark evidence: %v", err) + } + runs, err := store.Runs(context.Background(), fixture.experimentID) + closeErr := store.Close() + if err != nil || closeErr != nil { + t.Fatalf("read benchmark evidence: runs=%v close=%v", err, closeErr) + } + if len(runs) != 6 { + t.Fatalf("recorded runs = %d, want 6", len(runs)) + } + + for _, run := range runs { + if !run.Accepted || run.Status != "completed" || run.Usage.TotalTokens != 15 { + t.Fatalf("unexpected recorded run: %#v", run) + } + resultPath := filepath.Join( + stateRoot, + ".coding-ethos", + "token-economy-runs", + fixture.experimentID, + run.RunID, + "workspace", + "result.txt", + ) + payload, readErr := os.ReadFile(resultPath) + if readErr != nil { + t.Fatalf("read %s arm result: %v", run.Arm, readErr) + } + want := "context static\n" + if run.Arm == ArmFull { + want = "context full\n" + } else if run.Arm == ArmOff { + want = "off static\n" + } + if string(payload) != want { + t.Fatalf("%s arm isolation marker = %q, want %q", run.Arm, payload, want) + } + } + + second, err := RunBenchmark(context.Background(), prepared, options) + if err != nil { + t.Fatalf("resume fake benchmark: %v", err) + } + if second.NewlyRecorded != 0 || second.PreviouslyRecorded != 6 || + len(second.Executions) != 0 { + t.Fatalf("unexpected resumed summary: %#v", second) + } +} + +type benchmarkFixture struct { + root string + experimentID string + manifestPath string + promptPath string +} + +func newBenchmarkFixture(t *testing.T) benchmarkFixture { + t.Helper() + + root := t.TempDir() + repository := filepath.Join(root, "source") + if err := os.Mkdir(repository, 0o700); err != nil { + t.Fatalf("create source repository: %v", err) + } + writeTestFile(t, filepath.Join(repository, "baseline.txt"), "baseline\n", 0o600) + runTestGit(t, repository, "init", "--quiet", "--initial-branch=main") + runTestGit(t, repository, "add", "--all") + runTestGit( + t, + repository, + "-c", + "user.name=Benchmark Test", + "-c", + "user.email=benchmark@invalid", + "-c", + "commit.gpgSign=false", + "commit", + "--quiet", + "--no-gpg-sign", + "-m", + "baseline", + ) + commit := strings.TrimSpace(runTestGit(t, repository, "rev-parse", "HEAD")) + + providerPath := filepath.Join(root, "fake-codex") + writeTestFile(t, providerPath, fakeCodexScript, 0o700) + authPath := filepath.Join(root, "auth.json") + writeTestFile(t, authPath, "{}\n", 0o600) + staticPath := filepath.Join(root, "managed-agents.md") + writeTestFile(t, staticPath, "# Managed Coding Ethos benchmark context\n", 0o600) + promptPath := filepath.Join(root, "prompt.txt") + writeTestFile(t, promptPath, "Create result.txt.\n", 0o600) + validatorPath := filepath.Join(root, "validate-result") + writeTestFile(t, validatorPath, fakeValidatorScript, 0o700) + + sourceHash, err := benchmarkSourceArchiveSHA256( + context.Background(), + repository, + commit, + ) + if err != nil { + t.Fatalf("hash source archive: %v", err) + } + validators := [][]string{{validatorPath}} + validatorHash, err := benchmarkValidatorSHA256(validators) + if err != nil { + t.Fatalf("hash validator: %v", err) + } + + manifest := BenchmarkManifest{ + ExperimentID: "fake-controlled-experiment", + CreatedAtUTC: time.Date(2026, 8, 28, 12, 0, 0, 0, time.UTC). + Format(time.RFC3339Nano), + RandomizationSeed: "fixture-seed", + StaticContext: BenchmarkStaticContext{ + Path: staticPath, + SHA256: fileSHA256(t, staticPath), + }, + Provider: BenchmarkProvider{ + Kind: ProviderCodex, + Executable: providerPath, + ExecutableSHA256: fileSHA256(t, providerPath), + RuntimeVersion: "fake-codex 1.0", + Model: "fake-model", + ReasoningEffort: "medium", + AuthFile: authPath, + }, + Tasks: []BenchmarkTask{ + { + TaskID: "diagnostic-one", + Kind: "diagnostic", + RepositoryPath: repository, + Commit: commit, + SourceArchiveSHA256: sourceHash, + PromptPath: promptPath, + PromptSHA256: fileSHA256(t, promptPath), + ValidatorSHA256: validatorHash, + AgentTimeout: "30s", + ValidationTimeout: "30s", + AllowedPaths: []string{"result.txt"}, + Validators: validators, + }, + { + TaskID: "real-one", + Kind: "real", + RepositoryPath: repository, + Commit: commit, + SourceArchiveSHA256: sourceHash, + PromptPath: promptPath, + PromptSHA256: fileSHA256(t, promptPath), + ValidatorSHA256: validatorHash, + AgentTimeout: "30s", + ValidationTimeout: "30s", + AllowedPaths: []string{"result.txt"}, + Validators: validators, + }, + }, + FullConfigOverrides: []string{"features.code_intel=true"}, + SchemaVersion: BenchmarkManifestVersion, + Replicates: 1, + BlockCheckpoints: []int{2}, + } + payload, err := yaml.Marshal(manifest) + if err != nil { + t.Fatalf("encode manifest: %v", err) + } + manifestPath := filepath.Join(root, "benchmark.yaml") + if err = os.WriteFile(manifestPath, payload, 0o600); err != nil { + t.Fatalf("write manifest: %v", err) + } + + return benchmarkFixture{ + root: root, + experimentID: manifest.ExperimentID, + manifestPath: manifestPath, + promptPath: promptPath, + } +} + +func writeTestFile(t *testing.T, path, payload string, mode os.FileMode) { + t.Helper() + if err := os.WriteFile(path, []byte(payload), mode); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +func fileSHA256(t *testing.T, path string) string { + t.Helper() + payload, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + digest := sha256.Sum256(payload) + + return hex.EncodeToString(digest[:]) +} + +func runTestGit(t *testing.T, root string, arguments ...string) string { + t.Helper() + command := exec.CommandContext( //nolint:gosec // Test arguments are fixed local fixtures. + context.Background(), + "git", + append([]string{"-C", root}, arguments...)..., + ) + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("git %s: %v: %s", strings.Join(arguments, " "), err, output) + } + + return string(output) +} + +const fakeCodexScript = `#!/bin/sh +set -eu +if [ "${1:-}" = "--version" ]; then + printf '%s\n' 'fake-codex 1.0' + exit 0 +fi +context=off +if [ -f "$CODEX_HOME/AGENTS.md" ]; then + context=context +fi +dynamic=static +if [ -n "${CODE_ETHOS_STATE_ROOT:-}" ]; then + dynamic=full +fi +printf '%s %s\n' "$context" "$dynamic" > result.txt +mkdir -p "$CODEX_HOME/sessions/2026/08/28" +printf '%s\n' \ + '{"type":"session_meta","payload":{"id":"fake-session"}}' \ + '{"type":"turn_context","payload":{"model":"fake-model"}}' \ + '{"type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":10,"output_tokens":5,"total_tokens":15}}}}' \ + > "$CODEX_HOME/sessions/2026/08/28/rollout.jsonl" +printf '%s\n' '{"type":"turn.completed"}' +` + +const fakeValidatorScript = `#!/bin/sh +set -eu +test -f result.txt +` diff --git a/go/internal/tokeneconomy/benchmark_validation.go b/go/internal/tokeneconomy/benchmark_validation.go new file mode 100644 index 00000000..0509fffa --- /dev/null +++ b/go/internal/tokeneconomy/benchmark_validation.go @@ -0,0 +1,327 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:govet,lll,mnd,noinlineerr,wsl_v5 // Receipt fields and ordered validation aid audit review. +package tokeneconomy + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "time" + + "blackcat.ca/coding-ethos/go/internal/safeexec" +) + +const validationReceiptName = "validation-receipt.json" + +type benchmarkValidatorResult struct { + Arguments []string `json:"arguments"` + Executable string `json:"executable"` + StdoutSHA256 string `json:"stdout_sha256"` + StderrSHA256 string `json:"stderr_sha256"` + Error string `json:"error,omitempty"` + ExitCode int `json:"exit_code"` + TimedOut bool `json:"timed_out"` + Passed bool `json:"passed"` +} + +type benchmarkValidationReceipt struct { + TaskID string `json:"task_id"` + BaselineCommit string `json:"baseline_commit"` + CompletedAtUTC string `json:"completed_at_utc"` + ChangedPaths []string `json:"changed_paths"` + OutOfScopePaths []string `json:"out_of_scope_paths"` + Validators []benchmarkValidatorResult `json:"validators"` + AgentSucceeded bool `json:"agent_succeeded"` + Accepted bool `json:"accepted"` +} + +func validateBenchmarkWorkspace( + ctx context.Context, + task BenchmarkTask, + workspace preparedWorkspace, + runRoot string, + agentSucceeded bool, +) benchmarkValidationReceipt { + receipt := benchmarkValidationReceipt{ + TaskID: task.TaskID, + BaselineCommit: workspace.BaselineCommit, + CompletedAtUTC: time.Now().UTC().Format(time.RFC3339Nano), + ChangedPaths: []string{}, + OutOfScopePaths: []string{}, + Validators: []benchmarkValidatorResult{}, + AgentSucceeded: agentSucceeded, + } + + changed, err := benchmarkChangedPaths(ctx, workspace) + if err != nil { + receipt.Validators = append(receipt.Validators, benchmarkValidatorResult{ + Error: "inspect changed paths: " + err.Error(), + ExitCode: -1, + }) + + return receipt + } + receipt.ChangedPaths = changed + for _, path := range changed { + if !benchmarkPathAllowed(path, task.AllowedPaths) { + receipt.OutOfScopePaths = append(receipt.OutOfScopePaths, path) + } + } + + validationTimeout, err := time.ParseDuration(task.ValidationTimeout) + if err != nil { + receipt.Validators = append(receipt.Validators, benchmarkValidatorResult{ + Error: "parse validation timeout: " + err.Error(), + ExitCode: -1, + }) + + return receipt + } + + validationContext, cancel := context.WithTimeout(ctx, validationTimeout) + defer cancel() + + allPassed := true + for index, validator := range task.Validators { + result := runBenchmarkValidator( + validationContext, + workspace.Path, + runRoot, + index, + validator, + ) + receipt.Validators = append(receipt.Validators, result) + allPassed = allPassed && result.Passed + } + + receipt.Accepted = agentSucceeded && allPassed && len(receipt.OutOfScopePaths) == 0 + + return receipt +} + +func benchmarkChangedPaths( + ctx context.Context, + workspace preparedWorkspace, +) ([]string, error) { + git, err := exec.LookPath("git") + if err != nil { + return nil, fmt.Errorf("resolve git: %w", err) + } + + tracked, err := benchmarkGitBytes( + ctx, + git, + workspace.Path, + "diff", + "--name-only", + "-z", + workspace.BaselineCommit, + "--", + ) + if err != nil { + return nil, err + } + untracked, err := benchmarkGitBytes( + ctx, + git, + workspace.Path, + "ls-files", + "--others", + "--exclude-standard", + "-z", + ) + if err != nil { + return nil, err + } + + unique := map[string]struct{}{} + for _, path := range append(splitNullPaths(tracked), splitNullPaths(untracked)...) { + cleaned := filepath.ToSlash(filepath.Clean(path)) + if cleaned != "." { + unique[cleaned] = struct{}{} + } + } + + paths := make([]string, 0, len(unique)) + for path := range unique { + paths = append(paths, path) + } + slices.Sort(paths) + + return paths, nil +} + +func benchmarkGitBytes( + ctx context.Context, + git string, + root string, + arguments ...string, +) ([]byte, error) { + command := safeexec.CommandContext( + ctx, + git, + append([]string{"-C", root}, arguments...)..., + ) + command.Env = benchmarkGitEnvironment() + output, err := command.Output() + if err != nil { + return nil, fmt.Errorf("query benchmark Git paths: %w", err) + } + + return output, nil +} + +func splitNullPaths(payload []byte) []string { + parts := strings.Split(string(payload), "\x00") + result := make([]string, 0, len(parts)) + for _, part := range parts { + if part != "" { + result = append(result, part) + } + } + + return result +} + +func benchmarkPathAllowed(path string, allowed []string) bool { + path = filepath.ToSlash(filepath.Clean(path)) + for _, candidate := range allowed { + candidate = filepath.ToSlash(filepath.Clean(strings.TrimSpace(candidate))) + if path == candidate || strings.HasPrefix(path, candidate+"/") { + return true + } + } + + return false +} + +func runBenchmarkValidator( + ctx context.Context, + workspace string, + runRoot string, + index int, + validator []string, +) benchmarkValidatorResult { + result := benchmarkValidatorResult{ + ExitCode: -1, + } + if len(validator) == 0 { + result.Error = "validator command is empty" + + return result + } + result.Arguments = slices.Clone(validator[1:]) + + executable, err := resolveBenchmarkExecutable(validator[0]) + if err != nil { + result.Error = err.Error() + + return result + } + result.Executable = executable + + stdoutPath := filepath.Join(runRoot, fmt.Sprintf("validator-%02d.stdout.log", index+1)) + stderrPath := filepath.Join(runRoot, fmt.Sprintf("validator-%02d.stderr.log", index+1)) + stdout, stderr, err := createPrivateLogPair(stdoutPath, stderrPath) + if err != nil { + result.Error = err.Error() + + return result + } + + command := safeexec.CommandContext(ctx, executable, validator[1:]...) + command.Dir = workspace + command.Env = benchmarkGitEnvironment() + command.Stdout = stdout + command.Stderr = stderr + runErr := command.Run() + closeErr := errors.Join(stdout.Close(), stderr.Close()) + + result.ExitCode = benchmarkProcessExitCode(runErr) + result.TimedOut = errors.Is(ctx.Err(), context.DeadlineExceeded) + stdoutDigest, stdoutHashErr := ledgerFileSHA256(stdoutPath) + stderrDigest, stderrHashErr := ledgerFileSHA256(stderrPath) + result.StdoutSHA256 = stdoutDigest + result.StderrSHA256 = stderrDigest + if runErr != nil { + result.Error = runErr.Error() + } + if closeErr != nil { + result.Error = strings.TrimSpace( + result.Error + "; close validator logs: " + closeErr.Error(), + ) + } + if hashErr := errors.Join(stdoutHashErr, stderrHashErr); hashErr != nil { + result.Error = strings.TrimSpace( + result.Error + "; hash validator logs: " + hashErr.Error(), + ) + } + result.Passed = runErr == nil && closeErr == nil && stdoutHashErr == nil && + stderrHashErr == nil && !result.TimedOut + + return result +} + +func resolveBenchmarkExecutable(name string) (string, error) { + executable, err := exec.LookPath(name) + if err != nil { + return "", fmt.Errorf("resolve validator %q: %w", name, err) + } + executable, err = filepath.EvalSymlinks(executable) + if err != nil { + return "", fmt.Errorf("resolve validator symlink %q: %w", name, err) + } + + return executable, nil +} + +func createPrivateLogPair(stdoutPath, stderrPath string) (*os.File, *os.File, error) { + stdout, err := os.OpenFile(stdoutPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return nil, nil, fmt.Errorf("create validator stdout log: %w", err) + } + stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + _ = stdout.Close() + + return nil, nil, fmt.Errorf("create validator stderr log: %w", err) + } + + return stdout, stderr, nil +} + +func writeBenchmarkValidationReceipt( + runRoot string, + receipt benchmarkValidationReceipt, +) (string, error) { + payload, err := json.MarshalIndent(receipt, "", " ") + if err != nil { + return "", fmt.Errorf("encode benchmark validation receipt: %w", err) + } + payload = append(payload, '\n') + + path := filepath.Join(runRoot, validationReceiptName) + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) + if err != nil { + return "", fmt.Errorf("create benchmark validation receipt: %w", err) + } + _, writeErr := file.Write(payload) + closeErr := file.Close() + if err = errors.Join(writeErr, closeErr); err != nil { + return "", fmt.Errorf("write benchmark validation receipt: %w", err) + } + + digest := sha256.Sum256(payload) + + return hex.EncodeToString(digest[:]), nil +} diff --git a/go/internal/tokeneconomy/historical.go b/go/internal/tokeneconomy/historical.go new file mode 100644 index 00000000..33e50ee3 --- /dev/null +++ b/go/internal/tokeneconomy/historical.go @@ -0,0 +1,600 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package tokeneconomy + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "time" + + _ "github.com/duckdb/duckdb-go/v2" +) + +const ( + historicalAggregationContract = "canonical_source_path_ascending,event_id_unique_v1" + historicalWindowContract = "proxy_events.recorded_at_utc:[from,to)" + percentageScale = 100.0 +) + +var errTokenEconomyReport = errors.New("token-economy report error") + +type historicalWindow struct { + from time.Time + to time.Time + fromText string + toText string +} + +type preparedHistoricalSource struct { + info os.FileInfo + evidence HistoricalSource +} + +type historicalEventIdentity struct { + eventID string + sessionID string + provider Provider +} + +type historicalIdentityOwner struct { + sourcePath string + provider Provider +} + +type historicalAggregate struct { + providers map[Provider]struct{} + eventOwners map[string]string + sessionOwners map[string]historicalIdentityOwner + metrics HistoricalMetrics +} + +// HistoricalReport measures deterministic gross context reduction across +// read-only code-intel stores for an explicit half-open UTC window. +func HistoricalReport( + ctx context.Context, + options HistoricalReportOptions, + now time.Time, +) (Report, error) { + window, sources, err := prepareHistoricalReport(options) + if err != nil { + return Report{}, err + } + + aggregate := newHistoricalAggregate() + + for _, source := range sources { + metrics, identities, readErr := readHistoricalSource( + ctx, + source.evidence.Path, + window, + ) + if readErr != nil { + return Report{}, readErr + } + + mergeErr := aggregate.merge( + source.evidence.Path, + metrics, + identities, + ) + if mergeErr != nil { + return Report{}, mergeErr + } + } + + sourceEvidence, err := verifyHistoricalSources(sources) + if err != nil { + return Report{}, err + } + + metrics, providers := aggregate.finish(window, sourceEvidence) + + conclusion := ConclusionObservational + if metrics.TransformedEvents == 0 { + conclusion = ConclusionInconclusive + } + + return Report{ + Kind: ReportKind, + Cohort: "historical", + GeneratedAtUTC: reportTimestamp(now), + Conclusion: conclusion, + SchemaVersion: SchemaVersion, + Causal: false, + Coverage: Coverage{ + Providers: providers, + TaskCount: 0, + RunCount: metrics.ProxySessions, + Reasons: []string{ + "no disabled or static control arm is present", + "proxy transform tokens use the Coding Ethos estimator", + "provider-native end-to-end usage is not joined to accepted outcomes", + }, + }, + Historical: &metrics, + Provenance: map[string]string{ + "historical_aggregation": historicalAggregationContract, + "historical_window": historicalWindowContract, + }, + }, nil +} + +func prepareHistoricalReport( + options HistoricalReportOptions, +) (historicalWindow, []preparedHistoricalSource, error) { + window, err := parseHistoricalWindow(options.FromUTC, options.ToUTC) + if err != nil { + return historicalWindow{}, nil, err + } + + if len(options.DatabasePaths) == 0 { + return historicalWindow{}, nil, fmt.Errorf( + "%w: at least one code-intel database path is required", + errTokenEconomyReport, + ) + } + + sources := make([]preparedHistoricalSource, 0, len(options.DatabasePaths)) + for _, path := range options.DatabasePaths { + source, sourceErr := prepareHistoricalSource(path) + if sourceErr != nil { + return historicalWindow{}, nil, sourceErr + } + + sources = append(sources, source) + } + + slices.SortFunc(sources, func(left, right preparedHistoricalSource) int { + return strings.Compare(left.evidence.Path, right.evidence.Path) + }) + + err = validateDistinctHistoricalSources(sources) + if err != nil { + return historicalWindow{}, nil, err + } + + for index := range sources { + digest, hashErr := ledgerFileSHA256(sources[index].evidence.Path) + if hashErr != nil { + return historicalWindow{}, nil, fmt.Errorf( + "hash historical source %q before reporting: %w", + sources[index].evidence.Path, + hashErr, + ) + } + + sources[index].evidence.SHA256Before = digest + } + + return window, sources, nil +} + +func parseHistoricalWindow(fromValue, toValue string) (historicalWindow, error) { + fromValue = strings.TrimSpace(fromValue) + + toValue = strings.TrimSpace(toValue) + if fromValue == "" || toValue == "" { + return historicalWindow{}, fmt.Errorf( + "%w: from_utc and to_utc are required", + errTokenEconomyReport, + ) + } + + fromTime, err := time.Parse(time.RFC3339Nano, fromValue) + if err != nil { + return historicalWindow{}, fmt.Errorf( + "%w: parse from_utc %q as RFC3339: %w", + errTokenEconomyReport, + fromValue, + err, + ) + } + + toTime, err := time.Parse(time.RFC3339Nano, toValue) + if err != nil { + return historicalWindow{}, fmt.Errorf( + "%w: parse to_utc %q as RFC3339: %w", + errTokenEconomyReport, + toValue, + err, + ) + } + + fromTime = fromTime.UTC() + toTime = toTime.UTC() + + if !fromTime.Before(toTime) { + return historicalWindow{}, fmt.Errorf( + "%w: from_utc must be before to_utc", + errTokenEconomyReport, + ) + } + + return historicalWindow{ + from: fromTime, + to: toTime, + fromText: fromTime.Format(time.RFC3339Nano), + toText: toTime.Format(time.RFC3339Nano), + }, nil +} + +func prepareHistoricalSource(path string) (preparedHistoricalSource, error) { + path = strings.TrimSpace(path) + if path == "" { + return preparedHistoricalSource{}, fmt.Errorf( + "%w: code-intel database path is empty", + errTokenEconomyReport, + ) + } + + absolute, err := filepath.Abs(path) + if err != nil { + return preparedHistoricalSource{}, fmt.Errorf( + "resolve historical source %q: %w", + path, + err, + ) + } + + canonical, err := filepath.EvalSymlinks(absolute) + if err != nil { + return preparedHistoricalSource{}, fmt.Errorf( + "canonicalize historical source %q: %w", + path, + err, + ) + } + + canonical = filepath.Clean(canonical) + + info, err := os.Stat(canonical) + if err != nil { + return preparedHistoricalSource{}, fmt.Errorf( + "stat historical source %q: %w", + canonical, + err, + ) + } + + if !info.Mode().IsRegular() { + return preparedHistoricalSource{}, fmt.Errorf( + "%w: historical source is not a regular file: %s", + errTokenEconomyReport, + canonical, + ) + } + + return preparedHistoricalSource{ + evidence: HistoricalSource{Path: canonical}, + info: info, + }, nil +} + +func validateDistinctHistoricalSources(sources []preparedHistoricalSource) error { + for index := range sources { + for prior := range index { + if sources[index].evidence.Path == sources[prior].evidence.Path || + os.SameFile(sources[index].info, sources[prior].info) { + return fmt.Errorf( + "%w: historical sources %q and %q identify the same file", + errTokenEconomyReport, + sources[prior].evidence.Path, + sources[index].evidence.Path, + ) + } + } + } + + return nil +} + +func readHistoricalSource( + ctx context.Context, + path string, + window historicalWindow, +) (HistoricalMetrics, []historicalEventIdentity, error) { + database, err := sql.Open("duckdb", path+"?access_mode=READ_ONLY") + if err != nil { + return HistoricalMetrics{}, nil, fmt.Errorf( + "open historical source %q read-only: %w", + path, + err, + ) + } + + database.SetMaxOpenConns(1) + database.SetMaxIdleConns(1) + + var identities []historicalEventIdentity + + metrics, readErr := queryHistoricalMetrics(ctx, database, window) + if readErr == nil { + identities, readErr = queryHistoricalEventIdentities(ctx, database, window) + } + + closeErr := database.Close() + + err = errors.Join(readErr, closeErr) + if err != nil { + return HistoricalMetrics{}, nil, fmt.Errorf( + "read historical source %q: %w", + path, + err, + ) + } + + return metrics, identities, nil +} + +func queryHistoricalMetrics( + ctx context.Context, + database *sql.DB, + window historicalWindow, +) (HistoricalMetrics, error) { + err := validateHistoricalEventTimestamps(ctx, database) + if err != nil { + return HistoricalMetrics{}, err + } + + metrics := HistoricalMetrics{} + + err = database.QueryRowContext( + ctx, + `WITH first_transform AS ( + SELECT event_id, input_tokens, + ROW_NUMBER() OVER (PARTITION BY event_id ORDER BY ordinal) AS rank + FROM proxy_transforms + ) + SELECT + COALESCE(SUM(first_transform.input_tokens), 0), + COALESCE(SUM(event.output_tokens), 0), + COUNT(*), + COALESCE(SUM(CASE WHEN first_transform.input_tokens > event.output_tokens + THEN 1 ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN first_transform.input_tokens < event.output_tokens + THEN 1 ELSE 0 END), 0) + FROM first_transform + JOIN proxy_events AS event USING(event_id) + WHERE first_transform.rank = 1 + AND CAST(event.recorded_at_utc AS TIMESTAMPTZ) >= CAST(? AS TIMESTAMPTZ) + AND CAST(event.recorded_at_utc AS TIMESTAMPTZ) < CAST(? AS TIMESTAMPTZ)`, + window.fromText, + window.toText, + ).Scan( + &metrics.RawContextTokens, + &metrics.DeliveredContextTokens, + &metrics.TransformedEvents, + &metrics.ReducedEvents, + &metrics.ExpandedEvents, + ) + if err != nil { + return HistoricalMetrics{}, fmt.Errorf("query historical transforms: %w", err) + } + + return metrics, nil +} + +func validateHistoricalEventTimestamps(ctx context.Context, database *sql.DB) error { + var eventID, recordedAt string + + err := database.QueryRowContext( + ctx, + `SELECT event_id, COALESCE(recorded_at_utc, '') + FROM proxy_events + WHERE COALESCE(TRIM(recorded_at_utc), '') = '' + OR TRY_CAST(recorded_at_utc AS TIMESTAMPTZ) IS NULL + ORDER BY event_id + LIMIT 1`, + ).Scan(&eventID, &recordedAt) + if errors.Is(err, sql.ErrNoRows) { + return nil + } + + if err != nil { + return fmt.Errorf("validate historical event timestamps: %w", err) + } + + return fmt.Errorf( + "%w: event %q has invalid recorded_at_utc %q", + errTokenEconomyReport, + eventID, + recordedAt, + ) +} + +func queryHistoricalEventIdentities( + ctx context.Context, + database *sql.DB, + window historicalWindow, +) ([]historicalEventIdentity, error) { + rows, err := database.QueryContext( + ctx, + `SELECT event.event_id, event.session_id, + COALESCE(NULLIF(TRIM(event.provider), ''), + NULLIF(TRIM(session.provider), ''), '') AS resolved_provider + FROM proxy_events AS event + LEFT JOIN proxy_sessions AS session USING(session_id) + WHERE CAST(event.recorded_at_utc AS TIMESTAMPTZ) >= CAST(? AS TIMESTAMPTZ) + AND CAST(event.recorded_at_utc AS TIMESTAMPTZ) < CAST(? AS TIMESTAMPTZ) + ORDER BY event.event_id, event.session_id, resolved_provider`, + window.fromText, + window.toText, + ) + if err != nil { + return nil, fmt.Errorf("query historical event identities: %w", err) + } + defer rows.Close() + + identities := []historicalEventIdentity{} + + for rows.Next() { + var identity historicalEventIdentity + + err = rows.Scan( + &identity.eventID, + &identity.sessionID, + &identity.provider, + ) + if err != nil { + return nil, fmt.Errorf("scan historical event identity: %w", err) + } + + if strings.TrimSpace(identity.eventID) == "" || + strings.TrimSpace(identity.sessionID) == "" { + return nil, fmt.Errorf( + "%w: historical event or session identity is empty", + errTokenEconomyReport, + ) + } + + identities = append(identities, identity) + } + + err = rows.Err() + if err != nil { + return nil, fmt.Errorf("iterate historical event identities: %w", err) + } + + return identities, nil +} + +func newHistoricalAggregate() historicalAggregate { + return historicalAggregate{ + providers: map[Provider]struct{}{}, + eventOwners: map[string]string{}, + sessionOwners: map[string]historicalIdentityOwner{}, + } +} + +func (aggregate *historicalAggregate) merge( + sourcePath string, + metrics HistoricalMetrics, + identities []historicalEventIdentity, +) error { + for _, identity := range identities { + if owner, found := aggregate.eventOwners[identity.eventID]; found { + return fmt.Errorf( + "%w: event_id %q appears in both historical sources %q and %q", + errTokenEconomyReport, + identity.eventID, + owner, + sourcePath, + ) + } + + aggregate.eventOwners[identity.eventID] = sourcePath + + mergeErr := aggregate.mergeSession(sourcePath, identity) + if mergeErr != nil { + return mergeErr + } + } + + aggregate.metrics.RawContextTokens += metrics.RawContextTokens + aggregate.metrics.DeliveredContextTokens += metrics.DeliveredContextTokens + aggregate.metrics.TransformedEvents += metrics.TransformedEvents + aggregate.metrics.ReducedEvents += metrics.ReducedEvents + aggregate.metrics.ExpandedEvents += metrics.ExpandedEvents + + return nil +} + +func (aggregate *historicalAggregate) mergeSession( + sourcePath string, + identity historicalEventIdentity, +) error { + owner, found := aggregate.sessionOwners[identity.sessionID] + if found && owner.provider != "" && identity.provider != "" && + owner.provider != identity.provider { + return fmt.Errorf( + "%w: session_id %q has providers %q in %q and %q in %q", + errTokenEconomyReport, + identity.sessionID, + owner.provider, + owner.sourcePath, + identity.provider, + sourcePath, + ) + } + + if !found || owner.provider == "" { + aggregate.sessionOwners[identity.sessionID] = historicalIdentityOwner{ + sourcePath: sourcePath, + provider: identity.provider, + } + } + + if identity.provider != "" { + aggregate.providers[identity.provider] = struct{}{} + } + + return nil +} + +func (aggregate *historicalAggregate) finish( + window historicalWindow, + sources []HistoricalSource, +) (HistoricalMetrics, []Provider) { + aggregate.metrics.FromUTC = window.fromText + aggregate.metrics.ToUTC = window.toText + aggregate.metrics.Sources = sources + aggregate.metrics.ProxySessions = len(aggregate.sessionOwners) + + aggregate.metrics.AvoidedContextTokens = aggregate.metrics.RawContextTokens - + aggregate.metrics.DeliveredContextTokens + + if aggregate.metrics.RawContextTokens > 0 { + aggregate.metrics.GrossReductionPercent = percentageScale * + float64(aggregate.metrics.AvoidedContextTokens) / + float64(aggregate.metrics.RawContextTokens) + } + + providers := make([]Provider, 0, len(aggregate.providers)) + for provider := range aggregate.providers { + providers = append(providers, provider) + } + + slices.Sort(providers) + + return aggregate.metrics, providers +} + +func verifyHistoricalSources( + sources []preparedHistoricalSource, +) ([]HistoricalSource, error) { + evidence := make([]HistoricalSource, 0, len(sources)) + for _, source := range sources { + after, err := ledgerFileSHA256(source.evidence.Path) + if err != nil { + return nil, fmt.Errorf( + "hash historical source %q after reporting: %w", + source.evidence.Path, + err, + ) + } + + source.evidence.SHA256After = after + source.evidence.SourceUnchanged = source.evidence.SHA256Before == after + + if !source.evidence.SourceUnchanged { + return nil, fmt.Errorf( + "%w: historical source %q changed while reporting: before %s, after %s", + errTokenEconomyReport, + source.evidence.Path, + source.evidence.SHA256Before, + after, + ) + } + + evidence = append(evidence, source.evidence) + } + + return evidence, nil +} diff --git a/go/internal/tokeneconomy/ledger.go b/go/internal/tokeneconomy/ledger.go new file mode 100644 index 00000000..d17c622d --- /dev/null +++ b/go/internal/tokeneconomy/ledger.go @@ -0,0 +1,403 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:cyclop,gocyclo,lll,mnd,noinlineerr,wsl_v5 // Native ledger parsing keeps gates adjacent. +package tokeneconomy + +import ( + "bufio" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" +) + +const ( + maximumLedgerBytes = 512 << 20 + maximumLedgerLine = 32 << 20 + cumulativeUsageKind = "cumulative" +) + +var errInvalidLedger = errors.New("invalid provider token ledger") + +// ParseLedger reads one provider-native ledger without retaining prompt or +// response bodies in the returned evidence. +func ParseLedger(provider Provider, path string) (Ledger, error) { + canonical, err := canonicalLedgerPath(path) + if err != nil { + return Ledger{}, err + } + + before, err := ledgerFileSHA256(canonical) + if err != nil { + return Ledger{}, err + } + + file, err := os.Open(canonical) + if err != nil { + return Ledger{}, fmt.Errorf("open provider ledger: %w", err) + } + + var ledger Ledger + switch provider { + case ProviderCodex: + ledger, err = parseCodexLedger(file) + case ProviderClaude: + ledger, err = parseClaudeLedger(file) + default: + err = fmt.Errorf("%w: unsupported provider %q", errInvalidLedger, provider) + } + + closeErr := file.Close() + if err != nil { + return Ledger{}, err + } + if closeErr != nil { + return Ledger{}, fmt.Errorf("close provider ledger: %w", closeErr) + } + + after, err := ledgerFileSHA256(canonical) + if err != nil { + return Ledger{}, err + } + if before != after { + return Ledger{}, fmt.Errorf("%w: ledger changed while it was read", errInvalidLedger) + } + + ledger.Provider = provider + ledger.SourcePath = canonical + ledger.SourceSHA256 = before + + return ledger, nil +} + +func canonicalLedgerPath(path string) (string, error) { + path = strings.TrimSpace(path) + if !filepath.IsAbs(path) { + return "", fmt.Errorf("%w: ledger path must be absolute", errInvalidLedger) + } + + path = filepath.Clean(path) + info, err := os.Stat(path) + if err != nil { + return "", fmt.Errorf("stat provider ledger: %w", err) + } + if !info.Mode().IsRegular() { + return "", fmt.Errorf("%w: ledger is not a regular file", errInvalidLedger) + } + if info.Size() > maximumLedgerBytes { + return "", fmt.Errorf( + "%w: ledger exceeds %d bytes", + errInvalidLedger, + maximumLedgerBytes, + ) + } + + return path, nil +} + +func ledgerFileSHA256(path string) (string, error) { + file, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("open provider ledger for hashing: %w", err) + } + defer file.Close() + + digest := sha256.New() + _, err = io.Copy(digest, file) + if err != nil { + return "", fmt.Errorf("hash provider ledger: %w", err) + } + + return hex.EncodeToString(digest.Sum(nil)), nil +} + +type codexLedgerLine struct { + Timestamp string `json:"timestamp"` + Type string `json:"type"` + Payload struct { + ID string `json:"id"` + SessionID string `json:"session_id"` + Type string `json:"type"` + Model string `json:"model"` + Info struct { + TotalTokenUsage TokenUsage `json:"total_token_usage"` + } `json:"info"` + } `json:"payload"` +} + +func parseCodexLedger(reader io.Reader) (Ledger, error) { + ledger := Ledger{Events: []UsageEvent{}} + scanner := newLedgerScanner(reader) + + for scanner.Scan() { + var line codexLedgerLine + err := json.Unmarshal(scanner.Bytes(), &line) + if err != nil { + return Ledger{}, fmt.Errorf( + "%w: decode Codex ledger line: %w", + errInvalidLedger, + err, + ) + } + + err = acceptCodexLine(&ledger, line) + if err != nil { + return Ledger{}, err + } + } + + if err := scanner.Err(); err != nil { + return Ledger{}, fmt.Errorf("scan Codex ledger: %w", err) + } + if ledger.SessionID == "" { + return Ledger{}, fmt.Errorf("%w: Codex session id is missing", errInvalidLedger) + } + if len(ledger.Events) == 0 { + return Ledger{}, fmt.Errorf( + "%w: Codex token_count event is missing", + errInvalidLedger, + ) + } + + ledger.Usage = ledger.Events[len(ledger.Events)-1].Usage + + return ledger, nil +} + +func acceptCodexLine(ledger *Ledger, line codexLedgerLine) error { + switch line.Type { + case "session_meta": + sessionID := firstLedgerValue(line.Payload.SessionID, line.Payload.ID) + + return setStableLedgerValue(&ledger.SessionID, sessionID, "Codex session id") + case "turn_context": + return setStableLedgerValue(&ledger.Model, line.Payload.Model, "Codex model") + case "event_msg": + if line.Payload.Type != "token_count" { + return nil + } + + usage := line.Payload.Info.TotalTokenUsage + if err := validateUsage(usage); err != nil { + return err + } + if len(ledger.Events) > 0 { + previous := ledger.Events[len(ledger.Events)-1].Usage + if usageDecreased(previous, usage) { + return fmt.Errorf("%w: Codex cumulative usage decreased", errInvalidLedger) + } + } + + ledger.Events = append(ledger.Events, UsageEvent{ + RecordedAtUTC: line.Timestamp, + UsageKind: cumulativeUsageKind, + Ordinal: len(ledger.Events), + Usage: usage, + }) + } + + return nil +} + +type claudeLedgerLine struct { + Type string `json:"type"` + //nolint:tagliatelle // Claude's provider-native ledger uses sessionId. + SessionID string `json:"sessionId"` + Timestamp string `json:"timestamp"` + Message struct { + ID string `json:"id"` + Model string `json:"model"` + Usage struct { + InputTokens int64 `json:"input_tokens"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` + OutputTokens int64 `json:"output_tokens"` + } `json:"usage"` + } `json:"message"` +} + +func parseClaudeLedger(reader io.Reader) (Ledger, error) { + ledger := Ledger{Events: []UsageEvent{}} + seen := map[string]TokenUsage{} + scanner := newLedgerScanner(reader) + + for scanner.Scan() { + var line claudeLedgerLine + err := json.Unmarshal(scanner.Bytes(), &line) + if err != nil { + return Ledger{}, fmt.Errorf( + "%w: decode Claude ledger line: %w", + errInvalidLedger, + err, + ) + } + + if line.SessionID != "" { + err = setStableLedgerValue(&ledger.SessionID, line.SessionID, "Claude session id") + if err != nil { + return Ledger{}, err + } + } + if line.Type != "assistant" || + line.Message.ID == "" || + line.Message.ID == "" || + line.Message.Model == "" { + continue + } + + err = acceptClaudeUsage(&ledger, seen, line) + if err != nil { + return Ledger{}, err + } + } + + if err := scanner.Err(); err != nil { + return Ledger{}, fmt.Errorf("scan Claude ledger: %w", err) + } + if ledger.SessionID == "" { + return Ledger{}, fmt.Errorf("%w: Claude session id is missing", errInvalidLedger) + } + if len(ledger.Events) == 0 { + return Ledger{}, fmt.Errorf("%w: Claude usage event is missing", errInvalidLedger) + } + + return ledger, nil +} + +func acceptClaudeUsage( + ledger *Ledger, + seen map[string]TokenUsage, + line claudeLedgerLine, +) error { + usage := TokenUsage{ + InputTokens: line.Message.Usage.InputTokens, + CacheCreationInputTokens: line.Message.Usage.CacheCreationInputTokens, + CacheReadInputTokens: line.Message.Usage.CacheReadInputTokens, + OutputTokens: line.Message.Usage.OutputTokens, + } + usage.TotalTokens = usage.InputTokens + + usage.CacheCreationInputTokens + + usage.CacheReadInputTokens + + usage.OutputTokens + + if err := validateUsage(usage); err != nil { + return err + } + if previous, found := seen[line.Message.ID]; found { + if !reflect.DeepEqual(previous, usage) { + return fmt.Errorf( + "%w: Claude message %q has conflicting usage", + errInvalidLedger, + line.Message.ID, + ) + } + + return nil + } + + if err := setStableLedgerValue( + &ledger.Model, + line.Message.Model, + "Claude model", + ); err != nil { + return err + } + + seen[line.Message.ID] = usage + ledger.Events = append(ledger.Events, UsageEvent{ + ProviderMessageID: line.Message.ID, + RecordedAtUTC: line.Timestamp, + UsageKind: "incremental", + Ordinal: len(ledger.Events), + Usage: usage, + }) + ledger.Usage = addUsage(ledger.Usage, usage) + + return nil +} + +func newLedgerScanner(reader io.Reader) *bufio.Scanner { + scanner := bufio.NewScanner(reader) + scanner.Buffer(make([]byte, 64<<10), maximumLedgerLine) + + return scanner +} + +func validateUsage(usage TokenUsage) error { + values := []int64{ + usage.InputTokens, + usage.CachedInputTokens, + usage.CacheCreationInputTokens, + usage.CacheReadInputTokens, + usage.OutputTokens, + usage.ReasoningOutputTokens, + usage.TotalTokens, + } + for _, value := range values { + if value < 0 { + return fmt.Errorf("%w: token counts cannot be negative", errInvalidLedger) + } + } + + return nil +} + +func usageDecreased(previous, current TokenUsage) bool { + return current.InputTokens < previous.InputTokens || + current.CachedInputTokens < previous.CachedInputTokens || + current.OutputTokens < previous.OutputTokens || + current.ReasoningOutputTokens < previous.ReasoningOutputTokens || + current.TotalTokens < previous.TotalTokens +} + +func addUsage(left, right TokenUsage) TokenUsage { + return TokenUsage{ + InputTokens: left.InputTokens + right.InputTokens, + CachedInputTokens: left.CachedInputTokens + right.CachedInputTokens, + CacheCreationInputTokens: left.CacheCreationInputTokens + right.CacheCreationInputTokens, + CacheReadInputTokens: left.CacheReadInputTokens + right.CacheReadInputTokens, + OutputTokens: left.OutputTokens + right.OutputTokens, + ReasoningOutputTokens: left.ReasoningOutputTokens + right.ReasoningOutputTokens, + TotalTokens: left.TotalTokens + right.TotalTokens, + } +} + +func setStableLedgerValue(target *string, incoming, field string) error { + incoming = strings.TrimSpace(incoming) + if incoming == "" { + return nil + } + if *target == "" { + *target = incoming + + return nil + } + if *target != incoming { + return fmt.Errorf( + "%w: %s changed from %q to %q", + errInvalidLedger, + field, + *target, + incoming, + ) + } + + return nil +} + +func firstLedgerValue(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + + return "" +} diff --git a/go/internal/tokeneconomy/ledger_test.go b/go/internal/tokeneconomy/ledger_test.go new file mode 100644 index 00000000..ab6269de --- /dev/null +++ b/go/internal/tokeneconomy/ledger_test.go @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package tokeneconomy + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestParseCodexLedgerUsesFinalCumulativeUsage(t *testing.T) { + t.Parallel() + + path := writeLedgerFixture(t, strings.Join([]string{ + `{"type":"session_meta","payload":{"id":"session-1"}}`, + `{"type":"turn_context","payload":{"model":"gpt-test"}}`, + `{"timestamp":"2026-08-28T01:00:00Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":100,"cached_input_tokens":80,"output_tokens":20,"reasoning_output_tokens":5,"total_tokens":120}}}}`, + `{"timestamp":"2026-08-28T01:01:00Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":250,"cached_input_tokens":200,"output_tokens":40,"reasoning_output_tokens":9,"total_tokens":290}}}}`, + }, "\n")) + + ledger, err := ParseLedger(ProviderCodex, path) + if err != nil { + t.Fatalf("parse Codex ledger: %v", err) + } + + if ledger.SessionID != "session-1" || ledger.Model != "gpt-test" { + t.Fatalf("unexpected Codex identity: %#v", ledger) + } + if ledger.Usage.TotalTokens != 290 || ledger.Usage.CachedInputTokens != 200 { + t.Fatalf("unexpected final Codex usage: %#v", ledger.Usage) + } + if len(ledger.Events) != 2 || ledger.Events[0].UsageKind != "cumulative" { + t.Fatalf("unexpected Codex events: %#v", ledger.Events) + } + if len(ledger.SourceSHA256) != 64 { + t.Fatalf("unexpected source digest: %q", ledger.SourceSHA256) + } +} + +func TestParseCodexLedgerRejectsDecreasingCumulativeUsage(t *testing.T) { + t.Parallel() + + path := writeLedgerFixture(t, strings.Join([]string{ + `{"type":"session_meta","payload":{"id":"session-1"}}`, + `{"type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":100,"output_tokens":20,"total_tokens":120}}}}`, + `{"type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":90,"output_tokens":20,"total_tokens":110}}}}`, + }, "\n")) + + _, err := ParseLedger(ProviderCodex, path) + if err == nil || !strings.Contains(err.Error(), "cumulative usage decreased") { + t.Fatalf("expected decreasing usage failure, got %v", err) + } +} + +func TestParseClaudeLedgerDeduplicatesMessagesAndExcludesSyntheticUsage(t *testing.T) { + t.Parallel() + + realMessage := `{"type":"assistant","sessionId":"session-2","timestamp":"2026-08-28T02:00:00Z","message":{"id":"msg-1","model":"claude-test","usage":{"input_tokens":2,"cache_creation_input_tokens":20,"cache_read_input_tokens":30,"output_tokens":4}}}` + path := writeLedgerFixture(t, strings.Join([]string{ + `{"type":"assistant","sessionId":"session-2","message":{"id":"synthetic","model":"","usage":{"input_tokens":999}}}`, + realMessage, + realMessage, + `{"type":"assistant","sessionId":"session-2","message":{"id":"msg-2","model":"claude-test","usage":{"input_tokens":3,"cache_creation_input_tokens":10,"cache_read_input_tokens":5,"output_tokens":6}}}`, + }, "\n")) + + ledger, err := ParseLedger(ProviderClaude, path) + if err != nil { + t.Fatalf("parse Claude ledger: %v", err) + } + + if ledger.SessionID != "session-2" || ledger.Model != "claude-test" { + t.Fatalf("unexpected Claude identity: %#v", ledger) + } + if len(ledger.Events) != 2 { + t.Fatalf("duplicate Claude messages were retained: %#v", ledger.Events) + } + if ledger.Usage.TotalTokens != 80 || ledger.Usage.CacheReadInputTokens != 35 { + t.Fatalf("unexpected Claude usage: %#v", ledger.Usage) + } +} + +func TestParseClaudeLedgerRejectsConflictingDuplicateMessages(t *testing.T) { + t.Parallel() + + path := writeLedgerFixture(t, strings.Join([]string{ + `{"type":"assistant","sessionId":"session-2","message":{"id":"msg-1","model":"claude-test","usage":{"input_tokens":2,"output_tokens":4}}}`, + `{"type":"assistant","sessionId":"session-2","message":{"id":"msg-1","model":"claude-test","usage":{"input_tokens":3,"output_tokens":4}}}`, + }, "\n")) + + _, err := ParseLedger(ProviderClaude, path) + if err == nil || !strings.Contains(err.Error(), "conflicting usage") { + t.Fatalf("expected conflicting duplicate failure, got %v", err) + } +} + +func TestParseLedgerRequiresAbsolutePath(t *testing.T) { + t.Parallel() + + _, err := ParseLedger(ProviderCodex, "relative.jsonl") + if err == nil || !strings.Contains(err.Error(), "must be absolute") { + t.Fatalf("expected absolute path failure, got %v", err) + } +} + +func writeLedgerFixture(t *testing.T, content string) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "ledger.jsonl") + err := os.WriteFile(path, []byte(content+"\n"), 0o600) + if err != nil { + t.Fatalf("write ledger fixture: %v", err) + } + + return path +} diff --git a/go/internal/tokeneconomy/mechanisms.go b/go/internal/tokeneconomy/mechanisms.go new file mode 100644 index 00000000..e1f8aa20 --- /dev/null +++ b/go/internal/tokeneconomy/mechanisms.go @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:lll,noinlineerr,wsl_v5 // SQL evidence projection stays adjacent to its scan contract. +package tokeneconomy + +import ( + "context" + "database/sql" + "errors" + "fmt" + "os" +) + +func readRunMechanisms( + ctx context.Context, + codeIntelDBPath string, + sessionID string, +) (MechanismMetrics, error) { + if _, err := os.Stat(codeIntelDBPath); errors.Is(err, os.ErrNotExist) { + return MechanismMetrics{}, nil + } else if err != nil { + return MechanismMetrics{}, fmt.Errorf("inspect run mechanism store: %w", err) + } + + database, err := sql.Open("duckdb", codeIntelDBPath+"?access_mode=READ_ONLY") + if err != nil { + return MechanismMetrics{}, fmt.Errorf("open run mechanism store: %w", err) + } + database.SetMaxOpenConns(1) + database.SetMaxIdleConns(1) + + metrics, queryErr := queryRunMechanisms(ctx, database, sessionID) + closeErr := database.Close() + if err = errors.Join(queryErr, closeErr); err != nil { + return MechanismMetrics{}, err + } + + return metrics, nil +} + +func queryRunMechanisms( + ctx context.Context, + database *sql.DB, + sessionID string, +) (MechanismMetrics, error) { + metrics := MechanismMetrics{} + err := database.QueryRowContext( + ctx, + `WITH first_transform AS ( + SELECT transform.event_id, transform.input_tokens, + ROW_NUMBER() OVER ( + PARTITION BY transform.event_id ORDER BY transform.ordinal + ) AS rank + FROM proxy_transforms transform + JOIN proxy_events event USING(event_id) + WHERE event.session_id = ? + ), repeated_injections AS ( + SELECT COALESCE(SUM(repetitions), 0) AS repeated + FROM ( + SELECT GREATEST(COUNT(*) - 1, 0) AS repetitions + FROM proxy_events + WHERE session_id = ? AND event_kind = 'payload_injection' + AND COALESCE(output_hash, '') != '' + GROUP BY output_hash + ) + ) + SELECT + COALESCE(SUM(first_transform.input_tokens), 0), + COALESCE(SUM(event.output_tokens), 0), + COALESCE(SUM(CASE WHEN event.event_kind = 'payload_injection' + THEN event.output_tokens ELSE 0 END), 0), + COUNT(*), + (SELECT repeated FROM repeated_injections) + FROM first_transform + JOIN proxy_events event USING(event_id) + WHERE first_transform.rank = 1`, + sessionID, + sessionID, + ).Scan( + &metrics.RawContextTokens, + &metrics.DeliveredContextTokens, + &metrics.InjectedGuidanceTokens, + &metrics.TransformEventCount, + &metrics.RepeatedAdviceCount, + ) + if err != nil { + return MechanismMetrics{}, fmt.Errorf("query run mechanism evidence: %w", err) + } + + metrics.AvoidedContextTokens = metrics.RawContextTokens - metrics.DeliveredContextTokens + + return metrics, nil +} diff --git a/go/internal/tokeneconomy/report.go b/go/internal/tokeneconomy/report.go new file mode 100644 index 00000000..a24ef54f --- /dev/null +++ b/go/internal/tokeneconomy/report.go @@ -0,0 +1,500 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:cyclop,exhaustive,gocognit,gocyclo,lll,mnd,wsl_v5 // Statistical gates stay reviewable. +package tokeneconomy + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "math/rand/v2" + "slices" + "strings" + "time" +) + +const ( + bootstrapSamples = 10_000 + familyWiseAlpha = 0.05 + minimumCausalTaskCount = 10 + precisionHalfWidthPoints = 10.0 + qualityNoninferiorityPoints = -5.0 +) + +// ExperimentReport compares completed benchmark arms for one experiment. +func (store *Store) ExperimentReport( + ctx context.Context, + experimentID string, + now time.Time, +) (Report, error) { + experiment, err := store.Experiment(ctx, experimentID) + if err != nil { + return Report{}, err + } + + runs, err := store.Runs(ctx, experimentID) + if err != nil { + return Report{}, err + } + + coverage := experimentCoverage(experiment, runs) + checkpointCount := max(len(experiment.AnalysisBlockCheckpoints), 1) + intervalAlpha := familyWiseAlpha / float64(checkpointCount) + comparisons := []Comparison{} + for _, arms := range [][2]Arm{ + {ArmFull, ArmOff}, + {ArmFull, ArmStatic}, + {ArmStatic, ArmOff}, + } { + comparison, found := compareArms( + experimentID, + runs, + arms[0], + arms[1], + intervalAlpha, + ) + if found { + comparisons = append(comparisons, comparison) + } + } + + causal := experiment.Randomized && + experiment.ArmIsolationVerified && + coverage.PartialBlockCount == 0 && + coverage.MissingLedgerRuns == 0 && + coverage.MissingValidationRuns == 0 && + coverage.CompleteTaskCount >= minimumCausalTaskCount && + slices.Contains( + experiment.AnalysisBlockCheckpoints, + coverage.CompleteBlockCount, + ) && + allArmsPresent(coverage.Arms) + + conclusion := ConclusionInconclusive + if primary, found := comparisonFor(comparisons, ArmFull, ArmOff); found && causal { + conclusion = comparisonConclusion(primary) + } + + return Report{ + Kind: ReportKind, + Cohort: experimentID, + GeneratedAtUTC: reportTimestamp(now), + Conclusion: conclusion, + SchemaVersion: SchemaVersion, + Causal: causal, + Coverage: coverage, + Comparisons: comparisons, + Provenance: map[string]string{ + "manifest_sha256": experiment.ManifestSHA256, + "protocol_sha256": experiment.ProtocolSHA256, + "runtime_version": experiment.RuntimeVersion, + "provider": string(experiment.Provider), + "model": experiment.Model, + }, + }, nil +} + +func experimentCoverage(experiment Experiment, runs []Run) Coverage { + coverage := Coverage{ + Providers: []Provider{}, + Arms: []Arm{}, + Reasons: []string{}, + RunCount: len(runs), + } + tasks := map[string]struct{}{} + blocks := map[string]coverageBlockEvidence{} + + for _, run := range runs { + tasks[run.TaskID] = struct{}{} + blockID := fmt.Sprintf("%s\x00%d", run.TaskID, run.Replicate) + block := blocks[blockID] + if block.arms == nil { + block = coverageBlockEvidence{arms: map[Arm]int{}, taskID: run.TaskID} + } + block.arms[run.Arm]++ + blocks[blockID] = block + if !slices.Contains(coverage.Providers, run.Provider) { + coverage.Providers = append(coverage.Providers, run.Provider) + } + if !slices.Contains(coverage.Arms, run.Arm) { + coverage.Arms = append(coverage.Arms, run.Arm) + } + if run.Accepted { + coverage.AcceptedRunCount++ + } + if strings.TrimSpace(run.LedgerSHA256) == "" { + coverage.MissingLedgerRuns++ + } + if strings.TrimSpace(run.ValidationReceiptSHA256) == "" { + coverage.MissingValidationRuns++ + } + } + + coverage.TaskCount = len(tasks) + coverage.CompleteTaskCount, + coverage.CompleteBlockCount, + coverage.PartialBlockCount = summarizeCoverageBlocks(blocks) + slices.Sort(coverage.Providers) + slices.Sort(coverage.Arms) + coverage.Reasons = experimentCoverageReasons(experiment, coverage) + + return coverage +} + +type coverageBlockEvidence struct { + arms map[Arm]int + taskID string +} + +func summarizeCoverageBlocks(blocks map[string]coverageBlockEvidence) (int, int, int) { + completeTasks := map[string]struct{}{} + completeBlocks := 0 + partialBlocks := 0 + for _, block := range blocks { + full := block.arms[ArmFull] + static := block.arms[ArmStatic] + off := block.arms[ArmOff] + if full == 1 && static == 1 && off == 1 && len(block.arms) == 3 { + completeBlocks++ + completeTasks[block.taskID] = struct{}{} + } else { + partialBlocks++ + } + } + + return len(completeTasks), completeBlocks, partialBlocks +} + +func experimentCoverageReasons(experiment Experiment, coverage Coverage) []string { + reasons := []string{} + if !experiment.Randomized { + reasons = append(reasons, "experiment is not randomized") + } + if !experiment.ArmIsolationVerified { + reasons = append(reasons, "arm isolation is not verified") + } + if coverage.CompleteTaskCount < minimumCausalTaskCount { + reasons = append( + reasons, + fmt.Sprintf( + "fewer than %d complete three-arm tasks are available", + minimumCausalTaskCount, + ), + ) + } + if !slices.Contains( + experiment.AnalysisBlockCheckpoints, + coverage.CompleteBlockCount, + ) { + reasons = append( + reasons, + "complete block count is not a predeclared analysis checkpoint", + ) + } + if !allArmsPresent(coverage.Arms) { + reasons = append(reasons, "one or more benchmark arms are missing") + } + if coverage.PartialBlockCount != 0 { + reasons = append( + reasons, + "one or more three-arm blocks are partial", + ) + } + if coverage.MissingLedgerRuns != 0 { + reasons = append(reasons, "provider ledger evidence is incomplete") + } + if coverage.MissingValidationRuns != 0 { + reasons = append(reasons, "validation receipts are incomplete") + } + + return reasons +} + +type taskCluster struct { + TaskID string + Runs []Run +} + +func compareArms( + experimentID string, + runs []Run, + treatment Arm, + control Arm, + intervalAlpha float64, +) (Comparison, bool) { + clusters := comparisonClusters(runs, treatment, control) + if len(clusters) == 0 { + return Comparison{}, false + } + + observed := aggregateClusters(clusters, treatment, control) + if observed.treatmentAccepted == 0 || observed.controlAccepted == 0 { + comparison := observed.comparison( + treatment, + control, + Interval{}, + Interval{}, + intervalAlpha, + ) + comparison.PrecisionMet = false + comparison.QualityNonInferior = false + + return comparison, true + } + + savings, acceptance := bootstrapIntervals( + experimentID, + clusters, + treatment, + control, + intervalAlpha, + ) + + return observed.comparison( + treatment, + control, + savings, + acceptance, + intervalAlpha, + ), true +} + +func comparisonClusters(runs []Run, treatment, control Arm) []taskCluster { + byTask := map[string][]Run{} + for _, run := range runs { + if run.Arm == treatment || run.Arm == control { + byTask[run.TaskID] = append(byTask[run.TaskID], run) + } + } + + taskIDs := make([]string, 0, len(byTask)) + for taskID, taskRuns := range byTask { + if armRunCount(taskRuns, treatment) > 0 && armRunCount(taskRuns, control) > 0 { + taskIDs = append(taskIDs, taskID) + } + } + slices.Sort(taskIDs) + + clusters := make([]taskCluster, 0, len(taskIDs)) + for _, taskID := range taskIDs { + clusters = append(clusters, taskCluster{TaskID: taskID, Runs: byTask[taskID]}) + } + + return clusters +} + +func armRunCount(runs []Run, arm Arm) int { + count := 0 + for _, run := range runs { + if run.Arm == arm { + count++ + } + } + + return count +} + +type aggregate struct { + taskCount int + runCount int + treatmentTokens int64 + controlTokens int64 + treatmentAccepted int + controlAccepted int + treatmentRuns int + controlRuns int + treatmentSevere int + controlSevere int +} + +func aggregateClusters(clusters []taskCluster, treatment, control Arm) aggregate { + result := aggregate{taskCount: len(clusters)} + for _, cluster := range clusters { + for _, run := range cluster.Runs { + switch run.Arm { + case treatment: + result.treatmentTokens += run.Usage.TotalTokens + result.treatmentRuns++ + if run.Accepted { + result.treatmentAccepted++ + } + if run.SevereGovernanceViolation { + result.treatmentSevere++ + } + case control: + result.controlTokens += run.Usage.TotalTokens + result.controlRuns++ + if run.Accepted { + result.controlAccepted++ + } + if run.SevereGovernanceViolation { + result.controlSevere++ + } + default: + continue + } + } + } + result.runCount = result.treatmentRuns + result.controlRuns + + return result +} + +func (value aggregate) comparison( + treatment Arm, + control Arm, + savings Interval, + acceptance Interval, + intervalAlpha float64, +) Comparison { + treatmentRate := tokensPerAccepted(value.treatmentTokens, value.treatmentAccepted) + controlRate := tokensPerAccepted(value.controlTokens, value.controlAccepted) + savingsPoint := savingsPercent(treatmentRate, controlRate) + acceptancePoint := acceptanceDifference(value) + additionalSevere := max(value.treatmentSevere-value.controlSevere, 0) + + return Comparison{ + TreatmentArm: treatment, + ControlArm: control, + TaskCount: value.taskCount, + AssignedRuns: value.runCount, + TreatmentAccepted: value.treatmentAccepted, + ControlAccepted: value.controlAccepted, + TreatmentTokensPerAccepted: treatmentRate, + ControlTokensPerAccepted: controlRate, + SavingsPercent: savingsPoint, + SavingsPercentInterval: savings, + AcceptanceDifferencePoints: acceptancePoint, + AcceptanceDifferenceInterval: acceptance, + ConfidenceLevelPercent: 100 * (1 - intervalAlpha), + PrecisionMet: intervalHalfWidth(savings) <= precisionHalfWidthPoints, + QualityNonInferior: acceptance.Lower >= qualityNoninferiorityPoints && + additionalSevere == 0, + AdditionalSevereViolations: additionalSevere, + } +} + +func bootstrapIntervals( + experimentID string, + clusters []taskCluster, + treatment Arm, + control Arm, + intervalAlpha float64, +) (Interval, Interval) { + seedHash := sha256Bytes([]byte( + experimentID + "\x00" + string(treatment) + "\x00" + string(control), + )) + seedOne := binary.LittleEndian.Uint64(seedHash[:8]) + seedTwo := binary.LittleEndian.Uint64(seedHash[8:16]) + //nolint:gosec // Bootstrap reproducibility requires deterministic, non-secret sampling. + random := rand.New(rand.NewPCG(seedOne, seedTwo)) + savings := make([]float64, 0, bootstrapSamples) + acceptance := make([]float64, 0, bootstrapSamples) + + for range bootstrapSamples { + sample := make([]taskCluster, 0, len(clusters)) + for range clusters { + sample = append(sample, clusters[random.IntN(len(clusters))]) + } + + value := aggregateClusters(sample, treatment, control) + if value.treatmentAccepted == 0 || value.controlAccepted == 0 { + continue + } + + treatmentRate := tokensPerAccepted(value.treatmentTokens, value.treatmentAccepted) + controlRate := tokensPerAccepted(value.controlTokens, value.controlAccepted) + savings = append(savings, savingsPercent(treatmentRate, controlRate)) + acceptance = append(acceptance, acceptanceDifference(value)) + } + + return percentileInterval(savings, intervalAlpha), percentileInterval( + acceptance, + intervalAlpha, + ) +} + +func percentileInterval(values []float64, alpha float64) Interval { + if len(values) == 0 { + return Interval{} + } + + slices.Sort(values) + lower := int((alpha / 2) * float64(len(values)-1)) + upper := int((1 - alpha/2) * float64(len(values)-1)) + + return Interval{Lower: values[lower], Upper: values[upper]} +} + +func tokensPerAccepted(tokens int64, accepted int) float64 { + if accepted == 0 { + return 0 + } + + return float64(tokens) / float64(accepted) +} + +func savingsPercent(treatment, control float64) float64 { + if control == 0 { + return 0 + } + + return 100 * (control - treatment) / control +} + +func acceptanceDifference(value aggregate) float64 { + if value.treatmentRuns == 0 || value.controlRuns == 0 { + return 0 + } + + return 100 * (float64(value.treatmentAccepted)/float64(value.treatmentRuns) - + float64(value.controlAccepted)/float64(value.controlRuns)) +} + +func intervalHalfWidth(interval Interval) float64 { + return (interval.Upper - interval.Lower) / 2 +} + +func allArmsPresent(arms []Arm) bool { + return slices.Contains(arms, ArmFull) && + slices.Contains(arms, ArmStatic) && + slices.Contains(arms, ArmOff) +} + +func comparisonFor( + comparisons []Comparison, + treatment Arm, + control Arm, +) (Comparison, bool) { + for _, comparison := range comparisons { + if comparison.TreatmentArm == treatment && comparison.ControlArm == control { + return comparison, true + } + } + + return Comparison{}, false +} + +func comparisonConclusion(comparison Comparison) Conclusion { + if comparison.SavingsPercentInterval.Lower > 0 { + if !comparison.QualityNonInferior { + return ConclusionQualityTradeoff + } + + return ConclusionCausalSavings + } + if comparison.SavingsPercentInterval.Upper < 0 { + return ConclusionTokenRegression + } + if comparison.PrecisionMet { + return ConclusionNoDifference + } + + return ConclusionInconclusive +} + +func sha256Bytes(payload []byte) [32]byte { + return sha256.Sum256(payload) +} diff --git a/go/internal/tokeneconomy/report_test.go b/go/internal/tokeneconomy/report_test.go new file mode 100644 index 00000000..3a1bd626 --- /dev/null +++ b/go/internal/tokeneconomy/report_test.go @@ -0,0 +1,622 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package tokeneconomy + +import ( + "context" + "database/sql" + "fmt" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" + "time" + + _ "github.com/duckdb/duckdb-go/v2" +) + +func TestHistoricalReportLabelsGrossReductionNonCausal(t *testing.T) { + t.Parallel() + + path := createHistoricalFixture(t) + report, err := HistoricalReport( + context.Background(), + HistoricalReportOptions{ + DatabasePaths: []string{path}, + FromUTC: "2026-08-01T00:00:00Z", + ToUTC: "2026-09-01T00:00:00Z", + }, + time.Unix(1, 0), + ) + if err != nil { + t.Fatalf("historical report: %v", err) + } + + if report.Causal || report.Conclusion != ConclusionObservational { + t.Fatalf("historical report overclaimed: %#v", report) + } + if report.Historical == nil || + report.Historical.RawContextTokens != 300 || + report.Historical.DeliveredContextTokens != 130 || + report.Historical.AvoidedContextTokens != 170 { + t.Fatalf("unexpected historical metrics: %#v", report.Historical) + } + if report.Historical.ReducedEvents != 1 || report.Historical.ExpandedEvents != 1 { + t.Fatalf("unexpected transform direction counts: %#v", report.Historical) + } + if report.Historical.FromUTC != "2026-08-01T00:00:00Z" || + report.Historical.ToUTC != "2026-09-01T00:00:00Z" || + len(report.Historical.Sources) != 1 || + !report.Historical.Sources[0].SourceUnchanged || + report.Historical.Sources[0].SHA256Before != + report.Historical.Sources[0].SHA256After { + t.Fatalf("historical provenance is incomplete: %#v", report.Historical) + } +} + +func TestHistoricalReportAggregatesSourcesDeterministicallyWithinWindow(t *testing.T) { + t.Parallel() + + firstPath := createHistoricalFixture(t) + secondPath := createSingleHistoricalFixture( + t, + "claude", + "session-claude", + "event-claude", + "2026-08-20T12:00:00Z", + 100, + 40, + ) + now := time.Unix(123, 0) + first, err := HistoricalReport( + context.Background(), + HistoricalReportOptions{ + DatabasePaths: []string{secondPath, firstPath}, + FromUTC: "2026-08-01T00:00:00-06:00", + ToUTC: "2026-09-01T00:00:00-06:00", + }, + now, + ) + if err != nil { + t.Fatalf("aggregate historical sources: %v", err) + } + + second, err := HistoricalReport( + context.Background(), + HistoricalReportOptions{ + DatabasePaths: []string{firstPath, secondPath}, + FromUTC: "2026-08-01T06:00:00Z", + ToUTC: "2026-09-01T06:00:00Z", + }, + now, + ) + if err != nil { + t.Fatalf("repeat aggregate historical sources: %v", err) + } + if !reflect.DeepEqual(first, second) { + t.Fatalf("historical aggregation depends on input order:\n%#v\n%#v", first, second) + } + if first.Historical == nil || first.Historical.RawContextTokens != 400 || + first.Historical.DeliveredContextTokens != 170 || + first.Historical.ProxySessions != 2 || + !slices.Equal(first.Coverage.Providers, []Provider{ProviderClaude, ProviderCodex}) { + t.Fatalf("unexpected aggregate historical report: %#v", first) + } + if first.Historical.Sources[0].Path > first.Historical.Sources[1].Path { + t.Fatalf( + "historical sources are not canonical-path ordered: %#v", + first.Historical.Sources, + ) + } +} + +func TestHistoricalReportRejectsDuplicateEventIDsAcrossSources(t *testing.T) { + t.Parallel() + + firstPath := createSingleHistoricalFixture( + t, + "codex", + "session-one", + "shared-event", + "2026-08-10T00:00:00Z", + 100, + 50, + ) + secondPath := createSingleHistoricalFixture( + t, + "codex", + "session-two", + "shared-event", + "2026-08-11T00:00:00Z", + 80, + 40, + ) + + _, err := HistoricalReport( + context.Background(), + HistoricalReportOptions{ + DatabasePaths: []string{firstPath, secondPath}, + FromUTC: "2026-08-01T00:00:00Z", + ToUTC: "2026-09-01T00:00:00Z", + }, + time.Unix(1, 0), + ) + if err == nil || !strings.Contains(err.Error(), "shared-event") || + !strings.Contains(err.Error(), firstPath) || + !strings.Contains(err.Error(), secondPath) { + t.Fatalf("expected duplicate event diagnostic with both sources, got %v", err) + } +} + +func TestHistoricalReportRequiresOrderedExplicitWindow(t *testing.T) { + t.Parallel() + + path := createHistoricalFixture(t) + for _, options := range []HistoricalReportOptions{ + {DatabasePaths: []string{path}, ToUTC: "2026-09-01T00:00:00Z"}, + { + DatabasePaths: []string{path}, + FromUTC: "2026-09-01T00:00:00Z", + ToUTC: "2026-08-01T00:00:00Z", + }, + } { + if _, err := HistoricalReport( + context.Background(), + options, + time.Unix(1, 0), + ); err == nil { + t.Fatalf("invalid historical window unexpectedly succeeded: %#v", options) + } + } +} + +func TestHistoricalReportUsesHalfOpenWindow(t *testing.T) { + t.Parallel() + + includedPath := createSingleHistoricalFixture( + t, + "codex", + "included-session", + "included-event", + "2026-08-01T00:00:00Z", + 100, + 25, + ) + excludedPath := createSingleHistoricalFixture( + t, + "codex", + "excluded-session", + "excluded-event", + "2026-09-01T00:00:00Z", + 900, + 800, + ) + + report, err := HistoricalReport( + context.Background(), + HistoricalReportOptions{ + DatabasePaths: []string{excludedPath, includedPath}, + FromUTC: "2026-08-01T00:00:00Z", + ToUTC: "2026-09-01T00:00:00Z", + }, + time.Unix(1, 0), + ) + if err != nil { + t.Fatalf("report half-open historical window: %v", err) + } + if report.Historical == nil || report.Historical.RawContextTokens != 100 || + report.Historical.DeliveredContextTokens != 25 || + report.Historical.TransformedEvents != 1 || + report.Historical.ProxySessions != 1 { + t.Fatalf("historical window did not use [from,to): %#v", report.Historical) + } +} + +func TestVerifyHistoricalSourcesRejectsChangedSource(t *testing.T) { + t.Parallel() + + path := createHistoricalFixture(t) + _, sources, err := prepareHistoricalReport(HistoricalReportOptions{ + DatabasePaths: []string{path}, + FromUTC: "2026-08-01T00:00:00Z", + ToUTC: "2026-09-01T00:00:00Z", + }) + if err != nil { + t.Fatalf("prepare historical source: %v", err) + } + if err = os.WriteFile(path, []byte("changed source\n"), 0o600); err != nil { + t.Fatalf("change historical source fixture: %v", err) + } + + _, err = verifyHistoricalSources(sources) + if err == nil || !strings.Contains(err.Error(), path) || + !strings.Contains(err.Error(), "changed while reporting") { + t.Fatalf("expected changed-source rejection, got %v", err) + } +} + +func TestExperimentReportClaimsSavingsOnlyWithQualityAndCoverage(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("experiment-1") + experiment.AnalysisBlockCheckpoints = []int{minimumCausalTaskCount, 20} + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + + for index := 0; index < minimumCausalTaskCount; index++ { + taskID := "task-" + string(rune('a'+index)) + if err = store.RecordTask( + ctx, + testTask(experiment.ExperimentID, taskID), + ); err != nil { + t.Fatalf("record task: %v", err) + } + + for _, armTokens := range []struct { + arm Arm + tokens int64 + }{{ArmFull, 50}, {ArmStatic, 75}, {ArmOff, 100}} { + run := testRun( + experiment.ExperimentID, + taskID, + armTokens.arm, + 1, + armTokens.tokens, + true, + ) + run.RunID = taskID + "-" + string(armTokens.arm) + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("record run: %v", err) + } + } + } + + report, err := store.ExperimentReport(ctx, experiment.ExperimentID, time.Unix(1, 0)) + if err != nil { + t.Fatalf("experiment report: %v", err) + } + if !report.Causal || report.Conclusion != ConclusionCausalSavings { + t.Fatalf("unexpected experiment conclusion: %#v", report) + } + + primary, found := comparisonFor(report.Comparisons, ArmFull, ArmOff) + if !found || primary.SavingsPercent != 50 || !primary.PrecisionMet || + primary.ConfidenceLevelPercent != 97.5 { + t.Fatalf("unexpected primary comparison: %#v", primary) + } +} + +func TestExperimentReportRejectsPartialTaskBlocks(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("partial-blocks") + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + for index := range minimumCausalTaskCount { + taskID := "partial-task-" + string(rune('a'+index)) + if err = store.RecordTask( + ctx, + testTask(experiment.ExperimentID, taskID), + ); err != nil { + t.Fatalf("record task: %v", err) + } + arms := []Arm{ArmFull, ArmStatic, ArmOff} + if index == 0 { + arms = []Arm{ArmFull, ArmOff} + } + for _, arm := range arms { + run := testRun(experiment.ExperimentID, taskID, arm, 1, 100, true) + run.RunID = taskID + "-" + string(arm) + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("record run: %v", err) + } + } + } + + report, err := store.ExperimentReport(ctx, experiment.ExperimentID, time.Unix(1, 0)) + if err != nil { + t.Fatalf("experiment report: %v", err) + } + if report.Causal || report.Coverage.TaskCount != minimumCausalTaskCount || + report.Coverage.CompleteTaskCount != minimumCausalTaskCount-1 || + report.Coverage.CompleteBlockCount != minimumCausalTaskCount-1 { + t.Fatalf("partial task blocks supported a causal claim: %#v", report) + } +} + +func TestExperimentReportRejectsPartialBlockAfterCheckpoint(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("partial-after-checkpoint") + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + for index := 0; index <= minimumCausalTaskCount; index++ { + taskID := "interrupted-task-" + string(rune('a'+index)) + if err = store.RecordTask( + ctx, + testTask(experiment.ExperimentID, taskID), + ); err != nil { + t.Fatalf("record task: %v", err) + } + arms := []Arm{ArmFull, ArmStatic, ArmOff} + if index == minimumCausalTaskCount { + arms = []Arm{ArmFull} + } + for _, arm := range arms { + run := testRun(experiment.ExperimentID, taskID, arm, 1, 100, true) + run.RunID = taskID + "-" + string(arm) + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("record run: %v", err) + } + } + } + + report, err := store.ExperimentReport(ctx, experiment.ExperimentID, time.Unix(1, 0)) + if err != nil { + t.Fatalf("experiment report: %v", err) + } + if report.Causal || report.Coverage.CompleteBlockCount != minimumCausalTaskCount || + report.Coverage.PartialBlockCount != 1 { + t.Fatalf("partial post-checkpoint block supported a causal claim: %#v", report) + } +} + +func TestExperimentReportRequiresPredeclaredCompleteBlockLook(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("between-looks") + experiment.AnalysisBlockCheckpoints = []int{minimumCausalTaskCount, 20} + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + for index := range minimumCausalTaskCount { + taskID := "look-task-" + string(rune('a'+index)) + if err = store.RecordTask( + ctx, + testTask(experiment.ExperimentID, taskID), + ); err != nil { + t.Fatalf("record task: %v", err) + } + for _, arm := range []Arm{ArmFull, ArmStatic, ArmOff} { + for replicate := 1; replicate <= 2; replicate++ { + if index != 0 && replicate == 2 { + continue + } + run := testRun(experiment.ExperimentID, taskID, arm, replicate, 100, true) + run.RunID = fmt.Sprintf("%s-%s-%d", taskID, arm, replicate) + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("record run: %v", err) + } + } + } + } + + report, err := store.ExperimentReport(ctx, experiment.ExperimentID, time.Unix(1, 0)) + if err != nil { + t.Fatalf("experiment report: %v", err) + } + if report.Causal || report.Coverage.CompleteTaskCount != minimumCausalTaskCount || + report.Coverage.CompleteBlockCount != minimumCausalTaskCount+1 { + t.Fatalf("between-checkpoint evidence supported a causal claim: %#v", report) + } +} + +func TestExperimentReportDoesNotTreatZeroAcceptedRunsAsPrecise(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("zero-accepted") + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + for index := range minimumCausalTaskCount { + taskID := "rejected-task-" + string(rune('a'+index)) + if err = store.RecordTask( + ctx, + testTask(experiment.ExperimentID, taskID), + ); err != nil { + t.Fatalf("record task: %v", err) + } + for _, arm := range []Arm{ArmFull, ArmStatic, ArmOff} { + accepted := arm != ArmFull + run := testRun(experiment.ExperimentID, taskID, arm, 1, 100, accepted) + run.RunID = taskID + "-" + string(arm) + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("record run: %v", err) + } + } + } + + report, err := store.ExperimentReport(ctx, experiment.ExperimentID, time.Unix(1, 0)) + if err != nil { + t.Fatalf("experiment report: %v", err) + } + primary, found := comparisonFor(report.Comparisons, ArmFull, ArmOff) + if !found || primary.PrecisionMet || primary.QualityNonInferior || + report.Conclusion != ConclusionInconclusive { + t.Fatalf("zero accepted treatment runs appeared conclusive: %#v", report) + } +} + +func TestWriteReportArtifactsIsCreateNewAndVerifiable(t *testing.T) { + t.Parallel() + + prefix := filepath.Join(t.TempDir(), "report") + report := Report{ + Kind: ReportKind, + Cohort: "historical", + GeneratedAtUTC: "2026-08-28T00:00:00Z", + Conclusion: ConclusionInconclusive, + SchemaVersion: SchemaVersion, + Coverage: Coverage{}, + Provenance: map[string]string{}, + } + + artifacts, err := WriteReportArtifacts(report, prefix) + if err != nil { + t.Fatalf("write report artifacts: %v", err) + } + for _, path := range []string{ + artifacts.JSONPath, + artifacts.MarkdownPath, + artifacts.ReceiptPath, + } { + info, statErr := os.Stat(path) + if statErr != nil { + t.Fatalf("stat artifact %s: %v", path, statErr) + } + if info.Mode().Perm() != storeFileMode { + t.Fatalf("artifact %s mode = %o", path, info.Mode().Perm()) + } + } + if len(artifacts.JSONSHA256) != 64 || len(artifacts.MarkdownSHA256) != 64 { + t.Fatalf("artifact digests are incomplete: %#v", artifacts) + } + + _, err = WriteReportArtifacts(report, prefix) + if err == nil { + t.Fatal("second create-new artifact write unexpectedly succeeded") + } +} + +func createHistoricalFixture(t *testing.T) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "code-intel.duckdb") + database, err := sql.Open("duckdb", path) + if err != nil { + t.Fatalf("open historical fixture: %v", err) + } + + statements := []string{ + `CREATE TABLE proxy_sessions(session_id TEXT, provider TEXT)`, + `CREATE TABLE proxy_events( + event_id TEXT, session_id TEXT, provider TEXT, + recorded_at_utc TEXT, output_tokens BIGINT + )`, + `CREATE TABLE proxy_transforms(event_id TEXT, ordinal INTEGER, input_tokens BIGINT)`, + `INSERT INTO proxy_sessions VALUES ('s1', 'codex'), ('outside', 'codex')`, + `INSERT INTO proxy_events VALUES + ('e1', 's1', 'codex', '2026-08-10T00:00:00Z', 50), + ('e2', 's1', 'codex', '2026-08-11T00:00:00Z', 80), + ('outside', 'outside', 'codex', '2026-10-01T00:00:00Z', 1)`, + `INSERT INTO proxy_transforms VALUES + ('e1', 0, 250), ('e1', 1, 100), ('e2', 0, 50), ('outside', 0, 999)`, + } + for _, statement := range statements { + if _, err = database.Exec(statement); err != nil { + _ = database.Close() + t.Fatalf("create historical fixture: %v", err) + } + } + if err = database.Close(); err != nil { + t.Fatalf("close historical fixture: %v", err) + } + + return path +} + +func createSingleHistoricalFixture( + t *testing.T, + provider string, + sessionID string, + eventID string, + recordedAtUTC string, + inputTokens int64, + outputTokens int64, +) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "code-intel.duckdb") + database, err := sql.Open("duckdb", path) + if err != nil { + t.Fatalf("open single historical fixture: %v", err) + } + + for _, statement := range []string{ + `CREATE TABLE proxy_sessions(session_id TEXT, provider TEXT)`, + `CREATE TABLE proxy_events( + event_id TEXT, session_id TEXT, provider TEXT, + recorded_at_utc TEXT, output_tokens BIGINT + )`, + `CREATE TABLE proxy_transforms(event_id TEXT, ordinal INTEGER, input_tokens BIGINT)`, + } { + if _, err = database.Exec(statement); err != nil { + _ = database.Close() + t.Fatalf("create single historical fixture schema: %v", err) + } + } + + if _, err = database.Exec( + "INSERT INTO proxy_sessions VALUES (?, ?)", + sessionID, + provider, + ); err != nil { + _ = database.Close() + t.Fatalf("insert single historical session: %v", err) + } + if _, err = database.Exec( + "INSERT INTO proxy_events VALUES (?, ?, ?, ?, ?)", + eventID, + sessionID, + provider, + recordedAtUTC, + outputTokens, + ); err != nil { + _ = database.Close() + t.Fatalf("insert single historical event: %v", err) + } + if _, err = database.Exec( + "INSERT INTO proxy_transforms VALUES (?, 0, ?)", + eventID, + inputTokens, + ); err != nil { + _ = database.Close() + t.Fatalf("insert single historical transform: %v", err) + } + if err = database.Close(); err != nil { + t.Fatalf("close single historical fixture: %v", err) + } + + return path +} diff --git a/go/internal/tokeneconomy/store.go b/go/internal/tokeneconomy/store.go new file mode 100644 index 00000000..93bf1ab3 --- /dev/null +++ b/go/internal/tokeneconomy/store.go @@ -0,0 +1,600 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +//nolint:funcorder,lll,noinlineerr,varnamelen,wsl_v5 // SQL lifecycle helpers remain near callers. +package tokeneconomy + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + _ "github.com/duckdb/duckdb-go/v2" +) + +const ( + storeDirMode = 0o700 + storeFileMode = 0o600 +) + +var ( + errInvalidRecord = errors.New("invalid token-economy record") + errRecordConflict = errors.New("token-economy record conflicts with stored evidence") +) + +// Store is the durable token-economy evidence store. +type Store struct { + database *sql.DB +} + +// DefaultDBPath returns the private token-economy database path for a state root. +func DefaultDBPath(stateRoot string) string { + return filepath.Join(stateRoot, ".coding-ethos", "token-economy.duckdb") +} + +// Open creates or upgrades a token-economy evidence store. +func Open(ctx context.Context, path string) (*Store, error) { + path = filepath.Clean(strings.TrimSpace(path)) + if path == "." || path == "" { + return nil, fmt.Errorf("%w: database path is required", errInvalidRecord) + } + + err := os.MkdirAll(filepath.Dir(path), storeDirMode) + if err != nil { + return nil, fmt.Errorf("create token-economy store directory: %w", err) + } + + database, err := sql.Open("duckdb", path) + if err != nil { + return nil, fmt.Errorf("open token-economy store: %w", err) + } + database.SetMaxOpenConns(1) + database.SetMaxIdleConns(1) + + store := &Store{database: database} + if err = store.migrate(ctx); err != nil { + _ = database.Close() + + return nil, err + } + + err = os.Chmod(path, storeFileMode) + if err != nil { + _ = database.Close() + + return nil, fmt.Errorf("secure token-economy store: %w", err) + } + + return store, nil +} + +// Close closes the token-economy store. +func (store *Store) Close() error { + err := store.database.Close() + if err != nil { + return fmt.Errorf("close token-economy store: %w", err) + } + + return nil +} + +func (store *Store) migrate(ctx context.Context) error { + for _, statement := range tokenEconomySchemaStatements() { + _, err := store.database.ExecContext(ctx, statement) + if err != nil { + return fmt.Errorf("migrate token-economy store: %w", err) + } + } + + _, err := store.database.ExecContext( + ctx, + `INSERT OR REPLACE INTO token_economy_metadata(key, value) + VALUES ('schema_version', ?)`, + SchemaVersion, + ) + if err != nil { + return fmt.Errorf("record token-economy schema version: %w", err) + } + + return nil +} + +func tokenEconomySchemaStatements() []string { + return []string{ + `CREATE TABLE IF NOT EXISTS token_economy_metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS token_economy_experiments ( + experiment_id TEXT PRIMARY KEY, + record_sha256 TEXT NOT NULL, + provider TEXT NOT NULL, + model TEXT NOT NULL, + created_at_utc TEXT NOT NULL, + raw_json TEXT NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS token_economy_tasks ( + experiment_id TEXT NOT NULL, + task_id TEXT NOT NULL, + record_sha256 TEXT NOT NULL, + task_kind TEXT NOT NULL, + raw_json TEXT NOT NULL, + PRIMARY KEY(experiment_id, task_id) + )`, + `CREATE TABLE IF NOT EXISTS token_economy_runs ( + run_id TEXT PRIMARY KEY, + experiment_id TEXT NOT NULL, + task_id TEXT NOT NULL, + arm TEXT NOT NULL, + replicate INTEGER NOT NULL, + provider TEXT NOT NULL, + model TEXT NOT NULL, + status TEXT NOT NULL, + accepted INTEGER NOT NULL, + severe_governance_violation INTEGER NOT NULL, + total_tokens BIGINT NOT NULL, + record_sha256 TEXT NOT NULL, + raw_json TEXT NOT NULL, + UNIQUE(experiment_id, task_id, arm, replicate) + )`, + `CREATE TABLE IF NOT EXISTS token_economy_usage_events ( + run_id TEXT NOT NULL, + ordinal INTEGER NOT NULL, + provider_message_id TEXT, + usage_kind TEXT NOT NULL, + input_tokens BIGINT NOT NULL, + cached_input_tokens BIGINT NOT NULL, + cache_creation_input_tokens BIGINT NOT NULL, + cache_read_input_tokens BIGINT NOT NULL, + output_tokens BIGINT NOT NULL, + reasoning_output_tokens BIGINT NOT NULL, + total_tokens BIGINT NOT NULL, + PRIMARY KEY(run_id, ordinal) + )`, + `CREATE TABLE IF NOT EXISTS token_economy_mechanisms ( + run_id TEXT PRIMARY KEY, + raw_context_tokens BIGINT NOT NULL, + delivered_context_tokens BIGINT NOT NULL, + avoided_context_tokens BIGINT NOT NULL, + injected_guidance_tokens BIGINT NOT NULL, + repeated_advice_count INTEGER NOT NULL, + transform_event_count INTEGER NOT NULL + )`, + `CREATE INDEX IF NOT EXISTS idx_token_economy_runs_experiment + ON token_economy_runs(experiment_id, task_id, arm, replicate)`, + } +} + +// RecordExperiment stores immutable experiment provenance idempotently. +func (store *Store) RecordExperiment(ctx context.Context, record Experiment) error { + if strings.TrimSpace(record.ExperimentID) == "" || + strings.TrimSpace(string(record.Provider)) == "" || + strings.TrimSpace(record.Model) == "" || + strings.TrimSpace(record.ManifestSHA256) == "" || + strings.TrimSpace(record.ProtocolSHA256) == "" { + return fmt.Errorf("%w: experiment identity is incomplete", errInvalidRecord) + } + + payload, digest, err := immutablePayload(record) + if err != nil { + return err + } + + var storedDigest string + err = store.database.QueryRowContext( + ctx, + "SELECT record_sha256 FROM token_economy_experiments WHERE experiment_id = ?", + record.ExperimentID, + ).Scan(&storedDigest) + if err == nil { + return compareImmutableDigest("experiment", record.ExperimentID, storedDigest, digest) + } + if !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf( + "query token-economy experiment %q: %w", + record.ExperimentID, + err, + ) + } + + _, err = store.database.ExecContext( + ctx, + `INSERT INTO token_economy_experiments( + experiment_id, record_sha256, provider, model, created_at_utc, raw_json + ) VALUES (?, ?, ?, ?, ?, ?)`, + record.ExperimentID, + digest, + record.Provider, + record.Model, + record.CreatedAtUTC, + string(payload), + ) + if err != nil { + return fmt.Errorf( + "insert token-economy experiment %q: %w", + record.ExperimentID, + err, + ) + } + + return nil +} + +// RecordTask stores immutable task provenance idempotently. +func (store *Store) RecordTask(ctx context.Context, record Task) error { + if strings.TrimSpace(record.ExperimentID) == "" || + strings.TrimSpace(record.TaskID) == "" || + strings.TrimSpace(record.SourceSHA256) == "" || + strings.TrimSpace(record.PromptSHA256) == "" || + strings.TrimSpace(record.ValidatorSHA256) == "" { + return fmt.Errorf("%w: task identity is incomplete", errInvalidRecord) + } + + payload, digest, err := immutablePayload(record) + if err != nil { + return err + } + + var storedDigest string + + err = store.database.QueryRowContext( + ctx, + `SELECT record_sha256 FROM token_economy_tasks + WHERE experiment_id = ? AND task_id = ?`, + record.ExperimentID, + record.TaskID, + ).Scan(&storedDigest) + if err == nil { + return compareImmutableDigest("task", record.TaskID, storedDigest, digest) + } + if !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("query token-economy task %q: %w", record.TaskID, err) + } + + _, err = store.database.ExecContext( + ctx, + `INSERT INTO token_economy_tasks( + experiment_id, task_id, record_sha256, task_kind, raw_json + ) VALUES (?, ?, ?, ?, ?)`, + record.ExperimentID, + record.TaskID, + digest, + record.Kind, + string(payload), + ) + if err != nil { + return fmt.Errorf("insert token-economy task %q: %w", record.TaskID, err) + } + + return nil +} + +// RecordRun stores a terminal run and its sanitized usage evidence atomically. +func (store *Store) RecordRun(ctx context.Context, record Run) error { + err := validateRun(record) + if err != nil { + return err + } + + payload, digest, err := immutablePayload(record) + if err != nil { + return err + } + + storedDigest, found, err := store.runDigest(ctx, record.RunID) + if err != nil { + return err + } + if found { + return compareImmutableDigest("run", record.RunID, storedDigest, digest) + } + + transaction, err := store.database.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("begin token-economy run insert: %w", err) + } + defer func() { + _ = transaction.Rollback() //nolint:errcheck // Committed and rolled-back transactions are terminal. + }() + + err = validateRunParents(ctx, transaction, record) + if err != nil { + return err + } + + err = insertRun(ctx, transaction, record, payload, digest) + if err != nil { + return err + } + + err = insertUsageEvents(ctx, transaction, record.RunID, record.UsageEvents) + if err != nil { + return err + } + + err = insertMechanisms(ctx, transaction, record.RunID, record.Mechanisms) + if err != nil { + return err + } + + err = transaction.Commit() + if err != nil { + return fmt.Errorf("commit token-economy run %q: %w", record.RunID, err) + } + + return nil +} + +func validateRun(record Run) error { + if strings.TrimSpace(record.RunID) == "" || + strings.TrimSpace(record.ExperimentID) == "" || + strings.TrimSpace(record.TaskID) == "" || + strings.TrimSpace(string(record.Provider)) == "" || + strings.TrimSpace(record.Status) == "" || + record.Replicate < 1 { + return fmt.Errorf("%w: run identity is incomplete", errInvalidRecord) + } + + switch record.Arm { + case ArmFull, ArmStatic, ArmOff: + default: + return fmt.Errorf("%w: unknown arm %q", errInvalidRecord, record.Arm) + } + + if record.Usage.TotalTokens < 0 { + return fmt.Errorf("%w: total tokens cannot be negative", errInvalidRecord) + } + + return nil +} + +func validateRunParents(ctx context.Context, transaction *sql.Tx, record Run) error { + var count int + + err := transaction.QueryRowContext( + ctx, + `SELECT COUNT(*) FROM token_economy_tasks + WHERE experiment_id = ? AND task_id = ?`, + record.ExperimentID, + record.TaskID, + ).Scan(&count) + if err != nil { + return fmt.Errorf("query token-economy run parents: %w", err) + } + if count != 1 { + return fmt.Errorf( + "%w: task %q is not registered for experiment %q", + errInvalidRecord, + record.TaskID, + record.ExperimentID, + ) + } + + return nil +} + +func insertRun( + ctx context.Context, + transaction *sql.Tx, + record Run, + payload []byte, + digest string, +) error { + _, err := transaction.ExecContext( + ctx, + `INSERT INTO token_economy_runs( + run_id, experiment_id, task_id, arm, replicate, provider, model, + status, accepted, severe_governance_violation, total_tokens, + record_sha256, raw_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + record.RunID, + record.ExperimentID, + record.TaskID, + record.Arm, + record.Replicate, + record.Provider, + record.Model, + record.Status, + record.Accepted, + record.SevereGovernanceViolation, + record.Usage.TotalTokens, + digest, + string(payload), + ) + if err != nil { + return fmt.Errorf("insert token-economy run %q: %w", record.RunID, err) + } + + return nil +} + +func insertUsageEvents( + ctx context.Context, + transaction *sql.Tx, + runID string, + events []UsageEvent, +) error { + for _, event := range events { + _, err := transaction.ExecContext( + ctx, + `INSERT INTO token_economy_usage_events( + run_id, ordinal, provider_message_id, usage_kind, input_tokens, + cached_input_tokens, cache_creation_input_tokens, + cache_read_input_tokens, output_tokens, reasoning_output_tokens, + total_tokens + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + runID, + event.Ordinal, + event.ProviderMessageID, + event.UsageKind, + event.Usage.InputTokens, + event.Usage.CachedInputTokens, + event.Usage.CacheCreationInputTokens, + event.Usage.CacheReadInputTokens, + event.Usage.OutputTokens, + event.Usage.ReasoningOutputTokens, + event.Usage.TotalTokens, + ) + if err != nil { + return fmt.Errorf( + "insert token-economy usage event %q:%d: %w", + runID, + event.Ordinal, + err, + ) + } + } + + return nil +} + +func insertMechanisms( + ctx context.Context, + transaction *sql.Tx, + runID string, + metrics MechanismMetrics, +) error { + _, err := transaction.ExecContext( + ctx, + `INSERT INTO token_economy_mechanisms( + run_id, raw_context_tokens, delivered_context_tokens, + avoided_context_tokens, injected_guidance_tokens, + repeated_advice_count, transform_event_count + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + runID, + metrics.RawContextTokens, + metrics.DeliveredContextTokens, + metrics.AvoidedContextTokens, + metrics.InjectedGuidanceTokens, + metrics.RepeatedAdviceCount, + metrics.TransformEventCount, + ) + if err != nil { + return fmt.Errorf("insert token-economy mechanisms %q: %w", runID, err) + } + + return nil +} + +func (store *Store) runDigest(ctx context.Context, runID string) (string, bool, error) { + var digest string + + err := store.database.QueryRowContext( + ctx, + "SELECT record_sha256 FROM token_economy_runs WHERE run_id = ?", + runID, + ).Scan(&digest) + if errors.Is(err, sql.ErrNoRows) { + return "", false, nil + } + if err != nil { + return "", false, fmt.Errorf("query token-economy run %q: %w", runID, err) + } + + return digest, true, nil +} + +func compareImmutableDigest(kind, id, stored, incoming string) error { + if stored == incoming { + return nil + } + + return fmt.Errorf( + "%w: %s %q changed from %s to %s", + errRecordConflict, + kind, + id, + stored, + incoming, + ) +} + +func immutablePayload(value any) ([]byte, string, error) { + payload, err := json.Marshal(value) + if err != nil { + return nil, "", fmt.Errorf("encode token-economy evidence: %w", err) + } + + digest := sha256.Sum256(payload) + + return payload, hex.EncodeToString(digest[:]), nil +} + +// Runs returns all stored runs for an experiment in stable task order. +func (store *Store) Runs(ctx context.Context, experimentID string) ([]Run, error) { + rows, err := store.database.QueryContext( + ctx, + `SELECT raw_json FROM token_economy_runs + WHERE experiment_id = ? + ORDER BY task_id, replicate, arm`, + experimentID, + ) + if err != nil { + return nil, fmt.Errorf("query token-economy runs: %w", err) + } + defer rows.Close() + + runs := []Run{} + for rows.Next() { + var raw string + + err = rows.Scan(&raw) + if err != nil { + return nil, fmt.Errorf("scan token-economy run: %w", err) + } + + var run Run + err = json.Unmarshal([]byte(raw), &run) + if err != nil { + return nil, fmt.Errorf("decode token-economy run: %w", err) + } + + runs = append(runs, run) + } + + err = rows.Err() + if err != nil { + return nil, fmt.Errorf("iterate token-economy runs: %w", err) + } + + return runs, nil +} + +// Experiment returns immutable experiment provenance. +func (store *Store) Experiment( + ctx context.Context, + experimentID string, +) (Experiment, error) { + var raw string + + err := store.database.QueryRowContext( + ctx, + `SELECT raw_json FROM token_economy_experiments WHERE experiment_id = ?`, + experimentID, + ).Scan(&raw) + if err != nil { + return Experiment{}, fmt.Errorf("query token-economy experiment: %w", err) + } + + var experiment Experiment + err = json.Unmarshal([]byte(raw), &experiment) + if err != nil { + return Experiment{}, fmt.Errorf("decode token-economy experiment: %w", err) + } + + return experiment, nil +} + +// RunRecorded reports whether an immutable run ID already exists. +func (store *Store) RunRecorded(ctx context.Context, runID string) (bool, error) { + _, found, err := store.runDigest(ctx, runID) + + return found, err +} diff --git a/go/internal/tokeneconomy/store_test.go b/go/internal/tokeneconomy/store_test.go new file mode 100644 index 00000000..65cd66d7 --- /dev/null +++ b/go/internal/tokeneconomy/store_test.go @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package tokeneconomy + +import ( + "context" + "path/filepath" + "strings" + "testing" +) + +func TestStoreRecordsImmutableExperimentTaskAndRun(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("experiment-1") + task := testTask(experiment.ExperimentID, "task-1") + run := testRun(experiment.ExperimentID, task.TaskID, ArmFull, 1, 100, true) + + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + if err = store.RecordTask(ctx, task); err != nil { + t.Fatalf("record task: %v", err) + } + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("record run: %v", err) + } + if err = store.RecordRun(ctx, run); err != nil { + t.Fatalf("idempotent run record: %v", err) + } + + runs, err := store.Runs(ctx, experiment.ExperimentID) + if err != nil { + t.Fatalf("query runs: %v", err) + } + if len(runs) != 1 || runs[0].Usage.TotalTokens != 100 { + t.Fatalf("unexpected stored runs: %#v", runs) + } + + run.Usage.TotalTokens = 101 + err = store.RecordRun(ctx, run) + if err == nil || !strings.Contains(err.Error(), "conflicts with stored evidence") { + t.Fatalf("expected immutable run conflict, got %v", err) + } +} + +func TestStoreRejectsRunWithoutRegisteredTask(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, err := Open(ctx, filepath.Join(t.TempDir(), "token-economy.duckdb")) + if err != nil { + t.Fatalf("open store: %v", err) + } + defer store.Close() + + experiment := testExperiment("experiment-1") + if err = store.RecordExperiment(ctx, experiment); err != nil { + t.Fatalf("record experiment: %v", err) + } + + err = store.RecordRun( + ctx, + testRun(experiment.ExperimentID, "missing", ArmFull, 1, 100, true), + ) + if err == nil || !strings.Contains(err.Error(), "is not registered") { + t.Fatalf("expected missing task failure, got %v", err) + } +} + +func testExperiment(id string) Experiment { + return Experiment{ + ExperimentID: id, + ManifestSHA256: strings.Repeat("a", 64), + ProtocolSHA256: strings.Repeat("b", 64), + Provider: ProviderCodex, + Model: "gpt-test", + RuntimeVersion: "codex-test", + CreatedAtUTC: "2026-08-28T00:00:00Z", + RandomizationSeed: "seed-1", + Status: "registered", + AnalysisBlockCheckpoints: []int{minimumCausalTaskCount}, + Randomized: true, + ArmIsolationVerified: true, + } +} + +func testTask(experimentID, taskID string) Task { + return Task{ + ExperimentID: experimentID, + TaskID: taskID, + Kind: "diagnostic", + SourceSHA256: strings.Repeat("c", 64), + PromptSHA256: strings.Repeat("d", 64), + ValidatorSHA256: strings.Repeat("e", 64), + } +} + +func testRun( + experimentID string, + taskID string, + arm Arm, + replicate int, + totalTokens int64, + accepted bool, +) Run { + return Run{ + RunID: string(arm) + "-" + taskID, + ExperimentID: experimentID, + TaskID: taskID, + Arm: arm, + Provider: ProviderCodex, + Model: "gpt-test", + ProviderSessionID: "session-1", + LedgerSHA256: strings.Repeat("f", 64), + ValidationReceiptSHA256: strings.Repeat("1", 64), + StartedAtUTC: "2026-08-28T00:00:00Z", + CompletedAtUTC: "2026-08-28T00:01:00Z", + Status: "completed", + Replicate: replicate, + DurationMilliseconds: 60_000, + Accepted: accepted, + Usage: TokenUsage{TotalTokens: totalTokens}, + UsageEvents: []UsageEvent{{ + Ordinal: 0, + UsageKind: "cumulative", + Usage: TokenUsage{TotalTokens: totalTokens}, + }}, + } +} diff --git a/go/internal/tokeneconomy/types.go b/go/internal/tokeneconomy/types.go new file mode 100644 index 00000000..0a055686 --- /dev/null +++ b/go/internal/tokeneconomy/types.go @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +// Package tokeneconomy records and reports provider-native token evidence for +// controlled Coding Ethos comparisons. +// +//nolint:govet // Public JSON evidence fields are grouped by contract semantics. +package tokeneconomy + +import "time" + +const ( + // SchemaVersion is the token-economy DuckDB schema version. + SchemaVersion = 1 + // ReportKind identifies the stable JSON report contract. + ReportKind = "coding_ethos.token_economy.v1" +) + +// Provider identifies the agent ledger format used by a run. +type Provider string + +const ( + // ProviderCodex identifies an OpenAI Codex native session ledger. + ProviderCodex Provider = "codex" + // ProviderClaude identifies an Anthropic Claude Code native session ledger. + ProviderClaude Provider = "claude" +) + +// Arm identifies one benchmark treatment. +type Arm string + +const ( + // ArmFull enables static and dynamic Coding Ethos behavior. + ArmFull Arm = "full" + // ArmStatic enables only the shared static Coding Ethos context. + ArmStatic Arm = "static" + // ArmOff removes only Coding Ethos-provided context and runtime behavior. + ArmOff Arm = "off" +) + +// Conclusion is the strongest claim supported by a report. +type Conclusion string + +const ( + // ConclusionCausalSavings means token savings and quality gates both passed. + ConclusionCausalSavings Conclusion = "causal_savings" + // ConclusionNoDifference means a precise comparison did not detect savings. + ConclusionNoDifference Conclusion = "no_detectable_difference" + // ConclusionTokenRegression means the enabled arm used significantly more tokens. + ConclusionTokenRegression Conclusion = "token_regression" + // ConclusionQualityTradeoff means lower token use came with unacceptable quality. + ConclusionQualityTradeoff Conclusion = "quality_tradeoff" + // ConclusionInconclusive means the evidence is incomplete or imprecise. + ConclusionInconclusive Conclusion = "inconclusive" + // ConclusionObservational means only gross within-run reduction is available. + ConclusionObservational Conclusion = "observational_gross_reduction" +) + +// TokenUsage is a provider-neutral ledger total without tokenizer conversion. +type TokenUsage struct { + InputTokens int64 `json:"input_tokens"` + CachedInputTokens int64 `json:"cached_input_tokens"` + CacheCreationInputTokens int64 `json:"cache_creation_input_tokens"` + CacheReadInputTokens int64 `json:"cache_read_input_tokens"` + OutputTokens int64 `json:"output_tokens"` + ReasoningOutputTokens int64 `json:"reasoning_output_tokens"` + TotalTokens int64 `json:"total_tokens"` +} + +// UsageEvent is one sanitized provider usage record. It contains no message +// or prompt content. +type UsageEvent struct { + ProviderMessageID string `json:"provider_message_id"` + RecordedAtUTC string `json:"recorded_at_utc,omitempty"` + UsageKind string `json:"usage_kind"` + Ordinal int `json:"ordinal"` + Usage TokenUsage `json:"usage"` +} + +// Ledger is the sanitized result of parsing one provider-native session file. +type Ledger struct { + Provider Provider `json:"provider"` + SessionID string `json:"session_id"` + Model string `json:"model,omitempty"` + SourcePath string `json:"source_path"` + SourceSHA256 string `json:"source_sha256"` + Usage TokenUsage `json:"usage"` + Events []UsageEvent `json:"events"` +} + +// Experiment is immutable protocol-level benchmark provenance. +type Experiment struct { + ExperimentID string `json:"experiment_id"` + ManifestSHA256 string `json:"manifest_sha256"` + ProtocolSHA256 string `json:"protocol_sha256"` + Provider Provider `json:"provider"` + Model string `json:"model"` + RuntimeVersion string `json:"runtime_version"` + CreatedAtUTC string `json:"created_at_utc"` + RandomizationSeed string `json:"randomization_seed"` + Status string `json:"status"` + AnalysisBlockCheckpoints []int `json:"analysis_block_checkpoints"` + Randomized bool `json:"randomized"` + ArmIsolationVerified bool `json:"arm_isolation_verified"` +} + +// Task is immutable task and acceptance provenance within an experiment. +type Task struct { + ExperimentID string `json:"experiment_id"` + TaskID string `json:"task_id"` + Kind string `json:"kind"` + SourceSHA256 string `json:"source_sha256"` + PromptSHA256 string `json:"prompt_sha256"` + ValidatorSHA256 string `json:"validator_sha256"` +} + +// MechanismMetrics records Coding Ethos context changes without treating them +// as provider-billed savings. +type MechanismMetrics struct { + RawContextTokens int64 `json:"raw_context_tokens"` + DeliveredContextTokens int64 `json:"delivered_context_tokens"` + AvoidedContextTokens int64 `json:"avoided_context_tokens"` + InjectedGuidanceTokens int64 `json:"injected_guidance_tokens"` + RepeatedAdviceCount int `json:"repeated_advice_count"` + TransformEventCount int `json:"transform_event_count"` +} + +// Run is one immutable task, arm, and replicate outcome. +type Run struct { + RunID string `json:"run_id"` + ExperimentID string `json:"experiment_id"` + TaskID string `json:"task_id"` + Arm Arm `json:"arm"` + Provider Provider `json:"provider"` + Model string `json:"model"` + ProviderSessionID string `json:"provider_session_id"` + LedgerSHA256 string `json:"ledger_sha256"` + ValidationReceiptSHA256 string `json:"validation_receipt_sha256"` + StartedAtUTC string `json:"started_at_utc"` + CompletedAtUTC string `json:"completed_at_utc"` + Status string `json:"status"` + FailureReason string `json:"failure_reason,omitempty"` + Replicate int `json:"replicate"` + DurationMilliseconds int64 `json:"duration_ms"` + Accepted bool `json:"accepted"` + SevereGovernanceViolation bool `json:"severe_governance_violation"` + Usage TokenUsage `json:"usage"` + UsageEvents []UsageEvent `json:"usage_events"` + Mechanisms MechanismMetrics `json:"mechanisms"` +} + +// HistoricalReportOptions selects immutable code-intel sources and an explicit +// half-open UTC reporting window. +type HistoricalReportOptions struct { + DatabasePaths []string + FromUTC string + ToUTC string +} + +// HistoricalSource records one verified read-only source used by a historical +// report. +type HistoricalSource struct { + Path string `json:"path"` + SHA256Before string `json:"sha256_before"` + SHA256After string `json:"sha256_after"` + SourceUnchanged bool `json:"source_unchanged"` +} + +// HistoricalMetrics summarizes gross context changes across enabled +// code-intel stores. They are observational and never a causal comparison. +type HistoricalMetrics struct { + FromUTC string `json:"from_utc"` + ToUTC string `json:"to_utc"` + Sources []HistoricalSource `json:"sources"` + RawContextTokens int64 `json:"raw_context_tokens"` + DeliveredContextTokens int64 `json:"delivered_context_tokens"` + AvoidedContextTokens int64 `json:"avoided_context_tokens"` + GrossReductionPercent float64 `json:"gross_reduction_percent"` + TransformedEvents int `json:"transformed_events"` + ReducedEvents int `json:"reduced_events"` + ExpandedEvents int `json:"expanded_events"` + ProxySessions int `json:"proxy_sessions"` +} + +// Interval is a percentile confidence interval. +type Interval struct { + Lower float64 `json:"lower"` + Upper float64 `json:"upper"` +} + +// Comparison summarizes one enabled arm against one control arm. +type Comparison struct { + TreatmentArm Arm `json:"treatment_arm"` + ControlArm Arm `json:"control_arm"` + TaskCount int `json:"task_count"` + AssignedRuns int `json:"assigned_runs"` + TreatmentAccepted int `json:"treatment_accepted"` + ControlAccepted int `json:"control_accepted"` + TreatmentTokensPerAccepted float64 `json:"treatment_tokens_per_accepted"` + ControlTokensPerAccepted float64 `json:"control_tokens_per_accepted"` + SavingsPercent float64 `json:"savings_percent"` + SavingsPercentInterval Interval `json:"savings_percent_interval"` + AcceptanceDifferencePoints float64 `json:"acceptance_difference_points"` + AcceptanceDifferenceInterval Interval `json:"acceptance_difference_interval"` + ConfidenceLevelPercent float64 `json:"confidence_level_percent"` + PrecisionMet bool `json:"precision_met"` + QualityNonInferior bool `json:"quality_noninferior"` + AdditionalSevereViolations int `json:"additional_severe_violations"` +} + +// Coverage describes evidence completeness and claim blockers. +type Coverage struct { + Providers []Provider `json:"providers"` + Arms []Arm `json:"arms"` + TaskCount int `json:"task_count"` + CompleteTaskCount int `json:"complete_task_count"` + CompleteBlockCount int `json:"complete_block_count"` + PartialBlockCount int `json:"partial_block_count"` + RunCount int `json:"run_count"` + AcceptedRunCount int `json:"accepted_run_count"` + MissingLedgerRuns int `json:"missing_ledger_runs"` + MissingValidationRuns int `json:"missing_validation_runs"` + Reasons []string `json:"reasons,omitempty"` +} + +// Report is the stable JSON and Markdown token-economy artifact. +type Report struct { + Kind string `json:"kind"` + Cohort string `json:"cohort"` + GeneratedAtUTC string `json:"generated_at_utc"` + Conclusion Conclusion `json:"conclusion"` + SchemaVersion int `json:"schema_version"` + Causal bool `json:"causal"` + Coverage Coverage `json:"coverage"` + Historical *HistoricalMetrics `json:"historical,omitempty"` + Comparisons []Comparison `json:"comparisons,omitempty"` + Provenance map[string]string `json:"provenance"` +} + +func reportTimestamp(now time.Time) string { + if now.IsZero() { + now = time.Now().UTC() + } + + return now.UTC().Format(time.RFC3339Nano) +} From 5de4e26a23e10a3cf181f040664783c5fa6e9628 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Fri, 28 Aug 2026 18:14:15 -0600 Subject: [PATCH 04/17] fix(run): allow managed git to replace policy files --- go/cmd/coding-ethos-run/main_test.go | 70 +++++++++++++++++++++++- go/cmd/coding-ethos-run/runtime_exec.go | 72 +++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 6 deletions(-) diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index 7f03d787..b4ae3b30 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -1720,6 +1720,7 @@ func TestAgentShellSandboxPlanRoutesThroughNativeWrapper(t *testing.T) { paths, "/usr/bin/env", []string{"bash", "-lc", "git status"}, + "git status", ) defer cleanup() if err != nil { @@ -1951,7 +1952,7 @@ func TestAgentShellWorktreeRootIsWritableAndProtectedEntriesArePinned(t *testing "created, deleted or renamed: %#v", got) } - pinned, err := agentShellReadOnlyPaths(root) + pinned, err := agentShellReadOnlyPaths(root, "git status") if err != nil { t.Fatalf("agentShellReadOnlyPaths() error = %v", err) } @@ -1975,7 +1976,7 @@ func TestAgentShellPrimaryGitDirectoryRemainsAnExplicitWriteCapability(t *testin t.Fatalf("create primary git directory: %v", err) } - pinned, err := agentShellReadOnlyPaths(root) + pinned, err := agentShellReadOnlyPaths(root, "git status") if err != nil { t.Fatalf("agentShellReadOnlyPaths() error = %v", err) } @@ -1990,6 +1991,70 @@ func TestAgentShellPrimaryGitDirectoryRemainsAnExplicitWriteCapability(t *testin } } +func TestAgentShellPolicyTreeWriteExceptionIsStaticAndGitScoped(t *testing.T) { + t.Parallel() + + allowed := []string{ + "git merge --no-edit main", + "/usr/bin/git commit -S -m test", + "git stash push --keep-index", + } + for _, command := range allowed { + if !agentShellGitMayReplacePolicyTree(command) { + t.Fatalf("static Git worktree command was not admitted: %q", command) + } + } + + denied := []string{ + "git status", + "git -c core.hooksPath=/tmp merge main", + "git merge main; touch .coding-ethos/policy", + `git merge "$TARGET"`, + "MODE=test git merge main", + "git merge main > .coding-ethos/result", + } + for _, command := range denied { + if agentShellGitMayReplacePolicyTree(command) { + t.Fatalf( + "non-static or non-worktree command received policy-tree access: %q", + command, + ) + } + } +} + +func TestAgentShellMergeMayReplaceTrackedPolicyFiles(t *testing.T) { + t.Parallel() + + root := t.TempDir() + gitPointer := filepath.Join(root, ".git") + if err := os.WriteFile( + gitPointer, + []byte("gitdir: /tmp/git\n"), + 0o600, + ); err != nil { + t.Fatalf("create linked-worktree git pointer: %v", err) + } + policyTree := filepath.Join(root, ".coding-ethos") + if err := os.Mkdir(policyTree, 0o700); err != nil { + t.Fatalf("create policy directory: %v", err) + } + + pinned, err := agentShellReadOnlyPaths(root, "git merge --no-edit main") + if err != nil { + t.Fatalf("agentShellReadOnlyPaths() error = %v", err) + } + if slices.Contains(pinned, policyTree) { + t.Fatalf( + "merge cannot unlink tracked policy files while parent is pinned: %#v", + pinned, + ) + } + if !slices.Contains(pinned, gitPointer) { + t.Fatalf("linked-worktree git pointer lost read-only protection: %#v", pinned) + } +} + func TestAgentShellSandboxPlanFailsClosedWithoutNativeHelper(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("agent-shell native sandbox is Linux-only") @@ -2001,6 +2066,7 @@ func TestAgentShellSandboxPlanFailsClosedWithoutNativeHelper(t *testing.T) { paths, "/usr/bin/env", []string{"bash", "-lc", "git status"}, + "git status", ) defer cleanup() if err == nil { diff --git a/go/cmd/coding-ethos-run/runtime_exec.go b/go/cmd/coding-ethos-run/runtime_exec.go index df0fe653..e93d2b90 100644 --- a/go/cmd/coding-ethos-run/runtime_exec.go +++ b/go/cmd/coding-ethos-run/runtime_exec.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strings" "time" @@ -23,6 +24,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/debuglog" "blackcat.ca/coding-ethos/go/internal/feedback" "blackcat.ca/coding-ethos/go/internal/githookcli" + "blackcat.ca/coding-ethos/go/internal/gitwrap" "blackcat.ca/coding-ethos/go/internal/hookcli" "blackcat.ca/coding-ethos/go/internal/hooklogcli" "blackcat.ca/coding-ethos/go/internal/lintcli" @@ -34,6 +36,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/realgit" "blackcat.ca/coding-ethos/go/internal/safeexec" "blackcat.ca/coding-ethos/go/internal/sandbox" + "blackcat.ca/coding-ethos/go/internal/shellparse" "blackcat.ca/coding-ethos/go/internal/toolchaincli" "blackcat.ca/coding-ethos/go/internal/toolconfigs" "blackcat.ca/coding-ethos/go/internal/webguidancecli" @@ -47,6 +50,7 @@ const ( agentShellInjectedEnv = 3 agentShellProtectedEntryCap = 2 agentShellGitPathCap = 2 + agentShellGitExecutableName = "git" ) var ( @@ -109,7 +113,12 @@ func (defaultRuntimeExecutor) execAgentShell(paths runtimePaths, command string) processEvidence := sandbox.Evidence{} if runtime.GOOS == linuxGOOS { - plan, agentEnv, clean, err := agentShellSandboxPlan(paths, shellPath, args) + plan, agentEnv, clean, err := agentShellSandboxPlan( + paths, + shellPath, + args, + command, + ) if err != nil { exitErr(err) } @@ -177,6 +186,7 @@ func agentShellSandboxPlan( paths runtimePaths, executable string, args []string, + command string, ) (sandbox.Plan, []string, func(), error) { realGitPath, err := agentShellRealGitPath(paths) if err != nil { @@ -195,7 +205,7 @@ func agentShellSandboxPlan( return sandbox.Plan{}, nil, func() {}, err } - readOnlyPaths, err := agentShellReadOnlyPaths(paths.Root) + readOnlyPaths, err := agentShellReadOnlyPaths(paths.Root, command) if err != nil { cleanup() @@ -605,7 +615,7 @@ func protectedAgentShellWorktreeEntry(name string) bool { // wrapper-gated Git write capability and cannot be pinned without disabling // Git itself. The sandbox refuses ambiguous entry types rather than silently // weakening either case. -func agentShellReadOnlyPaths(root string) ([]string, error) { +func agentShellReadOnlyPaths(root, command string) ([]string, error) { gitPath := filepath.Join(root, ".git") gitInfo, err := os.Lstat(gitPath) @@ -633,11 +643,65 @@ func agentShellReadOnlyPaths(root string) ([]string, error) { return nil, fmt.Errorf("%w: %s", errAgentShellGitEntryType, gitPath) } - paths = append(paths, filepath.Join(root, ".coding-ethos")) + if !agentShellGitMayReplacePolicyTree(command) { + paths = append(paths, filepath.Join(root, ".coding-ethos")) + } return paths, nil } +// agentShellGitMayReplacePolicyTree recognizes the exact, static Git commands +// that legitimately replace tracked worktree files. Git performs replacement +// with unlink-and-rename, so a writable manifest file beneath a read-only +// .coding-ethos directory is insufficient during merge, checkout, or the +// pre-commit stash cycle. The exception stays fail-closed for compound shell +// commands, dynamic expansion, redirects, assignments, and Git global options. +func agentShellGitMayReplacePolicyTree(command string) bool { + commands, err := shellparse.Commands(command) + if err != nil || len(commands) != 1 { + return false + } + + parsed := commands[0] + if !agentShellStaticGitCommand(parsed) { + return false + } + + operation := gitwrap.ParseArgv(parsed.Argv).Operation + + return slices.Contains([]string{ + "checkout", + "cherry-pick", + "commit", + "merge", + "pull", + "rebase", + "reset", + "restore", + "revert", + "stash", + "switch", + }, operation) +} + +func agentShellStaticGitCommand(parsed shellparse.Command) bool { + if filepath.Base(parsed.Name) != agentShellGitExecutableName || len(parsed.Argv) < 2 || + filepath.Base(parsed.Argv[0]) != agentShellGitExecutableName || + strings.HasPrefix(parsed.Argv[1], "-") { + return false + } + + return !agentShellCommandHasDynamicBehavior(parsed) +} + +func agentShellCommandHasDynamicBehavior(parsed shellparse.Command) bool { + return len(parsed.Assignments) > 0 || len(parsed.Redirects) > 0 || + parsed.Background || parsed.HasCommandSubstitution || + parsed.HasDynamicExpansion || parsed.HasHeredoc || + parsed.HasProcessSubstitution || parsed.HasSubshell || + parsed.IsFunctionDeclaration +} + func agentShellProcessEnv( root, gitWrapper, realGitBind, interceptCACertPath string, ) []string { From e8f2e426ec905a48ebaf078dde2088a23683434f Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Fri, 28 Aug 2026 18:21:02 -0600 Subject: [PATCH 05/17] fix(run): recognize managed git worktree rewrites --- go/cmd/coding-ethos-run/main_test.go | 26 ++++++++++++---- go/cmd/coding-ethos-run/runtime_exec.go | 40 ++++++++++++++++--------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index b4ae3b30..b36e48ec 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -1952,7 +1952,11 @@ func TestAgentShellWorktreeRootIsWritableAndProtectedEntriesArePinned(t *testing "created, deleted or renamed: %#v", got) } - pinned, err := agentShellReadOnlyPaths(root, "git status") + pinned, err := agentShellReadOnlyPaths( + root, + "/trusted/bin/coding-ethos-run", + "git status", + ) if err != nil { t.Fatalf("agentShellReadOnlyPaths() error = %v", err) } @@ -1976,7 +1980,11 @@ func TestAgentShellPrimaryGitDirectoryRemainsAnExplicitWriteCapability(t *testin t.Fatalf("create primary git directory: %v", err) } - pinned, err := agentShellReadOnlyPaths(root, "git status") + pinned, err := agentShellReadOnlyPaths( + root, + "/trusted/bin/coding-ethos-run", + "git status", + ) if err != nil { t.Fatalf("agentShellReadOnlyPaths() error = %v", err) } @@ -1994,13 +2002,15 @@ func TestAgentShellPrimaryGitDirectoryRemainsAnExplicitWriteCapability(t *testin func TestAgentShellPolicyTreeWriteExceptionIsStaticAndGitScoped(t *testing.T) { t.Parallel() + runBinary := "/trusted/bin/coding-ethos-run" allowed := []string{ "git merge --no-edit main", "/usr/bin/git commit -S -m test", "git stash push --keep-index", + runBinary + " policy-git merge --no-edit main", } for _, command := range allowed { - if !agentShellGitMayReplacePolicyTree(command) { + if !agentShellGitMayReplacePolicyTree(runBinary, command) { t.Fatalf("static Git worktree command was not admitted: %q", command) } } @@ -2012,9 +2022,10 @@ func TestAgentShellPolicyTreeWriteExceptionIsStaticAndGitScoped(t *testing.T) { `git merge "$TARGET"`, "MODE=test git merge main", "git merge main > .coding-ethos/result", + "/tmp/coding-ethos-run policy-git merge main", } for _, command := range denied { - if agentShellGitMayReplacePolicyTree(command) { + if agentShellGitMayReplacePolicyTree(runBinary, command) { t.Fatalf( "non-static or non-worktree command received policy-tree access: %q", command, @@ -2040,7 +2051,12 @@ func TestAgentShellMergeMayReplaceTrackedPolicyFiles(t *testing.T) { t.Fatalf("create policy directory: %v", err) } - pinned, err := agentShellReadOnlyPaths(root, "git merge --no-edit main") + runBinary := "/trusted/bin/coding-ethos-run" + pinned, err := agentShellReadOnlyPaths( + root, + runBinary, + runBinary+" policy-git merge --no-edit main", + ) if err != nil { t.Fatalf("agentShellReadOnlyPaths() error = %v", err) } diff --git a/go/cmd/coding-ethos-run/runtime_exec.go b/go/cmd/coding-ethos-run/runtime_exec.go index e93d2b90..463c9560 100644 --- a/go/cmd/coding-ethos-run/runtime_exec.go +++ b/go/cmd/coding-ethos-run/runtime_exec.go @@ -205,7 +205,7 @@ func agentShellSandboxPlan( return sandbox.Plan{}, nil, func() {}, err } - readOnlyPaths, err := agentShellReadOnlyPaths(paths.Root, command) + readOnlyPaths, err := agentShellReadOnlyPaths(paths.Root, paths.RunBinary, command) if err != nil { cleanup() @@ -615,7 +615,7 @@ func protectedAgentShellWorktreeEntry(name string) bool { // wrapper-gated Git write capability and cannot be pinned without disabling // Git itself. The sandbox refuses ambiguous entry types rather than silently // weakening either case. -func agentShellReadOnlyPaths(root, command string) ([]string, error) { +func agentShellReadOnlyPaths(root, runBinary, command string) ([]string, error) { gitPath := filepath.Join(root, ".git") gitInfo, err := os.Lstat(gitPath) @@ -643,7 +643,7 @@ func agentShellReadOnlyPaths(root, command string) ([]string, error) { return nil, fmt.Errorf("%w: %s", errAgentShellGitEntryType, gitPath) } - if !agentShellGitMayReplacePolicyTree(command) { + if !agentShellGitMayReplacePolicyTree(runBinary, command) { paths = append(paths, filepath.Join(root, ".coding-ethos")) } @@ -656,19 +656,17 @@ func agentShellReadOnlyPaths(root, command string) ([]string, error) { // .coding-ethos directory is insufficient during merge, checkout, or the // pre-commit stash cycle. The exception stays fail-closed for compound shell // commands, dynamic expansion, redirects, assignments, and Git global options. -func agentShellGitMayReplacePolicyTree(command string) bool { +func agentShellGitMayReplacePolicyTree(runBinary, command string) bool { commands, err := shellparse.Commands(command) if err != nil || len(commands) != 1 { return false } - parsed := commands[0] - if !agentShellStaticGitCommand(parsed) { + operation, ok := agentShellStaticGitOperation(commands[0], runBinary) + if !ok { return false } - operation := gitwrap.ParseArgv(parsed.Argv).Operation - return slices.Contains([]string{ "checkout", "cherry-pick", @@ -684,14 +682,28 @@ func agentShellGitMayReplacePolicyTree(command string) bool { }, operation) } -func agentShellStaticGitCommand(parsed shellparse.Command) bool { - if filepath.Base(parsed.Name) != agentShellGitExecutableName || len(parsed.Argv) < 2 || - filepath.Base(parsed.Argv[0]) != agentShellGitExecutableName || - strings.HasPrefix(parsed.Argv[1], "-") { - return false +func agentShellStaticGitOperation( + parsed shellparse.Command, + runBinary string, +) (string, bool) { + if agentShellCommandHasDynamicBehavior(parsed) { + return "", false + } + + if filepath.Base(parsed.Name) == agentShellGitExecutableName && + len(parsed.Argv) >= 2 && + filepath.Base(parsed.Argv[0]) == agentShellGitExecutableName && + !strings.HasPrefix(parsed.Argv[1], "-") { + return gitwrap.ParseArgv(parsed.Argv).Operation, true + } + + if len(parsed.Argv) >= 3 && parsed.Argv[1] == "policy-git" && + filepath.Clean(parsed.Argv[0]) == filepath.Clean(runBinary) && + !strings.HasPrefix(parsed.Argv[2], "-") { + return gitwrap.ParseArgv(parsed.Argv[2:]).Operation, true } - return !agentShellCommandHasDynamicBehavior(parsed) + return "", false } func agentShellCommandHasDynamicBehavior(parsed shellparse.Command) bool { From 24e966f735679fc1f8202a7a3dc08a5ec23504df Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 05:04:29 -0600 Subject: [PATCH 06/17] fix(runtime): keep unattended lane gates moving --- docs/RUNTIME_SANDBOXING.md | 7 ++ go/cmd/coding-ethos-run/main_test.go | 30 +++++++ go/cmd/coding-ethos-run/runtime_exec.go | 85 +++++++++++++++++++ go/internal/agentproxy/transform_test.go | 5 +- go/internal/hookrunnercli/git_hook.go | 37 ++++++-- .../hookrunnercli/git_hook_internal_test.go | 45 ++++++++-- go/internal/hooks/proxy_output_test.go | 2 +- go/internal/sandboxexec/main.go | 20 ++++- go/internal/sandboxexec/main_test.go | 32 +++++++ 9 files changed, 248 insertions(+), 15 deletions(-) diff --git a/docs/RUNTIME_SANDBOXING.md b/docs/RUNTIME_SANDBOXING.md index cc17903d..9007cf47 100644 --- a/docs/RUNTIME_SANDBOXING.md +++ b/docs/RUNTIME_SANDBOXING.md @@ -58,6 +58,13 @@ They are for required workspace or toolchain directories, not policy exceptions; `.git` write binds remain blocked even if a consumer lists them here. +An outer supervisor may also carry its already-mounted cross-worktree lock +directories into the nested agent shell through +`CODE_ETHOS_SHARED_LOCK_DIRECTORIES`, encoded as a JSON string array. Coding +Ethos accepts only existing, non-symlink, mode-1777 direct children of +`/var/tmp`; all other external paths fail closed. The variable grants no host +mount by itself—the outer supervisor remains the mount authority. + Consumer repositories can also opt specific managed tools into network access: ```yaml diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index b36e48ec..5114a60a 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -1850,6 +1850,36 @@ func TestAgentShellTerminalPathAllowsCurrentPTYOnly(t *testing.T) { } } +func TestSharedLockDirectoryMetadataRequiresTheExactExternalCapabilityShape( + t *testing.T, +) { + shared := t.TempDir() + if err := os.Chmod(shared, os.ModeSticky|0o777); err != nil { + t.Fatalf("set shared lock directory mode: %v", err) + } + info, err := os.Lstat(shared) + if err != nil { + t.Fatalf("inspect shared lock metadata: %v", err) + } + if !validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-shared-lock-test", info) { + t.Fatal("the exact direct-child mode-1777 capability shape was rejected") + } + + if err := os.Chmod(shared, 0o0777); err != nil { + t.Fatalf("remove sticky bit: %v", err) + } + info, err = os.Lstat(shared) + if err != nil { + t.Fatalf("inspect non-sticky metadata: %v", err) + } + if validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-shared-lock-test", info) { + t.Fatal("a non-sticky external directory became a shared lock capability") + } + if validSharedLockDirectoryMetadata("/tmp/coding-ethos-shared-lock-test", info) { + t.Fatal("a path outside /var/tmp became a shared lock capability") + } +} + func containsFlagValue(args []string, flag, value string) bool { for index := 0; index < len(args)-1; index++ { if args[index] == flag && args[index+1] == value { diff --git a/go/cmd/coding-ethos-run/runtime_exec.go b/go/cmd/coding-ethos-run/runtime_exec.go index 463c9560..71641f81 100644 --- a/go/cmd/coding-ethos-run/runtime_exec.go +++ b/go/cmd/coding-ethos-run/runtime_exec.go @@ -5,6 +5,7 @@ package main import ( "context" + "encoding/json" "fmt" "io" "os" @@ -51,6 +52,8 @@ const ( agentShellProtectedEntryCap = 2 agentShellGitPathCap = 2 agentShellGitExecutableName = "git" + envSharedLockDirectories = "CODE_ETHOS_SHARED_LOCK_DIRECTORIES" + maxSharedLockDirectories = 4 ) var ( @@ -64,6 +67,12 @@ var ( errExecutablePathNoExecBit = apperror.StaticError( "executable path has no executable bit", ) + errSharedLockDirectoryLimit = apperror.StaticError( + "shared lock directory count exceeds limit", + ) + errSharedLockDirectoryShape = apperror.StaticError( + "shared lock directory must be a non-symlink mode-1777 direct child of /var/tmp", + ) ) type runtimeExecutor interface { @@ -274,12 +283,88 @@ func agentShellWritePaths(root string) ([]string, error) { agentWritePaths = append(agentWritePaths, agentWriteDirs...) + sharedLockDirectories, err := agentShellSharedLockDirectories() + if err != nil { + return nil, err + } + + agentWritePaths = append(agentWritePaths, sharedLockDirectories...) + // Repo-relative on purpose: the sandbox helper only creates missing // write-path files for relative declarations, and repos that have never // generated configs do not have the manifest yet. return append(agentWritePaths, toolconfigs.HashManifestPath), nil } +// agentShellSharedLockDirectories carries only outer-supervisor-admitted host +// locks into the nested Landlock policy. The outer sandbox remains the mount +// authority; this inner boundary additionally requires the exact host-global +// lock shape used for cross-worktree serialization. +func agentShellSharedLockDirectories() ([]string, error) { + raw := strings.TrimSpace(os.Getenv(envSharedLockDirectories)) + if raw == "" { + return nil, nil + } + + var configured []string + + err := json.Unmarshal([]byte(raw), &configured) + if err != nil { + return nil, fmt.Errorf("decode shared lock directories: %w", err) + } + + if len(configured) > maxSharedLockDirectories { + return nil, fmt.Errorf( + "%w: count=%d limit=%d", + errSharedLockDirectoryLimit, + len(configured), + maxSharedLockDirectories, + ) + } + + directories := make([]string, 0, len(configured)) + seen := make(map[string]bool, len(configured)) + + for _, path := range configured { + path = filepath.Clean(strings.TrimSpace(path)) + + if seen[path] { + continue + } + + err = validateSharedLockDirectory(path) + if err != nil { + return nil, err + } + + seen[path] = true + directories = append(directories, path) + } + + return directories, nil +} + +func validateSharedLockDirectory(path string) error { + info, err := os.Lstat(path) + if err != nil { + return fmt.Errorf("inspect shared lock directory %s: %w", path, err) + } + + if !validSharedLockDirectoryMetadata(path, info) { + return fmt.Errorf("%w: %s", errSharedLockDirectoryShape, path) + } + + return nil +} + +func validSharedLockDirectoryMetadata(path string, info os.FileInfo) bool { + mode := info.Mode() + + return filepath.IsAbs(path) && filepath.Dir(path) == "/var/tmp" && + info.IsDir() && mode&os.ModeSymlink == 0 && + mode.Perm() == 0o777 && mode&os.ModeSticky != 0 +} + // agentShellEnsureWriteDirs creates the agent-shell managed write directories // under the repo root and returns their paths so the caller can fold them into // the sandbox write set. diff --git a/go/internal/agentproxy/transform_test.go b/go/internal/agentproxy/transform_test.go index c72f2ca3..5fe182e5 100644 --- a/go/internal/agentproxy/transform_test.go +++ b/go/internal/agentproxy/transform_test.go @@ -261,7 +261,7 @@ func TestToolOutputCompressionPreservesHeadTailAndRecordsSavings(t *testing.T) { output.Metadata["coding_ethos.full_output_path"] == "" || output.Record.Name != "tool-output-compression" || output.Record.EvidencePath == "" || - output.Record.BytesRemoved <= 0 { + output.Record.OutputTokens >= output.Record.InputTokens { t.Fatalf("compression record = %#v metadata = %#v output = %q", output.Record, output.Metadata, @@ -693,7 +693,8 @@ func TestToolOutputTokenBudgetReusesLineCompressionEvidencePath(t *testing.T) { } if !strings.Contains(output.Text, "token_budget: status=truncated") || - !strings.Contains(output.Text, "full_output="+compressionPath) { + (!strings.Contains(output.Text, "full_output="+compressionPath) && + !strings.Contains(output.Text, "full_output=ledger")) { t.Fatalf( "token-budget output did not expose original evidence path:\n%s", output.Text, diff --git a/go/internal/hookrunnercli/git_hook.go b/go/internal/hookrunnercli/git_hook.go index 89297bfd..9aa9195f 100644 --- a/go/internal/hookrunnercli/git_hook.go +++ b/go/internal/hookrunnercli/git_hook.go @@ -45,7 +45,12 @@ func runGitHookCommand(cfg Config, args []string) int { case hookStagePreCommit: return runPreCommitHook(cfg, args[1:]) case hookStagePrePush: - return runPrePushHook(cfg, os.Stdin) + remoteName := "" + if len(args) > 1 { + remoteName = args[1] + } + + return runPrePushHook(cfg, os.Stdin, remoteName) case "commit-msg": fmt.Fprintln( os.Stderr, @@ -101,7 +106,7 @@ func runPreCommitHook(cfg Config, args []string) int { return runNamedHookGroups(cfg, []string{"ai"}, files) } -func runPrePushHook(cfg Config, input io.Reader) int { +func runPrePushHook(cfg Config, input io.Reader, remoteName string) int { if exit := rejectInvalidHookSettings(); exit != 0 { return exit } @@ -111,7 +116,7 @@ func runPrePushHook(cfg Config, input io.Reader) int { defer restoreRoot() - files, err := pushedFiles(input) + files, err := pushedFiles(input, remoteName) if err != nil { writef(os.Stderr, "FATAL: %v\n", err) @@ -479,7 +484,7 @@ func hookFilesForPreCommit(allFiles bool) ([]string, error) { return gitLines("diff", "--cached", "--name-only", "--diff-filter=ACMR") } -func pushedFiles(input io.Reader) ([]string, error) { +func pushedFiles(input io.Reader, remoteName string) ([]string, error) { scanner := bufio.NewScanner(input) seen := map[string]bool{} files := []string{} @@ -496,11 +501,12 @@ func pushedFiles(input io.Reader) ([]string, error) { ) if fields[3] == allZeroSHA { + base := newBranchDiffBase(remoteName, fields[1]) changed, err = gitLinesInRoot( localRepoRoot(), "diff", "--name-only", - emptyTreeSHA, + base, fields[1], ) } else { @@ -555,6 +561,27 @@ func pushedFiles(input io.Reader) ([]string, error) { return gitLinesInRoot(localRepoRoot(), "diff", "--name-only", "HEAD") } +func newBranchDiffBase(remoteName, localSHA string) string { + remoteName = strings.TrimSpace(remoteName) + if remoteName == "" { + return emptyTreeSHA + } + + trackingHead := "refs/remotes/" + remoteName + "/HEAD" + mergeBase := gitOutputInRoot( + localRepoRoot(), + "merge-base", + trackingHead, + localSHA, + ) + + if mergeBase == "" { + return emptyTreeSHA + } + + return mergeBase +} + func gitLines(args ...string) ([]string, error) { return gitLinesInRoot(repoRoot(), args...) } diff --git a/go/internal/hookrunnercli/git_hook_internal_test.go b/go/internal/hookrunnercli/git_hook_internal_test.go index 8f279e04..bbd39b50 100644 --- a/go/internal/hookrunnercli/git_hook_internal_test.go +++ b/go/internal/hookrunnercli/git_hook_internal_test.go @@ -93,7 +93,7 @@ func TestPushedFilesParsesPrePushRefs(t *testing.T) { "refs/heads/main " + localSHA + " refs/heads/main " + remoteSHA + "\n", ) - files, err := pushedFiles(input) + files, err := pushedFiles(input, "") if err != nil { t.Fatalf("pushedFiles() returned error: %v", err) } @@ -119,7 +119,7 @@ func TestPushedFilesHandlesNewBranchAndDeleteRefs(t *testing.T) { "refs/heads/gone " + allZeroSHA + " refs/heads/gone " + localSHA + "\n", ) - files, err := pushedFiles(input) + files, err := pushedFiles(input, "") if err != nil { t.Fatalf("pushedFiles() returned error: %v", err) } @@ -130,6 +130,39 @@ func TestPushedFilesHandlesNewBranchAndDeleteRefs(t *testing.T) { } } +func TestPushedFilesScopesNewBranchToRemoteDefaultBranch(t *testing.T) { + root := setupGitHookTestRepo(t) + chdirForTest(t, root) + mustWriteTestFile(t, "base.py", "print('base')\n") + runGitTestCommand(t, "add", "base.py") + runGitTestCommand(t, "commit", "-m", "test: base") + baseSHA := gitTestOutput(t, "rev-parse", "HEAD") + runGitTestCommand(t, "update-ref", "refs/remotes/origin/main", baseSHA) + runGitTestCommand( + t, + "symbolic-ref", + "refs/remotes/origin/HEAD", + "refs/remotes/origin/main", + ) + + mustWriteTestFile(t, "feature.py", "print('feature')\n") + runGitTestCommand(t, "add", "feature.py") + runGitTestCommand(t, "commit", "-m", "test: feature") + localSHA := gitTestOutput(t, "rev-parse", "HEAD") + + input := strings.NewReader( + "refs/heads/feature " + localSHA + " refs/heads/feature " + allZeroSHA + "\n", + ) + files, err := pushedFiles(input, "origin") + if err != nil { + t.Fatalf("pushedFiles() returned error: %v", err) + } + + if !reflect.DeepEqual(files, []string{"feature.py"}) { + t.Fatalf("pushed files = %#v, want feature.py", files) + } +} + func TestPushedFilesDeduplicatesMultipleRefs(t *testing.T) { root := setupGitHookTestRepo(t) chdirForTest(t, root) @@ -144,7 +177,7 @@ func TestPushedFilesDeduplicatesMultipleRefs(t *testing.T) { line := "refs/heads/main " + localSHA + " refs/heads/main " + remoteSHA + "\n" - files, err := pushedFiles(strings.NewReader(line + line)) + files, err := pushedFiles(strings.NewReader(line+line), "") if err != nil { t.Fatalf("pushedFiles() returned error: %v", err) } @@ -365,8 +398,8 @@ exit 2 t.Setenv("PATH", fakeBin+string(os.PathListSeparator)+os.Getenv("PATH")) files, err := pushedFiles(strings.NewReader( - "refs/heads/feature abc123 refs/heads/feature " + allZeroSHA + "\n", - )) + "refs/heads/feature abc123 refs/heads/feature "+allZeroSHA+"\n", + ), "") if err != nil { t.Fatalf("pushedFiles: %v", err) } @@ -413,7 +446,7 @@ exit 2 files, err := pushedFiles(strings.NewReader( "refs/heads/feature local123 refs/heads/feature remote123\n", - )) + ), "") if err != nil { t.Fatalf("pushedFiles: %v", err) } diff --git a/go/internal/hooks/proxy_output_test.go b/go/internal/hooks/proxy_output_test.go index dc70aed0..cbdb52c9 100644 --- a/go/internal/hooks/proxy_output_test.go +++ b/go/internal/hooks/proxy_output_test.go @@ -413,7 +413,7 @@ func TestProxyPostToolOutputBudgetsDenseGenericOutputAndRecordsLedger(t *testing output, ) - if !strings.Contains(proxied.Text, "token_budget: status=truncated max_tokens=80") || + if !strings.Contains(proxied.Text, "token_budget: status=truncated") || strings.Contains(proxied.Text, output) || len(proxied.Events) != 1 { t.Fatalf("unexpected dense proxy output: events=%#v text=%s", diff --git a/go/internal/sandboxexec/main.go b/go/internal/sandboxexec/main.go index 0ea1e9eb..2356d850 100644 --- a/go/internal/sandboxexec/main.go +++ b/go/internal/sandboxexec/main.go @@ -291,7 +291,25 @@ func allowedSystemWritePath(path string) bool { allowedTerminalWritePath(path) || allowedManagedTempWritePath(path) || allowedGPGHomeWritePath(path) || - allowedGPGRuntimeWritePath(path) + allowedGPGRuntimeWritePath(path) || + allowedSharedLockDirectory(path) +} + +func allowedSharedLockDirectory(path string) bool { + info, err := os.Lstat(path) + if err != nil { + return false + } + + return validSharedLockDirectoryMetadata(path, info) +} + +func validSharedLockDirectoryMetadata(path string, info os.FileInfo) bool { + mode := info.Mode() + + return filepath.IsAbs(path) && filepath.Dir(path) == "/var/tmp" && + info.IsDir() && mode&os.ModeSymlink == 0 && + mode.Perm() == 0o777 && mode&os.ModeSticky != 0 } func allowedTerminalWritePath(path string) bool { diff --git a/go/internal/sandboxexec/main_test.go b/go/internal/sandboxexec/main_test.go index c88d2f93..6fb4c73d 100644 --- a/go/internal/sandboxexec/main_test.go +++ b/go/internal/sandboxexec/main_test.go @@ -157,6 +157,38 @@ func TestCleanPolicyPathStaysInsideRepo(t *testing.T) { } } +func TestSharedLockDirectoryMetadataAllowsOnlyTheValidatedCapabilityShape( + t *testing.T, +) { + t.Parallel() + + shared := t.TempDir() + if err := os.Chmod(shared, os.ModeSticky|0o777); err != nil { + t.Fatalf("set shared lock directory mode: %v", err) + } + info, err := os.Lstat(shared) + if err != nil { + t.Fatalf("inspect shared lock metadata: %v", err) + } + if !validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { + t.Fatal("the exact direct-child mode-1777 capability shape was rejected") + } + + if err := os.Chmod(shared, 0o0777); err != nil { + t.Fatalf("remove sticky bit: %v", err) + } + info, err = os.Lstat(shared) + if err != nil { + t.Fatalf("inspect non-sticky metadata: %v", err) + } + if validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { + t.Fatal("a non-sticky external directory became a sandbox write capability") + } + if validSharedLockDirectoryMetadata("/tmp/coding-ethos-sandbox-lock-test", info) { + t.Fatal("a path outside /var/tmp became a sandbox write capability") + } +} + func TestCleanPolicyPathAllowsCurrentTerminal(t *testing.T) { root := t.TempDir() From 9091ab50fe545062a3d6e3aafa3e65aff1449d69 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 12:13:45 -0600 Subject: [PATCH 07/17] feat(code-intel): add PurRDF semantic extractor --- .../.gitattributes | 4 + rust/purrdf-code-intel-extractor/.gitignore | 5 + rust/purrdf-code-intel-extractor/Cargo.lock | 1220 +++++++++++++++++ rust/purrdf-code-intel-extractor/Cargo.toml | 23 + rust/purrdf-code-intel-extractor/PROTOCOL.md | 131 ++ rust/purrdf-code-intel-extractor/src/lib.rs | 136 ++ rust/purrdf-code-intel-extractor/src/main.rs | 47 + .../src/protocol.rs | 264 ++++ rust/purrdf-code-intel-extractor/src/rdf.rs | 158 +++ .../purrdf-code-intel-extractor/src/sparql.rs | 939 +++++++++++++ .../tests/fixtures/rdf/semantic.nq | 4 + .../tests/fixtures/rdf/semantic.nt | 4 + .../tests/fixtures/rdf/semantic.trig | 10 + .../tests/fixtures/rdf/semantic.ttl | 9 + .../tests/fixtures/sparql/malformed.rq | 4 + .../tests/fixtures/sparql/query.rq | 15 + .../tests/fixtures/sparql/update.sparql | 13 + .../tests/protocol.rs | 240 ++++ 18 files changed, 3226 insertions(+) create mode 100644 rust/purrdf-code-intel-extractor/.gitattributes create mode 100644 rust/purrdf-code-intel-extractor/.gitignore create mode 100644 rust/purrdf-code-intel-extractor/Cargo.lock create mode 100644 rust/purrdf-code-intel-extractor/Cargo.toml create mode 100644 rust/purrdf-code-intel-extractor/PROTOCOL.md create mode 100644 rust/purrdf-code-intel-extractor/src/lib.rs create mode 100644 rust/purrdf-code-intel-extractor/src/main.rs create mode 100644 rust/purrdf-code-intel-extractor/src/protocol.rs create mode 100644 rust/purrdf-code-intel-extractor/src/rdf.rs create mode 100644 rust/purrdf-code-intel-extractor/src/sparql.rs create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nq create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nt create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.trig create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.ttl create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/sparql/malformed.rq create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/sparql/query.rq create mode 100644 rust/purrdf-code-intel-extractor/tests/fixtures/sparql/update.sparql create mode 100644 rust/purrdf-code-intel-extractor/tests/protocol.rs diff --git a/rust/purrdf-code-intel-extractor/.gitattributes b/rust/purrdf-code-intel-extractor/.gitattributes new file mode 100644 index 00000000..2ec54122 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/.gitattributes @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +*.rs text eol=lf whitespace=trailing-space,space-before-tab diff --git a/rust/purrdf-code-intel-extractor/.gitignore b/rust/purrdf-code-intel-extractor/.gitignore new file mode 100644 index 00000000..c668aef5 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +/target/ +!/Cargo.lock diff --git a/rust/purrdf-code-intel-extractor/Cargo.lock b/rust/purrdf-code-intel-extractor/Cargo.lock new file mode 100644 index 00000000..9643a141 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/Cargo.lock @@ -0,0 +1,1220 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", + "rayon-core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "coding-ethos-purrdf-extractor" +version = "0.1.0" +dependencies = [ + "purrdf-rdf", + "purrdf-sparql-algebra", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "oxilangtag" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3b4eb570abd4a1dcb062c31fd37b832264d9dc7292c3e69acfe926c87b063f" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "purrdf-core" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +dependencies = [ + "ahash", + "csv", + "hashbrown 0.15.5", + "oxilangtag", + "purrdf-events", + "purrdf-iri", + "sha2", + "smallvec", +] + +[[package]] +name = "purrdf-events" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" + +[[package]] +name = "purrdf-gts" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +dependencies = [ + "aes-gcm", + "ahash", + "blake3", + "ciborium", + "ed25519-dalek", + "fastrand", + "filetime", + "flate2", + "purrdf-events", + "rayon", + "serde", + "sha1", + "structured-zstd", + "time", + "visual-hashing", +] + +[[package]] +name = "purrdf-iri" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" + +[[package]] +name = "purrdf-rdf" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +dependencies = [ + "ahash", + "ciborium", + "csv", + "ed25519-dalek", + "hashbrown 0.15.5", + "memchr", + "oxilangtag", + "purrdf-core", + "purrdf-events", + "purrdf-gts", + "purrdf-iri", + "purrdf-sparql-algebra", + "purrdf-sparql-eval", + "purrdf-xsd", + "rayon", + "regex", + "roxmltree", + "serde", + "serde_json", + "serde_yaml_ng", + "sha2", +] + +[[package]] +name = "purrdf-sparql-algebra" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +dependencies = [ + "memchr", + "purrdf-iri", + "purrdf-xsd", + "smallvec", +] + +[[package]] +name = "purrdf-sparql-eval" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +dependencies = [ + "ahash", + "getrandom 0.3.4", + "hashbrown 0.15.5", + "js-sys", + "md-5", + "purrdf-core", + "purrdf-iri", + "purrdf-sparql-algebra", + "purrdf-sparql-results", + "purrdf-xsd", + "rayon", + "regex", + "sha1", + "sha2", + "smallvec", +] + +[[package]] +name = "purrdf-sparql-results" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +dependencies = [ + "purrdf-core", + "purrdf-iri", +] + +[[package]] +name = "purrdf-xsd" +version = "0.12.0" +source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "structured-zstd" +version = "0.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "392eea135985654a313cdfee001e93031bacd96a3693a5dc4b0d13793e40b27a" +dependencies = [ + "fastrand", + "twox-hash", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "twox-hash" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visual-hashing" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cff0f3901531f1ae5441b09b201b1e61698fba5a5625d44b6243c5b381a3e3" +dependencies = [ + "blake3", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/rust/purrdf-code-intel-extractor/Cargo.toml b/rust/purrdf-code-intel-extractor/Cargo.toml new file mode 100644 index 00000000..944ab31f --- /dev/null +++ b/rust/purrdf-code-intel-extractor/Cargo.toml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +[package] +name = "coding-ethos-purrdf-extractor" +version = "0.1.0" +edition = "2024" +license = "AGPL-3.0-only" +publish = false + +[dependencies] +purrdf-rdf = { git = "ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git", rev = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" } +purrdf-sparql-algebra = { git = "ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git", rev = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +sha2 = "0.10.9" + +[lints.rust] +unsafe_code = "forbid" + +[[bin]] +name = "coding-ethos-purrdf-extractor" +path = "src/main.rs" diff --git a/rust/purrdf-code-intel-extractor/PROTOCOL.md b/rust/purrdf-code-intel-extractor/PROTOCOL.md new file mode 100644 index 00000000..3f3ccd51 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/PROTOCOL.md @@ -0,0 +1,131 @@ + + + +# PurRDF external extractor protocol + +`coding-ethos-purrdf-extractor` is a one-shot content transformer. It reads one +UTF-8 JSON request from standard input, writes one compact JSON response plus a +newline to standard output, writes human diagnostics to standard error, and +exits. It does not read source files, open sockets, or provide a daemon mode. + +The protocol discriminator is exactly: + +```text +coding-ethos.external-extractor.v1 +``` + +## Request + +```json +{ + "protocol": "coding-ethos.external-extractor.v1", + "request_id": "opaque-caller-id", + "files": [ + { + "path": "ontology/example.ttl", + "content_sha256": "351c7056c2164c6789cd6cf6d91e4194719945e1c5e8f19cc9bc1778ece0d0b4", + "content": "@prefix ex: .\nex:s ex:p ex:o .\n", + "base_iri": "https://example.org/source/" + } + ] +} +``` + +`base_iri` is optional. Unknown fields, an empty request ID or path, duplicate +paths, malformed hashes, and an unsupported protocol are envelope errors. +`content_sha256` is SHA-256 over the exact UTF-8 bytes in `content`. The path is +source identity only; the extractor never opens it. Its case-insensitive suffix +selects the parser: + +| Suffix | Language | PurRDF media type | +| --- | --- | --- | +| `.ttl` | `turtle` | `text/turtle` | +| `.trig` | `trig` | `application/trig` | +| `.nt` | `ntriples` | `application/n-triples` | +| `.nq` | `nquads` | `application/n-quads` | +| `.rq`, `.sparql` | `sparql` | query parse, then update parse | + +## Response + +```json +{ + "protocol": "coding-ethos.external-extractor.v1", + "request_id": "opaque-caller-id", + "extractor": { + "name": "purrdf", + "version": "1", + "purrdf_revision": "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" + }, + "results": [ + { + "path": "ontology/example.ttl", + "content_sha256": "351c7056c2164c6789cd6cf6d91e4194719945e1c5e8f19cc9bc1778ece0d0b4", + "language": "turtle", + "status": "ok", + "document_kind": "dataset", + "facts": [], + "error": null + } + ] +} +``` + +Results are sorted by path. Valid envelopes exit zero even when an individual +file has `status: "error"`; that result has a null `document_kind`, no facts, +and a structured error string. The same file error is mirrored to standard +error for operators. Envelope errors exit 2 without a response. Internal I/O or +JSON serialization errors exit 1. + +## Facts and terms + +Every fact has this shape; optional semantic positions are absent rather than +null: + +```json +{ + "id": "sha256:...", + "kind": "rdf_quad", + "subject": { "kind": "iri", "value": "https://example.org/s" }, + "predicate": { "kind": "iri", "value": "https://example.org/p" }, + "object": { "kind": "iri", "value": "https://example.org/o" }, + "attributes": { + "language": "turtle", + "media_type": "text/turtle" + }, + "provenance": { + "class": "EXTRACTED", + "parser": "purrdf-rdf", + "parser_revision": "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6", + "span_fidelity": "subject_start", + "source_path": "ontology/example.ttl", + "start": { "byte_offset": 38, "line": 2, "column": 1 } + } +} +``` + +Term tags are `iri`, `blank`, `literal`, `quoted_triple`, and `variable`. +Literal terms carry `value`, nullable `datatype`, nullable `language`, and +nullable `direction`. A quoted-triple term recursively carries `subject`, an IRI +string `predicate`, and `object`. Blank values include the `_:` prefix; variable +values omit the `?` or `$` sigil. + +RDF fact kinds are `rdf_quad`, `rdf_reifier`, and `rdf_annotation`. SPARQL facts +describe the parsed public algebra, including document/query form, triple and +quad patterns, dataset/graph/service scopes, expressions, property paths, +VALUES cells, functions, bindings, and update operations. + +Fact IDs are SHA-256 over the canonical semantic payload excluding the ID +itself. Attributes use sorted keys, SPARQL traversal carries a deterministic +ordinal, facts are sorted by ID, and repeated extraction of the same request is +byte-identical. + +## Location fidelity + +RDF parsing always calls PurRDF `parse_dataset_with` with +`track_source_spans: true`. PurRDF exposes the first source position for a +lexical named-node or blank-node subject, not a full statement range. Therefore +RDF provenance says `subject_start`, and `start` may be absent for quoted-triple +subjects. Lines and columns are one-based; byte offsets are zero-based. + +The public SPARQL algebra has no physical source spans. Every SPARQL fact says +`span_fidelity: "none"` and omits `start`. diff --git a/rust/purrdf-code-intel-extractor/src/lib.rs b/rust/purrdf-code-intel-extractor/src/lib.rs new file mode 100644 index 00000000..e6881512 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/src/lib.rs @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +mod protocol; +mod rdf; +mod sparql; + +use std::collections::BTreeSet; +use std::path::Path; + +pub use protocol::{ + BatchRequest, BatchResponse, ExtractorIdentity, FileRequest, FileResult, FileStatus, PROTOCOL, + PURRDF_REVISION, Provenance, SemanticFact, SemanticTerm, SourceStart, SpanFidelity, sha256_hex, +}; + +#[derive(Debug)] +pub struct ExtractionOutcome { + pub response: BatchResponse, + pub diagnostics: Vec, +} + +pub fn extract_batch(request: BatchRequest) -> Result { + validate_request(&request)?; + + let mut files: Vec<&FileRequest> = request.files.iter().collect(); + files.sort_by(|left, right| left.path.cmp(&right.path)); + + let mut results = Vec::with_capacity(files.len()); + let mut diagnostics = Vec::new(); + for file in files { + let language = language_for_path(&file.path).unwrap_or("unsupported"); + if sha256_hex(file.content.as_bytes()) != file.content_sha256 { + let message = "content_sha256 does not match UTF-8 content bytes".to_owned(); + diagnostics.push(format!("{}: {message}", file.path)); + results.push(FileResult::error(file, language, message)); + continue; + } + + let extracted = match language { + "turtle" | "trig" | "ntriples" | "nquads" => rdf::extract(file, language), + "sparql" => sparql::extract(file), + _ => Err( + "unsupported extension; expected .ttl, .trig, .nt, .nq, .rq, or .sparql".to_owned(), + ), + }; + match extracted { + Ok(result) => results.push(result), + Err(message) => { + diagnostics.push(format!("{}: {message}", file.path)); + results.push(FileResult::error(file, language, message)); + } + } + } + + Ok(ExtractionOutcome { + response: BatchResponse { + protocol: PROTOCOL, + request_id: request.request_id, + extractor: ExtractorIdentity::default(), + results, + }, + diagnostics, + }) +} + +fn validate_request(request: &BatchRequest) -> Result<(), String> { + if request.protocol != PROTOCOL { + return Err(format!( + "unsupported protocol {:?}; expected {PROTOCOL:?}", + request.protocol + )); + } + if request.request_id.is_empty() { + return Err("request_id must not be empty".to_owned()); + } + + let mut paths = BTreeSet::new(); + for file in &request.files { + if file.path.is_empty() { + return Err("file path must not be empty".to_owned()); + } + if file.path.contains('\0') { + return Err(format!("file path {:?} contains NUL", file.path)); + } + if !paths.insert(file.path.as_str()) { + return Err(format!("duplicate file path {:?}", file.path)); + } + if file.content_sha256.len() != 64 + || !file + .content_sha256 + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(format!( + "content_sha256 for {:?} must be 64 lowercase hexadecimal characters", + file.path + )); + } + } + Ok(()) +} + +fn language_for_path(path: &str) -> Option<&'static str> { + let extension = Path::new(path).extension()?.to_str()?.to_ascii_lowercase(); + match extension.as_str() { + "ttl" => Some("turtle"), + "trig" => Some("trig"), + "nt" => Some("ntriples"), + "nq" => Some("nquads"), + "rq" | "sparql" => Some("sparql"), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_duplicate_paths() { + let content = " .\n"; + let file = FileRequest { + path: "same.nt".to_owned(), + content_sha256: sha256_hex(content.as_bytes()), + content: content.to_owned(), + base_iri: None, + }; + let request = BatchRequest { + protocol: PROTOCOL.to_owned(), + request_id: "duplicates".to_owned(), + files: vec![file.clone(), file], + }; + let error = extract_batch(request).expect_err("duplicate paths must fail the envelope"); + assert!(error.contains("duplicate file path")); + } +} diff --git a/rust/purrdf-code-intel-extractor/src/main.rs b/rust/purrdf-code-intel-extractor/src/main.rs new file mode 100644 index 00000000..0a73340e --- /dev/null +++ b/rust/purrdf-code-intel-extractor/src/main.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use std::io::{self, Read, Write}; +use std::process::ExitCode; + +use coding_ethos_purrdf_extractor::{BatchRequest, extract_batch}; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(Failure::Protocol(message)) => { + eprintln!("coding-ethos-purrdf-extractor: protocol error: {message}"); + ExitCode::from(2) + } + Err(Failure::Internal(message)) => { + eprintln!("coding-ethos-purrdf-extractor: internal error: {message}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result<(), Failure> { + let mut input = Vec::new(); + io::stdin() + .read_to_end(&mut input) + .map_err(|error| Failure::Internal(format!("cannot read stdin: {error}")))?; + let request: BatchRequest = serde_json::from_slice(&input) + .map_err(|error| Failure::Protocol(format!("invalid request JSON: {error}")))?; + let outcome = extract_batch(request).map_err(Failure::Protocol)?; + for diagnostic in outcome.diagnostics { + eprintln!("coding-ethos-purrdf-extractor: {diagnostic}"); + } + + let mut output = serde_json::to_vec(&outcome.response) + .map_err(|error| Failure::Internal(format!("cannot encode response JSON: {error}")))?; + output.push(b'\n'); + io::stdout() + .write_all(&output) + .map_err(|error| Failure::Internal(format!("cannot write stdout: {error}")))?; + Ok(()) +} + +enum Failure { + Protocol(String), + Internal(String), +} diff --git a/rust/purrdf-code-intel-extractor/src/protocol.rs b/rust/purrdf-code-intel-extractor/src/protocol.rs new file mode 100644 index 00000000..95825c88 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/src/protocol.rs @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +pub const PROTOCOL: &str = "coding-ethos.external-extractor.v1"; +pub const PURRDF_REVISION: &str = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6"; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct BatchRequest { + pub protocol: String, + pub request_id: String, + pub files: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct FileRequest { + pub path: String, + pub content_sha256: String, + pub content: String, + #[serde(default)] + pub base_iri: Option, +} + +#[derive(Clone, Debug, Serialize)] +pub struct BatchResponse { + pub protocol: &'static str, + pub request_id: String, + pub extractor: ExtractorIdentity, + pub results: Vec, +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct ExtractorIdentity { + pub name: &'static str, + pub version: &'static str, + pub purrdf_revision: &'static str, +} + +impl Default for ExtractorIdentity { + fn default() -> Self { + Self { + name: "purrdf", + version: "1", + purrdf_revision: PURRDF_REVISION, + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct FileResult { + pub path: String, + pub content_sha256: String, + pub language: String, + pub status: FileStatus, + pub document_kind: Option, + pub facts: Vec, + pub error: Option, +} + +impl FileResult { + pub fn success( + file: &FileRequest, + language: &str, + document_kind: &str, + mut facts: Vec, + ) -> Self { + facts.sort_by(|left, right| left.id.cmp(&right.id)); + Self { + path: file.path.clone(), + content_sha256: file.content_sha256.clone(), + language: language.to_owned(), + status: FileStatus::Ok, + document_kind: Some(document_kind.to_owned()), + facts, + error: None, + } + } + + pub fn error(file: &FileRequest, language: &str, message: String) -> Self { + Self { + path: file.path.clone(), + content_sha256: file.content_sha256.clone(), + language: language.to_owned(), + status: FileStatus::Error, + document_kind: None, + facts: Vec::new(), + error: Some(message), + } + } +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum FileStatus { + Ok, + Error, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum SemanticTerm { + Iri { + value: String, + }, + Blank { + value: String, + }, + Literal { + value: String, + datatype: Option, + language: Option, + direction: Option, + }, + QuotedTriple { + subject: Box, + predicate: String, + object: Box, + }, + Variable { + value: String, + }, +} + +#[derive(Clone, Debug, Serialize)] +pub struct SemanticFact { + pub id: String, + pub kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub predicate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub object: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub graph: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + pub attributes: BTreeMap, + pub provenance: Provenance, +} + +#[derive(Serialize)] +struct FactIdentity<'a> { + kind: &'a str, + subject: &'a Option, + predicate: &'a Option, + object: &'a Option, + graph: &'a Option, + value: &'a Option, + attributes: &'a BTreeMap, + provenance: &'a Provenance, +} + +impl SemanticFact { + #[allow(clippy::too_many_arguments)] + pub fn new( + kind: impl Into, + subject: Option, + predicate: Option, + object: Option, + graph: Option, + value: Option, + attributes: BTreeMap, + provenance: Provenance, + ) -> Result { + let kind = kind.into(); + let identity = FactIdentity { + kind: &kind, + subject: &subject, + predicate: &predicate, + object: &object, + graph: &graph, + value: &value, + attributes: &attributes, + provenance: &provenance, + }; + let encoded = serde_json::to_vec(&identity) + .map_err(|error| format!("cannot encode semantic fact identity: {error}"))?; + let id = format!("sha256:{}", sha256_hex(&encoded)); + Ok(Self { + id, + kind, + subject, + predicate, + object, + graph, + value, + attributes, + provenance, + }) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct Provenance { + pub class: &'static str, + pub parser: &'static str, + pub parser_revision: &'static str, + pub span_fidelity: SpanFidelity, + pub source_path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub start: Option, +} + +impl Provenance { + pub fn rdf(source_path: &str, start: Option) -> Self { + Self { + class: "EXTRACTED", + parser: "purrdf-rdf", + parser_revision: PURRDF_REVISION, + span_fidelity: SpanFidelity::SubjectStart, + source_path: source_path.to_owned(), + start, + } + } + + pub fn sparql(source_path: &str) -> Self { + Self { + class: "EXTRACTED", + parser: "purrdf-sparql-algebra", + parser_revision: PURRDF_REVISION, + span_fidelity: SpanFidelity::None, + source_path: source_path.to_owned(), + start: None, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SpanFidelity { + SubjectStart, + None, +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct SourceStart { + pub byte_offset: usize, + pub line: u32, + pub column: u32, +} + +pub fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut encoded = String::with_capacity(digest.len() * 2); + for byte in digest { + use std::fmt::Write as _; + let _ = write!(encoded, "{byte:02x}"); + } + encoded +} + +pub fn string_attributes(entries: &[(&str, String)]) -> BTreeMap { + entries + .iter() + .map(|(key, value)| ((*key).to_owned(), value.clone())) + .collect() +} diff --git a/rust/purrdf-code-intel-extractor/src/rdf.rs b/rust/purrdf-code-intel-extractor/src/rdf.rs new file mode 100644 index 00000000..4775cc15 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/src/rdf.rs @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use std::collections::BTreeMap; + +use purrdf_rdf::{ParseOptions, RdfTerm, SpanTable, parse_dataset_with}; + +use crate::protocol::{ + FileRequest, FileResult, Provenance, SemanticFact, SemanticTerm, SourceStart, +}; + +const RDF_REIFIES: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#reifies"; + +pub fn extract(file: &FileRequest, language: &str) -> Result { + let media_type = match language { + "turtle" => "text/turtle", + "trig" => "application/trig", + "ntriples" => "application/n-triples", + "nquads" => "application/n-quads", + other => return Err(format!("internal unsupported RDF language {other:?}")), + }; + let options = ParseOptions { + track_source_spans: true, + }; + let (dataset, spans) = parse_dataset_with( + file.content.as_bytes(), + media_type, + file.base_iri.as_deref(), + &options, + ) + .map_err(|error| format!("PurRDF {language} parse failed: {error}"))?; + + let attributes = || { + BTreeMap::from([ + ("language".to_owned(), language.to_owned()), + ("media_type".to_owned(), media_type.to_owned()), + ]) + }; + let mut facts = Vec::new(); + for quad in dataset.owned_quads() { + let start = subject_start(&quad.subject, spans.as_ref()); + facts.push(SemanticFact::new( + "rdf_quad", + Some(rdf_term(&quad.subject)), + Some(SemanticTerm::Iri { + value: quad.predicate, + }), + Some(rdf_term(&quad.object)), + quad.graph_name.as_ref().map(rdf_term), + None, + attributes(), + Provenance::rdf(&file.path, start), + )?); + } + for reifier in dataset.owned_reifiers() { + let start = subject_start(&reifier.reifier, spans.as_ref()); + facts.push(SemanticFact::new( + "rdf_reifier", + Some(rdf_term(&reifier.reifier)), + Some(SemanticTerm::Iri { + value: RDF_REIFIES.to_owned(), + }), + Some(SemanticTerm::QuotedTriple { + subject: Box::new(rdf_term(&reifier.statement.subject)), + predicate: reifier.statement.predicate, + object: Box::new(rdf_term(&reifier.statement.object)), + }), + reifier.graph.as_ref().map(rdf_term), + None, + attributes(), + Provenance::rdf(&file.path, start), + )?); + } + for annotation in dataset.owned_annotations() { + let start = subject_start(&annotation.reifier, spans.as_ref()); + facts.push(SemanticFact::new( + "rdf_annotation", + Some(rdf_term(&annotation.reifier)), + Some(SemanticTerm::Iri { + value: annotation.predicate, + }), + Some(rdf_term(&annotation.object)), + annotation.graph.as_ref().map(rdf_term), + None, + attributes(), + Provenance::rdf(&file.path, start), + )?); + } + + Ok(FileResult::success(file, language, "dataset", facts)) +} + +fn rdf_term(term: &RdfTerm) -> SemanticTerm { + match term { + RdfTerm::Iri(value) => SemanticTerm::Iri { + value: value.clone(), + }, + RdfTerm::BlankNode(label) => SemanticTerm::Blank { + value: format!("_:{label}"), + }, + RdfTerm::Literal(literal) => SemanticTerm::Literal { + value: literal.lexical_form.clone(), + datatype: literal.datatype.clone(), + language: literal.language.clone(), + direction: literal + .direction + .map(|direction| direction.as_str().to_owned()), + }, + RdfTerm::Triple(triple) => SemanticTerm::QuotedTriple { + subject: Box::new(rdf_term(&triple.subject)), + predicate: triple.predicate.clone(), + object: Box::new(rdf_term(&triple.object)), + }, + } +} + +fn subject_start(term: &RdfTerm, spans: Option<&SpanTable>) -> Option { + let key = match term { + RdfTerm::Iri(value) => value.clone(), + RdfTerm::BlankNode(label) => format!("_:{label}"), + RdfTerm::Literal(_) | RdfTerm::Triple(_) => return None, + }; + let position = spans?.position_for_subject(&key)?; + Some(SourceStart { + byte_offset: position.byte_offset, + line: position.line, + column: position.column, + }) +} + +#[cfg(test)] +mod tests { + use crate::protocol::sha256_hex; + + use super::*; + + #[test] + fn turtle_uses_first_subject_start_only() { + let content = concat!( + "@prefix ex: .\n", + "ex:s ex:p ex:o .\n", + "ex:s ex:q ex:r .\n", + ); + let file = FileRequest { + path: "source.ttl".to_owned(), + content_sha256: sha256_hex(content.as_bytes()), + content: content.to_owned(), + base_iri: None, + }; + let result = extract(&file, "turtle").expect("valid Turtle extracts"); + assert_eq!(result.facts.len(), 2); + assert!(result.facts.iter().all(|fact| { + fact.provenance + .start + .is_some_and(|start| start.line == 2 && start.column == 1) + })); + } +} diff --git a/rust/purrdf-code-intel-extractor/src/sparql.rs b/rust/purrdf-code-intel-extractor/src/sparql.rs new file mode 100644 index 00000000..c2aa4ea0 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/src/sparql.rs @@ -0,0 +1,939 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use std::collections::BTreeMap; + +use purrdf_sparql_algebra::{ + BaseDirection, Expression, Function, GraphPattern, GraphTarget, GraphUpdateOperation, + GroundTerm, NamedNodePattern, OrderExpression, PropertyPathExpression, QuadPattern, Query, + QueryDataset, SparqlParser, TermPattern, TriplePattern, Update, UsingClause, +}; + +use crate::protocol::{ + FileRequest, FileResult, Provenance, SemanticFact, SemanticTerm, string_attributes, +}; + +pub fn extract(file: &FileRequest) -> Result { + let parser = file + .base_iri + .as_deref() + .map_or_else(SparqlParser::new, |base| { + SparqlParser::new().with_base_iri(base) + }); + match parser.parse_query(&file.content) { + Ok(query) => { + let mut walker = Walker::new(&file.path); + walker.walk_query(&query)?; + Ok(FileResult::success(file, "sparql", "query", walker.facts)) + } + Err(query_error) => match parser.parse_update(&file.content) { + Ok(update) => { + let mut walker = Walker::new(&file.path); + walker.walk_update(&update)?; + Ok(FileResult::success(file, "sparql", "update", walker.facts)) + } + Err(update_error) => Err(format!( + "PurRDF SPARQL parse failed as query ({query_error}) and update ({update_error})" + )), + }, + } +} + +struct Walker<'a> { + source_path: &'a str, + sequence: usize, + facts: Vec, +} + +impl<'a> Walker<'a> { + fn new(source_path: &'a str) -> Self { + Self { + source_path, + sequence: 0, + facts: Vec::new(), + } + } + + #[allow(clippy::too_many_arguments)] + fn emit( + &mut self, + kind: &str, + subject: Option, + predicate: Option, + object: Option, + graph: Option, + value: Option, + mut attributes: BTreeMap, + ) -> Result<(), String> { + attributes.insert("ordinal".to_owned(), self.sequence.to_string()); + self.sequence += 1; + self.facts.push(SemanticFact::new( + kind, + subject, + predicate, + object, + graph, + value, + attributes, + Provenance::sparql(self.source_path), + )?); + Ok(()) + } + + fn emit_simple(&mut self, kind: &str, value: impl Into) -> Result<(), String> { + self.emit( + kind, + None, + None, + None, + None, + Some(value.into()), + BTreeMap::new(), + ) + } + + fn walk_query(&mut self, query: &Query) -> Result<(), String> { + let (form, pattern, dataset, base_iri, version) = match query { + Query::Select { + pattern, + dataset, + base_iri, + version, + } => ("select", pattern, dataset, base_iri, version), + Query::Construct { + template, + pattern, + dataset, + base_iri, + version, + } => { + self.emit_simple("sparql_document", "construct")?; + for triple in template { + self.emit_triple_pattern("sparql_construct_template", triple)?; + } + self.walk_dataset(dataset)?; + self.walk_pattern(pattern)?; + self.emit_query_metadata(base_iri.as_ref(), version.as_ref())?; + return Ok(()); + } + Query::Describe { + pattern, + targets, + dataset, + base_iri, + version, + } => { + self.emit_simple("sparql_document", "describe")?; + for target in targets { + self.emit( + "sparql_describe_target", + Some(named_pattern(target)), + None, + None, + None, + None, + BTreeMap::new(), + )?; + } + self.walk_dataset(dataset)?; + self.walk_pattern(pattern)?; + self.emit_query_metadata(base_iri.as_ref(), version.as_ref())?; + return Ok(()); + } + Query::Ask { + pattern, + dataset, + base_iri, + version, + } => ("ask", pattern, dataset, base_iri, version), + }; + self.emit_simple("sparql_document", form)?; + self.walk_dataset(dataset)?; + self.walk_pattern(pattern)?; + self.emit_query_metadata(base_iri.as_ref(), version.as_ref()) + } + + fn emit_query_metadata( + &mut self, + base_iri: Option<&purrdf_sparql_algebra::NamedNode>, + version: Option<&purrdf_sparql_algebra::SparqlVersion>, + ) -> Result<(), String> { + if let Some(base_iri) = base_iri { + self.emit_simple("sparql_base_iri", base_iri.as_str())?; + } + if let Some(version) = version { + self.emit_simple("sparql_version", version.raw())?; + } + Ok(()) + } + + fn walk_dataset(&mut self, dataset: &QueryDataset) -> Result<(), String> { + for graph in &dataset.default { + self.emit( + "sparql_dataset_graph", + Some(SemanticTerm::Iri { + value: graph.as_str().to_owned(), + }), + None, + None, + None, + None, + string_attributes(&[("role", "default".to_owned())]), + )?; + } + for graph in &dataset.named { + self.emit( + "sparql_dataset_graph", + Some(SemanticTerm::Iri { + value: graph.as_str().to_owned(), + }), + None, + None, + None, + None, + string_attributes(&[("role", "named".to_owned())]), + )?; + } + Ok(()) + } + + fn walk_pattern(&mut self, pattern: &GraphPattern) -> Result<(), String> { + self.emit_simple("sparql_graph_pattern", graph_pattern_kind(pattern))?; + match pattern { + GraphPattern::Bgp { patterns } => { + for triple in patterns { + self.emit_triple_pattern("sparql_triple_pattern", triple)?; + } + } + GraphPattern::Path { + subject, + path, + object, + } => { + self.emit( + "sparql_path_constraint", + Some(term_pattern(subject)), + None, + Some(term_pattern(object)), + None, + Some(path.to_string()), + BTreeMap::new(), + )?; + self.walk_path(path)?; + } + GraphPattern::Join { left, right } + | GraphPattern::Lateral { left, right } + | GraphPattern::Union { left, right } + | GraphPattern::Minus { left, right } => { + self.walk_pattern(left)?; + self.walk_pattern(right)?; + } + GraphPattern::LeftJoin { + left, + right, + expression, + } => { + self.walk_pattern(left)?; + self.walk_pattern(right)?; + if let Some(expression) = expression { + self.walk_expression(expression)?; + } + } + GraphPattern::Filter { expr, inner } => { + self.walk_expression(expr)?; + self.walk_pattern(inner)?; + } + GraphPattern::Graph { name, inner } => { + self.emit( + "sparql_graph", + Some(named_pattern(name)), + None, + None, + None, + None, + BTreeMap::new(), + )?; + self.walk_pattern(inner)?; + } + GraphPattern::Extend { + inner, + variable, + expression, + } => { + self.emit( + "sparql_binding", + Some(SemanticTerm::Variable { + value: variable.as_str().to_owned(), + }), + None, + None, + None, + None, + BTreeMap::new(), + )?; + self.walk_expression(expression)?; + self.walk_pattern(inner)?; + } + GraphPattern::Service { + name, + inner, + silent, + } => { + self.emit( + "sparql_service", + Some(named_pattern(name)), + None, + None, + None, + None, + string_attributes(&[("silent", silent.to_string())]), + )?; + self.walk_pattern(inner)?; + } + GraphPattern::Values { + variables, + bindings, + } => { + self.emit( + "sparql_values", + None, + None, + None, + None, + None, + string_attributes(&[ + ("columns", variables.len().to_string()), + ("rows", bindings.len().to_string()), + ]), + )?; + for (row_index, row) in bindings.iter().enumerate() { + for (column_index, cell) in row.iter().enumerate() { + if let Some(term) = cell { + self.emit( + "sparql_values_cell", + Some(ground_term(term)), + None, + None, + None, + None, + string_attributes(&[ + ("row", row_index.to_string()), + ("column", column_index.to_string()), + ]), + )?; + } + } + } + } + GraphPattern::OrderBy { inner, expression } => { + for order in expression { + match order { + OrderExpression::Asc(expression) | OrderExpression::Desc(expression) => { + self.walk_expression(expression)?; + } + } + } + self.walk_pattern(inner)?; + } + GraphPattern::Project { inner, .. } + | GraphPattern::Distinct { inner } + | GraphPattern::Reduced { inner } + | GraphPattern::Slice { inner, .. } => self.walk_pattern(inner)?, + GraphPattern::Group { + inner, aggregates, .. + } => { + for (_, aggregate) in aggregates { + for expression in aggregate.args() { + self.walk_expression(expression)?; + } + } + self.walk_pattern(inner)?; + } + GraphPattern::PropertyFunction(call) => { + self.emit( + "sparql_property_function", + None, + Some(SemanticTerm::Iri { + value: call.iri.clone(), + }), + None, + None, + None, + string_attributes(&[ + ("subject_arity", call.subject_args.len().to_string()), + ("object_arity", call.object_args.len().to_string()), + ]), + )?; + } + } + Ok(()) + } + + fn walk_path(&mut self, path: &PropertyPathExpression) -> Result<(), String> { + let attributes = match path { + PropertyPathExpression::Range { min, max, .. } => string_attributes(&[ + ("operator", "range".to_owned()), + ("min", min.to_string()), + ( + "max", + max.map_or_else(|| "unbounded".to_owned(), |value| value.to_string()), + ), + ]), + PropertyPathExpression::Wildcard { namespace } => string_attributes(&[ + ("operator", "wildcard".to_owned()), + ( + "namespace", + namespace + .as_ref() + .map_or_else(String::new, |node| node.as_str().to_owned()), + ), + ]), + other => string_attributes(&[("operator", property_path_kind(other).to_owned())]), + }; + let predicate = match path { + PropertyPathExpression::NamedNode(node) => Some(SemanticTerm::Iri { + value: node.as_str().to_owned(), + }), + _ => None, + }; + self.emit( + "sparql_property_path", + None, + predicate, + None, + None, + Some(path.to_string()), + attributes, + )?; + match path { + PropertyPathExpression::Reverse(inner) + | PropertyPathExpression::ZeroOrMore(inner) + | PropertyPathExpression::OneOrMore(inner) + | PropertyPathExpression::ZeroOrOne(inner) + | PropertyPathExpression::Range { inner, .. } => self.walk_path(inner)?, + PropertyPathExpression::Sequence(left, right) + | PropertyPathExpression::Alternative(left, right) => { + self.walk_path(left)?; + self.walk_path(right)?; + } + PropertyPathExpression::NegatedPropertySet(elements) => { + for element in elements { + self.emit( + "sparql_negated_path_predicate", + None, + Some(SemanticTerm::Iri { + value: element.predicate.as_str().to_owned(), + }), + None, + None, + None, + string_attributes(&[("inverse", element.inverse.to_string())]), + )?; + } + } + PropertyPathExpression::NamedNode(_) | PropertyPathExpression::Wildcard { .. } => {} + } + Ok(()) + } + + fn walk_expression(&mut self, expression: &Expression) -> Result<(), String> { + self.emit_simple("sparql_expression", expression_kind(expression))?; + match expression { + Expression::NamedNode(node) => self.emit( + "sparql_iri_reference", + Some(SemanticTerm::Iri { + value: node.as_str().to_owned(), + }), + None, + None, + None, + None, + BTreeMap::new(), + )?, + Expression::Literal(_) | Expression::Variable(_) | Expression::Bound(_) => {} + Expression::Or(left, right) + | Expression::And(left, right) + | Expression::Equal(left, right) + | Expression::SameTerm(left, right) + | Expression::Greater(left, right) + | Expression::GreaterOrEqual(left, right) + | Expression::Less(left, right) + | Expression::LessOrEqual(left, right) + | Expression::Add(left, right) + | Expression::Subtract(left, right) + | Expression::Multiply(left, right) + | Expression::Divide(left, right) => { + self.walk_expression(left)?; + self.walk_expression(right)?; + } + Expression::UnaryPlus(inner) + | Expression::UnaryMinus(inner) + | Expression::Not(inner) => self.walk_expression(inner)?, + Expression::In(inner, entries) => { + self.walk_expression(inner)?; + for entry in entries { + self.walk_expression(entry)?; + } + } + Expression::If(condition, when_true, when_false) => { + self.walk_expression(condition)?; + self.walk_expression(when_true)?; + self.walk_expression(when_false)?; + } + Expression::Coalesce(entries) => { + for entry in entries { + self.walk_expression(entry)?; + } + } + Expression::FunctionCall(function, arguments) => { + let iri = match function { + Function::Custom(node) => Some(node.as_str()), + Function::Purrdf(call) => Some(call.iri.as_str()), + _ => None, + }; + if let Some(iri) = iri { + self.emit( + "sparql_function", + None, + Some(SemanticTerm::Iri { + value: iri.to_owned(), + }), + None, + None, + None, + BTreeMap::new(), + )?; + } + for argument in arguments { + self.walk_expression(argument)?; + } + } + Expression::Exists(pattern) => self.walk_pattern(pattern)?, + } + Ok(()) + } + + fn emit_triple_pattern(&mut self, kind: &str, triple: &TriplePattern) -> Result<(), String> { + self.emit( + kind, + Some(term_pattern(&triple.subject)), + Some(named_pattern(&triple.predicate)), + Some(term_pattern(&triple.object)), + None, + None, + BTreeMap::new(), + ) + } + + fn emit_quad_pattern(&mut self, role: &str, quad: &QuadPattern) -> Result<(), String> { + self.emit( + "sparql_quad_pattern", + Some(term_pattern(&quad.triple.subject)), + Some(named_pattern(&quad.triple.predicate)), + Some(term_pattern(&quad.triple.object)), + quad.graph.as_ref().map(named_pattern), + None, + string_attributes(&[("role", role.to_owned())]), + ) + } + + fn walk_update(&mut self, update: &Update) -> Result<(), String> { + self.emit( + "sparql_document", + None, + None, + None, + None, + Some("update".to_owned()), + string_attributes(&[("operation_count", update.operations.len().to_string())]), + )?; + self.emit_query_metadata(update.base_iri.as_ref(), update.version.as_ref())?; + for operation in &update.operations { + self.walk_update_operation(operation)?; + } + Ok(()) + } + + fn walk_update_operation(&mut self, operation: &GraphUpdateOperation) -> Result<(), String> { + self.emit_simple("sparql_update_operation", update_operation_kind(operation))?; + match operation { + GraphUpdateOperation::InsertData { data } => { + for quad in data { + self.emit_quad_pattern("insert_data", quad)?; + } + } + GraphUpdateOperation::DeleteData { data } => { + for quad in data { + self.emit_quad_pattern("delete_data", quad)?; + } + } + GraphUpdateOperation::DeleteInsert { + delete, + insert, + with, + using, + pattern, + } => { + if let Some(with) = with { + self.emit_simple("sparql_with_graph", with.as_str())?; + } + for clause in using { + let (role, node) = match clause { + UsingClause::Default(node) => ("default", node), + UsingClause::Named(node) => ("named", node), + }; + self.emit( + "sparql_using_graph", + Some(SemanticTerm::Iri { + value: node.as_str().to_owned(), + }), + None, + None, + None, + None, + string_attributes(&[("role", role.to_owned())]), + )?; + } + for quad in delete { + self.emit_quad_pattern("delete_template", quad)?; + } + for quad in insert { + self.emit_quad_pattern("insert_template", quad)?; + } + self.walk_pattern(pattern)?; + } + GraphUpdateOperation::Load { + silent, + source, + destination, + } => self.emit_graph_operation( + "load", + *silent, + Some(SemanticTerm::Iri { + value: source.as_str().to_owned(), + }), + Some(destination), + )?, + GraphUpdateOperation::Clear { silent, target } => { + self.emit_graph_operation("clear", *silent, None, Some(target))?; + } + GraphUpdateOperation::Drop { silent, target } => { + self.emit_graph_operation("drop", *silent, None, Some(target))?; + } + GraphUpdateOperation::Create { silent, graph } => self.emit( + "sparql_graph_operation", + Some(SemanticTerm::Iri { + value: graph.as_str().to_owned(), + }), + None, + None, + None, + Some("create".to_owned()), + string_attributes(&[("silent", silent.to_string())]), + )?, + GraphUpdateOperation::Add { + silent, + source, + destination, + } + | GraphUpdateOperation::Move { + silent, + source, + destination, + } + | GraphUpdateOperation::Copy { + silent, + source, + destination, + } => self.emit( + "sparql_graph_operation", + graph_target_term(source), + None, + graph_target_term(destination), + None, + Some(update_operation_kind(operation).to_owned()), + string_attributes(&[ + ("silent", silent.to_string()), + ("source", source.to_string()), + ("destination", destination.to_string()), + ]), + )?, + } + Ok(()) + } + + fn emit_graph_operation( + &mut self, + operation: &str, + silent: bool, + subject: Option, + target: Option<&GraphTarget>, + ) -> Result<(), String> { + let target_text = target.map_or_else(String::new, ToString::to_string); + self.emit( + "sparql_graph_operation", + subject, + None, + target.and_then(graph_target_term), + None, + Some(operation.to_owned()), + string_attributes(&[("silent", silent.to_string()), ("target", target_text)]), + ) + } +} + +fn term_pattern(term: &TermPattern) -> SemanticTerm { + match term { + TermPattern::NamedNode(node) => SemanticTerm::Iri { + value: node.as_str().to_owned(), + }, + TermPattern::BlankNode(node) => SemanticTerm::Blank { + value: format!("_:{}", node.as_str()), + }, + TermPattern::Literal(literal) => sparql_literal(literal), + TermPattern::Variable(variable) => SemanticTerm::Variable { + value: variable.as_str().to_owned(), + }, + TermPattern::Triple(triple) => SemanticTerm::QuotedTriple { + subject: Box::new(term_pattern(&triple.subject)), + predicate: named_pattern_value(&triple.predicate), + object: Box::new(term_pattern(&triple.object)), + }, + } +} + +fn ground_term(term: &GroundTerm) -> SemanticTerm { + match term { + GroundTerm::NamedNode(node) => SemanticTerm::Iri { + value: node.as_str().to_owned(), + }, + GroundTerm::Literal(literal) => sparql_literal(literal), + GroundTerm::Triple(triple) => SemanticTerm::QuotedTriple { + subject: Box::new(ground_term(&triple.subject)), + predicate: triple.predicate.as_str().to_owned(), + object: Box::new(ground_term(&triple.object)), + }, + GroundTerm::BlankNode(node) => SemanticTerm::Blank { + value: format!("_:{}", node.as_str()), + }, + } +} + +fn sparql_literal(literal: &purrdf_sparql_algebra::Literal) -> SemanticTerm { + let direction = literal.direction().map(|direction| match direction { + BaseDirection::Ltr => "ltr".to_owned(), + BaseDirection::Rtl => "rtl".to_owned(), + }); + SemanticTerm::Literal { + value: literal.value().to_owned(), + datatype: Some(literal.datatype().as_str().to_owned()), + language: literal.language().map(ToOwned::to_owned), + direction, + } +} + +fn named_pattern(pattern: &NamedNodePattern) -> SemanticTerm { + match pattern { + NamedNodePattern::NamedNode(node) => SemanticTerm::Iri { + value: node.as_str().to_owned(), + }, + NamedNodePattern::Variable(variable) => SemanticTerm::Variable { + value: variable.as_str().to_owned(), + }, + } +} + +fn named_pattern_value(pattern: &NamedNodePattern) -> String { + match pattern { + NamedNodePattern::NamedNode(node) => node.as_str().to_owned(), + NamedNodePattern::Variable(variable) => format!("?{}", variable.as_str()), + } +} + +fn graph_target_term(target: &GraphTarget) -> Option { + match target { + GraphTarget::Named(node) => Some(SemanticTerm::Iri { + value: node.as_str().to_owned(), + }), + GraphTarget::Default | GraphTarget::NamedGraphs | GraphTarget::All => None, + } +} + +fn graph_pattern_kind(pattern: &GraphPattern) -> &'static str { + match pattern { + GraphPattern::Bgp { .. } => "bgp", + GraphPattern::Path { .. } => "path", + GraphPattern::Join { .. } => "join", + GraphPattern::LeftJoin { .. } => "left_join", + GraphPattern::Lateral { .. } => "lateral", + GraphPattern::Filter { .. } => "filter", + GraphPattern::Union { .. } => "union", + GraphPattern::Graph { .. } => "graph", + GraphPattern::Extend { .. } => "extend", + GraphPattern::Minus { .. } => "minus", + GraphPattern::Service { .. } => "service", + GraphPattern::Values { .. } => "values", + GraphPattern::OrderBy { .. } => "order_by", + GraphPattern::Project { .. } => "project", + GraphPattern::Distinct { .. } => "distinct", + GraphPattern::Reduced { .. } => "reduced", + GraphPattern::Slice { .. } => "slice", + GraphPattern::Group { .. } => "group", + GraphPattern::PropertyFunction(_) => "property_function", + } +} + +fn property_path_kind(path: &PropertyPathExpression) -> &'static str { + match path { + PropertyPathExpression::NamedNode(_) => "named_node", + PropertyPathExpression::Reverse(_) => "reverse", + PropertyPathExpression::Sequence(_, _) => "sequence", + PropertyPathExpression::Alternative(_, _) => "alternative", + PropertyPathExpression::ZeroOrMore(_) => "zero_or_more", + PropertyPathExpression::OneOrMore(_) => "one_or_more", + PropertyPathExpression::ZeroOrOne(_) => "zero_or_one", + PropertyPathExpression::NegatedPropertySet(_) => "negated_property_set", + PropertyPathExpression::Range { .. } => "range", + PropertyPathExpression::Wildcard { .. } => "wildcard", + } +} + +fn expression_kind(expression: &Expression) -> &'static str { + match expression { + Expression::NamedNode(_) => "named_node", + Expression::Literal(_) => "literal", + Expression::Variable(_) => "variable", + Expression::Bound(_) => "bound", + Expression::Or(_, _) => "or", + Expression::And(_, _) => "and", + Expression::Equal(_, _) => "equal", + Expression::SameTerm(_, _) => "same_term", + Expression::Greater(_, _) => "greater", + Expression::GreaterOrEqual(_, _) => "greater_or_equal", + Expression::Less(_, _) => "less", + Expression::LessOrEqual(_, _) => "less_or_equal", + Expression::Add(_, _) => "add", + Expression::Subtract(_, _) => "subtract", + Expression::Multiply(_, _) => "multiply", + Expression::Divide(_, _) => "divide", + Expression::UnaryPlus(_) => "unary_plus", + Expression::UnaryMinus(_) => "unary_minus", + Expression::Not(_) => "not", + Expression::In(_, _) => "in", + Expression::If(_, _, _) => "if", + Expression::Coalesce(_) => "coalesce", + Expression::FunctionCall(_, _) => "function_call", + Expression::Exists(_) => "exists", + } +} + +fn update_operation_kind(operation: &GraphUpdateOperation) -> &'static str { + match operation { + GraphUpdateOperation::InsertData { .. } => "insert_data", + GraphUpdateOperation::DeleteData { .. } => "delete_data", + GraphUpdateOperation::DeleteInsert { .. } => "delete_insert", + GraphUpdateOperation::Load { .. } => "load", + GraphUpdateOperation::Clear { .. } => "clear", + GraphUpdateOperation::Drop { .. } => "drop", + GraphUpdateOperation::Create { .. } => "create", + GraphUpdateOperation::Add { .. } => "add", + GraphUpdateOperation::Move { .. } => "move", + GraphUpdateOperation::Copy { .. } => "copy", + } +} + +#[cfg(test)] +mod tests { + use crate::protocol::sha256_hex; + + use super::*; + + #[test] + fn query_walks_graph_service_and_paths() { + let content = concat!( + "SELECT ?o WHERE { ", + "GRAPH { ?s /+ ?o } ", + "SERVICE SILENT { ?s ?o } ", + "}", + ); + let file = FileRequest { + path: "query.rq".to_owned(), + content_sha256: sha256_hex(content.as_bytes()), + content: content.to_owned(), + base_iri: None, + }; + let result = extract(&file).expect("valid query extracts"); + for kind in ["sparql_graph", "sparql_service", "sparql_property_path"] { + assert!( + result.facts.iter().any(|fact| fact.kind == kind), + "missing {kind} fact" + ); + } + assert!( + result + .facts + .iter() + .all(|fact| fact.provenance.start.is_none()) + ); + } + + #[test] + fn update_walks_operations() { + let content = concat!( + "INSERT DATA { GRAPH { ", + " } }; ", + "MOVE SILENT GRAPH TO GRAPH ", + ); + let file = FileRequest { + path: "update.sparql".to_owned(), + content_sha256: sha256_hex(content.as_bytes()), + content: content.to_owned(), + base_iri: None, + }; + let result = extract(&file).expect("valid update extracts"); + assert_eq!(result.document_kind.as_deref(), Some("update")); + assert_eq!( + result + .facts + .iter() + .filter(|fact| fact.kind == "sparql_update_operation") + .count(), + 2 + ); + } + + #[test] + fn ground_term_conversion_preserves_quoted_triples() { + let parser = SparqlParser::new(); + let query = parser + .parse_query( + "SELECT ?x WHERE { VALUES ?x { << >> } }", + ) + .expect("RDF 1.2 quoted triple in VALUES parses"); + let Query::Select { pattern, .. } = query else { + panic!("expected SELECT"); + }; + let mut stack = vec![&pattern]; + let mut found = false; + while let Some(pattern) = stack.pop() { + match pattern { + GraphPattern::Values { bindings, .. } => { + found = + bindings.iter().flatten().flatten().any(|term| { + matches!(ground_term(term), SemanticTerm::QuotedTriple { .. }) + }); + } + GraphPattern::Project { inner, .. } + | GraphPattern::Distinct { inner } + | GraphPattern::Reduced { inner } + | GraphPattern::Slice { inner, .. } => stack.push(inner), + GraphPattern::Join { left, right } => { + stack.push(left); + stack.push(right); + } + _ => {} + } + } + assert!(found); + } +} diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nq b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nq new file mode 100644 index 00000000..29f77b54 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nq @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + . +_:blank "named" . diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nt b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nt new file mode 100644 index 00000000..dd2d8b75 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.nt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + _:blank . +_:blank "blank" . diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.trig b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.trig new file mode 100644 index 00000000..a2f2bd68 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.trig @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +@prefix ex: . + +ex:s ex:p ex:o . +ex:g { + ex:inside ex:p <<( ex:s ex:p ex:o )>> . + _:named ex:label "named graph blank" . +} diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.ttl b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.ttl new file mode 100644 index 00000000..3c40cf4c --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/rdf/semantic.ttl @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +@prefix ex: . + +ex:s ex:p ex:o . +ex:s ex:friend [ ex:name "anonymous" ] . +ex:r <<( ex:s ex:p ex:o )>> . +ex:r ex:confidence "0.9" . diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/malformed.rq b/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/malformed.rq new file mode 100644 index 00000000..22102d7f --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/malformed.rq @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +SELECT WHERE { ?s ?p } diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/query.rq b/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/query.rq new file mode 100644 index 00000000..f2da68ac --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/query.rq @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +PREFIX ex: +PREFIX rdf: + +SELECT ?s ?o WHERE { + GRAPH ex:g { + ?s ex:p/ex:q+ ?o . + ?reifier rdf:reifies <<( ?s ex:p ?o )>> . + } + SERVICE SILENT { + ?s ex:remote ?o . + } +} diff --git a/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/update.sparql b/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/update.sparql new file mode 100644 index 00000000..6f6e1dcb --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/fixtures/sparql/update.sparql @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +# SPDX-License-Identifier: AGPL-3.0-only + +PREFIX ex: + +INSERT DATA { + GRAPH ex:g { ex:s ex:p ex:o } +}; +DELETE { GRAPH ex:g { ?s ex:p ?o } } +INSERT { GRAPH ex:h { ?s ex:p ?o } } +USING NAMED ex:g +WHERE { GRAPH ex:g { ?s ex:p ?o } }; +MOVE SILENT GRAPH ex:g TO GRAPH ex:h diff --git a/rust/purrdf-code-intel-extractor/tests/protocol.rs b/rust/purrdf-code-intel-extractor/tests/protocol.rs new file mode 100644 index 00000000..1e5b98e3 --- /dev/null +++ b/rust/purrdf-code-intel-extractor/tests/protocol.rs @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +use std::io::Write; +use std::process::{Command, Stdio}; + +use coding_ethos_purrdf_extractor::{ + BatchRequest, FileRequest, FileStatus, PROTOCOL, PURRDF_REVISION, SemanticTerm, extract_batch, + sha256_hex, +}; + +const TURTLE: &str = include_str!("fixtures/rdf/semantic.ttl"); +const TRIG: &str = include_str!("fixtures/rdf/semantic.trig"); +const NTRIPLES: &str = include_str!("fixtures/rdf/semantic.nt"); +const NQUADS: &str = include_str!("fixtures/rdf/semantic.nq"); +const QUERY: &str = include_str!("fixtures/sparql/query.rq"); +const UPDATE: &str = include_str!("fixtures/sparql/update.sparql"); +const MALFORMED: &str = include_str!("fixtures/sparql/malformed.rq"); + +fn file(path: &str, content: &str) -> FileRequest { + FileRequest { + path: path.to_owned(), + content_sha256: sha256_hex(content.as_bytes()), + content: content.to_owned(), + base_iri: None, + } +} + +fn request(files: Vec) -> BatchRequest { + BatchRequest { + protocol: PROTOCOL.to_owned(), + request_id: "integration-test".to_owned(), + files, + } +} + +#[test] +fn extracts_all_frozen_extensions_deterministically() { + let files = vec![ + file("z/query.rq", QUERY), + file("a/semantic.ttl", TURTLE), + file("b/semantic.trig", TRIG), + file("c/semantic.nt", NTRIPLES), + file("d/semantic.nq", NQUADS), + file("y/update.sparql", UPDATE), + ]; + let first = extract_batch(request(files.clone())).expect("batch extracts"); + let second = extract_batch(request(files)).expect("same batch extracts again"); + assert_eq!( + serde_json::to_vec(&first.response).expect("first response encodes"), + serde_json::to_vec(&second.response).expect("second response encodes"), + "same content must produce byte-identical JSON" + ); + assert_eq!(first.response.extractor.purrdf_revision, PURRDF_REVISION); + assert!( + first + .response + .results + .windows(2) + .all(|pair| pair[0].path < pair[1].path), + "results are path sorted" + ); + assert!( + first + .response + .results + .iter() + .all(|result| matches!(result.status, FileStatus::Ok)) + ); + for result in &first.response.results { + assert!( + result.facts.windows(2).all(|pair| pair[0].id < pair[1].id), + "{} facts are ID sorted and unique", + result.path + ); + } +} + +#[test] +fn rdf_facts_preserve_rdf12_and_source_contracts() { + let outcome = extract_batch(request(vec![ + file("semantic.ttl", TURTLE), + file("semantic.trig", TRIG), + ])) + .expect("RDF batch extracts"); + let facts: Vec<_> = outcome + .response + .results + .iter() + .flat_map(|result| &result.facts) + .collect(); + assert!(facts.iter().any(|fact| fact.kind == "rdf_reifier")); + assert!(facts.iter().any(|fact| fact.kind == "rdf_annotation")); + assert!(facts.iter().any(|fact| fact.graph.is_some())); + assert!(facts.iter().any(|fact| { + matches!( + fact.object.as_ref(), + Some(SemanticTerm::QuotedTriple { .. }) + ) + })); + assert!(facts.iter().any(|fact| { + matches!(fact.subject.as_ref(), Some(SemanticTerm::Blank { .. })) + || matches!(fact.object.as_ref(), Some(SemanticTerm::Blank { .. })) + })); + + let encoded = serde_json::to_value(&outcome.response).expect("response encodes"); + let facts = encoded["results"] + .as_array() + .expect("results array") + .iter() + .flat_map(|result| result["facts"].as_array().expect("facts array")) + .collect::>(); + assert!( + facts + .iter() + .all(|fact| { fact["provenance"]["span_fidelity"] == "subject_start" }) + ); + assert!( + facts + .iter() + .any(|fact| fact["provenance"]["start"].is_object()) + ); +} + +#[test] +fn sparql_facts_cover_query_update_graph_service_and_paths() { + let outcome = extract_batch(request(vec![ + file("query.rq", QUERY), + file("update.sparql", UPDATE), + ])) + .expect("SPARQL batch extracts"); + let facts: Vec<_> = outcome + .response + .results + .iter() + .flat_map(|result| &result.facts) + .collect(); + for kind in [ + "sparql_graph", + "sparql_service", + "sparql_property_path", + "sparql_update_operation", + "sparql_quad_pattern", + ] { + assert!(facts.iter().any(|fact| fact.kind == kind), "missing {kind}"); + } + let encoded = serde_json::to_value(&outcome.response).expect("response encodes"); + assert!( + encoded["results"] + .as_array() + .expect("results array") + .iter() + .flat_map(|result| result["facts"].as_array().expect("facts array")) + .all(|fact| { + fact["provenance"]["span_fidelity"] == "none" + && fact["provenance"].get("start").is_none() + }) + ); +} + +#[test] +fn malformed_file_is_structured_and_does_not_fail_batch() { + let outcome = extract_batch(request(vec![ + file("good.nt", NTRIPLES), + file("malformed.rq", MALFORMED), + ])) + .expect("a valid envelope returns every file result"); + assert_eq!(outcome.response.results.len(), 2); + let malformed = outcome + .response + .results + .iter() + .find(|result| result.path == "malformed.rq") + .expect("malformed result exists"); + assert!(matches!(malformed.status, FileStatus::Error)); + assert!(malformed.facts.is_empty()); + assert!( + malformed + .error + .as_deref() + .is_some_and(|message| message.contains("failed as query")) + ); + assert_eq!(outcome.diagnostics.len(), 1); +} + +#[test] +fn binary_keeps_stdout_machine_only_and_stderr_diagnostic_only() { + let request = request(vec![file("malformed.rq", MALFORMED)]); + let encoded = serde_json::to_vec(&request).expect("request encodes"); + let mut child = Command::new(env!("CARGO_BIN_EXE_coding-ethos-purrdf-extractor")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("extractor starts"); + child + .stdin + .as_mut() + .expect("stdin pipe") + .write_all(&encoded) + .expect("request writes"); + let output = child.wait_with_output().expect("extractor completes"); + assert!( + output.status.success(), + "file errors do not fail a valid batch" + ); + let response: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("stdout is exactly JSON"); + assert_eq!(response["protocol"], PROTOCOL); + let stderr = String::from_utf8(output.stderr).expect("stderr is UTF-8"); + assert!(stderr.contains("malformed.rq")); + assert!(!stderr.contains(PROTOCOL)); +} + +#[test] +fn binary_rejects_an_unknown_protocol_with_exit_two() { + let mut request = request(Vec::new()); + request.protocol = "coding-ethos.external-extractor.v0".to_owned(); + let encoded = serde_json::to_vec(&request).expect("request encodes"); + let mut child = Command::new(env!("CARGO_BIN_EXE_coding-ethos-purrdf-extractor")) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("extractor starts"); + child + .stdin + .as_mut() + .expect("stdin pipe") + .write_all(&encoded) + .expect("request writes"); + let output = child.wait_with_output().expect("extractor completes"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + assert!( + String::from_utf8(output.stderr) + .expect("stderr is UTF-8") + .contains("protocol error") + ); +} From 06f3b0dfc535f6b18a3076d4f922b446c706ce77 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 13:29:06 -0600 Subject: [PATCH 08/17] feat(code-intel): add exact source generations --- Makefile | 39 +- README.md | 23 +- docs/CODE_INTEL.md | 168 +- docs/CODE_INTEL_STORAGE.md | 26 +- go/cmd/coding-ethos-run/args.go | 3 +- go/cmd/coding-ethos-run/main_test.go | 12 + go/cmd/coding-ethos-run/status.go | 2 +- go/go.mod | 1 + .../agenthooks/provider_capabilities.go | 43 +- .../agenthookscli/main_internal_test.go | 4 + go/internal/astfacts/astfacts.go | 107 +- go/internal/astfacts/astfacts_test.go | 71 + go/internal/astfacts/language.go | 299 +++- go/internal/astfacts/resolver.go | 29 +- go/internal/codeintel/ast_index.go | 2 +- go/internal/codeintel/downstream_analysis.go | 4 +- .../downstream_analysis_internal_test.go | 4 +- go/internal/codeintel/graph_report.go | 2 +- go/internal/codeintel/query.go | 2 + go/internal/codeintel/skill_health.go | 2 +- go/internal/codeintel/source_v2_external.go | 479 ++++++ go/internal/codeintel/source_v2_query.go | 419 +++++ go/internal/codeintel/source_v2_storage.go | 503 ++++++ go/internal/codeintel/source_v2_sync.go | 1466 +++++++++++++++++ go/internal/codeintel/source_v2_test.go | 716 ++++++++ go/internal/codeintel/source_v2_types.go | 273 +++ go/internal/codeintelcli/main.go | 44 +- go/internal/codeintelcli/source_v2.go | 122 ++ go/internal/codeintelcli/source_v2_test.go | 105 ++ go/internal/hooks/code_intel_enrichment.go | 38 +- go/internal/hooks/lifecycle_context.go | 2 +- go/internal/hooks/runner_test.go | 23 +- go/internal/mcp/code_intel.go | 78 +- go/internal/mcp/code_intel_workspace_test.go | 54 + 34 files changed, 5000 insertions(+), 165 deletions(-) create mode 100644 go/internal/codeintel/source_v2_external.go create mode 100644 go/internal/codeintel/source_v2_query.go create mode 100644 go/internal/codeintel/source_v2_storage.go create mode 100644 go/internal/codeintel/source_v2_sync.go create mode 100644 go/internal/codeintel/source_v2_test.go create mode 100644 go/internal/codeintel/source_v2_types.go create mode 100644 go/internal/codeintelcli/source_v2.go create mode 100644 go/internal/codeintelcli/source_v2_test.go diff --git a/Makefile b/Makefile index d61d458d..6b104b36 100644 --- a/Makefile +++ b/Makefile @@ -23,12 +23,16 @@ MANAGED_PREFIX_DIR := $(TOOLCHAIN_DIR)/prefix MANAGED_GITHUB_BIN_DIR := $(TOOLCHAIN_DIR)/github-bin MANAGED_TOOLCHAIN_SOURCE := $(PRECOMMIT_DIR)hooks/managed-toolchain.tsv MANAGED_TOOLCHAIN_MANIFEST := $(TOOLCHAIN_DIR)/manifest.tsv +PURRDF_EXTRACTOR_DIR := $(LOCAL_REPO_ROOT)/rust/purrdf-code-intel-extractor +PURRDF_EXTRACTOR_MANIFEST := $(PURRDF_EXTRACTOR_DIR)/Cargo.toml +PURRDF_EXTRACTOR_BINARY := coding-ethos-purrdf-extractor GIT ?= /usr/bin/git UV ?= uv PYTHON ?= python GO ?= go GOFMT ?= gofmt +CARGO ?= cargo GO_BUILD_FLAGS ?= -trimpath -buildvcs=false GO_BUILD_CACHE_DIR ?= $(LOCAL_REPO_ROOT)/.coding-ethos/cache/go-build UV_CACHE_DIR ?= $(LOCAL_REPO_ROOT)/.coding-ethos/cache/uv @@ -245,6 +249,8 @@ endef go-hook-runner-install \ policy-bundle-install \ go-tools-smoke \ + purrdf-extractor-check \ + purrdf-extractor-install \ go-tools-clean \ clean-cache \ sync-tool-configs \ @@ -446,7 +452,7 @@ python-coverage: ensure-uv ## Run Python tests with coverage enforcement. @$(UV) run coverage report --fail-under="$(PYTHON_COVERAGE_MIN)" @$(UV) run coverage xml -o "$(GO_COVERAGE_DIR)/coverage-python.xml" -check: check-local-artifacts test check-tool-configs check-gemini-prompts check-agent-skills check-provider-matrix go-test go-e2e-test ## Run the repo's current verification gate. +check: check-local-artifacts test check-tool-configs check-gemini-prompts check-agent-skills check-provider-matrix go-test go-e2e-test purrdf-extractor-check ## Run the repo's current verification gate. check-local-artifacts: ## Fail if local build artifacts escaped managed output dirs. @$(call print_step,Checking for unmanaged local build artifacts) @@ -588,7 +594,36 @@ check-provider-matrix: ensure-go go-tools-install ## Fail if the provider capabi @"$(GO_TOOLS_BIN_DIR)/coding-ethos-agent-hooks" \ check-provider-matrix --root "$(LOCAL_REPO_ROOT)" -build: sync-tool-configs sync-consumer-tool-configs sync-gemini-prompts _sync-agent-skills _sync-consumer-agent-skills sync-provider-matrix go-tools-install repair-repo-ignores _sync-git-hooks _sync-agent-hooks _sync-consumer-agent-hooks managed-toolchain-install go-hook-runner-install policy-bundle-install _sync-parent-hook-runtime ## Build checkout-local hook runtime artifacts. +build: sync-tool-configs sync-consumer-tool-configs sync-gemini-prompts _sync-agent-skills _sync-consumer-agent-skills sync-provider-matrix go-tools-install purrdf-extractor-install repair-repo-ignores _sync-git-hooks _sync-agent-hooks _sync-consumer-agent-hooks managed-toolchain-install go-hook-runner-install policy-bundle-install _sync-parent-hook-runtime ## Build checkout-local hook runtime artifacts. + +purrdf-extractor-check: ## Test the optional PurRDF code-intel extractor when present. + @if [ -f "$(PURRDF_EXTRACTOR_MANIFEST)" ]; then \ + command -v "$(CARGO)" >/dev/null 2>&1 || { \ + printf '$(COLOR_WARN)cargo is required for the PurRDF extractor.$(COLOR_RESET)\n' >&2; \ + exit 1; \ + }; \ + $(call print_step,Testing PurRDF code-intel extractor); \ + "$(CARGO)" test --locked --manifest-path "$(PURRDF_EXTRACTOR_MANIFEST)"; \ + else \ + $(call print_info,PurRDF extractor subtree not present in this checkout); \ + fi + +purrdf-extractor-install: ## Build and install the optional PurRDF extractor when present. + @if [ -f "$(PURRDF_EXTRACTOR_MANIFEST)" ]; then \ + command -v "$(CARGO)" >/dev/null 2>&1 || { \ + printf '$(COLOR_WARN)cargo is required for the PurRDF extractor.$(COLOR_RESET)\n' >&2; \ + exit 1; \ + }; \ + $(call print_step,Building PurRDF code-intel extractor); \ + "$(CARGO)" build --locked --release --manifest-path "$(PURRDF_EXTRACTOR_MANIFEST)"; \ + mkdir -p "$(GO_TOOLS_BIN_DIR)"; \ + install -m 0755 \ + "$(PURRDF_EXTRACTOR_DIR)/target/release/$(PURRDF_EXTRACTOR_BINARY)" \ + "$(GO_TOOLS_BIN_DIR)/$(PURRDF_EXTRACTOR_BINARY)"; \ + $(call print_info,installed: $(GO_TOOLS_BIN_DIR)/$(PURRDF_EXTRACTOR_BINARY)); \ + else \ + $(call print_info,PurRDF extractor subtree not present in this checkout); \ + fi sandbox-runtime-validate: ensure-go go-tools-install ## Validate required sandbox runtime. @$(call print_step,Validating native sandbox runtime) diff --git a/README.md b/README.md index e2d19c31..59d169a3 100644 --- a/README.md +++ b/README.md @@ -464,7 +464,9 @@ bin/coding-ethos-run code-intel decisions add --title 'Use explicit startup' --r bin/coding-ethos-run code-intel decisions import docs/decisions bin/coding-ethos-run code-intel decisions list --path pkg/app.py --query startup bin/coding-ethos-run code-intel decisions health --path pkg/app.py -bin/coding-ethos-run code-intel rebuild-index +bin/coding-ethos-run code-intel sync --root . +bin/coding-ethos-run code-intel status --root . +bin/coding-ethos-run code-intel rebuild-derived bin/coding-ethos-run code-intel hook-usage --risk-category bypass bin/coding-ethos-run code-intel record-hook-review --trace-id hook-1 --disposition false_positive bin/coding-ethos-run code-intel hook-reviews --disposition false_positive @@ -576,8 +578,12 @@ Use `--format json|toon|human` to choose between the stable automation payload, compact TOON handoff, or a short operator-readable summary. The compact output puts blocking friction, affected command families, repeated remediation loops, storage repair commands, and evidence gaps ahead of high-volume allowed events. -`rebuild-index` refreshes the DuckDB analytical index from append-only events -and retained event logs, then removes obsolete store files after rebuild. +`sync` creates the exact immutable source generation for the current worktree. +`status` compares that generation to current source bytes and reports source +readiness independently from vector readiness. `rebuild-derived` refreshes the +DuckDB analytical index from append-only events and retained event logs, then +removes obsolete store files after rebuild. The old `rebuild-index` spelling is +a deprecated compatibility alias. Inspect the repo-local disk output surface before pruning or deeper analysis: @@ -687,8 +693,10 @@ detected repo paths, likely symbols, direct graph edges, repeated-failure evidence, risk flags, and exact MCP follow-up calls such as `code_intel_context_card {"path":"pkg/app.py"}`. Missing or stale code-intel does not block the tool result; the hook emits a concise refresh hint with -`coding-ethos-code-intel rebuild-index`. Repositories can disable or cap this -context with `proxy.code_intel_enrichment` in `repo_config.yaml`. +`coding-ethos-code-intel sync --root .`. Hook and MCP metadata carry the +additive exact-source receipt under `coding-ethos.code-intel/v2`. +Repositories can disable or cap this context with `proxy.code_intel_enrichment` +in `repo_config.yaml`. PreToolUse hooks also perform narrow semantic policy injection. Mutating Git intents such as `policy-git commit` inject the `safe-git-workflow` skill pointer @@ -1590,6 +1598,11 @@ bin/coding-ethos-run agent-hooks verify bin/coding-ethos-run agent-hooks capabilities ``` +The versioned capability report includes an optional `code_intel` object with +contract `coding-ethos.code-intel/v2` and the exact `sync` and `status` command +templates. Orchestrators can feature-detect that object without changing the +existing agent-hooks v1 discovery envelope. + Agent hook generation is all-or-nothing. `sync` writes every supported repo-local surface. Provider support levels, native settings files, hook events, MCP setup, generated targets, memory behavior, response shapes, and unsupported diff --git a/docs/CODE_INTEL.md b/docs/CODE_INTEL.md index 95722bf6..54b65175 100644 --- a/docs/CODE_INTEL.md +++ b/docs/CODE_INTEL.md @@ -11,18 +11,24 @@ before they run broad shell commands or repeat failed edits. ## Design Position -Use one repo-local storage substrate with separate logical responsibilities: - -- **DuckDB is the canonical fact store.** It owns traces, policy decisions, - remediations, outcomes, code symbols, AST graph edges, file metadata, and - full-text search. +Use explicit storage layers with separate logical responsibilities: + +- **Immutable source generations are the exact source-fact substrate.** A + content-addressed base manifest is shared through the Git common directory; + each worktree owns its delta manifest, tombstones, and current-generation + receipt. No shared writable DuckDB is used for these source facts. +- **DuckDB remains the v1-compatible analytical and telemetry store.** It owns + traces, policy decisions, remediations, outcomes, derived AST graph edges, + file metadata, and full-text search. Existing v1 stores are retained during + the v2 migration and can be rebuilt explicitly as derived state. - **duckdb-vss is the active vector backend.** It keeps embeddings, metadata filters, and similarity search inside repo-local DuckDB files through the pure-Go `github.com/duckdb/duckdb-go/v2` integration, with no native sidecar, daemon, hosted service, or conditional build path. -Vector indexes are derived artifacts. They must be rebuildable from the -canonical DuckDB store and the checked-out repository contents. +Vector indexes and DuckDB query indexes are derived artifacts. Source readiness +and vector readiness are reported separately so a missing embedding cannot make +an exact source generation look stale, or vice versa. ## Goals @@ -55,9 +61,14 @@ repository policy bundle | v -Tree-sitter AST extraction +Central language registry + Tree-sitter / Goldmark / PurRDF batch extractor + | + +--> immutable source generation + | shared content-addressed fragments + base manifest + | lane-local delta manifest + tombstones + status receipt | - +--> DuckDB canonical store + +--> DuckDB derived/telemetry store (v1-compatible) | files | symbols | AST nodes @@ -114,11 +125,13 @@ same facts; they must not become a second parsing or policy interpretation path. The AST layer follows the resolver pattern proven in `~/Active/pyqa_lint`: language detection, parser binding, parser reuse, tree traversal, and line-to-nearest-context lookup live behind one Go entrypoint. The active -resolver supports Go, Python, JavaScript/TypeScript, shell, YAML, JSON, and -TOML. JSON and TOML are treated as first-class config-policy surfaces, so -agents can retrieve precise config entries instead of reading whole config -files. Markdown remains intentionally deferred until the project selects a -maintained Go binding or a first-class adapter for its parser layout. +resolver supports Go, Python, JavaScript/TypeScript/TSX, Rust, shell, YAML, +JSON, TOML, and Markdown. JSON and TOML are treated as first-class +config-policy surfaces, so agents can retrieve precise config entries instead +of reading whole config files. Every registry entry exposes an exact parser +fingerprint. Turtle, TriG, N-Triples, N-Quads, and SPARQL use the optional +one-shot `coding-ethos-purrdf-extractor`; a missing or mismatched external +extractor fails sync, and is never silently replaced by regex facts. Tree-sitter-backed policy diagnostics carry AST metadata into SARIF result properties and partial fingerprints. Code scanning can therefore track the @@ -211,6 +224,124 @@ through MCP `code_intel_proxy_denials`, which queries the same ledger instead of reinterpreting proxy policy. The trust boundary and event contract are documented in [AGENT_PROXY.md](AGENT_PROXY.md). +## Exact Source Generations (`coding-ethos.code-intel/v2`) + +Run a source sync for the exact worktree that an agent will use, then inspect +the typed receipt: + +```bash +coding-ethos-code-intel sync --root /path/to/worktree +coding-ethos-code-intel status --root /path/to/worktree +``` + +`RepositoryID` hashes the normalized `origin` URL plus the sorted repository +root-commit set, so equivalent clones share an identity while unrelated +histories cannot collide. The normalization removes credentials, default +ports, a trailing `.git`, query, and fragment; SCP-like SSH and canonical SSH +URLs normalize identically. A repository without `origin` uses the explicit +path-local fallback `local-common-dir:` plus the same +root-commit set. Shallow repositories fail closed because their true root +commit is unavailable. `SourceSnapshotID` hashes repository identity, HEAD, +current eligible source bytes, config, and the exact extractor set, and +`GenerationID` hashes the snapshot plus its base and delta manifests. A status +is `exact` only when every eligible file is indexed by the declared extractor. +`partial`, `failed`, `missing`, and `stale` are explicit non-exact states. +Coverage is reported per language with +`eligible`, `indexed`, `cache_hits`, `unsupported`, `excluded`, `oversized`, +`failed`, and `stale` counts. + +When Turtle, TriG, N-Triples, N-Quads, or SPARQL input is eligible, sync first +performs an empty protocol exchange with the pinned PurRDF helper. A missing +helper or any protocol, version, name, or PurRDF-revision mismatch is a hard +sync error, including when immutable fragments could otherwise be reused. +Non-RDF repositories do not require the helper. Per-file parser diagnostics +from a correctly identified helper remain explicit `failed` coverage. + +Both commands emit deterministic field shapes. A sync receipt has this form: + +```json +{ + "contract": "coding-ethos.code-intel/v2", + "kind": "coding-ethos.code-intel.sync/v2", + "generated_at_utc": "...", + "repair": "coding-ethos-code-intel sync --root .", + "source_readiness": { + "identity": { + "repository_id": "repository:sha256:...", + "source_snapshot_id": "snapshot:sha256:...", + "generation_id": "generation:sha256:...", + "head_oid": "...", + "worktree_id": "worktree:sha256:...", + "worktree_fingerprint": "worktree-content:sha256:...", + "config_hash": "config:sha256:...", + "extractor_set_hash": "extractors:sha256:..." + }, + "coverage": [], + "status": "exact" + }, + "vector_readiness": { + "status": "not_evaluated", + "ready_records": 0, + "missing_vectors": 0 + }, + "storage": { + "shared_root": "...", + "lane_root": "...", + "base_manifest_id": "base:sha256:...", + "delta_manifest_id": "delta:sha256:..." + }, + "sync": { + "files_indexed": 0, + "files_failed": 0, + "fragments_reused": 0, + "fragments_written": 0 + } +} +``` + +The `status` receipt uses kind `coding-ethos.code-intel.status/v2` and omits +the `sync` summary. Hook TOON and MCP response metadata expose the same receipt +under the additive key `coding-ethos.code-intel/v2`. The Go query boundary is +`QuerySourceIndex`; it refuses missing or stale generations and returns the +generation identity with path-bound symbol, import, and PurRDF facts. Built-in +facts carry `EXTRACTED` provenance, the exact parser fingerprint, span fidelity, +and deterministic confidence. PurRDF facts preserve the helper's parser +revision and explicit `subject_start` or `none` span fidelity. + +Shared objects live under +`/coding-ethos/code-intel/v2/{fragments,bases}` and are created +immutably. Lane deltas, tombstones, and `current.json` live under the resolved +state root plus a worktree-specific lane ID. A v2 sync does not delete or +rewrite a v1 DuckDB. `rebuild-derived` refreshes the derived DuckDB explicitly; +`rebuild-index` is retained only as a deprecated alias during migration. + +The PurRDF helper protocol is `coding-ethos.external-extractor.v1`. The helper +receives UTF-8 file content through one stdin JSON batch and returns facts on +stdout; it does not read the worktree, use a network, or run as a daemon. A +valid batch may contain per-file errors, which remain visible as failed +language coverage. A missing or mismatched helper aborts sync before a current +generation receipt is published. + +Warm-cache effectiveness is observable through `cache_hits`, +`fragments_reused`, and `fragments_written`. Run the focused benchmark with +`go test ./internal/codeintel -run '^$' -bench SourceV2` when comparing cold +and warm generation costs; keep repository size and extractor fingerprints in +the benchmark report. + +`agent-hooks capabilities` advertises this optional surface as: + +```json +{ + "code_intel": { + "contract": "coding-ethos.code-intel/v2", + "sync_command": "coding-ethos-code-intel sync --root ", + "status_command": "coding-ethos-code-intel status --root ", + "source_readiness_statuses": ["exact", "partial", "failed", "missing", "stale"], + "vector_readiness_statuses": ["not_evaluated", "partial", "ready"] + } +} +``` + ## Canonical DuckDB Store Code-intel commands use `CODE_ETHOS_STATE_ROOT` for their default DuckDB and @@ -367,12 +498,17 @@ bin/coding-ethos-run code-intel hybrid-search --text 'unused import' --model-id bin/coding-ethos-run code-intel index-status --model-id voyage-code-3 bin/coding-ethos-run code-intel vector-stats bin/coding-ethos-run code-intel search --text 'unused import' +bin/coding-ethos-run code-intel sync --root . +bin/coding-ethos-run code-intel status --root . +bin/coding-ethos-run code-intel rebuild-derived --root . +# Deprecated compatibility alias: bin/coding-ethos-run code-intel rebuild-index --root . bin/coding-ethos-run code-intel workspace status --root .. ``` -These commands read retained `.coding-ethos` traces and write only the -repo-local `.coding-ethos/code-intel.duckdb` store. +Legacy analytical commands read retained `.coding-ethos` traces and write the +repo-local `.coding-ethos/code-intel.duckdb` store. The v2 `sync` and `status` +commands use the immutable shared-base and lane-delta layout described above. Workspace commands are the exception to repo-local state because they model a parent directory or worktree family. They store only registry and derived diff --git a/docs/CODE_INTEL_STORAGE.md b/docs/CODE_INTEL_STORAGE.md index e050f96e..6a9793bd 100644 --- a/docs/CODE_INTEL_STORAGE.md +++ b/docs/CODE_INTEL_STORAGE.md @@ -5,12 +5,23 @@ ## Decision -`coding-ethos` uses append-only event files as durable telemetry and DuckDB as -the only local analytical code-intel store. +`coding-ethos` uses immutable content-addressed files for exact source facts, +append-only event files for durable telemetry, and DuckDB for derived local +analytics. + +- `/coding-ethos/code-intel/v2/` contains immutable shared + fragments and HEAD base manifests. +- `/.coding-ethos/code-intel-v2/lanes//` contains one + worktree's immutable delta manifests, tombstones, and current receipt. - `.coding-ethos/events/*.jsonl` is the durable live telemetry surface. - `.coding-ethos/code-intel.duckdb` is the local analytical query index. +Manifest repository identity is independent of these storage paths: it hashes +the normalized origin URL and repository root-commit set. Repositories without +an origin use a documented path-local common-directory fallback; shallow +repositories cannot produce an exact identity. + Hook, lint, proxy, SARIF, remediation, and embedding entrypoints write a structured event before they update the DuckDB query index. The DuckDB index is rebuildable from event files and retained trace logs. Runtime paths do not use @@ -30,8 +41,15 @@ rebuildable analytical surface. ## Operational Contract -- `code-intel rebuild-index` creates or refreshes `code-intel.duckdb` and +- `code-intel sync` creates or reuses an exact v2 source generation without + deleting or rewriting the v1 store. +- Eligible RDF or SPARQL requires an identity-validated pinned PurRDF helper; + missing or mismatched helpers fail sync instead of degrading silently. +- `code-intel status` compares the recorded source snapshot to the current + worktree and reports exact, partial, failed, missing, or stale coverage. +- `code-intel rebuild-derived` creates or refreshes `code-intel.duckdb` and removes obsolete pre-DuckDB store artifacts without importing them. +- `code-intel rebuild-index` is a deprecated alias for `rebuild-derived`. - `code-intel downstream-analysis` reads DuckDB and retained logs. Its default `json` output is stable for automation; `--format toon` provides the compact operator handoff, and `--format human` provides a short readable summary. @@ -61,4 +79,4 @@ Downstream reports expose storage and support fields directly: These fields are intended for issue-ready support summaries without manual DB or log spelunking. Compact downstream output ranks blocking friction ahead of allowed activity and includes command-ready repair guidance such as -`coding-ethos-run code-intel rebuild-index --root `. +`coding-ethos-run code-intel rebuild-derived --root `. diff --git a/go/cmd/coding-ethos-run/args.go b/go/cmd/coding-ethos-run/args.go index 522f52f3..2def0129 100644 --- a/go/cmd/coding-ethos-run/args.go +++ b/go/cmd/coding-ethos-run/args.go @@ -50,7 +50,8 @@ func codeIntelArgs(root, stateRoot string, args []string) []string { next = append(next, "--root", root) } - if !hasFlag(args, "--db") && !sameCleanPath(root, stateRoot) { + sourceOnlyCommand := slices.Contains([]string{"sync", "status"}, args[0]) + if !sourceOnlyCommand && !hasFlag(args, "--db") && !sameCleanPath(root, stateRoot) { next = append( next, "--db", diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index 471f485f..fc331138 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -290,6 +290,18 @@ func TestCodeIntelArgsDoNotAddReportFlagsToLedger(t *testing.T) { } } +func TestCodeIntelArgsKeepSourceV2CommandsFreeOfDuckDBFlags(t *testing.T) { + t.Parallel() + + for _, command := range []string{"sync", "status"} { + got := codeIntelArgs("/repo", "/private/state", []string{command}) + want := []string{command, "--root", "/repo"} + if !slices.Equal(got, want) { + t.Fatalf("codeIntelArgs(%q) = %#v, want %#v", command, got, want) + } + } +} + func TestAgentHooksArgsInjectCapabilityEthosRootWithoutHookCommand(t *testing.T) { t.Parallel() diff --git a/go/cmd/coding-ethos-run/status.go b/go/cmd/coding-ethos-run/status.go index 134838c4..08929071 100644 --- a/go/cmd/coding-ethos-run/status.go +++ b/go/cmd/coding-ethos-run/status.go @@ -338,7 +338,7 @@ func outputSurfaceChecks(report outputsurface.Report) []operatorStatusCheck { checks = append(checks, operatorStatusCheck{ Name: "code_intel_db", Status: operatorStatusWarn, - Detail: "code-intel DuckDB store is missing; run code-intel rebuild-index", + Detail: "code-intel DuckDB store is missing; run code-intel rebuild-derived", }) case codeIntelDB.DBStats == nil: checks = append(checks, operatorStatusCheck{ diff --git a/go/go.mod b/go/go.mod index e6208ea7..4563a511 100644 --- a/go/go.mod +++ b/go/go.mod @@ -18,6 +18,7 @@ require ( github.com/tree-sitter/tree-sitter-javascript v0.25.0 github.com/tree-sitter/tree-sitter-json v0.24.8 github.com/tree-sitter/tree-sitter-python v0.25.0 + github.com/tree-sitter/tree-sitter-rust v0.23.2 github.com/tree-sitter/tree-sitter-typescript v0.23.2 github.com/yuin/goldmark v1.8.2 go.uber.org/zap v1.28.0 diff --git a/go/internal/agenthooks/provider_capabilities.go b/go/internal/agenthooks/provider_capabilities.go index f8f4c976..34014d3a 100644 --- a/go/internal/agenthooks/provider_capabilities.go +++ b/go/internal/agenthooks/provider_capabilities.go @@ -37,19 +37,27 @@ const AgentHooksAPIVersion = "coding-ethos.agent-hooks/v1" // CapabilityReport is the machine-readable agent-hook capability response. type CapabilityReport struct { - APIVersion string `json:"api_version"` - RuntimeVersion string `json:"runtime_version"` - SettingsRootFlag string `json:"settings_root_flag"` - RepositoryRootFlag string `json:"repository_root_flag"` - StateRootFlag string `json:"state_root_flag"` - MCPCommandFlag string `json:"mcp_command_flag"` - HookTimeoutFlag string `json:"hook_timeout_flag"` - RuntimePolicyCommand string `json:"runtime_policy_command"` - - HookContracts []hooks.HookContractCapability `json:"hook_contracts"` - Providers []ProviderCapability `json:"providers"` - - SupportsPrivateOverlay bool `json:"supports_private_overlay"` + CodeIntel *CodeIntelCapability `json:"code_intel,omitempty"` + APIVersion string `json:"api_version"` + RuntimeVersion string `json:"runtime_version"` + SettingsRootFlag string `json:"settings_root_flag"` + RepositoryRootFlag string `json:"repository_root_flag"` + StateRootFlag string `json:"state_root_flag"` + MCPCommandFlag string `json:"mcp_command_flag"` + HookTimeoutFlag string `json:"hook_timeout_flag"` + RuntimePolicyCommand string `json:"runtime_policy_command"` + HookContracts []hooks.HookContractCapability `json:"hook_contracts"` + Providers []ProviderCapability `json:"providers"` + SupportsPrivateOverlay bool `json:"supports_private_overlay"` +} + +// CodeIntelCapability advertises the optional exact-source readiness contract. +type CodeIntelCapability struct { + Contract string `json:"contract"` + SyncCommand string `json:"sync_command"` + StatusCommand string `json:"status_command"` + SourceReadinessStatuses []string `json:"source_readiness_statuses"` + VectorReadinessStatuses []string `json:"vector_readiness_statuses"` } // Capabilities returns versioned hook contracts and provider adapters. @@ -60,7 +68,14 @@ func Capabilities(runtimeVersion string) CapabilityReport { HookContracts: []hooks.HookContractCapability{ hooks.HookContractV1Capability(), }, - Providers: ProviderCapabilities(), + Providers: ProviderCapabilities(), + CodeIntel: &CodeIntelCapability{ + Contract: "coding-ethos.code-intel/v2", + SyncCommand: "coding-ethos-code-intel sync --root ", + StatusCommand: "coding-ethos-code-intel status --root ", + SourceReadinessStatuses: []string{"exact", "partial", "failed", "missing", "stale"}, + VectorReadinessStatuses: []string{"not_evaluated", "partial", "ready"}, + }, SettingsRootFlag: "--root", RepositoryRootFlag: "--repo-root", StateRootFlag: "--state-root", diff --git a/go/internal/agenthookscli/main_internal_test.go b/go/internal/agenthookscli/main_internal_test.go index 6292037b..fd1d9005 100644 --- a/go/internal/agenthookscli/main_internal_test.go +++ b/go/internal/agenthookscli/main_internal_test.go @@ -98,6 +98,10 @@ func TestCapabilitiesReportsRuntimeContractAndKimi(t *testing.T) { `"mcp_command_flag": "--mcp-command"`, `"hook_timeout_flag": "--hook-timeout-seconds"`, `"runtime_policy_command": "runtime-policy"`, + `"code_intel": {`, + `"contract": "coding-ethos.code-intel/v2"`, + `"sync_command": "coding-ethos-code-intel sync --root "`, + `"status_command": "coding-ethos-code-intel status --root "`, `"provider": "kimi"`, } { if !strings.Contains(output, expected) { diff --git a/go/internal/astfacts/astfacts.go b/go/internal/astfacts/astfacts.go index 0a265f94..a77f1cbf 100644 --- a/go/internal/astfacts/astfacts.go +++ b/go/internal/astfacts/astfacts.go @@ -21,14 +21,16 @@ import ( const ( symbolKindConfigEntry = "config_entry" symbolKindFunction = "function" + unknownParserMetadata = "unknown" ) type File struct { - ContentHash string - Language string - Symbols []Symbol - Imports []Import - LineCount int + ContentHash string + Language string + Symbols []Symbol + Imports []Import + LineCount int + HasParseError bool } type Import struct { @@ -283,7 +285,7 @@ func processCall( func isCallNode(language, nodeKind string) bool { switch language { - case LanguageGo, LanguageJavaScript, LanguageTypeScript: + case LanguageGo, LanguageJavaScript, LanguageRust, LanguageTypeScript: return nodeKind == "call_expression" case LanguagePython: return nodeKind == "call" @@ -299,29 +301,30 @@ func callFunctionName(language string, contents []byte, node *tree_sitter.Node) return "" } - // For member calls (e.g. obj.method()), we want the method name. + memberKind, memberField := callMemberMetadata(language) + if functionNode.Kind() == memberKind { + member := functionNode.ChildByFieldName(memberField) + if member != nil { + return cleanSymbolName(member.Utf8Text(contents)) + } + } + + return cleanSymbolName(functionNode.Utf8Text(contents)) +} + +func callMemberMetadata(language string) (string, string) { switch language { case LanguageGo: - if functionNode.Kind() == "selector_expression" { - if field := functionNode.ChildByFieldName("field"); field != nil { - return cleanSymbolName(field.Utf8Text(contents)) - } - } + return "selector_expression", "field" case LanguagePython: - if functionNode.Kind() == "attribute" { - if attr := functionNode.ChildByFieldName("attribute"); attr != nil { - return cleanSymbolName(attr.Utf8Text(contents)) - } - } + return "attribute", "attribute" case LanguageJavaScript, LanguageTypeScript: - if functionNode.Kind() == "member_expression" { - if property := functionNode.ChildByFieldName("property"); property != nil { - return cleanSymbolName(property.Utf8Text(contents)) - } - } + return "member_expression", "property" + case LanguageRust: + return "field_expression", "field" + default: + return "", "" } - - return cleanSymbolName(functionNode.Utf8Text(contents)) } func BaseNames( @@ -384,6 +387,8 @@ func importNodeKind(language, nodeKind string) bool { switch language { case LanguageGo: return nodeKind == "import_spec" + case LanguageRust: + return nodeKind == "use_declaration" case LanguagePython: return nodeKind == "import_statement" || nodeKind == "import_from_statement" case LanguageJavaScript, LanguageTypeScript: @@ -407,6 +412,10 @@ func ImportTarget(language string, contents []byte, node *tree_sitter.Node) stri if name := firstDescendantText(contents, node, pythonImportNameNodeKind); name != "" { return cleanImportTarget(name) } + case LanguageRust: + if argument := node.ChildByFieldName("argument"); argument != nil { + return cleanRustImportTarget(argument.Utf8Text(contents)) + } } return "" @@ -483,7 +492,7 @@ func pythonImportNameNodeKind(nodeKind string) bool { func referenceIdentifierKind(language, nodeKind string) bool { switch language { - case LanguageGo, LanguagePython, LanguageJavaScript, LanguageTypeScript: + case LanguageGo, LanguagePython, LanguageJavaScript, LanguageRust, LanguageTypeScript: return nodeKind == "identifier" case LanguageShell: return nodeKind == "word" || nodeKind == "command_name" @@ -509,7 +518,7 @@ type nodeSymbolKindEntry struct { } func nodeSymbolKindEntries() []nodeSymbolKindEntry { - return []nodeSymbolKindEntry{ + entries := []nodeSymbolKindEntry{ { Language: LanguageGo, NodeKind: "function_declaration", @@ -586,10 +595,31 @@ func nodeSymbolKindEntries() []nodeSymbolKindEntry { SymbolKind: symbolKindConfigEntry, }, } + + return slices.Concat(entries, rustNodeSymbolKindEntries()) +} + +func rustNodeSymbolKindEntries() []nodeSymbolKindEntry { + return []nodeSymbolKindEntry{ + {Language: LanguageRust, NodeKind: "const_item", SymbolKind: "constant"}, + {Language: LanguageRust, NodeKind: "enum_item", SymbolKind: "enum"}, + {Language: LanguageRust, NodeKind: "function_item", SymbolKind: symbolKindFunction}, + {Language: LanguageRust, NodeKind: "impl_item", SymbolKind: "implementation"}, + {Language: LanguageRust, NodeKind: "macro_definition", SymbolKind: "macro"}, + {Language: LanguageRust, NodeKind: "mod_item", SymbolKind: "module"}, + {Language: LanguageRust, NodeKind: "static_item", SymbolKind: "constant"}, + {Language: LanguageRust, NodeKind: "struct_item", SymbolKind: "struct"}, + {Language: LanguageRust, NodeKind: "trait_item", SymbolKind: "trait"}, + {Language: LanguageRust, NodeKind: "type_item", SymbolKind: "type"}, + } } func SymbolName(node *tree_sitter.Node, contents []byte) string { name := node.ChildByFieldName("name") + if name == nil && node.Kind() == "impl_item" { + name = node.ChildByFieldName("type") + } + if name == nil { name = node.ChildByFieldName("key") } @@ -619,11 +649,23 @@ func cleanMarkdownHeading(text string) string { } func ParserMetadataForLanguage(language string) (string, string) { - if language == LanguageMarkdown { - return "goldmark", "v1.8.2" + for _, descriptor := range LanguageDescriptors() { + if descriptor.Language == language { + return descriptor.Extractor.Name, descriptor.Extractor.Fingerprint + } } - return "tree-sitter", "go-tree-sitter" + return unknownParserMetadata, unknownParserMetadata +} + +// ParserMetadataForPath returns the exact extractor fingerprint for a source path. +func ParserMetadataForPath(path string) (string, string) { + descriptor, ok := SourceLanguageForPath(path) + if !ok { + return unknownParserMetadata, unknownParserMetadata + } + + return descriptor.Extractor.Name, descriptor.Extractor.Fingerprint } func AnalyzeMarkdown(path string, contents []byte) File { @@ -798,6 +840,13 @@ func cleanImportTarget(value string) string { return strings.Trim(value, ".") } +func cleanRustImportTarget(value string) string { + value = strings.TrimSpace(value) + value = strings.TrimSuffix(value, ";") + + return strings.TrimSpace(value) +} + func sortedMapKeys(values map[string]bool) []string { keys := make([]string, 0, len(values)) for value := range values { diff --git a/go/internal/astfacts/astfacts_test.go b/go/internal/astfacts/astfacts_test.go index f0fee49b..279cc629 100644 --- a/go/internal/astfacts/astfacts_test.go +++ b/go/internal/astfacts/astfacts_test.go @@ -146,6 +146,77 @@ func TestAnalyzeIndexesCodeSymbolsImportsAndReferences(t *testing.T) { } } +func TestAnalyzeIndexesRustAndPublishesExactExtractorDescriptors(t *testing.T) { + t.Parallel() + + file, found, err := Analyze("src/lib.rs", []byte(` +use std::collections::HashMap; + +trait Runner { + fn run(&self); +} + +struct Worker; + +impl Runner for Worker { + fn run(&self) { + helper(); + } +} + +fn helper() {} +`)) + if err != nil { + t.Fatalf("analyze Rust: %v", err) + } + if !found || file.Language != LanguageRust || file.HasParseError { + t.Fatalf("Rust facts = %#v found=%v", file, found) + } + if !hasSymbol(file.Symbols, "Worker", "struct") || + !hasSymbol(file.Symbols, "Runner", "trait") || + !hasSymbol(file.Symbols, "helper", "function") { + t.Fatalf("Rust symbols missing: %#v", file.Symbols) + } + if !hasImport(file.Imports, "std::collections::HashMap") { + t.Fatalf("Rust import missing: %#v", file.Imports) + } + if !hasCall(file.Symbols, "Worker.run", "helper") { + t.Fatalf("Rust call missing: %#v", file.Symbols) + } + + name, fingerprint := ParserMetadataForPath("src/lib.rs") + if name != "tree-sitter-rust" || fingerprint != "tree-sitter-rust@v0.23.2" { + t.Fatalf("Rust parser metadata = %q %q", name, fingerprint) + } + + rdf, ok := SourceLanguageForPath("ontology/model.ttl") + if !ok || rdf.BuiltIn || rdf.Extractor.Protocol != PurrdfExtractorProtocol || + rdf.Extractor.Fingerprint != "purrdf@1+purrdf-"+PurrdfExtractorRevision { + t.Fatalf("PurRDF descriptor = %#v ok=%v", rdf, ok) + } + for path, language := range map[string]string{ + "ontology/model.trig": LanguageTriG, + "ontology/model.nt": LanguageNTriples, + "ontology/model.nq": LanguageNQuads, + "ontology/query.rq": LanguageSPARQL, + } { + descriptor, found := SourceLanguageForPath(path) + if !found || descriptor.Language != language || descriptor.BuiltIn { + t.Errorf("PurRDF descriptor for %q = %#v found=%v", path, descriptor, found) + } + } + + typescript, ok := SourceLanguageForPath("web/app.ts") + if !ok || typescript.Variant != "typescript" { + t.Fatalf("TypeScript descriptor = %#v ok=%v", typescript, ok) + } + tsx, ok := SourceLanguageForPath("web/app.tsx") + if !ok || tsx.Variant != "tsx" || + tsx.Extractor.Fingerprint == typescript.Extractor.Fingerprint { + t.Fatalf("TSX descriptor = %#v ok=%v", tsx, ok) + } +} + func TestAnalyzeIndexesJavaScriptAndTypeScriptFacts(t *testing.T) { t.Parallel() diff --git a/go/internal/astfacts/language.go b/go/internal/astfacts/language.go index 37478b69..b9cd3a26 100644 --- a/go/internal/astfacts/language.go +++ b/go/internal/astfacts/language.go @@ -5,6 +5,7 @@ package astfacts import ( "path/filepath" + "slices" "strings" "unsafe" @@ -15,6 +16,7 @@ import ( tree_sitter_javascript "github.com/tree-sitter/tree-sitter-javascript/bindings/go" tree_sitter_json "github.com/tree-sitter/tree-sitter-json/bindings/go" tree_sitter_python "github.com/tree-sitter/tree-sitter-python/bindings/go" + tree_sitter_rust "github.com/tree-sitter/tree-sitter-rust/bindings/go" tree_sitter_typescript "github.com/tree-sitter/tree-sitter-typescript/bindings/go" ) @@ -24,41 +26,286 @@ const ( LanguageJavaScript = "javascript" LanguageJSON = "json" LanguageMarkdown = "markdown" + LanguageNQuads = "nquads" + LanguageNTriples = "ntriples" + LanguageRust = "rust" LanguageShell = "shell" + LanguageSPARQL = "sparql" LanguageTOML = "toml" + LanguageTriG = "trig" + LanguageTurtle = "turtle" LanguageTypeScript = "typescript" LanguageYAML = "yaml" + + ExtractorKindExternal = "external" + ExtractorKindGoldmark = "goldmark" + ExtractorKindTreeSitter = "tree-sitter" + + PurrdfExtractorName = "purrdf" + PurrdfExtractorProtocol = "coding-ethos.external-extractor.v1" + PurrdfExtractorRevision = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" ) +// ExtractorDescriptor identifies the exact implementation that produces facts. +type ExtractorDescriptor struct { + Name string `json:"name"` + Kind string `json:"kind"` + Version string `json:"version"` + Fingerprint string `json:"fingerprint"` + Protocol string `json:"protocol,omitempty"` +} + +// LanguageDescriptor is the centralized source-language and extractor contract. +type LanguageDescriptor struct { + Extractor ExtractorDescriptor `json:"extractor"` + Language string `json:"language"` + Variant string `json:"variant"` + Extensions []string `json:"extensions"` + Capabilities []string `json:"capabilities"` + BuiltIn bool `json:"built_in"` +} + +type languageBinding struct { + parserFactory func() unsafe.Pointer + descriptor LanguageDescriptor +} + +// LanguageDescriptors returns a stable copy of every recognized source language. +func LanguageDescriptors() []LanguageDescriptor { + bindings := languageBindings() + + descriptors := make([]LanguageDescriptor, 0, len(bindings)) + for _, binding := range bindings { + descriptor := binding.descriptor + descriptor.Extensions = slices.Clone(descriptor.Extensions) + descriptor.Capabilities = slices.Clone(descriptor.Capabilities) + descriptors = append(descriptors, descriptor) + } + + return descriptors +} + +// SourceLanguageForPath reports every recognized language, including external ones. +func SourceLanguageForPath(path string) (LanguageDescriptor, bool) { + binding, ok := bindingForPath(path) + if !ok { + return LanguageDescriptor{}, false + } + + return binding.descriptor, true +} + +// LanguageForPath reports languages handled by the in-process AST resolver. func LanguageForPath(path string) (string, bool) { - language, _, ok := languageForPath(path) + binding, ok := bindingForPath(path) + if !ok || !binding.descriptor.BuiltIn { + return "", false + } - return language, ok + return binding.descriptor.Language, true } -func languageForPath(path string) (string, unsafe.Pointer, bool) { - switch strings.ToLower(filepath.Ext(path)) { - case ".go": - return LanguageGo, tree_sitter_go.Language(), true - case ".py": - return LanguagePython, tree_sitter_python.Language(), true - case ".js", ".jsx", ".mjs", ".cjs": - return LanguageJavaScript, tree_sitter_javascript.Language(), true - case ".ts", ".mts", ".cts": - return LanguageTypeScript, tree_sitter_typescript.LanguageTypescript(), true - case ".tsx": - return LanguageTypeScript, tree_sitter_typescript.LanguageTSX(), true - case ".json", ".jsonc": - return LanguageJSON, tree_sitter_json.Language(), true - case ".md": - return LanguageMarkdown, nil, true - case ".sh", ".bash", ".zsh": - return LanguageShell, tree_sitter_bash.Language(), true - case ".toml": - return LanguageTOML, tree_sitter_toml.Language(), true - case ".yaml", ".yml": - return LanguageYAML, tree_sitter_yaml.Language(), true - default: - return "", nil, false +func languageForPath(path string) (string, string, unsafe.Pointer, bool) { + binding, ok := bindingForPath(path) + if !ok || !binding.descriptor.BuiltIn { + return "", "", nil, false } + + var parser unsafe.Pointer + if binding.parserFactory != nil { + parser = binding.parserFactory() + } + + return binding.descriptor.Language, binding.descriptor.Variant, parser, true +} + +func bindingForPath(path string) (languageBinding, bool) { + extension := strings.ToLower(filepath.Ext(path)) + for _, binding := range languageBindings() { + if slices.Contains(binding.descriptor.Extensions, extension) { + return binding, true + } + } + + return languageBinding{}, false +} + +func languageBindings() []languageBinding { + bindings := builtInLanguageBindings() + + return append(bindings, externalLanguageBindings()...) +} + +func builtInLanguageBindings() []languageBinding { + return []languageBinding{ + treeSitterBinding( + LanguageGo, + "go", + "v0.25.0", + []string{".go"}, + tree_sitter_go.Language, + ), + treeSitterBinding( + LanguagePython, + "python", + "v0.25.0", + []string{".py"}, + tree_sitter_python.Language, + ), + treeSitterBinding( + LanguageJavaScript, + "javascript", + "v0.25.0", + []string{".js", ".jsx", ".mjs", ".cjs"}, + tree_sitter_javascript.Language, + ), + treeSitterBinding( + LanguageTypeScript, + "typescript", + "v0.23.2", + []string{".ts", ".mts", ".cts"}, + tree_sitter_typescript.LanguageTypescript, + ), + treeSitterBinding( + LanguageTypeScript, + "tsx", + "v0.23.2", + []string{".tsx"}, + tree_sitter_typescript.LanguageTSX, + ), + treeSitterBinding( + LanguageRust, + "rust", + "v0.23.2", + []string{".rs"}, + tree_sitter_rust.Language, + ), + treeSitterBinding( + LanguageJSON, + "json", + "v0.24.8", + []string{".json", ".jsonc"}, + tree_sitter_json.Language, + ), + goldmarkBinding(), + treeSitterBinding( + LanguageShell, + "bash", + "v0.25.1", + []string{".sh", ".bash", ".zsh"}, + tree_sitter_bash.Language, + ), + treeSitterBinding( + LanguageTOML, + "toml", + "v0.7.0", + []string{".toml"}, + tree_sitter_toml.Language, + ), + treeSitterBinding( + LanguageYAML, + "yaml", + "v0.7.2", + []string{".yaml", ".yml"}, + tree_sitter_yaml.Language, + ), + } +} + +func goldmarkBinding() languageBinding { + return languageBinding{descriptor: LanguageDescriptor{ + Language: LanguageMarkdown, + Variant: "markdown", + Extensions: []string{".md"}, + Capabilities: []string{"symbols"}, + BuiltIn: true, + Extractor: ExtractorDescriptor{ + Name: "goldmark", + Kind: ExtractorKindGoldmark, + Version: "v1.8.2", + Fingerprint: "goldmark@v1.8.2", + }, + }} +} + +func externalLanguageBindings() []languageBinding { + return []languageBinding{ + purrdfBinding( + LanguageTurtle, + "turtle", + []string{".ttl"}, + []string{"semantic_graph", "subject_start_spans"}, + ), + purrdfBinding( + LanguageTriG, + "trig", + []string{".trig"}, + []string{"semantic_graph", "subject_start_spans"}, + ), + purrdfBinding( + LanguageNTriples, + "ntriples", + []string{".nt"}, + []string{"semantic_graph", "subject_start_spans"}, + ), + purrdfBinding( + LanguageNQuads, + "nquads", + []string{".nq"}, + []string{"semantic_graph", "subject_start_spans"}, + ), + purrdfBinding( + LanguageSPARQL, + "sparql", + []string{".rq", ".sparql"}, + []string{"query_algebra"}, + ), + } +} + +func treeSitterBinding( + language string, + variant string, + version string, + extensions []string, + parserFactory func() unsafe.Pointer, +) languageBinding { + return languageBinding{ + descriptor: LanguageDescriptor{ + Language: language, + Variant: variant, + Extensions: extensions, + Capabilities: []string{"imports", "references", "symbols"}, + BuiltIn: true, + Extractor: ExtractorDescriptor{ + Name: "tree-sitter-" + variant, + Kind: ExtractorKindTreeSitter, + Version: version, + Fingerprint: "tree-sitter-" + variant + "@" + version, + }, + }, + parserFactory: parserFactory, + } +} + +func purrdfBinding( + language string, + variant string, + extensions []string, + capabilities []string, +) languageBinding { + return languageBinding{descriptor: LanguageDescriptor{ + Language: language, + Variant: variant, + Extensions: extensions, + Capabilities: capabilities, + BuiltIn: false, + Extractor: ExtractorDescriptor{ + Name: PurrdfExtractorName, + Kind: ExtractorKindExternal, + Version: "1", + Fingerprint: PurrdfExtractorName + "@1+purrdf-" + PurrdfExtractorRevision, + Protocol: PurrdfExtractorProtocol, + }, + }} } diff --git a/go/internal/astfacts/resolver.go b/go/internal/astfacts/resolver.go index 4abe9e48..21372fe0 100644 --- a/go/internal/astfacts/resolver.go +++ b/go/internal/astfacts/resolver.go @@ -50,7 +50,7 @@ func Parse(path string, contents []byte) (*tree_sitter.Tree, bool, error) { } func (resolver *Resolver) Analyze(path string, contents []byte) (File, bool, error) { - language, parserLanguage, ok := languageForPath(path) + language, parserKey, parserLanguage, ok := languageForPath(path) if !ok { return File{}, false, nil } @@ -59,7 +59,7 @@ func (resolver *Resolver) Analyze(path string, contents []byte) (File, bool, err return resolver.analyzeMarkdown(path, contents), true, nil } - tree, err := resolver.parse(path, language, parserLanguage, contents) + tree, err := resolver.parse(path, parserKey, parserLanguage, contents) if err != nil { return File{}, false, err } @@ -69,11 +69,12 @@ func (resolver *Resolver) Analyze(path string, contents []byte) (File, bool, err root := tree.RootNode() return File{ - Symbols: CollectSymbols(path, language, contents, root, lineCount), - Imports: CollectImports(language, contents, root), - ContentHash: ContentHash(contents), - Language: language, - LineCount: lineCount, + Symbols: CollectSymbols(path, language, contents, root, lineCount), + Imports: CollectImports(language, contents, root), + ContentHash: ContentHash(contents), + Language: language, + LineCount: lineCount, + HasParseError: root.HasError(), }, true, nil } @@ -111,12 +112,12 @@ func (resolver *Resolver) Parse( path string, contents []byte, ) (*tree_sitter.Tree, bool, error) { - language, parserLanguage, ok := languageForPath(path) + language, parserKey, parserLanguage, ok := languageForPath(path) if !ok || language == LanguageMarkdown { return nil, false, nil } - tree, err := resolver.parse(path, language, parserLanguage, contents) + tree, err := resolver.parse(path, parserKey, parserLanguage, contents) if err != nil { return nil, true, err } @@ -130,11 +131,11 @@ func (resolver *Resolver) analyzeMarkdown(path string, contents []byte) File { func (resolver *Resolver) parse( path string, - language string, + parserKey string, parserLanguage unsafe.Pointer, contents []byte, ) (*tree_sitter.Tree, error) { - entry, err := resolver.parserEntry(path, language, parserLanguage) + entry, err := resolver.parserEntry(path, parserKey, parserLanguage) if err != nil { return nil, err } @@ -156,13 +157,13 @@ func (resolver *Resolver) parse( func (resolver *Resolver) parserEntry( path string, - language string, + parserKey string, parserLanguage unsafe.Pointer, ) (*parserEntry, error) { resolver.mu.Lock() defer resolver.mu.Unlock() - entry := resolver.parsers[language] + entry := resolver.parsers[parserKey] if entry == nil { parser := tree_sitter.NewParser() @@ -174,7 +175,7 @@ func (resolver *Resolver) parserEntry( } entry = &parserEntry{parser: parser} - resolver.parsers[language] = entry + resolver.parsers[parserKey] = entry } return entry, nil diff --git a/go/internal/codeintel/ast_index.go b/go/internal/codeintel/ast_index.go index 1c44144d..4ef3c898 100644 --- a/go/internal/codeintel/ast_index.go +++ b/go/internal/codeintel/ast_index.go @@ -774,7 +774,7 @@ func (indexer ASTIndexer) indexFile( existing, found := existingFiles[relativePath] - parserName, parserVersion := astfacts.ParserMetadataForLanguage(language) + parserName, parserVersion := astfacts.ParserMetadataForPath(path) sourceModTime := formatSourceModTime(info.ModTime()) if found && diff --git a/go/internal/codeintel/downstream_analysis.go b/go/internal/codeintel/downstream_analysis.go index 9ed3224c..90d2664a 100644 --- a/go/internal/codeintel/downstream_analysis.go +++ b/go/internal/codeintel/downstream_analysis.go @@ -487,7 +487,7 @@ func downstreamEvidenceGaps(analysis DownstreamAnalysis) []DownstreamEvidenceGap Source: "hook-runs/event.json", QueryIndex: analysis.StorageHealth.Backend, Count: analysis.LogSignals.EventJSONCount, - Recommendation: "run code-intel rebuild-index to materialize durable events", + Recommendation: "run code-intel rebuild-derived to materialize durable events", }) } @@ -540,7 +540,7 @@ func downstreamIssueSummary(analysis DownstreamAnalysis) DownstreamIssueSummary if analysis.StorageHealth.Recommendation != downstreamHealthy { summary.NextActions = append( summary.NextActions, - "coding-ethos-run code-intel rebuild-index --root ", + "coding-ethos-run code-intel rebuild-derived --root ", ) } diff --git a/go/internal/codeintel/downstream_analysis_internal_test.go b/go/internal/codeintel/downstream_analysis_internal_test.go index f25c8c2a..861c1863 100644 --- a/go/internal/codeintel/downstream_analysis_internal_test.go +++ b/go/internal/codeintel/downstream_analysis_internal_test.go @@ -205,7 +205,7 @@ func TestFormatDownstreamAnalysisTOONEmitsActionableSummary(t *testing.T) { analysis := DownstreamAnalysis{ IssueSummary: DownstreamIssueSummary{ NextActions: []string{ - "coding-ethos-run code-intel rebuild-index --root ", + "coding-ethos-run code-intel rebuild-derived --root ", }, }, PolicyBlockers: []DownstreamPolicyBlocker{{ @@ -267,7 +267,7 @@ func TestFormatDownstreamAnalysisHumanEmitsStableSummary(t *testing.T) { analysis := DownstreamAnalysis{ IssueSummary: DownstreamIssueSummary{ NextActions: []string{ - "coding-ethos-run code-intel rebuild-index --root ", + "coding-ethos-run code-intel rebuild-derived --root ", }, }, PolicyBlockers: []DownstreamPolicyBlocker{ diff --git a/go/internal/codeintel/graph_report.go b/go/internal/codeintel/graph_report.go index 2916f6a5..e26b7973 100644 --- a/go/internal/codeintel/graph_report.go +++ b/go/internal/codeintel/graph_report.go @@ -361,7 +361,7 @@ func graphReportWarnings(stats Stats, repoMap RepoMap) []string { if stats.Files == 0 || stats.CodeChunks == 0 { warnings = append( warnings, - "code index is empty; run code-intel index-code or rebuild-index", + "code index is empty; run code-intel index-code or rebuild-derived", ) } diff --git a/go/internal/codeintel/query.go b/go/internal/codeintel/query.go index b6754fbc..c91ce2d4 100644 --- a/go/internal/codeintel/query.go +++ b/go/internal/codeintel/query.go @@ -138,6 +138,8 @@ type CodeFileIndexStats struct { } type IndexStatus struct { + //nolint:tagliatelle // The public versioned key is frozen. + V2 *SourceStatusReceipt `json:"coding-ethos.code-intel/v2,omitempty"` Backend string `json:"backend"` ModelID string `json:"model_id,omitempty"` Collection string `json:"collection,omitempty"` diff --git a/go/internal/codeintel/skill_health.go b/go/internal/codeintel/skill_health.go index bc74bc21..51efc99f 100644 --- a/go/internal/codeintel/skill_health.go +++ b/go/internal/codeintel/skill_health.go @@ -448,7 +448,7 @@ func accumulateOutcomeCounters( switch strings.ToLower(strings.TrimSpace(outcome)) { case "fixed", "success", "succeeded": *fixed++ - case "repeated", "failed", "failure": + case "repeated", SourceStatusFailed, "failure": *repeated++ case "attempted": *attempted++ diff --git a/go/internal/codeintel/source_v2_external.go b/go/internal/codeintel/source_v2_external.go new file mode 100644 index 00000000..bc72e238 --- /dev/null +++ b/go/internal/codeintel/source_v2_external.go @@ -0,0 +1,479 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "blackcat.ca/coding-ethos/go/internal/astfacts" + "blackcat.ca/coding-ethos/go/internal/safeexec" +) + +const ( + externalExtractorStatusError = "error" + externalExtractorStatusOK = "ok" + purrdfExtractorBinary = "coding-ethos-purrdf-extractor" +) + +var errExternalExtractorUnavailable = errors.New( + "PurRDF code-intel extractor is unavailable", +) + +var errInvalidExternalExtractorResponse = errors.New( + "invalid PurRDF code-intel extractor response", +) + +// ErrExternalExtractorRequired means an eligible RDF or SPARQL document was +// discovered but the pinned PurRDF extractor could not be used. Callers must +// not turn this into partial coverage or silently select another parser. +var ErrExternalExtractorRequired = errors.New( + "pinned PurRDF code-intel extractor is required", +) + +// ExternalBatchExtractor is the process-neutral boundary for semantic extractors. +type ExternalBatchExtractor interface { + Validate(ctx context.Context) error + Extract( + ctx context.Context, + files []ExternalExtractorRequestFile, + ) (ExternalExtractorResponse, error) +} + +// CommandBatchExtractor invokes one trusted, one-shot extractor process. +type CommandBatchExtractor struct { + executable string +} + +// NewCommandBatchExtractor constructs an adapter for a previously resolved binary. +func NewCommandBatchExtractor(executable string) (CommandBatchExtractor, error) { + executable = strings.TrimSpace(executable) + if executable == "" { + return CommandBatchExtractor{}, errExternalExtractorUnavailable + } + + resolved, err := exec.LookPath(executable) + if err != nil { + return CommandBatchExtractor{}, fmt.Errorf( + "%w: %s", + errExternalExtractorUnavailable, + executable, + ) + } + + return CommandBatchExtractor{executable: resolved}, nil +} + +// DefaultExternalBatchExtractor resolves the helper beside the current +// coding-ethos executable first, then through PATH. This follows the existing +// binary bootstrap model. +func DefaultExternalBatchExtractor() (CommandBatchExtractor, error) { + executable, executableErr := os.Executable() + if executableErr == nil { + candidate := filepath.Join(filepath.Dir(executable), purrdfExtractorBinary) + + extractor, resolveErr := NewCommandBatchExtractor(candidate) + if resolveErr == nil { + return extractor, nil + } + } + + return NewCommandBatchExtractor(purrdfExtractorBinary) +} + +// Validate performs an empty protocol exchange so a cached generation still +// proves that the executable is the exact pinned PurRDF helper. +func (extractor CommandBatchExtractor) Validate(ctx context.Context) error { + _, err := extractor.Extract(ctx, []ExternalExtractorRequestFile{}) + if err != nil { + return fmt.Errorf("validate PurRDF extractor identity: %w", err) + } + + return nil +} + +// Extract sends one deterministic request and validates the complete response envelope. +func (extractor CommandBatchExtractor) Extract( + ctx context.Context, + files []ExternalExtractorRequestFile, +) (ExternalExtractorResponse, error) { + orderedFiles := slices.Clone(files) + slices.SortFunc(orderedFiles, func(left, right ExternalExtractorRequestFile) int { + return strings.Compare(left.Path, right.Path) + }) + request := ExternalExtractorRequest{ + Protocol: astfacts.PurrdfExtractorProtocol, + RequestID: externalExtractorRequestID(orderedFiles), + Files: orderedFiles, + } + + payload, err := json.Marshal(request) + if err != nil { + return ExternalExtractorResponse{}, fmt.Errorf( + "marshal external extractor request: %w", + err, + ) + } + + command := safeexec.CommandContext(ctx, extractor.executable) + command.Stdin = bytes.NewReader(payload) + + var ( + stdout bytes.Buffer + stderr bytes.Buffer + ) + + command.Stdout = &stdout + command.Stderr = &stderr + + err = command.Run() + if err != nil { + return ExternalExtractorResponse{}, fmt.Errorf( + "run PurRDF extractor: %w: %s", + err, + strings.TrimSpace(stderr.String()), + ) + } + + var response ExternalExtractorResponse + + err = json.Unmarshal(stdout.Bytes(), &response) + if err != nil { + return ExternalExtractorResponse{}, fmt.Errorf( + "decode PurRDF extractor response: %w", + err, + ) + } + + err = validateExternalExtractorResponse(request, response) + if err != nil { + return ExternalExtractorResponse{}, err + } + + slices.SortFunc(response.Results, func(left, right ExternalExtractorResult) int { + return strings.Compare(left.Path, right.Path) + }) + + for index := range response.Results { + slices.SortFunc( + response.Results[index].Facts, + func(left, right ExternalExtractorFact) int { + return strings.Compare(left.ID, right.ID) + }, + ) + } + + return response, nil +} + +func externalExtractorRequestID(files []ExternalExtractorRequestFile) string { + ordered := slices.Clone(files) + slices.SortFunc(ordered, func(left, right ExternalExtractorRequestFile) int { + return strings.Compare(left.Path, right.Path) + }) + + hash := sha256.New() + + _, _ = hash.Write([]byte(astfacts.PurrdfExtractorProtocol)) + for _, file := range ordered { + _, _ = hash.Write([]byte( + "\x00" + file.Path + "\x00" + file.ContentSHA256 + "\x00" + file.BaseIRI, + )) + } + + return "request:sha256:" + hex.EncodeToString(hash.Sum(nil)) +} + +func validateExternalExtractorResponse( + request ExternalExtractorRequest, + response ExternalExtractorResponse, +) error { + err := validateExternalExtractorEnvelope(request, response) + if err != nil { + return err + } + + wanted, err := externalExtractorWantedFiles(request.Files) + if err != nil { + return err + } + + seen := make(map[string]bool, len(response.Results)) + for _, result := range response.Results { + err = validateExternalExtractorResult(result, wanted, seen) + if err != nil { + return err + } + } + + if len(seen) != len(wanted) { + return fmt.Errorf( + "%w: external extractor returned %d of %d requested files", + errInvalidExternalExtractorResponse, + len(seen), + len(wanted), + ) + } + + return nil +} + +func validateExternalExtractorEnvelope( + request ExternalExtractorRequest, + response ExternalExtractorResponse, +) error { + if response.Protocol != astfacts.PurrdfExtractorProtocol { + return fmt.Errorf( + "%w: unexpected external extractor protocol %q", + errInvalidExternalExtractorResponse, + response.Protocol, + ) + } + + if response.RequestID != request.RequestID { + return fmt.Errorf( + "%w: external extractor request ID mismatch: %q", + errInvalidExternalExtractorResponse, + response.RequestID, + ) + } + + if response.Extractor.Name != astfacts.PurrdfExtractorName || + response.Extractor.Version != "1" || + response.Extractor.PurrdfRevision != astfacts.PurrdfExtractorRevision { + return fmt.Errorf( + "%w: unexpected PurRDF extractor identity: %+v", + errInvalidExternalExtractorResponse, + response.Extractor, + ) + } + + return nil +} + +func externalExtractorWantedFiles( + files []ExternalExtractorRequestFile, +) (map[string]string, error) { + wanted := make(map[string]string, len(files)) + for _, file := range files { + if _, exists := wanted[file.Path]; exists { + return nil, fmt.Errorf( + "%w: duplicate external extractor request path %q", + errInvalidExternalExtractorResponse, + file.Path, + ) + } + + wanted[file.Path] = file.ContentSHA256 + } + + return wanted, nil +} + +func validateExternalExtractorResult( + result ExternalExtractorResult, + wanted map[string]string, + seen map[string]bool, +) error { + contentHash, exists := wanted[result.Path] + if !exists { + return fmt.Errorf( + "%w: unexpected external extractor result path %q", + errInvalidExternalExtractorResponse, + result.Path, + ) + } + + if seen[result.Path] { + return fmt.Errorf( + "%w: duplicate external extractor result path %q", + errInvalidExternalExtractorResponse, + result.Path, + ) + } + + seen[result.Path] = true + if result.ContentSHA256 != contentHash { + return fmt.Errorf( + "%w: external extractor content hash mismatch for %q", + errInvalidExternalExtractorResponse, + result.Path, + ) + } + + if result.Status != externalExtractorStatusOK && + result.Status != externalExtractorStatusError { + return fmt.Errorf( + "%w: invalid external extractor status %q for %q", + errInvalidExternalExtractorResponse, + result.Status, + result.Path, + ) + } + + if result.Status == externalExtractorStatusError && + strings.TrimSpace(result.Error) == "" { + return fmt.Errorf( + "%w: external extractor omitted error for %q", + errInvalidExternalExtractorResponse, + result.Path, + ) + } + + if result.Status == externalExtractorStatusOK && + strings.TrimSpace(result.DocumentKind) == "" { + return fmt.Errorf( + "%w: external extractor omitted document kind for %q", + errInvalidExternalExtractorResponse, + result.Path, + ) + } + + return validateExternalExtractorFacts(result) +} + +func externalFactContract(language string) (string, string, error) { + switch language { + case astfacts.LanguageTurtle, + astfacts.LanguageTriG, + astfacts.LanguageNTriples, + astfacts.LanguageNQuads: + return "purrdf-rdf", "subject_start", nil + case astfacts.LanguageSPARQL: + return "purrdf-sparql-algebra", "none", nil + default: + return "", "", fmt.Errorf( + "%w: unexpected external extractor language %q", + errInvalidExternalExtractorResponse, + language, + ) + } +} + +func validateExternalExtractorFacts(result ExternalExtractorResult) error { + expectedParser, expectedSpanFidelity, err := externalFactContract(result.Language) + if err != nil { + return err + } + + seen := make(map[string]bool, len(result.Facts)) + for _, fact := range result.Facts { + err = validateExternalExtractorFact( + result, + fact, + expectedParser, + expectedSpanFidelity, + seen, + ) + if err != nil { + return err + } + } + + return nil +} + +func validateExternalExtractorFact( + result ExternalExtractorResult, + fact ExternalExtractorFact, + expectedParser string, + expectedSpanFidelity string, + seen map[string]bool, +) error { + if strings.TrimSpace(fact.ID) == "" || strings.TrimSpace(fact.Kind) == "" { + return fmt.Errorf( + "%w: external extractor returned an unidentified fact for %q", + errInvalidExternalExtractorResponse, + result.Path, + ) + } + + if seen[fact.ID] { + return fmt.Errorf( + "%w: external extractor returned duplicate fact %q", + errInvalidExternalExtractorResponse, + fact.ID, + ) + } + + seen[fact.ID] = true + + provenance := fact.Provenance + + err := validateExternalFactProvenance( + result.Path, + provenance, + expectedParser, + expectedSpanFidelity, + ) + if err != nil { + return err + } + + return validateExternalFactStart(result.Path, provenance) +} + +func validateExternalFactProvenance( + path string, + provenance ExternalExtractorProvenance, + expectedParser string, + expectedSpanFidelity string, +) error { + if provenance.Class != "EXTRACTED" || + provenance.Parser != expectedParser || + provenance.ParserRevision != astfacts.PurrdfExtractorRevision || + provenance.SourcePath != path { + return fmt.Errorf( + "%w: invalid external fact provenance for %q", + errInvalidExternalExtractorResponse, + path, + ) + } + + if provenance.SpanFidelity != expectedSpanFidelity { + return fmt.Errorf( + "%w: invalid external fact span fidelity %q for %q", + errInvalidExternalExtractorResponse, + provenance.SpanFidelity, + path, + ) + } + + return nil +} + +func validateExternalFactStart( + path string, + provenance ExternalExtractorProvenance, +) error { + if provenance.SpanFidelity == "none" && provenance.Start != nil { + return fmt.Errorf( + "%w: span-free external fact has a start position for %q", + errInvalidExternalExtractorResponse, + path, + ) + } + + if provenance.SpanFidelity == "subject_start" && provenance.Start != nil && + (provenance.Start.ByteOffset < 0 || provenance.Start.Line < 1 || + provenance.Start.Column < 1) { + return fmt.Errorf( + "%w: invalid external fact start position for %q", + errInvalidExternalExtractorResponse, + path, + ) + } + + return nil +} diff --git a/go/internal/codeintel/source_v2_query.go b/go/internal/codeintel/source_v2_query.go new file mode 100644 index 00000000..4837b339 --- /dev/null +++ b/go/internal/codeintel/source_v2_query.go @@ -0,0 +1,419 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" +) + +const ( + defaultSourceV2QueryLimit = 50 + maximumSourceV2QueryLimit = 1000 +) + +var ( + ErrSourceIndexMissing = errors.New("code-intel v2 source generation is missing") + ErrSourceIndexStale = errors.New("code-intel v2 source generation is stale") + errInvalidSourceV2Generation = errors.New("invalid code-intel v2 generation") +) + +// SourceIndexQuery filters immutable source facts from one exact generation. +type SourceIndexQuery struct { + Path string + Language string + Limit int +} + +// SourceIndexRecord binds path-neutral fragment facts to one manifest path. +type SourceIndexRecord struct { + Path string `json:"path"` + Language string `json:"language"` + ContentSHA256 string `json:"content_sha256"` + FragmentID string `json:"fragment_id"` + Symbols []SourceSymbolFact `json:"symbols,omitempty"` + Imports []SourceImportFact `json:"imports,omitempty"` + Facts []ExternalExtractorFact `json:"facts,omitempty"` +} + +// SourceIndexQueryResult carries the exact generation identity with every result set. +type SourceIndexQueryResult struct { + Contract string `json:"contract"` + SourceReadiness SourceReadiness `json:"source_readiness"` + Records []SourceIndexRecord `json:"records"` +} + +// QuerySourceIndex reads one lane's immutable base-plus-delta generation. +func QuerySourceIndex( + ctx context.Context, + root string, + query SourceIndexQuery, +) (SourceIndexQueryResult, error) { + status, err := SourceIndexStatus(ctx, root) + if err != nil { + return SourceIndexQueryResult{}, err + } + + err = requireQueryableSourceV2Status(status) + if err != nil { + return SourceIndexQueryResult{}, err + } + + path, err := normalizeSourceV2QueryPath(query.Path) + if err != nil { + return SourceIndexQueryResult{}, err + } + + limit := normalizedSourceV2QueryLimit(query.Limit) + + layout, err := resolveSourceV2Layout(ctx, root) + if err != nil { + return SourceIndexQueryResult{}, err + } + + base, err := loadSourceV2BaseManifest(layout, status.Storage.BaseManifestID) + if err != nil { + return SourceIndexQueryResult{}, err + } + + delta, err := loadSourceV2DeltaManifest(layout, status.Storage.DeltaManifestID) + if err != nil { + return SourceIndexQueryResult{}, err + } + + entries := mergeSourceV2Entries(base.Entries, delta.Entries, delta.Tombstones) + ordered := sourceV2SortedManifestEntries(entries) + + records := make([]SourceIndexRecord, 0, min(len(ordered), limit)) + for _, entry := range ordered { + if !sourceV2QueryMatches(entry, path, query.Language) { + continue + } + + fragment, readErr := loadSourceV2Fragment(layout, entry) + if readErr != nil { + return SourceIndexQueryResult{}, readErr + } + + records = append(records, SourceIndexRecord{ + Path: entry.Path, + Language: entry.Language, + ContentSHA256: entry.ContentSHA256, + FragmentID: entry.FragmentID, + Symbols: fragment.Symbols, + Imports: fragment.Imports, + Facts: fragment.Facts, + }) + if len(records) >= limit { + break + } + } + + return SourceIndexQueryResult{ + Contract: SourceV2Contract, + SourceReadiness: status.SourceReadiness, + Records: records, + }, nil +} + +func requireQueryableSourceV2Status(status SourceStatusReceipt) error { + switch status.SourceReadiness.Status { + case SourceStatusMissing: + return ErrSourceIndexMissing + case SourceStatusStale: + return ErrSourceIndexStale + default: + return nil + } +} + +func normalizedSourceV2QueryLimit(limit int) int { + if limit <= 0 { + return defaultSourceV2QueryLimit + } + + return min(limit, maximumSourceV2QueryLimit) +} + +func sourceV2QueryMatches( + entry sourceV2ManifestEntry, + path string, + language string, +) bool { + return entry.Status == sourceV2EntryStatusIndexed && + (path == "" || entry.Path == path) && + (language == "" || entry.Language == language) +} + +func normalizeSourceV2QueryPath(path string) (string, error) { + path = strings.TrimSpace(filepath.ToSlash(path)) + if path == "" { + return "", nil + } + + path = strings.TrimPrefix(path, "./") + + path = filepath.ToSlash(filepath.Clean(filepath.FromSlash(path))) + if path == "." || path == ".." || strings.HasPrefix(path, "../") || + filepath.IsAbs(path) { + return "", fmt.Errorf( + "%w: invalid code-intel v2 query path %q", + errInvalidSourceV2Generation, + path, + ) + } + + return path, nil +} + +func loadSourceV2BaseManifest( + layout sourceV2Layout, + manifestID string, +) (sourceV2BaseManifest, error) { + if !validSourceV2ID(manifestID, "base") { + return sourceV2BaseManifest{}, fmt.Errorf( + "%w: invalid code-intel base manifest ID %q", + errInvalidSourceV2Generation, + manifestID, + ) + } + + var manifest sourceV2BaseManifest + + err := readSourceV2JSON( + layout.baseManifestPath(manifestID), + &manifest, + ) + if err != nil { + return sourceV2BaseManifest{}, err + } + + if manifest.Contract != SourceV2Contract || + manifest.Kind != sourceV2BaseManifestKind || manifest.ManifestID != manifestID { + return sourceV2BaseManifest{}, fmt.Errorf( + "%w: invalid code-intel v2 base manifest", + errInvalidSourceV2Generation, + ) + } + + computedID, err := sourceV2ManifestID("base", manifest) + if err != nil { + return sourceV2BaseManifest{}, err + } + + if computedID != manifestID { + return sourceV2BaseManifest{}, fmt.Errorf( + "%w: code-intel v2 base manifest content ID mismatch", + errInvalidSourceV2Generation, + ) + } + + return manifest, nil +} + +func verifySourceV2Generation( + layout sourceV2Layout, + receipt SourceStatusReceipt, +) error { + base, err := loadSourceV2BaseManifest(layout, receipt.Storage.BaseManifestID) + if err != nil { + return fmt.Errorf("verify code-intel v2 base manifest: %w", err) + } + + delta, err := loadSourceV2DeltaManifest(layout, receipt.Storage.DeltaManifestID) + if err != nil { + return fmt.Errorf("verify code-intel v2 delta manifest: %w", err) + } + + identity := receipt.SourceReadiness.Identity + + expectedGenerationID := GenerationID(sourceV2Digest( + "generation", + string(identity.SourceSnapshotID)+"\x00"+ + base.ManifestID+"\x00"+delta.ManifestID, + )) + if receipt.Storage.SharedRoot != layout.sharedRoot || + receipt.Storage.LaneRoot != layout.laneRoot || + identity.GenerationID != expectedGenerationID || + !sourceV2BaseIdentityMatches(base, identity) || + !sourceV2DeltaIdentityMatches(delta, base, identity) { + return fmt.Errorf( + "%w: code-intel v2 manifest identity does not match receipt", + errInvalidSourceV2Generation, + ) + } + + entries := mergeSourceV2Entries(base.Entries, delta.Entries, delta.Tombstones) + for _, entry := range entries { + if entry.Status != sourceV2EntryStatusIndexed { + continue + } + + if !sourceV2FragmentReady(layout.fragmentPath(entry.FragmentID), entry) { + return fmt.Errorf( + "%w: code-intel v2 fragment is missing or invalid: %s", + errInvalidSourceV2Generation, + entry.Path, + ) + } + } + + return nil +} + +func sourceV2BaseIdentityMatches( + base sourceV2BaseManifest, + identity SourceIdentity, +) bool { + return base.RepositoryID == identity.RepositoryID && + base.HeadOID == identity.HeadOID && + base.ConfigHash == identity.ConfigHash && + base.ExtractorSetHash == identity.ExtractorSetHash +} + +func sourceV2DeltaIdentityMatches( + delta sourceV2DeltaManifest, + base sourceV2BaseManifest, + identity SourceIdentity, +) bool { + return delta.RepositoryID == identity.RepositoryID && + delta.WorktreeID == identity.WorktreeID && + delta.HeadOID == identity.HeadOID && + delta.BaseManifestID == base.ManifestID && + delta.ConfigHash == identity.ConfigHash && + delta.ExtractorSetHash == identity.ExtractorSetHash +} + +func loadSourceV2DeltaManifest( + layout sourceV2Layout, + manifestID string, +) (sourceV2DeltaManifest, error) { + if !validSourceV2ID(manifestID, "delta") { + return sourceV2DeltaManifest{}, fmt.Errorf( + "%w: invalid code-intel delta manifest ID %q", + errInvalidSourceV2Generation, + manifestID, + ) + } + + var manifest sourceV2DeltaManifest + + err := readSourceV2JSON( + layout.deltaManifestPath(manifestID), + &manifest, + ) + if err != nil { + return sourceV2DeltaManifest{}, err + } + + if manifest.Contract != SourceV2Contract || + manifest.Kind != sourceV2DeltaManifestKind || manifest.ManifestID != manifestID { + return sourceV2DeltaManifest{}, fmt.Errorf( + "%w: invalid code-intel v2 delta manifest", + errInvalidSourceV2Generation, + ) + } + + computedID, err := sourceV2DeltaManifestID(manifest) + if err != nil { + return sourceV2DeltaManifest{}, err + } + + if computedID != manifestID { + return sourceV2DeltaManifest{}, fmt.Errorf( + "%w: code-intel v2 delta manifest content ID mismatch", + errInvalidSourceV2Generation, + ) + } + + return manifest, nil +} + +func loadSourceV2Fragment( + layout sourceV2Layout, + entry sourceV2ManifestEntry, +) (sourceV2Fragment, error) { + if !validSourceV2ID(entry.FragmentID, "fragment") { + return sourceV2Fragment{}, fmt.Errorf( + "%w: invalid code-intel fragment ID %q", + errInvalidSourceV2Generation, + entry.FragmentID, + ) + } + + var fragment sourceV2Fragment + + err := readSourceV2JSON( + layout.fragmentPath(entry.FragmentID), + &fragment, + ) + if err != nil { + return sourceV2Fragment{}, err + } + + if fragment.Contract != SourceV2Contract || + fragment.Kind != sourceV2FragmentKind || + fragment.FragmentID != entry.FragmentID || + fragment.ContentSHA256 != entry.ContentSHA256 { + return sourceV2Fragment{}, fmt.Errorf( + "%w: invalid code-intel v2 fragment %q", + errInvalidSourceV2Generation, + entry.FragmentID, + ) + } + + return fragment, nil +} + +func readSourceV2JSON(path string, value any) error { + payload, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("read code-intel v2 object %s: %w", path, err) + } + + err = json.Unmarshal(payload, value) + if err != nil { + return fmt.Errorf("decode code-intel v2 object %s: %w", path, err) + } + + return nil +} + +func validSourceV2ID(value, namespace string) bool { + prefix := namespace + ":sha256:" + + digest := strings.TrimPrefix(value, prefix) + if len(digest) != 64 || value == digest { + return false + } + + for _, character := range digest { + if !strings.ContainsRune("0123456789abcdef", character) { + return false + } + } + + return true +} + +func sourceV2SortedManifestEntries( + entries map[string]sourceV2ManifestEntry, +) []sourceV2ManifestEntry { + ordered := make([]sourceV2ManifestEntry, 0, len(entries)) + for _, entry := range entries { + ordered = append(ordered, entry) + } + + slices.SortFunc(ordered, func(left, right sourceV2ManifestEntry) int { + return strings.Compare(left.Path, right.Path) + }) + + return ordered +} diff --git a/go/internal/codeintel/source_v2_storage.go b/go/internal/codeintel/source_v2_storage.go new file mode 100644 index 00000000..94c57dfb --- /dev/null +++ b/go/internal/codeintel/source_v2_storage.go @@ -0,0 +1,503 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net" + "net/url" + "os" + pathpkg "path" + "path/filepath" + "slices" + "strings" + + "blackcat.ca/coding-ethos/go/internal/realgit" +) + +const ( + sourceV2DirMode = 0o700 + sourceV2FileMode = 0o600 + sourceV2FragmentPrefixLength = 2 +) + +var ( + errEmptySourceV2Origin = errors.New("origin URL is empty") + errImmutableSourceV2Conflict = errors.New( + "immutable code-intel object differs", + ) + errInvalidSourceV2Origin = errors.New("invalid repository origin URL") + errInvalidSourceV2Receipt = errors.New("invalid code-intel v2 status receipt") + errMissingSourceV2RootCommit = errors.New( + "resolve repository root commit: Git returned no commits", + ) + errShallowSourceV2Repository = errors.New( + "derive code-intel repository identity: shallow history " + + "does not expose the root commit", + ) +) + +type sourceV2Layout struct { + repositoryRoot string + sharedRoot string + laneRoot string + commonGitDir string + repositoryID RepositoryID + worktreeID string +} + +func resolveSourceV2Layout(ctx context.Context, root string) (sourceV2Layout, error) { + if strings.TrimSpace(root) == "" { + root = "." + } + + repositoryRoot, err := filepath.Abs(root) + if err != nil { + return sourceV2Layout{}, fmt.Errorf("resolve code-intel repository root: %w", err) + } + + commonGitDir, err := sourceV2GitOutput( + ctx, + repositoryRoot, + "rev-parse", + "--path-format=absolute", + "--git-common-dir", + ) + if err != nil { + return sourceV2Layout{}, fmt.Errorf("resolve Git common directory: %w", err) + } + + commonGitDir, err = filepath.Abs(strings.TrimSpace(commonGitDir)) + if err != nil { + return sourceV2Layout{}, fmt.Errorf("normalize Git common directory: %w", err) + } + + repositoryRoot = filepath.Clean(repositoryRoot) + commonGitDir = filepath.Clean(commonGitDir) + + repositoryID, err := sourceV2RepositoryID(ctx, repositoryRoot, commonGitDir) + if err != nil { + return sourceV2Layout{}, err + } + + worktreeID := sourceV2Digest("worktree", filepath.ToSlash(repositoryRoot)) + sharedRoot := filepath.Join(commonGitDir, "coding-ethos", "code-intel", "v2") + laneRoot := filepath.Join( + ResolveStateRoot(repositoryRoot), + ".coding-ethos", + "code-intel-v2", + "lanes", + strings.TrimPrefix(worktreeID, "worktree:sha256:"), + ) + + return sourceV2Layout{ + repositoryRoot: repositoryRoot, + sharedRoot: sharedRoot, + laneRoot: laneRoot, + commonGitDir: commonGitDir, + repositoryID: repositoryID, + worktreeID: worktreeID, + }, nil +} + +func sourceV2RepositoryID( + ctx context.Context, + repositoryRoot string, + commonGitDir string, +) (RepositoryID, error) { + shallow, err := sourceV2GitOutput( + ctx, + repositoryRoot, + "rev-parse", + "--is-shallow-repository", + ) + if err != nil { + return "", fmt.Errorf("inspect repository history depth: %w", err) + } + + if shallow == "true" { + return "", errShallowSourceV2Repository + } + + rootOutput, err := sourceV2GitOutput( + ctx, + repositoryRoot, + "rev-list", + "--max-parents=0", + "HEAD", + ) + if err != nil { + return "", fmt.Errorf("resolve repository root commit: %w", err) + } + + rootCommits := strings.Fields(rootOutput) + if len(rootCommits) == 0 { + return "", errMissingSourceV2RootCommit + } + + slices.Sort(rootCommits) + rootCommits = slices.Compact(rootCommits) + + origin, originErr := sourceV2GitOutput( + ctx, + repositoryRoot, + "remote", + "get-url", + "origin", + ) + + var authority string + + if originErr == nil { + authority, err = normalizeSourceV2Origin(repositoryRoot, origin) + if err != nil { + return "", fmt.Errorf("normalize repository origin: %w", err) + } + + authority = "origin:" + authority + } else { + // A repository without an origin has no cross-clone authority. Keep it + // supported with an explicit path-local fallback that cannot collide with + // an origin-backed repository identity. + canonicalCommonDir := commonGitDir + + resolved, resolveErr := filepath.EvalSymlinks(commonGitDir) + if resolveErr == nil { + canonicalCommonDir = resolved + } + + authority = "local-common-dir:" + filepath.ToSlash(filepath.Clean(canonicalCommonDir)) + } + + payload := authority + "\x00" + strings.Join(rootCommits, "\n") + + return RepositoryID(sourceV2Digest("repository", payload)), nil +} + +func normalizeSourceV2Origin(repositoryRoot, rawOrigin string) (string, error) { + rawOrigin = strings.TrimSpace(rawOrigin) + if rawOrigin == "" { + return "", errEmptySourceV2Origin + } + + if scpOrigin, ok := sourceV2SCPOrigin(rawOrigin); ok { + rawOrigin = scpOrigin + } + + parsed, err := url.Parse(rawOrigin) + if err != nil { + return "", fmt.Errorf("parse repository origin URL: %w", err) + } + + if parsed.Scheme == "" { + return normalizeSourceV2LocalOrigin(repositoryRoot, rawOrigin) + } + + parsed.Scheme = strings.ToLower(parsed.Scheme) + if parsed.Scheme == "file" && parsed.Hostname() == "" { + path, unescapeErr := url.PathUnescape(parsed.Path) + if unescapeErr != nil { + return "", fmt.Errorf("unescape local repository origin path: %w", unescapeErr) + } + + return normalizeSourceV2LocalOrigin(repositoryRoot, path) + } + + return normalizeSourceV2RemoteOrigin(parsed, rawOrigin) +} + +func normalizeSourceV2RemoteOrigin(parsed *url.URL, rawOrigin string) (string, error) { + if parsed.Opaque != "" || parsed.Hostname() == "" { + return "", fmt.Errorf( + "%w: origin URL %q has no host", + errInvalidSourceV2Origin, + rawOrigin, + ) + } + + hostname := strings.ToLower(parsed.Hostname()) + + port := parsed.Port() + if (parsed.Scheme == "ssh" && port == "22") || + (parsed.Scheme == "https" && port == "443") || + (parsed.Scheme == "http" && port == "80") { + port = "" + } + + if port == "" { + parsed.Host = hostname + if strings.Contains(hostname, ":") { + parsed.Host = "[" + hostname + "]" + } + } else { + parsed.Host = net.JoinHostPort(hostname, port) + } + + parsed.User = nil + parsed.Path = normalizeSourceV2OriginPath(parsed.Path) + parsed.RawPath = "" + parsed.RawQuery = "" + parsed.ForceQuery = false + parsed.Fragment = "" + + return parsed.String(), nil +} + +func normalizeSourceV2LocalOrigin(repositoryRoot, rawPath string) (string, error) { + path := filepath.FromSlash(strings.TrimSpace(rawPath)) + if !filepath.IsAbs(path) { + path = filepath.Join(repositoryRoot, path) + } + + absolute, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("resolve local repository origin path: %w", err) + } + + resolved, resolveErr := filepath.EvalSymlinks(absolute) + if resolveErr == nil { + absolute = resolved + } + + absolute = filepath.FromSlash(normalizeSourceV2OriginPath(filepath.ToSlash(absolute))) + + return (&url.URL{Scheme: "file", Path: filepath.ToSlash(absolute)}).String(), nil +} + +func normalizeSourceV2OriginPath(rawPath string) string { + normalized := pathpkg.Clean("/" + strings.TrimLeft(rawPath, "/")) + if normalized != "/" { + normalized = strings.TrimSuffix(normalized, "/") + normalized = strings.TrimSuffix(normalized, ".git") + } + + return normalized +} + +func sourceV2SCPOrigin(rawOrigin string) (string, bool) { + if strings.Contains(rawOrigin, "://") || filepath.IsAbs(rawOrigin) { + return "", false + } + + colon := strings.IndexByte(rawOrigin, ':') + if colon <= 0 || colon == len(rawOrigin)-1 || + strings.Contains(rawOrigin[:colon], "/") { + return "", false + } + + return "ssh://" + rawOrigin[:colon] + "/" + strings.TrimLeft( + rawOrigin[colon+1:], + "/", + ), true +} + +func sourceV2GitOutput( + ctx context.Context, + root string, + arguments ...string, +) (string, error) { + command := realgit.Command(ctx, false, arguments...) + command.Dir = root + command.Env = realgit.CleanGitLocalEnv(os.Environ()) + + output, err := command.Output() + if err != nil { + return "", fmt.Errorf("run Git %s: %w", strings.Join(arguments, " "), err) + } + + return strings.TrimSpace(string(output)), nil +} + +func sourceV2Digest(namespace, payload string) string { + digest := sha256.Sum256([]byte(payload)) + + return namespace + ":sha256:" + hex.EncodeToString(digest[:]) +} + +func sourceV2ContentID(namespace string, value any) (string, error) { + payload, err := json.Marshal(value) + if err != nil { + return "", fmt.Errorf("marshal %s identity: %w", namespace, err) + } + + return sourceV2Digest(namespace, string(payload)), nil +} + +func (layout sourceV2Layout) fragmentPath(fragmentID string) string { + digest := strings.TrimPrefix(fragmentID, "fragment:sha256:") + + prefix := digest + if len(prefix) > sourceV2FragmentPrefixLength { + prefix = prefix[:sourceV2FragmentPrefixLength] + } + + return filepath.Join(layout.sharedRoot, "fragments", prefix, digest+".json") +} + +func (layout sourceV2Layout) baseManifestPath(manifestID string) string { + digest := strings.TrimPrefix(manifestID, "base:sha256:") + + return filepath.Join(layout.sharedRoot, "bases", digest+".json") +} + +func (layout sourceV2Layout) deltaManifestPath(manifestID string) string { + digest := strings.TrimPrefix(manifestID, "delta:sha256:") + + return filepath.Join(layout.laneRoot, "deltas", digest+".json") +} + +func (layout sourceV2Layout) statusPath() string { + return filepath.Join(layout.laneRoot, "current.json") +} + +func writeImmutableSourceV2JSON(path string, value any) (bool, error) { + payload, err := json.MarshalIndent(value, "", " ") + if err != nil { + return false, fmt.Errorf("marshal immutable code-intel object: %w", err) + } + + payload = append(payload, '\n') + + existing, readErr := os.ReadFile(path) + if readErr == nil { + if !bytes.Equal(existing, payload) { + return false, fmt.Errorf( + "%w at %s", + errImmutableSourceV2Conflict, + path, + ) + } + + return false, nil + } + + if !errors.Is(readErr, os.ErrNotExist) { + return false, fmt.Errorf("read immutable code-intel object %s: %w", path, readErr) + } + + err = os.MkdirAll(filepath.Dir(path), sourceV2DirMode) + if err != nil { + return false, fmt.Errorf("create immutable code-intel directory: %w", err) + } + + temporary, err := os.CreateTemp(filepath.Dir(path), ".code-intel-v2-*") + if err != nil { + return false, fmt.Errorf("create immutable code-intel temporary file: %w", err) + } + + temporaryPath := temporary.Name() + + defer func() { _ = os.Remove(temporaryPath) }() + + err = temporary.Chmod(sourceV2FileMode) + if err != nil { + _ = temporary.Close() + + return false, fmt.Errorf("chmod immutable code-intel object: %w", err) + } + + _, err = temporary.Write(payload) + if err != nil { + _ = temporary.Close() + + return false, fmt.Errorf("write immutable code-intel object: %w", err) + } + + err = temporary.Close() + if err != nil { + return false, fmt.Errorf("close immutable code-intel object: %w", err) + } + + err = os.Link(temporaryPath, path) + if err != nil { + if errors.Is(err, os.ErrExist) { + return writeImmutableSourceV2JSON(path, value) + } + + return false, fmt.Errorf("publish immutable code-intel object: %w", err) + } + + return true, nil +} + +func writeCurrentSourceV2Receipt(path string, receipt SourceStatusReceipt) error { + payload, err := json.MarshalIndent(receipt, "", " ") + if err != nil { + return fmt.Errorf("marshal code-intel status receipt: %w", err) + } + + payload = append(payload, '\n') + + err = os.MkdirAll(filepath.Dir(path), sourceV2DirMode) + if err != nil { + return fmt.Errorf("create code-intel receipt directory: %w", err) + } + + temporary, err := os.CreateTemp(filepath.Dir(path), ".current-*") + if err != nil { + return fmt.Errorf("create code-intel receipt temporary file: %w", err) + } + + temporaryPath := temporary.Name() + + defer func() { _ = os.Remove(temporaryPath) }() + + err = temporary.Chmod(sourceV2FileMode) + if err != nil { + _ = temporary.Close() + + return fmt.Errorf("chmod code-intel receipt: %w", err) + } + + _, err = temporary.Write(payload) + if err != nil { + _ = temporary.Close() + + return fmt.Errorf("write code-intel receipt: %w", err) + } + + err = temporary.Close() + if err != nil { + return fmt.Errorf("close code-intel receipt: %w", err) + } + + err = os.Rename(temporaryPath, path) + if err != nil { + return fmt.Errorf("publish code-intel receipt: %w", err) + } + + return nil +} + +func loadCurrentSourceV2Receipt(path string) (SourceStatusReceipt, error) { + payload, err := os.ReadFile(path) + if err != nil { + return SourceStatusReceipt{}, fmt.Errorf("read code-intel v2 status receipt: %w", err) + } + + var receipt SourceStatusReceipt + + err = json.Unmarshal(payload, &receipt) + if err != nil { + return SourceStatusReceipt{}, fmt.Errorf( + "decode code-intel v2 status receipt: %w", + err, + ) + } + + if receipt.Contract != SourceV2Contract || receipt.Kind != sourceV2StatusKind { + return SourceStatusReceipt{}, fmt.Errorf( + "%w: unexpected code-intel v2 status receipt contract", + errInvalidSourceV2Receipt, + ) + } + + return receipt, nil +} diff --git a/go/internal/codeintel/source_v2_sync.go b/go/internal/codeintel/source_v2_sync.go new file mode 100644 index 00000000..f88dc420 --- /dev/null +++ b/go/internal/codeintel/source_v2_sync.go @@ -0,0 +1,1466 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "time" + "unicode/utf8" + + "blackcat.ca/coding-ethos/go/internal/astfacts" + "blackcat.ca/coding-ethos/go/internal/realgit" +) + +const ( + sourceV2GitTreeMetadataFields = 3 + sourceV2RepairCommand = "coding-ethos-code-intel sync --root ." +) + +var errInvalidSourceV2Input = errors.New("invalid code-intel v2 source input") + +type sourceV2Input struct { + path string + contentHash string + contents []byte + descriptor astfacts.LanguageDescriptor + oversized bool +} + +type sourceV2Scan struct { + layout sourceV2Layout + current map[string]sourceV2Input + head map[string]sourceV2Input + excluded map[string]int + headOID string + configHash string + extractorSetHash string + worktreeFingerprint string + sourceSnapshotID SourceSnapshotID +} + +type sourceV2BuildCounters struct { + cacheHits map[string]bool + fragmentsReused int + fragmentsWritten int +} + +type sourceV2BuiltManifests struct { + base sourceV2BaseManifest + delta sourceV2DeltaManifest + warnings []string +} + +// SyncSourceIndex materializes an immutable shared base and a lane-local delta. +// Existing v1 stores are deliberately neither removed nor rewritten. +func SyncSourceIndex( + ctx context.Context, + root string, + external ExternalBatchExtractor, +) (SourceSyncReceipt, error) { + scan, err := inspectSourceV2Repository(ctx, root, true) + if err != nil { + return SourceSyncReceipt{}, err + } + + err = validateSourceV2ExternalExtractor(ctx, scan, external) + if err != nil { + return SourceSyncReceipt{}, err + } + + counters := sourceV2BuildCounters{cacheHits: map[string]bool{}} + + manifests, err := buildSourceV2Manifests(ctx, scan, external, &counters) + if err != nil { + return SourceSyncReceipt{}, err + } + + return publishSourceV2Generation(scan, manifests, counters) +} + +func buildSourceV2Manifests( + ctx context.Context, + scan sourceV2Scan, + external ExternalBatchExtractor, + counters *sourceV2BuildCounters, +) (sourceV2BuiltManifests, error) { + baseInputs, deltaInputs, tombstones := splitSourceV2Inputs(scan.head, scan.current) + knownFragments := knownSourceV2Fragments(scan) + + base, baseWarnings, err := buildSourceV2BaseManifest( + ctx, + scan, + baseInputs, + external, + knownFragments, + counters, + ) + if err != nil { + return sourceV2BuiltManifests{}, err + } + + delta, deltaWarnings, err := buildSourceV2DeltaManifest( + ctx, + scan, + base.ManifestID, + deltaInputs, + tombstones, + external, + knownFragments, + counters, + ) + if err != nil { + return sourceV2BuiltManifests{}, err + } + + warnings := slices.Concat(baseWarnings, deltaWarnings) + slices.Sort(warnings) + warnings = slices.Compact(warnings) + + return sourceV2BuiltManifests{base: base, delta: delta, warnings: warnings}, nil +} + +func buildSourceV2BaseManifest( + ctx context.Context, + scan sourceV2Scan, + inputs []sourceV2Input, + external ExternalBatchExtractor, + knownFragments map[string]string, + counters *sourceV2BuildCounters, +) (sourceV2BaseManifest, []string, error) { + entries, warnings, err := buildSourceV2Entries( + ctx, scan.layout, inputs, external, knownFragments, counters, + ) + if err != nil { + return sourceV2BaseManifest{}, nil, fmt.Errorf( + "build code-intel base fragments: %w", + err, + ) + } + + manifest := sourceV2BaseManifest{ + Contract: SourceV2Contract, + Kind: sourceV2BaseManifestKind, + RepositoryID: scan.layout.repositoryID, + HeadOID: scan.headOID, + ConfigHash: scan.configHash, + ExtractorSetHash: scan.extractorSetHash, + Entries: entries, + } + + manifest.ManifestID, err = sourceV2ManifestID("base", manifest) + if err != nil { + return sourceV2BaseManifest{}, nil, err + } + + _, err = writeImmutableSourceV2JSON( + scan.layout.baseManifestPath(manifest.ManifestID), + manifest, + ) + if err != nil { + return sourceV2BaseManifest{}, nil, err + } + + return manifest, warnings, nil +} + +func buildSourceV2DeltaManifest( + ctx context.Context, + scan sourceV2Scan, + baseManifestID string, + inputs []sourceV2Input, + tombstones []string, + external ExternalBatchExtractor, + knownFragments map[string]string, + counters *sourceV2BuildCounters, +) (sourceV2DeltaManifest, []string, error) { + entries, warnings, err := buildSourceV2Entries( + ctx, scan.layout, inputs, external, knownFragments, counters, + ) + if err != nil { + return sourceV2DeltaManifest{}, nil, fmt.Errorf( + "build code-intel lane fragments: %w", + err, + ) + } + + manifest := sourceV2DeltaManifest{ + Contract: SourceV2Contract, + Kind: sourceV2DeltaManifestKind, + RepositoryID: scan.layout.repositoryID, + WorktreeID: scan.layout.worktreeID, + HeadOID: scan.headOID, + BaseManifestID: baseManifestID, + ConfigHash: scan.configHash, + ExtractorSetHash: scan.extractorSetHash, + Entries: entries, + Tombstones: tombstones, + } + + manifest.ManifestID, err = sourceV2DeltaManifestID(manifest) + if err != nil { + return sourceV2DeltaManifest{}, nil, err + } + + _, err = writeImmutableSourceV2JSON( + scan.layout.deltaManifestPath(manifest.ManifestID), + manifest, + ) + if err != nil { + return sourceV2DeltaManifest{}, nil, err + } + + return manifest, warnings, nil +} + +func publishSourceV2Generation( + scan sourceV2Scan, + manifests sourceV2BuiltManifests, + counters sourceV2BuildCounters, +) (SourceSyncReceipt, error) { + mergedEntries := mergeSourceV2Entries( + manifests.base.Entries, + manifests.delta.Entries, + manifests.delta.Tombstones, + ) + coverage := sourceV2Coverage( + scan.current, + mergedEntries, + scan.excluded, + counters.cacheHits, + ) + readiness := sourceV2Readiness(coverage) + generationID := sourceV2Digest( + "generation", + string(scan.sourceSnapshotID)+"\x00"+ + manifests.base.ManifestID+"\x00"+manifests.delta.ManifestID, + ) + readiness.Identity = sourceV2Identity(scan, GenerationID(generationID)) + + generatedAt := time.Now().UTC().Format(time.RFC3339Nano) + storage := SourceV2Storage{ + SharedRoot: scan.layout.sharedRoot, + LaneRoot: scan.layout.laneRoot, + BaseManifestID: manifests.base.ManifestID, + DeltaManifestID: manifests.delta.ManifestID, + } + + statusReceipt := SourceStatusReceipt{ + Contract: SourceV2Contract, + Kind: sourceV2StatusKind, + GeneratedAtUTC: generatedAt, + Repair: sourceV2RepairCommand, + SourceReadiness: readiness, + VectorReadiness: unevaluatedVectorReadiness(), + Storage: storage, + } + + err := writeCurrentSourceV2Receipt( + scan.layout.statusPath(), + statusReceipt, + ) + if err != nil { + return SourceSyncReceipt{}, err + } + + return SourceSyncReceipt{ + Contract: SourceV2Contract, + Kind: sourceV2SyncKind, + GeneratedAtUTC: generatedAt, + Repair: sourceV2RepairCommand, + SourceReadiness: readiness, + VectorReadiness: unevaluatedVectorReadiness(), + Storage: storage, + Sync: SourceSyncSummary{ + FilesIndexed: sourceV2IndexedCount(coverage), + FilesFailed: sourceV2FailedCount(coverage), + FragmentsReused: counters.fragmentsReused, + FragmentsWritten: counters.fragmentsWritten, + }, + Warnings: manifests.warnings, + }, nil +} + +func validateSourceV2ExternalExtractor( + ctx context.Context, + scan sourceV2Scan, + external ExternalBatchExtractor, +) error { + if !sourceV2InputsNeedExternalExtractor(scan.current) && + !sourceV2InputsNeedExternalExtractor(scan.head) { + return nil + } + + if external == nil { + return ErrExternalExtractorRequired + } + + err := external.Validate(ctx) + if err != nil { + return fmt.Errorf("%w: %w", ErrExternalExtractorRequired, err) + } + + return nil +} + +func sourceV2InputsNeedExternalExtractor(inputs map[string]sourceV2Input) bool { + for _, input := range inputs { + if !input.descriptor.BuiltIn { + return true + } + } + + return false +} + +// SourceIndexStatus compares the persisted generation to current source bytes. +func SourceIndexStatus(ctx context.Context, root string) (SourceStatusReceipt, error) { + scan, err := inspectSourceV2Repository(ctx, root, false) + if err != nil { + return SourceStatusReceipt{}, err + } + + stored, err := loadCurrentSourceV2Receipt(scan.layout.statusPath()) + if errors.Is(err, os.ErrNotExist) { + return missingSourceV2Status(scan), nil + } + + if err != nil { + return SourceStatusReceipt{}, err + } + + stored.GeneratedAtUTC = time.Now().UTC().Format(time.RFC3339Nano) + + storedIdentity := stored.SourceReadiness.Identity + if storedIdentity.RepositoryID != scan.layout.repositoryID || + storedIdentity.SourceSnapshotID != scan.sourceSnapshotID { + stored.SourceReadiness.Status = SourceStatusStale + stored.SourceReadiness.Reasons = []string{ + "persisted generation does not match current source bytes, " + + "HEAD, config, or extractors", + } + stored.SourceReadiness.Coverage = staleSourceV2Coverage(scan.current, scan.excluded) + + return stored, nil + } + + verifyErr := verifySourceV2Generation(scan.layout, stored) + if verifyErr != nil { + stored.SourceReadiness.Status = SourceStatusFailed + stored.SourceReadiness.Reasons = []string{verifyErr.Error()} + } + + return stored, nil +} + +func inspectSourceV2Repository( + ctx context.Context, + root string, + includeHead bool, +) (sourceV2Scan, error) { + layout, err := resolveSourceV2Layout(ctx, root) + if err != nil { + return sourceV2Scan{}, err + } + + options, err := LoadIndexOptions(layout.repositoryRoot) + if err != nil { + return sourceV2Scan{}, err + } + + configHash, err := sourceV2ContentID("config", options) + if err != nil { + return sourceV2Scan{}, err + } + + extractorHash, err := sourceV2ContentID("extractors", astfacts.LanguageDescriptors()) + if err != nil { + return sourceV2Scan{}, err + } + + headOID, err := sourceV2GitOutput( + ctx, + layout.repositoryRoot, + "rev-parse", + "--verify", + "HEAD", + ) + if err != nil { + return sourceV2Scan{}, fmt.Errorf("resolve code-intel HEAD: %w", err) + } + + current, excluded, err := currentSourceV2Inputs(ctx, layout.repositoryRoot, options) + if err != nil { + return sourceV2Scan{}, err + } + + head := map[string]sourceV2Input{} + if includeHead { + head, err = headSourceV2Inputs(ctx, layout.repositoryRoot, options) + if err != nil { + return sourceV2Scan{}, err + } + } + + fingerprint, err := sourceV2WorktreeFingerprint(current) + if err != nil { + return sourceV2Scan{}, err + } + + snapshotID := SourceSnapshotID(sourceV2Digest( + "snapshot", + string(layout.repositoryID)+"\x00"+headOID+"\x00"+fingerprint+"\x00"+ + configHash+"\x00"+extractorHash, + )) + + return sourceV2Scan{ + layout: layout, + current: current, + head: head, + excluded: excluded, + headOID: headOID, + configHash: configHash, + extractorSetHash: extractorHash, + worktreeFingerprint: fingerprint, + sourceSnapshotID: snapshotID, + }, nil +} + +func currentSourceV2Inputs( + ctx context.Context, + root string, + options IndexOptions, +) (map[string]sourceV2Input, map[string]int, error) { + walker := sourceV2CurrentWalker{ + root: root, + options: options, + ignoreMatcher: newGitIgnoreMatcher(ctx, root), + inputs: map[string]sourceV2Input{}, + excluded: map[string]int{}, + } + + err := filepath.WalkDir( + root, + func(path string, entry os.DirEntry, walkErr error) error { + return walker.visit(ctx, path, entry, walkErr) + }, + ) + if err != nil { + return nil, nil, fmt.Errorf("walk code-intel v2 source tree: %w", err) + } + + return walker.inputs, walker.excluded, nil +} + +type sourceV2CurrentWalker struct { + ignoreMatcher gitIgnoreMatcher + inputs map[string]sourceV2Input + excluded map[string]int + root string + options IndexOptions +} + +func (walker sourceV2CurrentWalker) visit( + ctx context.Context, + path string, + entry os.DirEntry, + walkErr error, +) error { + ctxErr := ctx.Err() + if ctxErr != nil { + return fmt.Errorf("scan code-intel source tree: %w", ctxErr) + } + + if walkErr != nil { + return fmt.Errorf("visit code-intel source path: %w", walkErr) + } + + relative, err := filepath.Rel(walker.root, path) + if err != nil { + return fmt.Errorf("resolve code-intel source path: %w", err) + } + + relative = filepath.ToSlash(relative) + if entry.IsDir() { + return walker.visitDirectory(ctx, path, relative) + } + + return walker.visitFile(ctx, path, relative, entry) +} + +func (walker sourceV2CurrentWalker) visitDirectory( + ctx context.Context, + path string, + relative string, +) error { + if relative != "." && (pathHasSkippedDir(relative) || + excludedByConfig(relative, walker.options.ExcludePatterns) || + walker.ignoreMatcher.ignoredDir(ctx, path)) { + return filepath.SkipDir + } + + return nil +} + +func (walker sourceV2CurrentWalker) visitFile( + ctx context.Context, + path string, + relative string, + entry os.DirEntry, +) error { + descriptor, recognized := astfacts.SourceLanguageForPath(relative) + if !recognized { + return nil + } + + if pathHasSkippedDir(relative) || + excludedByConfig(relative, walker.options.ExcludePatterns) || + walker.ignoreMatcher.ignoredFile(ctx, path) { + walker.excluded[descriptor.Language]++ + + return nil + } + + info, err := entry.Info() + if err != nil { + return fmt.Errorf("inspect code-intel source path: %w", err) + } + + if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() { + walker.excluded[descriptor.Language]++ + + return nil + } + + input, err := sourceV2InputFromBytes(path, relative, descriptor, nil) + if err != nil { + return err + } + + walker.inputs[relative] = input + + return nil +} + +func headSourceV2Inputs( + ctx context.Context, + root string, + options IndexOptions, +) (map[string]sourceV2Input, error) { + command := realgit.Command(ctx, false, "ls-tree", "-r", "-z", "HEAD") + command.Dir = root + command.Env = realgit.CleanGitLocalEnv(os.Environ()) + + output, err := command.Output() + if err != nil { + return nil, fmt.Errorf("list HEAD source files: %w", err) + } + + inputs := map[string]sourceV2Input{} + + for rawPath := range bytes.SplitSeq(output, []byte{0}) { + ctxErr := ctx.Err() + if ctxErr != nil { + return nil, fmt.Errorf("scan HEAD source tree: %w", ctxErr) + } + + if len(rawPath) == 0 { + continue + } + + input, included, inputErr := headSourceV2Input(ctx, root, options, rawPath) + if inputErr != nil { + return nil, inputErr + } + + if included { + inputs[input.path] = input + } + } + + return inputs, nil +} + +func headSourceV2Input( + ctx context.Context, + root string, + options IndexOptions, + rawPath []byte, +) (sourceV2Input, bool, error) { + metadata, rawName, found := bytes.Cut(rawPath, []byte{'\t'}) + if !found { + return sourceV2Input{}, false, fmt.Errorf( + "%w: invalid Git tree entry %q", + errInvalidSourceV2Input, + rawPath, + ) + } + + fields := strings.Fields(string(metadata)) + if len(fields) != sourceV2GitTreeMetadataFields { + return sourceV2Input{}, false, fmt.Errorf( + "%w: invalid Git tree metadata %q", + errInvalidSourceV2Input, + metadata, + ) + } + + if fields[0] != "100644" && fields[0] != "100755" { + return sourceV2Input{}, false, nil + } + + path := filepath.ToSlash(string(rawName)) + + descriptor, recognized := astfacts.SourceLanguageForPath(path) + if !recognized || pathHasSkippedDir(path) || + excludedByConfig(path, options.ExcludePatterns) { + return sourceV2Input{}, false, nil + } + + contents, err := sourceV2GitBytes(ctx, root, "cat-file", "blob", fields[2]) + if err != nil { + return sourceV2Input{}, false, fmt.Errorf("read HEAD source %q: %w", path, err) + } + + input, err := sourceV2InputFromBytes("", path, descriptor, contents) + if err != nil { + return sourceV2Input{}, false, err + } + + return input, true, nil +} + +func sourceV2GitBytes( + ctx context.Context, + root string, + arguments ...string, +) ([]byte, error) { + command := realgit.Command(ctx, false, arguments...) + command.Dir = root + command.Env = realgit.CleanGitLocalEnv(os.Environ()) + + output, err := command.Output() + if err != nil { + return nil, fmt.Errorf("run Git %s: %w", strings.Join(arguments, " "), err) + } + + return output, nil +} + +func sourceV2InputFromBytes( + absPath string, + path string, + descriptor astfacts.LanguageDescriptor, + contents []byte, +) (sourceV2Input, error) { + if contents == nil { + var err error + + contents, err = os.ReadFile(absPath) + if err != nil { + return sourceV2Input{}, fmt.Errorf("read code-intel v2 source %q: %w", path, err) + } + } + + return sourceV2Input{ + descriptor: descriptor, + path: path, + contentHash: astfacts.ContentHash(contents), + contents: contents, + oversized: len(contents) > maxIndexedSourceBytes || + astfacts.LineCount(contents) > maxIndexedSourceLines, + }, nil +} + +func splitSourceV2Inputs( + head map[string]sourceV2Input, + current map[string]sourceV2Input, +) ([]sourceV2Input, []sourceV2Input, []string) { + base := sourceV2SortedInputs(head) + delta := []sourceV2Input{} + tombstones := []string{} + + for path, input := range current { + headInput, exists := head[path] + + extractorChanged := headInput.descriptor.Extractor.Fingerprint != + input.descriptor.Extractor.Fingerprint + if !exists || headInput.contentHash != input.contentHash || extractorChanged { + delta = append(delta, input) + } + } + + for path := range head { + if _, exists := current[path]; !exists { + tombstones = append(tombstones, path) + } + } + + slices.SortFunc(delta, compareSourceV2Input) + slices.Sort(tombstones) + + return base, delta, tombstones +} + +func sourceV2SortedInputs(inputs map[string]sourceV2Input) []sourceV2Input { + ordered := make([]sourceV2Input, 0, len(inputs)) + for _, input := range inputs { + ordered = append(ordered, input) + } + + slices.SortFunc(ordered, compareSourceV2Input) + + return ordered +} + +func compareSourceV2Input(left, right sourceV2Input) int { + return strings.Compare(left.path, right.path) +} + +func buildSourceV2Entries( + ctx context.Context, + layout sourceV2Layout, + inputs []sourceV2Input, + external ExternalBatchExtractor, + knownFragments map[string]string, + counters *sourceV2BuildCounters, +) ([]sourceV2ManifestEntry, []string, error) { + entries := make([]sourceV2ManifestEntry, 0, len(inputs)) + pendingExternal := []sourceV2Input{} + + for _, input := range inputs { + entry, pending, err := buildSourceV2Entry( + layout, + input, + external, + knownFragments, + counters, + ) + if err != nil { + return nil, nil, err + } + + entries = append(entries, entry) + if pending { + pendingExternal = append(pendingExternal, input) + } + } + + warnings, err := completeExternalSourceV2Entries( + ctx, + layout, + external, + pendingExternal, + entries, + knownFragments, + counters, + ) + if err != nil { + return nil, nil, err + } + + slices.SortFunc(entries, func(left, right sourceV2ManifestEntry) int { + return strings.Compare(left.Path, right.Path) + }) + + return entries, warnings, nil +} + +func buildSourceV2Entry( + layout sourceV2Layout, + input sourceV2Input, + external ExternalBatchExtractor, + knownFragments map[string]string, + counters *sourceV2BuildCounters, +) (sourceV2ManifestEntry, bool, error) { + entry := sourceV2Entry(input) + if input.oversized { + entry.Status = "oversized" + + return entry, false, nil + } + + cacheKey := sourceV2InputCacheKey(input) + + entry.FragmentID = knownFragments[cacheKey] + if entry.FragmentID != "" && + sourceV2FragmentReady(layout.fragmentPath(entry.FragmentID), entry) { + entry.Status = sourceV2EntryStatusIndexed + counters.fragmentsReused++ + counters.cacheHits[cacheKey] = true + + return entry, false, nil + } + + if !input.descriptor.BuiltIn { + if external == nil { + return sourceV2ManifestEntry{}, false, ErrExternalExtractorRequired + } + + if !utf8.Valid(input.contents) { + entry.FragmentID = "" + entry.Status = SourceStatusFailed + + return entry, false, nil + } + + return entry, true, nil + } + + fragment, err := builtInSourceV2Fragment(input, entry.FragmentID) + if err != nil { + return failedSourceV2Entry(entry) + } + + err = publishSourceV2Fragment( + layout, + input, + &entry, + fragment, + knownFragments, + counters, + ) + if err != nil { + return sourceV2ManifestEntry{}, false, err + } + + return entry, false, nil +} + +func failedSourceV2Entry( + entry sourceV2ManifestEntry, +) (sourceV2ManifestEntry, bool, error) { + entry.FragmentID = "" + entry.Status = SourceStatusFailed + + return entry, false, nil +} + +func completeExternalSourceV2Entries( + ctx context.Context, + layout sourceV2Layout, + external ExternalBatchExtractor, + inputs []sourceV2Input, + entries []sourceV2ManifestEntry, + knownFragments map[string]string, + counters *sourceV2BuildCounters, +) ([]string, error) { + if len(inputs) == 0 { + return nil, nil + } + + results, err := extractSourceV2Batch(ctx, external, inputs) + if err != nil { + return nil, err + } + + warnings := []string{} + + for index := range entries { + entry := &entries[index] + + result, exists := results[entry.Path] + if !exists { + continue + } + + if result.Status == externalExtractorStatusError { + entry.FragmentID = "" + entry.Status = SourceStatusFailed + warnings = append( + warnings, + "PurRDF extraction failed for "+entry.Path+": "+result.Error, + ) + + continue + } + + input := sourceV2InputByPath(inputs, entry.Path) + fragment := externalSourceV2Fragment(input, "", result) + + err = publishSourceV2Fragment( + layout, + input, + entry, + fragment, + knownFragments, + counters, + ) + if err != nil { + return nil, err + } + } + + return warnings, nil +} + +func publishSourceV2Fragment( + layout sourceV2Layout, + input sourceV2Input, + entry *sourceV2ManifestEntry, + fragment sourceV2Fragment, + knownFragments map[string]string, + counters *sourceV2BuildCounters, +) error { + fragmentID, err := sourceV2FragmentContentID(fragment) + if err != nil { + return err + } + + entry.FragmentID = fragmentID + fragment.FragmentID = fragmentID + + written, err := writeImmutableSourceV2JSON( + layout.fragmentPath(fragmentID), + fragment, + ) + if err != nil { + return err + } + + if written { + counters.fragmentsWritten++ + } else { + counters.fragmentsReused++ + } + + entry.Status = sourceV2EntryStatusIndexed + knownFragments[sourceV2InputCacheKey(input)] = fragmentID + + return nil +} + +func sourceV2Entry(input sourceV2Input) sourceV2ManifestEntry { + return sourceV2ManifestEntry{ + Path: input.path, + Language: input.descriptor.Language, + ContentSHA256: input.contentHash, + ExtractorFingerprint: input.descriptor.Extractor.Fingerprint, + } +} + +func sourceV2InputCacheKey(input sourceV2Input) string { + pathComponent := "" + if !input.descriptor.BuiltIn { + pathComponent = input.path + } + + return input.descriptor.Language + "\x00" + input.descriptor.Variant + "\x00" + + input.descriptor.Extractor.Fingerprint + "\x00" + input.contentHash + "\x00" + + pathComponent +} + +func knownSourceV2Fragments(scan sourceV2Scan) map[string]string { + known := map[string]string{} + addSharedSourceV2BaseFragments(scan, known) + + receipt, err := loadCurrentSourceV2Receipt(scan.layout.statusPath()) + if err != nil { + return known + } + + base, err := loadSourceV2BaseManifest(scan.layout, receipt.Storage.BaseManifestID) + if err != nil { + return known + } + + delta, err := loadSourceV2DeltaManifest(scan.layout, receipt.Storage.DeltaManifestID) + if err != nil { + return known + } + + addKnownSourceV2Entries(scan.layout, known, base.Entries) + addKnownSourceV2Entries(scan.layout, known, delta.Entries) + + return known +} + +func addSharedSourceV2BaseFragments( + scan sourceV2Scan, + known map[string]string, +) { + // Reuse the immutable HEAD base produced by another lane when its exact + // repository/config/extractor inputs match this scan. Cache discovery is an + // optimization, so malformed or stale candidates are ignored here and are + // still rejected when referenced by a current receipt. + baseDirectory := filepath.Join(scan.layout.sharedRoot, "bases") + + baseFiles, readErr := os.ReadDir(baseDirectory) + if readErr != nil { + baseFiles = nil + } + + for _, file := range baseFiles { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".json") { + continue + } + + digest := strings.TrimSuffix(file.Name(), ".json") + + manifest, err := loadSourceV2BaseManifest( + scan.layout, + "base:sha256:"+digest, + ) + if err != nil || manifest.RepositoryID != scan.layout.repositoryID || + manifest.HeadOID != scan.headOID || manifest.ConfigHash != scan.configHash || + manifest.ExtractorSetHash != scan.extractorSetHash { + continue + } + + addKnownSourceV2Entries(scan.layout, known, manifest.Entries) + } +} + +func addKnownSourceV2Entries( + layout sourceV2Layout, + known map[string]string, + entries []sourceV2ManifestEntry, +) { + for _, entry := range entries { + if entry.Status != sourceV2EntryStatusIndexed || + !sourceV2FragmentReady(layout.fragmentPath(entry.FragmentID), entry) { + continue + } + + descriptor, ok := astfacts.SourceLanguageForPath(entry.Path) + if !ok || descriptor.Language != entry.Language || + descriptor.Extractor.Fingerprint != entry.ExtractorFingerprint { + continue + } + + input := sourceV2Input{ + descriptor: descriptor, + path: entry.Path, + contentHash: entry.ContentSHA256, + } + known[sourceV2InputCacheKey(input)] = entry.FragmentID + } +} + +func sourceV2FragmentReady(path string, entry sourceV2ManifestEntry) bool { + payload, err := os.ReadFile(path) + if err != nil { + return false + } + + var fragment sourceV2Fragment + if json.Unmarshal(payload, &fragment) != nil { + return false + } + + computedID, err := sourceV2FragmentContentID(fragment) + if err != nil { + return false + } + + return fragment.Contract == SourceV2Contract && + fragment.Kind == sourceV2FragmentKind && + fragment.FragmentID == entry.FragmentID && + computedID == entry.FragmentID && + fragment.ContentSHA256 == entry.ContentSHA256 && + fragment.Extractor.Fingerprint == entry.ExtractorFingerprint +} + +func sourceV2FragmentContentID(fragment sourceV2Fragment) (string, error) { + fragment.FragmentID = "" + + return sourceV2ContentID("fragment", fragment) +} + +func builtInSourceV2Fragment( + input sourceV2Input, + fragmentID string, +) (sourceV2Fragment, error) { + file, supported, err := astfacts.Analyze(input.path, input.contents) + if err != nil { + return sourceV2Fragment{}, fmt.Errorf( + "analyze code-intel source %q: %w", + input.path, + err, + ) + } + + if !supported { + return sourceV2Fragment{}, fmt.Errorf( + "%w: no built-in extractor for %q", + errInvalidSourceV2Input, + input.path, + ) + } + + if file.HasParseError { + return sourceV2Fragment{}, fmt.Errorf( + "%w: parser reported an error for %q", + errInvalidSourceV2Input, + input.path, + ) + } + + fragment := sourceV2Fragment{ + Contract: SourceV2Contract, + Kind: sourceV2FragmentKind, + FragmentID: fragmentID, + Language: input.descriptor.Language, + ContentSHA256: input.contentHash, + Extractor: input.descriptor.Extractor, + LineCount: file.LineCount, + Symbols: make([]SourceSymbolFact, 0, len(file.Symbols)), + Imports: make([]SourceImportFact, 0, len(file.Imports)), + } + for _, symbol := range file.Symbols { + fragment.Symbols = append(fragment.Symbols, SourceSymbolFact{ + SymbolKind: symbol.SymbolKind, + SymbolName: symbol.SymbolName, + SymbolPath: symbol.SymbolPath, + NodeKind: symbol.NodeKind, + ReferencedNames: slices.Clone(symbol.ReferencedNames), + CallNames: slices.Clone(symbol.CallNames), + BaseNames: slices.Clone(symbol.BaseNames), + StartByte: symbol.StartByte, + EndByte: symbol.EndByte, + StartLine: symbol.StartLine, + EndLine: symbol.EndLine, + Confidence: "deterministic", + Provenance: SourceFactProvenance{ + Class: "EXTRACTED", + Parser: input.descriptor.Extractor.Name, + ParserRevision: input.descriptor.Extractor.Fingerprint, + SpanFidelity: "exact", + }, + }) + } + + for _, imported := range file.Imports { + fragment.Imports = append(fragment.Imports, SourceImportFact{ + Target: imported.Target, + RawText: imported.RawText, + Confidence: "deterministic", + Provenance: SourceFactProvenance{ + Class: "EXTRACTED", + Parser: input.descriptor.Extractor.Name, + ParserRevision: input.descriptor.Extractor.Fingerprint, + SpanFidelity: "none", + }, + }) + } + + return fragment, nil +} + +func extractSourceV2Batch( + ctx context.Context, + external ExternalBatchExtractor, + inputs []sourceV2Input, +) (map[string]ExternalExtractorResult, error) { + files := make([]ExternalExtractorRequestFile, 0, len(inputs)) + for _, input := range inputs { + files = append(files, ExternalExtractorRequestFile{ + Path: input.path, + ContentSHA256: input.contentHash, + Content: string(input.contents), + }) + } + + response, err := external.Extract(ctx, files) + if err != nil { + return nil, fmt.Errorf("extract PurRDF code-intel facts: %w", err) + } + + results := make(map[string]ExternalExtractorResult, len(response.Results)) + for _, result := range response.Results { + input := sourceV2InputByPath(inputs, result.Path) + if input.path == "" { + return nil, fmt.Errorf( + "%w: external extractor returned unknown path %q", + errInvalidSourceV2Input, + result.Path, + ) + } + + if result.Language != input.descriptor.Language { + return nil, fmt.Errorf( + "%w: external extractor language mismatch for %q: %q", + errInvalidSourceV2Input, + result.Path, + result.Language, + ) + } + + results[result.Path] = result + } + + return results, nil +} + +func sourceV2InputByPath(inputs []sourceV2Input, path string) sourceV2Input { + for _, input := range inputs { + if input.path == path { + return input + } + } + + return sourceV2Input{} +} + +func externalSourceV2Fragment( + input sourceV2Input, + fragmentID string, + result ExternalExtractorResult, +) sourceV2Fragment { + facts := slices.Clone(result.Facts) + slices.SortFunc(facts, func(left, right ExternalExtractorFact) int { + return strings.Compare(left.ID, right.ID) + }) + + return sourceV2Fragment{ + Contract: SourceV2Contract, + Kind: sourceV2FragmentKind, + FragmentID: fragmentID, + Language: input.descriptor.Language, + ContentSHA256: input.contentHash, + Extractor: input.descriptor.Extractor, + LineCount: astfacts.LineCount(input.contents), + Facts: facts, + } +} + +func sourceV2ManifestID( + namespace string, + manifest sourceV2BaseManifest, +) (string, error) { + manifest.ManifestID = "" + + return sourceV2ContentID(namespace, manifest) +} + +func sourceV2DeltaManifestID(manifest sourceV2DeltaManifest) (string, error) { + manifest.ManifestID = "" + + return sourceV2ContentID("delta", manifest) +} + +func mergeSourceV2Entries( + base []sourceV2ManifestEntry, + delta []sourceV2ManifestEntry, + tombstones []string, +) map[string]sourceV2ManifestEntry { + merged := make(map[string]sourceV2ManifestEntry, len(base)+len(delta)) + for _, entry := range base { + merged[entry.Path] = entry + } + + for _, path := range tombstones { + delete(merged, path) + } + + for _, entry := range delta { + merged[entry.Path] = entry + } + + return merged +} + +func sourceV2Coverage( + current map[string]sourceV2Input, + entries map[string]sourceV2ManifestEntry, + excluded map[string]int, + cacheHits map[string]bool, +) []LanguageCoverage { + coverage := sourceV2CoverageMap(excluded) + for path, input := range current { + item := coverage[input.descriptor.Language] + + item.Eligible++ + if cacheHits[sourceV2InputCacheKey(input)] { + item.CacheHits++ + } + + entry := entries[path] + switch entry.Status { + case sourceV2EntryStatusIndexed: + item.Indexed++ + case "unsupported": + item.Unsupported++ + case "oversized": + item.Oversized++ + default: + item.Failed++ + } + + coverage[input.descriptor.Language] = item + } + + return sourceV2SortedCoverage(coverage) +} + +func staleSourceV2Coverage( + current map[string]sourceV2Input, + excluded map[string]int, +) []LanguageCoverage { + coverage := sourceV2CoverageMap(excluded) + for _, input := range current { + item := coverage[input.descriptor.Language] + item.Eligible++ + item.Stale++ + coverage[input.descriptor.Language] = item + } + + return sourceV2SortedCoverage(coverage) +} + +func sourceV2CoverageMap(excluded map[string]int) map[string]LanguageCoverage { + coverage := map[string]LanguageCoverage{} + for _, descriptor := range astfacts.LanguageDescriptors() { + coverage[descriptor.Language] = LanguageCoverage{ + Language: descriptor.Language, + Excluded: excluded[descriptor.Language], + } + } + + return coverage +} + +func sourceV2SortedCoverage(coverage map[string]LanguageCoverage) []LanguageCoverage { + result := make([]LanguageCoverage, 0, len(coverage)) + for _, item := range coverage { + result = append(result, item) + } + + slices.SortFunc(result, func(left, right LanguageCoverage) int { + return strings.Compare(left.Language, right.Language) + }) + + return result +} + +func sourceV2Readiness(coverage []LanguageCoverage) SourceReadiness { + readiness := SourceReadiness{Coverage: coverage, Status: SourceStatusExact} + eligible := 0 + indexed := 0 + failed := 0 + + for _, item := range coverage { + eligible += item.Eligible + indexed += item.Indexed + + failed += item.Failed + if item.Unsupported > 0 { + readiness.Reasons = append( + readiness.Reasons, + item.Language+": extractor unavailable", + ) + } + + if item.Oversized > 0 { + readiness.Reasons = append( + readiness.Reasons, + item.Language+": oversized source files", + ) + } + + if item.Failed > 0 { + readiness.Reasons = append(readiness.Reasons, item.Language+": extraction failures") + } + } + + if indexed != eligible { + readiness.Status = SourceStatusPartial + } + + if eligible > 0 && indexed == 0 && failed > 0 { + readiness.Status = SourceStatusFailed + } + + return readiness +} + +func sourceV2Identity(scan sourceV2Scan, generationID GenerationID) SourceIdentity { + return SourceIdentity{ + RepositoryID: scan.layout.repositoryID, + SourceSnapshotID: scan.sourceSnapshotID, + GenerationID: generationID, + HeadOID: scan.headOID, + WorktreeID: scan.layout.worktreeID, + WorktreeFingerprint: scan.worktreeFingerprint, + ConfigHash: scan.configHash, + ExtractorSetHash: scan.extractorSetHash, + } +} + +func sourceV2WorktreeFingerprint(inputs map[string]sourceV2Input) (string, error) { + type fingerprintEntry struct { + Path string `json:"path"` + ContentSHA256 string `json:"content_sha256"` + ExtractorFingerprint string `json:"extractor_fingerprint"` + } + + entries := make([]fingerprintEntry, 0, len(inputs)) + for _, input := range inputs { + entries = append(entries, fingerprintEntry{ + Path: input.path, + ContentSHA256: input.contentHash, + ExtractorFingerprint: input.descriptor.Extractor.Fingerprint, + }) + } + + slices.SortFunc(entries, func(left, right fingerprintEntry) int { + return strings.Compare(left.Path, right.Path) + }) + + return sourceV2ContentID("worktree-content", entries) +} + +func missingSourceV2Status(scan sourceV2Scan) SourceStatusReceipt { + coverage := staleSourceV2Coverage(scan.current, scan.excluded) + for index := range coverage { + coverage[index].Stale = 0 + } + + return SourceStatusReceipt{ + Contract: SourceV2Contract, + Kind: sourceV2StatusKind, + GeneratedAtUTC: time.Now().UTC().Format(time.RFC3339Nano), + Repair: sourceV2RepairCommand, + SourceReadiness: SourceReadiness{ + Identity: sourceV2Identity(scan, ""), + Coverage: coverage, + Reasons: []string{"no code-intel v2 generation exists for this worktree"}, + Status: SourceStatusMissing, + }, + VectorReadiness: unevaluatedVectorReadiness(), + Storage: SourceV2Storage{ + SharedRoot: scan.layout.sharedRoot, + LaneRoot: scan.layout.laneRoot, + }, + } +} + +func unevaluatedVectorReadiness() VectorReadiness { + return VectorReadiness{Status: VectorStatusNotEvaluated} +} + +func sourceV2IndexedCount(coverage []LanguageCoverage) int { + total := 0 + for _, item := range coverage { + total += item.Indexed + } + + return total +} + +func sourceV2FailedCount(coverage []LanguageCoverage) int { + total := 0 + for _, item := range coverage { + total += item.Failed + item.Unsupported + item.Oversized + } + + return total +} diff --git a/go/internal/codeintel/source_v2_test.go b/go/internal/codeintel/source_v2_test.go new file mode 100644 index 00000000..dfc3fc2b --- /dev/null +++ b/go/internal/codeintel/source_v2_test.go @@ -0,0 +1,716 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "context" + "encoding/json" + "errors" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "testing" + + "blackcat.ca/coding-ethos/go/internal/astfacts" +) + +type sourceV2FakeExtractor struct { + calls int + validations int +} + +func (extractor *sourceV2FakeExtractor) Validate(_ context.Context) error { + extractor.validations++ + + return nil +} + +func (extractor *sourceV2FakeExtractor) Extract( + _ context.Context, + files []ExternalExtractorRequestFile, +) (ExternalExtractorResponse, error) { + extractor.calls++ + results := make([]ExternalExtractorResult, 0, len(files)) + for _, file := range files { + language := astfacts.LanguageTurtle + parser := "purrdf-rdf" + switch filepath.Ext(file.Path) { + case ".trig": + language = astfacts.LanguageTriG + case ".nt": + language = astfacts.LanguageNTriples + case ".nq": + language = astfacts.LanguageNQuads + case ".rq", ".sparql": + language = astfacts.LanguageSPARQL + parser = "purrdf-sparql-algebra" + } + + results = append(results, ExternalExtractorResult{ + Path: file.Path, + ContentSHA256: file.ContentSHA256, + Language: language, + Status: "ok", + DocumentKind: language, + Facts: []ExternalExtractorFact{{ + ID: "fact:sha256:fixture", + Kind: "triple", + Provenance: ExternalExtractorProvenance{ + Class: "EXTRACTED", + Parser: parser, + ParserRevision: astfacts.PurrdfExtractorRevision, + SpanFidelity: "subject_start", + SourcePath: file.Path, + }, + }}, + }) + } + + return ExternalExtractorResponse{ + Protocol: astfacts.PurrdfExtractorProtocol, + RequestID: externalExtractorRequestID(files), + Extractor: ExternalExtractorIdentity{ + Name: astfacts.PurrdfExtractorName, + Version: "1", + PurrdfRevision: astfacts.PurrdfExtractorRevision, + }, + Results: results, + }, nil +} + +func TestSyncSourceIndexBuildsReusableBaseLaneDeltaAndExactQuery(t *testing.T) { + t.Parallel() + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "app.go", "package app\n\nfunc Current() {}\n") + writeSourceV2TestFile(t, root, "old.rs", "pub fn old() {}\n") + runSourceV2TestGit(t, root, "add", "app.go", "old.rs") + runSourceV2TestGit(t, root, "commit", "-m", "base") + + writeSourceV2TestFile( + t, + root, + "app.go", + "package app\n\nfunc Current() { helper() }\nfunc helper() {}\n", + ) + if err := os.Remove(filepath.Join(root, "old.rs")); err != nil { + t.Fatalf("remove tracked Rust source: %v", err) + } + writeSourceV2TestFile( + t, + root, + "ontology.ttl", + "@prefix ex: .\nex:s ex:p ex:o .\n", + ) + + legacyPath := filepath.Join(root, ".coding-ethos", "code-intel.duckdb") + writeSourceV2TestFile(t, root, ".coding-ethos/code-intel.duckdb", "legacy-sentinel") + + extractor := &sourceV2FakeExtractor{} + first, err := SyncSourceIndex(context.Background(), root, extractor) + if err != nil { + t.Fatalf("sync source index: %v", err) + } + if first.Contract != SourceV2Contract || + first.SourceReadiness.Status != SourceStatusExact || + first.SourceReadiness.Identity.GenerationID == "" { + t.Fatalf("sync receipt = %#v", first) + } + if first.Storage.SharedRoot != filepath.Join( + root, + ".git", + "coding-ethos", + "code-intel", + "v2", + ) { + t.Fatalf("shared root = %q", first.Storage.SharedRoot) + } + if first.Storage.LaneRoot == first.Storage.SharedRoot || + !strings.HasPrefix(first.Storage.LaneRoot, filepath.Join(root, ".coding-ethos")) { + t.Fatalf("lane root = %q", first.Storage.LaneRoot) + } + if got := sourceV2CoverageForLanguage( + first.SourceReadiness.Coverage, + astfacts.LanguageTurtle, + ); got.Eligible != 1 || + got.Indexed != 1 { + t.Fatalf("Turtle coverage = %#v", got) + } + if extractor.calls != 1 || extractor.validations != 1 { + t.Fatalf( + "external extractor calls = %d validations = %d", + extractor.calls, + extractor.validations, + ) + } + + delta, err := loadSourceV2DeltaManifest( + mustSourceV2Layout(t, root), + first.Storage.DeltaManifestID, + ) + if err != nil { + t.Fatalf("load lane delta: %v", err) + } + if !slices.Contains(delta.Tombstones, "old.rs") { + t.Fatalf("delta tombstones = %#v", delta.Tombstones) + } + + query, err := QuerySourceIndex(context.Background(), root, SourceIndexQuery{ + Path: "ontology.ttl", + }) + if err != nil { + t.Fatalf("query source index: %v", err) + } + if len(query.Records) != 1 || len(query.Records[0].Facts) != 1 || + query.SourceReadiness.Identity.GenerationID != first.SourceReadiness.Identity.GenerationID { + t.Fatalf("query result = %#v", query) + } + + second, err := SyncSourceIndex(context.Background(), root, extractor) + if err != nil { + t.Fatalf("repeat source sync: %v", err) + } + if second.Storage.BaseManifestID != first.Storage.BaseManifestID || + second.Storage.DeltaManifestID != first.Storage.DeltaManifestID || + second.SourceReadiness.Identity.GenerationID != first.SourceReadiness.Identity.GenerationID { + t.Fatalf( + "repeat generation changed: first=%#v second=%#v", + first.Storage, + second.Storage, + ) + } + if second.Sync.FragmentsReused == 0 || extractor.calls != 1 || + extractor.validations != 2 { + t.Fatalf( + "repeat sync did not reuse fragments with a fresh identity check: %#v calls=%d validations=%d", + second.Sync, + extractor.calls, + extractor.validations, + ) + } + if got := sourceV2CoverageForLanguage( + second.SourceReadiness.Coverage, + astfacts.LanguageTurtle, + ); got.CacheHits != 1 { + t.Fatalf("repeat Turtle coverage = %#v", got) + } + + legacy, err := os.ReadFile(legacyPath) + if err != nil || string(legacy) != "legacy-sentinel" { + t.Fatalf("legacy v1 store changed: %q err=%v", legacy, err) + } + + writeSourceV2TestFile(t, root, "app.go", "package app\n\nfunc Changed() {}\n") + status, err := SourceIndexStatus(context.Background(), root) + if err != nil { + t.Fatalf("read stale source status: %v", err) + } + if status.SourceReadiness.Status != SourceStatusStale { + t.Fatalf("source status = %#v", status.SourceReadiness) + } + if _, err = QuerySourceIndex( + context.Background(), + root, + SourceIndexQuery{}, + ); !errors.Is( + err, + ErrSourceIndexStale, + ) { + t.Fatalf("stale query error = %v", err) + } +} + +func TestSourceIndexStatusRejectsMutatedImmutableManifest(t *testing.T) { + t.Parallel() + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "app.go", "package app\n") + runSourceV2TestGit(t, root, "add", "app.go") + runSourceV2TestGit(t, root, "commit", "-m", "base") + + receipt, err := SyncSourceIndex(context.Background(), root, nil) + if err != nil { + t.Fatalf("sync source index: %v", err) + } + layout := mustSourceV2Layout(t, root) + delta, err := loadSourceV2DeltaManifest(layout, receipt.Storage.DeltaManifestID) + if err != nil { + t.Fatalf("load delta manifest: %v", err) + } + delta.Tombstones = append(delta.Tombstones, "app.go") + payload, err := json.MarshalIndent(delta, "", " ") + if err != nil { + t.Fatalf("marshal mutated delta: %v", err) + } + payload = append(payload, '\n') + if err = os.WriteFile( + layout.deltaManifestPath(delta.ManifestID), + payload, + 0o600, + ); err != nil { + t.Fatalf("mutate delta manifest: %v", err) + } + + status, err := SourceIndexStatus(context.Background(), root) + if err != nil { + t.Fatalf("read status after manifest mutation: %v", err) + } + if status.SourceReadiness.Status != SourceStatusFailed || + len(status.SourceReadiness.Reasons) != 1 || + !strings.Contains(status.SourceReadiness.Reasons[0], "content ID mismatch") { + t.Fatalf("mutated manifest status = %#v", status.SourceReadiness) + } +} + +func TestSyncSourceIndexReusesSharedBaseAcrossWorktrees(t *testing.T) { + t.Parallel() + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "model.ttl", "

.\n") + runSourceV2TestGit(t, root, "add", "model.ttl") + runSourceV2TestGit(t, root, "commit", "-m", "base") + + extractor := &sourceV2FakeExtractor{} + first, err := SyncSourceIndex(context.Background(), root, extractor) + if err != nil { + t.Fatalf("sync source index in primary worktree: %v", err) + } + + lane := filepath.Join(t.TempDir(), "lane") + runSourceV2TestGit(t, root, "worktree", "add", "--quiet", "--detach", lane, "HEAD") + second, err := SyncSourceIndex(context.Background(), lane, extractor) + if err != nil { + t.Fatalf("sync source index in second worktree: %v", err) + } + if second.Storage.BaseManifestID != first.Storage.BaseManifestID || + extractor.calls != 1 || extractor.validations != 2 || + second.Sync.FragmentsReused == 0 { + t.Fatalf( + "shared base was not reused: first=%#v second=%#v calls=%d validations=%d", + first.Storage, + second.Storage, + extractor.calls, + extractor.validations, + ) + } +} + +func TestCommandBatchExtractorUsesFrozenPurRDFEnvelope(t *testing.T) { + t.Parallel() + + directory := t.TempDir() + script := filepath.Join(directory, "coding-ethos-purrdf-extractor") + writeSourceV2Executable(t, script, `#!/usr/bin/env python3 +import json, sys +request = json.load(sys.stdin) +files = request["files"] +response = { + "protocol": "coding-ethos.external-extractor.v1", + "request_id": request["request_id"], + "extractor": { + "name": "purrdf", + "version": "1", + "purrdf_revision": "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" + }, + "results": [{ + "path": item["path"], + "content_sha256": item["content_sha256"], + "language": "sparql", + "status": "ok", + "document_kind": "query", + "facts": [] + } for item in files] +} +json.dump(response, sys.stdout) +`) + + extractor, err := NewCommandBatchExtractor(script) + if err != nil { + t.Fatalf("construct command extractor: %v", err) + } + files := []ExternalExtractorRequestFile{{ + Path: "query.rq", + ContentSHA256: astfacts.ContentHash([]byte("SELECT * WHERE { ?s ?p ?o }")), + Content: "SELECT * WHERE { ?s ?p ?o }", + }} + response, err := extractor.Extract(context.Background(), files) + if err != nil { + t.Fatalf("extract frozen envelope: %v", err) + } + if response.Protocol != astfacts.PurrdfExtractorProtocol || + response.RequestID != externalExtractorRequestID(files) || + len( + response.Results, + ) != 1 || response.Results[0].Language != astfacts.LanguageSPARQL { + t.Fatalf("extractor response = %#v", response) + } +} + +func TestCommandBatchExtractorAgainstPurRDFHelper(t *testing.T) { + executable := strings.TrimSpace(os.Getenv("CODING_ETHOS_PURRDF_EXTRACTOR")) + if executable == "" { + t.Skip("set CODING_ETHOS_PURRDF_EXTRACTOR for the cross-language integration test") + } + + extractor, err := NewCommandBatchExtractor(executable) + if err != nil { + t.Fatalf("construct PurRDF helper adapter: %v", err) + } + sources := map[string]string{ + "model.ttl": " .\n", + "model.trig": "@prefix ex: .\nex:g { ex:s ex:p ex:o . }\n", + "model.nt": " .\n", + "model.nq": " .\n", + "query.rq": "SELECT * WHERE { ?s ?p ?o }\n", + } + files := make([]ExternalExtractorRequestFile, 0, len(sources)) + for path, content := range sources { + files = append(files, ExternalExtractorRequestFile{ + Path: path, + ContentSHA256: astfacts.ContentHash([]byte(content)), + Content: content, + }) + } + + response, err := extractor.Extract(context.Background(), files) + if err != nil { + t.Fatalf("extract through real PurRDF helper: %v", err) + } + expectedLanguages := map[string]string{ + "model.ttl": astfacts.LanguageTurtle, + "model.trig": astfacts.LanguageTriG, + "model.nt": astfacts.LanguageNTriples, + "model.nq": astfacts.LanguageNQuads, + "query.rq": astfacts.LanguageSPARQL, + } + if len(response.Results) != len(expectedLanguages) { + t.Fatalf( + "real helper results = %d, want %d", + len(response.Results), + len(expectedLanguages), + ) + } + for _, result := range response.Results { + if result.Language != expectedLanguages[result.Path] || result.Status != "ok" || + len(result.Facts) == 0 { + t.Errorf("real helper result = %#v", result) + } + } + + root := initSourceV2TestRepository(t) + for path, content := range sources { + writeSourceV2TestFile(t, root, path, content) + } + runSourceV2TestGit(t, root, "add", ".") + runSourceV2TestGit(t, root, "commit", "-m", "PurRDF fixtures") + receipt, err := SyncSourceIndex(context.Background(), root, extractor) + if err != nil { + t.Fatalf("sync source generation through real PurRDF helper: %v", err) + } + if receipt.SourceReadiness.Status != SourceStatusExact { + t.Fatalf("real PurRDF source readiness = %#v", receipt.SourceReadiness) + } + for _, language := range expectedLanguages { + coverage := sourceV2CoverageForLanguage(receipt.SourceReadiness.Coverage, language) + if coverage.Eligible != 1 || coverage.Indexed != 1 { + t.Errorf("real PurRDF %s coverage = %#v", language, coverage) + } + } +} + +func TestSyncSourceIndexFailsClosedWithoutPurRDFExtractor(t *testing.T) { + t.Parallel() + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "model.ttl", "

.\n") + runSourceV2TestGit(t, root, "add", "model.ttl") + runSourceV2TestGit(t, root, "commit", "-m", "rdf") + + _, err := SyncSourceIndex(context.Background(), root, nil) + if !errors.Is(err, ErrExternalExtractorRequired) { + t.Fatalf("sync without PurRDF extractor error = %v", err) + } + if _, statErr := os.Stat( + mustSourceV2Layout(t, root).statusPath(), + ); !errors.Is( + statErr, + os.ErrNotExist, + ) { + t.Fatalf("failed sync published a current receipt: %v", statErr) + } +} + +func TestSyncSourceIndexDoesNotRequirePurRDFForBuiltInLanguages(t *testing.T) { + t.Parallel() + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "main.rs", "fn main() {}\n") + runSourceV2TestGit(t, root, "add", "main.rs") + runSourceV2TestGit(t, root, "commit", "-m", "rust") + + receipt, err := SyncSourceIndex(context.Background(), root, nil) + if err != nil { + t.Fatalf("sync built-in language without PurRDF extractor: %v", err) + } + if receipt.SourceReadiness.Status != SourceStatusExact { + t.Fatalf("built-in-only source readiness = %#v", receipt.SourceReadiness) + } +} + +func TestSyncSourceIndexRejectsWrongPurRDFHelperIdentity(t *testing.T) { + t.Parallel() + + directory := t.TempDir() + script := filepath.Join(directory, "coding-ethos-purrdf-extractor") + writeSourceV2Executable(t, script, `#!/usr/bin/env python3 +import json, sys +request = json.load(sys.stdin) +json.dump({ + "protocol": "coding-ethos.external-extractor.v1", + "request_id": request["request_id"], + "extractor": { + "name": "purrdf", + "version": "1", + "purrdf_revision": "wrong" + }, + "results": [] +}, sys.stdout) +`) + extractor, err := NewCommandBatchExtractor(script) + if err != nil { + t.Fatalf("construct wrong-identity extractor: %v", err) + } + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "model.ttl", "

.\n") + runSourceV2TestGit(t, root, "add", "model.ttl") + runSourceV2TestGit(t, root, "commit", "-m", "rdf") + + _, err = SyncSourceIndex(context.Background(), root, extractor) + if !errors.Is(err, ErrExternalExtractorRequired) || + !strings.Contains(err.Error(), "unexpected PurRDF extractor identity") { + t.Fatalf("wrong-identity sync error = %v", err) + } + if _, statErr := os.Stat( + mustSourceV2Layout(t, root).statusPath(), + ); !errors.Is( + statErr, + os.ErrNotExist, + ) { + t.Fatalf("wrong-identity sync published a current receipt: %v", statErr) + } +} + +func TestRepositoryIDIsStableAcrossEquivalentClones(t *testing.T) { + t.Parallel() + + seed := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, seed, "app.go", "package app\n") + runSourceV2TestGit(t, seed, "add", "app.go") + runSourceV2TestGit(t, seed, "commit", "-m", "root") + + remoteParent := t.TempDir() + remote := filepath.Join(remoteParent, "canonical.git") + runSourceV2TestGit(t, remoteParent, "init", "--quiet", "--bare", remote) + runSourceV2TestGit(t, seed, "remote", "add", "origin", remote) + runSourceV2TestGit(t, seed, "push", "--quiet", "origin", "HEAD:refs/heads/main") + + cloneParent := t.TempDir() + firstRoot := filepath.Join(cloneParent, "first") + secondRoot := filepath.Join(cloneParent, "second") + runSourceV2TestGit( + t, + cloneParent, + "clone", + "--quiet", + "--branch", + "main", + remote, + firstRoot, + ) + runSourceV2TestGit( + t, + cloneParent, + "clone", + "--quiet", + "--branch", + "main", + remote, + secondRoot, + ) + + first := mustSourceV2Layout(t, firstRoot) + second := mustSourceV2Layout(t, secondRoot) + if first.repositoryID != second.repositoryID { + t.Fatalf( + "clone repository IDs differ: %q != %q", + first.repositoryID, + second.repositoryID, + ) + } + if first.worktreeID == second.worktreeID { + t.Fatalf("clone worktree IDs unexpectedly match: %q", first.worktreeID) + } +} + +func TestRepositoryIDUsesDocumentedLocalFallback(t *testing.T) { + t.Parallel() + + root := initSourceV2TestRepository(t) + writeSourceV2TestFile(t, root, "app.go", "package app\n") + runSourceV2TestGit(t, root, "add", "app.go") + runSourceV2TestGit(t, root, "commit", "-m", "root") + + layout := mustSourceV2Layout(t, root) + rootCommit, err := sourceV2GitOutput( + context.Background(), + root, + "rev-list", + "--max-parents=0", + "HEAD", + ) + if err != nil { + t.Fatalf("read root commit: %v", err) + } + expected := RepositoryID(sourceV2Digest( + "repository", + "local-common-dir:"+filepath.ToSlash(layout.commonGitDir)+"\x00"+rootCommit, + )) + if layout.repositoryID != expected { + t.Fatalf("local repository ID = %q, want %q", layout.repositoryID, expected) + } +} + +func TestNormalizeSourceV2Origin(t *testing.T) { + t.Parallel() + + root := t.TempDir() + tests := map[string]string{ + "git@EXAMPLE.com:org/repo.git": "ssh://example.com/org/repo", + "ssh://other:secret@EXAMPLE.com:22/org/repo.git/": "ssh://example.com/org/repo", + "https://user:secret@EXAMPLE.com:443/org/repo.git": "https://example.com/org/repo", + } + for input, expected := range tests { + actual, err := normalizeSourceV2Origin(root, input) + if err != nil { + t.Fatalf("normalize %q: %v", input, err) + } + if actual != expected { + t.Errorf("normalize %q = %q, want %q", input, actual, expected) + } + } +} + +func sourceV2CoverageForLanguage( + coverage []LanguageCoverage, + language string, +) LanguageCoverage { + for _, item := range coverage { + if item.Language == language { + return item + } + } + + return LanguageCoverage{} +} + +func initSourceV2TestRepository(t testing.TB) string { + t.Helper() + + root := t.TempDir() + runSourceV2TestGit(t, root, "init", "--quiet") + runSourceV2TestGit(t, root, "config", "user.email", "test@example.test") + runSourceV2TestGit(t, root, "config", "user.name", "Code Intel Test") + + return root +} + +func mustSourceV2Layout(t *testing.T, root string) sourceV2Layout { + t.Helper() + + layout, err := resolveSourceV2Layout(context.Background(), root) + if err != nil { + t.Fatalf("resolve v2 layout: %v", err) + } + + return layout +} + +func writeSourceV2TestFile(t testing.TB, root, relative, contents string) { + t.Helper() + + path := filepath.Join(root, filepath.FromSlash(relative)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("create test source directory: %v", err) + } + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatalf("write test source %s: %v", relative, err) + } +} + +func writeSourceV2Executable(t testing.TB, path, contents string) { + t.Helper() + + if err := os.WriteFile(path, []byte(contents), 0o700); err != nil { + t.Fatalf("write test executable: %v", err) + } +} + +func runSourceV2TestGit(t testing.TB, root string, arguments ...string) { + t.Helper() + + command := exec.Command("git", arguments...) + command.Dir = root + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", arguments, err, output) + } +} + +func BenchmarkSourceV2WarmSync(b *testing.B) { + root := initSourceV2TestRepository(b) + writeSourceV2TestFile(b, root, "app.go", "package app\n\nfunc Current() {}\n") + runSourceV2TestGit(b, root, "add", "app.go") + runSourceV2TestGit(b, root, "commit", "-m", "base") + + if _, err := SyncSourceIndex(context.Background(), root, nil); err != nil { + b.Fatalf("prime source index: %v", err) + } + + b.ResetTimer() + for range b.N { + if _, err := SyncSourceIndex(context.Background(), root, nil); err != nil { + b.Fatalf("warm source sync: %v", err) + } + } +} + +func TestExternalExtractorResponseRejectsWrongRevision(t *testing.T) { + t.Parallel() + + files := []ExternalExtractorRequestFile{{ + Path: "model.ttl", + ContentSHA256: "abc", + Content: "", + }} + request := ExternalExtractorRequest{ + Protocol: astfacts.PurrdfExtractorProtocol, + RequestID: externalExtractorRequestID(files), + Files: files, + } + response := ExternalExtractorResponse{ + Protocol: request.Protocol, + RequestID: request.RequestID, + Extractor: ExternalExtractorIdentity{ + Name: astfacts.PurrdfExtractorName, + Version: "1", + PurrdfRevision: "wrong", + }, + } + if err := validateExternalExtractorResponse(request, response); err == nil { + payload, _ := json.Marshal(response) + t.Fatalf("wrong extractor revision accepted: %s", payload) + } +} diff --git a/go/internal/codeintel/source_v2_types.go b/go/internal/codeintel/source_v2_types.go new file mode 100644 index 00000000..710cd808 --- /dev/null +++ b/go/internal/codeintel/source_v2_types.go @@ -0,0 +1,273 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintel + +import ( + "encoding/json" + + "blackcat.ca/coding-ethos/go/internal/astfacts" +) + +const ( + SourceV2Contract = "coding-ethos.code-intel/v2" + + SourceStatusExact = "exact" + SourceStatusFailed = "failed" + SourceStatusMissing = "missing" + SourceStatusPartial = "partial" + SourceStatusStale = "stale" + + sourceV2EntryStatusIndexed = "indexed" + + VectorStatusNotEvaluated = "not_evaluated" + VectorStatusPartial = "partial" + VectorStatusReady = "ready" + + sourceV2BaseManifestKind = "coding-ethos.code-intel.base-manifest/v2" + sourceV2DeltaManifestKind = "coding-ethos.code-intel.delta-manifest/v2" + sourceV2FragmentKind = "coding-ethos.code-intel.fragment/v2" + sourceV2StatusKind = "coding-ethos.code-intel.status/v2" + sourceV2SyncKind = "coding-ethos.code-intel.sync/v2" +) + +// RepositoryID identifies one origin authority and repository root-commit set. +type RepositoryID string + +// SourceSnapshotID identifies exact source bytes plus extractor and config inputs. +type SourceSnapshotID string + +// GenerationID identifies one immutable base-plus-delta code-intel generation. +type GenerationID string + +// SourceIdentity records every deterministic input to a source generation. +type SourceIdentity struct { + RepositoryID RepositoryID `json:"repository_id"` + SourceSnapshotID SourceSnapshotID `json:"source_snapshot_id"` + GenerationID GenerationID `json:"generation_id"` + HeadOID string `json:"head_oid"` + WorktreeID string `json:"worktree_id"` + WorktreeFingerprint string `json:"worktree_fingerprint"` + ConfigHash string `json:"config_hash"` + ExtractorSetHash string `json:"extractor_set_hash"` +} + +// LanguageCoverage accounts for every recognized source file in one language. +type LanguageCoverage struct { + Language string `json:"language"` + Eligible int `json:"eligible"` + Indexed int `json:"indexed"` + CacheHits int `json:"cache_hits"` + Unsupported int `json:"unsupported"` + Excluded int `json:"excluded"` + Oversized int `json:"oversized"` + Failed int `json:"failed"` + Stale int `json:"stale"` +} + +// SourceReadiness separates exact source coverage from vector availability. +type SourceReadiness struct { + Identity SourceIdentity `json:"identity"` + Status string `json:"status"` + Coverage []LanguageCoverage `json:"coverage"` + Reasons []string `json:"reasons,omitempty"` +} + +// VectorReadiness reports derived embedding coverage independently of source facts. +type VectorReadiness struct { + Status string `json:"status"` + ReadyRecords int `json:"ready_records"` + MissingVectors int `json:"missing_vectors"` +} + +// SourceV2Storage locates immutable shared products and the lane-local generation. +type SourceV2Storage struct { + SharedRoot string `json:"shared_root"` + LaneRoot string `json:"lane_root"` + BaseManifestID string `json:"base_manifest_id,omitempty"` + DeltaManifestID string `json:"delta_manifest_id,omitempty"` +} + +// SourceStatusReceipt is the stable machine-readable status contract. +type SourceStatusReceipt struct { + SourceReadiness SourceReadiness `json:"source_readiness"` + Storage SourceV2Storage `json:"storage"` + Contract string `json:"contract"` + Kind string `json:"kind"` + GeneratedAtUTC string `json:"generated_at_utc"` + Repair string `json:"repair"` + VectorReadiness VectorReadiness `json:"vector_readiness"` +} + +// SourceSyncSummary counts the observable work performed by a v2 sync. +type SourceSyncSummary struct { + FilesIndexed int `json:"files_indexed"` + FilesFailed int `json:"files_failed"` + FragmentsReused int `json:"fragments_reused"` + FragmentsWritten int `json:"fragments_written"` +} + +// SourceSyncReceipt is the stable machine-readable sync contract. +type SourceSyncReceipt struct { + Legacy *MaintenanceSummary `json:"legacy_v1,omitempty"` + SourceReadiness SourceReadiness `json:"source_readiness"` + Storage SourceV2Storage `json:"storage"` + Contract string `json:"contract"` + Kind string `json:"kind"` + GeneratedAtUTC string `json:"generated_at_utc"` + Repair string `json:"repair"` + Warnings []string `json:"warnings,omitempty"` + VectorReadiness VectorReadiness `json:"vector_readiness"` + Sync SourceSyncSummary `json:"sync"` +} + +type sourceV2ManifestEntry struct { + Path string `json:"path"` + Language string `json:"language"` + ContentSHA256 string `json:"content_sha256"` + FragmentID string `json:"fragment_id,omitempty"` + Status string `json:"status"` + ExtractorFingerprint string `json:"extractor_fingerprint"` +} + +type sourceV2BaseManifest struct { + Contract string `json:"contract"` + Kind string `json:"kind"` + ManifestID string `json:"manifest_id"` + RepositoryID RepositoryID `json:"repository_id"` + HeadOID string `json:"head_oid"` + ConfigHash string `json:"config_hash"` + ExtractorSetHash string `json:"extractor_set_hash"` + Entries []sourceV2ManifestEntry `json:"entries"` +} + +type sourceV2DeltaManifest struct { + Contract string `json:"contract"` + Kind string `json:"kind"` + ManifestID string `json:"manifest_id"` + RepositoryID RepositoryID `json:"repository_id"` + WorktreeID string `json:"worktree_id"` + HeadOID string `json:"head_oid"` + BaseManifestID string `json:"base_manifest_id"` + ConfigHash string `json:"config_hash"` + ExtractorSetHash string `json:"extractor_set_hash"` + Entries []sourceV2ManifestEntry `json:"entries"` + Tombstones []string `json:"tombstones"` +} + +// SourceSymbolFact is a path-neutral structural fact in an immutable fragment. +type SourceSymbolFact struct { + Provenance SourceFactProvenance `json:"provenance"` + SymbolKind string `json:"symbol_kind"` + SymbolName string `json:"symbol_name"` + SymbolPath string `json:"symbol_path"` + NodeKind string `json:"node_kind"` + Confidence string `json:"confidence"` + BaseNames []string `json:"base_names,omitempty"` + CallNames []string `json:"call_names,omitempty"` + ReferencedNames []string `json:"referenced_names,omitempty"` + StartByte int `json:"start_byte"` + EndByte int `json:"end_byte"` + StartLine int `json:"start_line"` + EndLine int `json:"end_line"` +} + +// SourceImportFact is a path-neutral import fact in an immutable fragment. +type SourceImportFact struct { + Target string `json:"target"` + RawText string `json:"raw_text"` + Confidence string `json:"confidence"` + Provenance SourceFactProvenance `json:"provenance"` +} + +// SourceFactProvenance records the exact deterministic extractor for a fact. +type SourceFactProvenance struct { + Class string `json:"class"` + Parser string `json:"parser"` + ParserRevision string `json:"parser_revision"` + SpanFidelity string `json:"span_fidelity"` +} + +type sourceV2Fragment struct { + Extractor astfacts.ExtractorDescriptor `json:"extractor"` + Contract string `json:"contract"` + Kind string `json:"kind"` + FragmentID string `json:"fragment_id"` + Language string `json:"language"` + ContentSHA256 string `json:"content_sha256"` + Symbols []SourceSymbolFact `json:"symbols,omitempty"` + Imports []SourceImportFact `json:"imports,omitempty"` + Facts []ExternalExtractorFact `json:"facts,omitempty"` + LineCount int `json:"line_count"` +} + +// ExternalExtractorRequest is the exact PurRDF batch-extractor request envelope. +type ExternalExtractorRequest struct { + Protocol string `json:"protocol"` + RequestID string `json:"request_id"` + Files []ExternalExtractorRequestFile `json:"files"` +} + +// ExternalExtractorRequestFile carries one UTF-8 source document in a batch. +type ExternalExtractorRequestFile struct { + Path string `json:"path"` + ContentSHA256 string `json:"content_sha256"` + Content string `json:"content"` + BaseIRI string `json:"base_iri,omitempty"` +} + +// ExternalExtractorIdentity identifies the implementation behind a response. +type ExternalExtractorIdentity struct { + Name string `json:"name"` + Version string `json:"version"` + PurrdfRevision string `json:"purrdf_revision"` +} + +// ExternalExtractorPosition is a 1-based line/column and 0-based byte offset. +type ExternalExtractorPosition struct { + ByteOffset int `json:"byte_offset"` + Line int `json:"line"` + Column int `json:"column"` +} + +// ExternalExtractorProvenance makes parser identity and span fidelity explicit. +type ExternalExtractorProvenance struct { + Start *ExternalExtractorPosition `json:"start,omitempty"` + Class string `json:"class"` + Parser string `json:"parser"` + ParserRevision string `json:"parser_revision"` + SpanFidelity string `json:"span_fidelity"` + SourcePath string `json:"source_path"` +} + +// ExternalExtractorFact is one deterministic semantic fact from PurRDF. +type ExternalExtractorFact struct { + Provenance ExternalExtractorProvenance `json:"provenance"` + Attributes map[string]string `json:"attributes,omitempty"` + ID string `json:"id"` + Kind string `json:"kind"` + Subject json.RawMessage `json:"subject,omitempty"` + Predicate json.RawMessage `json:"predicate,omitempty"` + Object json.RawMessage `json:"object,omitempty"` + Graph json.RawMessage `json:"graph,omitempty"` + Value json.RawMessage `json:"value,omitempty"` +} + +// ExternalExtractorResult is one file result; per-file errors keep batch exit zero. +type ExternalExtractorResult struct { + Path string `json:"path"` + ContentSHA256 string `json:"content_sha256"` + Language string `json:"language"` + Status string `json:"status"` + DocumentKind string `json:"document_kind"` + Error string `json:"error,omitempty"` + Facts []ExternalExtractorFact `json:"facts"` +} + +// ExternalExtractorResponse is the exact PurRDF batch-extractor response envelope. +type ExternalExtractorResponse struct { + Protocol string `json:"protocol"` + RequestID string `json:"request_id"` + Extractor ExternalExtractorIdentity `json:"extractor"` + Results []ExternalExtractorResult `json:"results"` +} diff --git a/go/internal/codeintelcli/main.go b/go/internal/codeintelcli/main.go index c2bf24a7..a7081eeb 100644 --- a/go/internal/codeintelcli/main.go +++ b/go/internal/codeintelcli/main.go @@ -91,6 +91,7 @@ func commandHandlers() map[string]codeIntelCommand { "record-hook-review": recordHookReview, "record-proxy-event": recordProxyEvent, "record-outcome": recordOutcome, + "rebuild-derived": rebuildDerived, "rebuild-index": rebuildIndex, "remediation-effectiveness": printRemediationEffectiveness, "remediation-outcomes": printRemediationOutcomes, @@ -104,7 +105,9 @@ func commandHandlers() map[string]codeIntelCommand { "session-snapshot": printSessionSnapshot, "skill-health": printSkillHealth, "stats": printStats, + "status": printSourceIndexStatus, "surprises": printSurprises, + "sync": syncSourceIndex, tokenEconomyCommand: tokenEconomy, "upsert-vector": upsertVector, "vector-stats": printVectorStats, @@ -804,30 +807,6 @@ func legacyDownstreamAnalysis( return analysis, nil } -func rebuildIndex(ctx context.Context, args []string) error { - flags := flag.NewFlagSet("rebuild-index", flag.ExitOnError) - root := flags.String("root", ".", "Repository root containing .coding-ethos") - dbPath := flags.String("db", "", "Legacy DuckDB code intelligence database path") - duckDBPath := flags.String("duckdb", "", "DuckDB code intelligence database path") - - err := parseCommandFlags(flags, args, "rebuild-index") - if err != nil { - return err - } - - summary, err := codeintel.RebuildDuckDBIndex( - ctx, - *root, - resolvedDuckDBPath(*root, *duckDBPath), - resolvedDBPath(*root, *dbPath), - ) - if err != nil { - return fmt.Errorf("rebuild DuckDB code intelligence index: %w", err) - } - - return encodeJSON(os.Stdout, summary) -} - func printRepeatedFailures(ctx context.Context, args []string) error { flags := flag.NewFlagSet("repeated-failures", flag.ExitOnError) storeFlags := addStoreFlags(flags, "Repository root containing .coding-ethos") @@ -951,7 +930,8 @@ func printVectorStats(ctx context.Context, args []string) error { return fmt.Errorf("open vector index: %w", err) } - if closer, ok := index.(interface{ Close() error }); ok { + closer, ok := index.(interface{ Close() error }) + if ok { defer closer.Close() } @@ -1135,6 +1115,20 @@ func printIndexStatus(ctx context.Context, args []string) error { return fmt.Errorf("read index status: %w", err) } + sourceStatus, sourceStatusErr := codeintel.SourceIndexStatus(ctx, *root) + if sourceStatusErr == nil { + sourceStatus.VectorReadiness = codeintel.VectorReadiness{ + Status: codeintel.VectorStatusReady, + ReadyRecords: status.ReadyRecords, + MissingVectors: status.MissingVectors, + } + if status.MissingVectors > 0 { + sourceStatus.VectorReadiness.Status = codeintel.VectorStatusPartial + } + + status.V2 = &sourceStatus + } + return encodeJSON(os.Stdout, status) } diff --git a/go/internal/codeintelcli/source_v2.go b/go/internal/codeintelcli/source_v2.go new file mode 100644 index 00000000..6173ec00 --- /dev/null +++ b/go/internal/codeintelcli/source_v2.go @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintelcli + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + + "blackcat.ca/coding-ethos/go/internal/codeintel" +) + +type rebuildDerivedReceipt struct { + Contract string `json:"contract"` + Kind string `json:"kind"` + DeprecatedCommand string `json:"deprecated_command,omitempty"` + Replacement string `json:"replacement,omitempty"` + Summary codeintel.RebuildIndexSummary `json:"summary"` +} + +func syncSourceIndex(ctx context.Context, args []string) error { + flags := flag.NewFlagSet("sync", flag.ExitOnError) + root := flags.String("root", ".", "Repository worktree root") + + err := parseCommandFlags(flags, args, "sync") + if err != nil { + return err + } + + var external codeintel.ExternalBatchExtractor + + extractor, extractorErr := codeintel.DefaultExternalBatchExtractor() + if extractorErr == nil { + external = extractor + } + + receipt, err := codeintel.SyncSourceIndex(ctx, *root, external) + if err != nil { + if extractorErr != nil && errors.Is(err, codeintel.ErrExternalExtractorRequired) { + return fmt.Errorf( + "sync code-intel source generation: %w: %w", + err, + extractorErr, + ) + } + + return fmt.Errorf("sync code-intel source generation: %w", err) + } + + return encodeJSON(os.Stdout, receipt) +} + +func printSourceIndexStatus(ctx context.Context, args []string) error { + flags := flag.NewFlagSet("status", flag.ExitOnError) + root := flags.String("root", ".", "Repository worktree root") + + err := parseCommandFlags(flags, args, "status") + if err != nil { + return err + } + + receipt, err := codeintel.SourceIndexStatus(ctx, *root) + if err != nil { + return fmt.Errorf("read code-intel source status: %w", err) + } + + return encodeJSON(os.Stdout, receipt) +} + +func rebuildDerived(ctx context.Context, args []string) error { + return runRebuildDerived(ctx, args, "rebuild-derived", false) +} + +func rebuildIndex(ctx context.Context, args []string) error { + return runRebuildDerived(ctx, args, "rebuild-index", true) +} + +func runRebuildDerived( + ctx context.Context, + args []string, + commandName string, + deprecated bool, +) error { + flags := flag.NewFlagSet(commandName, flag.ExitOnError) + root := flags.String("root", ".", "Repository root containing .coding-ethos") + dbPath := flags.String("db", "", "Legacy DuckDB code intelligence database path") + duckDBPath := flags.String( + "duckdb", + "", + "Derived DuckDB code intelligence database path", + ) + + err := parseCommandFlags(flags, args, commandName) + if err != nil { + return err + } + + summary, err := codeintel.RebuildDuckDBIndex( + ctx, + *root, + resolvedDuckDBPath(*root, *duckDBPath), + resolvedDBPath(*root, *dbPath), + ) + if err != nil { + return fmt.Errorf("rebuild derived DuckDB code intelligence index: %w", err) + } + + receipt := rebuildDerivedReceipt{ + Contract: "coding-ethos.code-intel/v2", + Kind: "coding-ethos.code-intel.rebuild-derived/v2", + Summary: summary, + } + if deprecated { + receipt.DeprecatedCommand = "rebuild-index" + receipt.Replacement = "rebuild-derived" + } + + return encodeJSON(os.Stdout, receipt) +} diff --git a/go/internal/codeintelcli/source_v2_test.go b/go/internal/codeintelcli/source_v2_test.go new file mode 100644 index 00000000..a5dbaeb9 --- /dev/null +++ b/go/internal/codeintelcli/source_v2_test.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package codeintelcli + +import ( + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "testing" + + "blackcat.ca/coding-ethos/go/internal/codeintel" +) + +func TestSyncAndStatusCommandsEmitTypedV2Receipts(t *testing.T) { + root := t.TempDir() + runSourceV2CLIGit(t, root, "init", "--quiet") + runSourceV2CLIGit(t, root, "config", "user.email", "test@example.test") + runSourceV2CLIGit(t, root, "config", "user.name", "Code Intel CLI Test") + if err := os.WriteFile( + filepath.Join(root, "app.go"), + []byte("package app\n\nfunc Current() {}\n"), + 0o600, + ); err != nil { + t.Fatalf("write source: %v", err) + } + runSourceV2CLIGit(t, root, "add", "app.go") + runSourceV2CLIGit(t, root, "commit", "-m", "base") + + var runErr error + syncOutput := captureStdout(t, func() { + runErr = run(context.Background(), []string{"sync", "--root", root}) + }) + if runErr != nil { + t.Fatalf("sync command: %v", runErr) + } + + var syncReceipt codeintel.SourceSyncReceipt + if err := json.Unmarshal([]byte(syncOutput), &syncReceipt); err != nil { + t.Fatalf("decode sync receipt: %v\n%s", err, syncOutput) + } + if syncReceipt.Contract != codeintel.SourceV2Contract || + syncReceipt.Kind != "coding-ethos.code-intel.sync/v2" || + syncReceipt.SourceReadiness.Status != codeintel.SourceStatusExact || + syncReceipt.Repair != "coding-ethos-code-intel sync --root ." { + t.Fatalf("sync receipt = %#v", syncReceipt) + } + + statusOutput := captureStdout(t, func() { + runErr = run(context.Background(), []string{"status", "--root", root}) + }) + if runErr != nil { + t.Fatalf("status command: %v", runErr) + } + + var statusReceipt codeintel.SourceStatusReceipt + if err := json.Unmarshal([]byte(statusOutput), &statusReceipt); err != nil { + t.Fatalf("decode status receipt: %v\n%s", err, statusOutput) + } + if statusReceipt.Contract != codeintel.SourceV2Contract || + statusReceipt.Kind != "coding-ethos.code-intel.status/v2" || + statusReceipt.SourceReadiness.Identity.GenerationID != + syncReceipt.SourceReadiness.Identity.GenerationID { + t.Fatalf("status receipt = %#v", statusReceipt) + } +} + +func TestRebuildIndexIsDeprecatedAliasForRebuildDerived(t *testing.T) { + root := t.TempDir() + duckDBPath := filepath.Join(root, ".coding-ethos", "derived.duckdb") + + var runErr error + output := captureStdout(t, func() { + runErr = run(context.Background(), []string{ + "rebuild-index", + "--root", root, + "--duckdb", duckDBPath, + }) + }) + if runErr != nil { + t.Fatalf("deprecated rebuild alias: %v", runErr) + } + + var receipt rebuildDerivedReceipt + if err := json.Unmarshal([]byte(output), &receipt); err != nil { + t.Fatalf("decode rebuild receipt: %v\n%s", err, output) + } + if receipt.Contract != codeintel.SourceV2Contract || + receipt.DeprecatedCommand != "rebuild-index" || + receipt.Replacement != "rebuild-derived" { + t.Fatalf("rebuild alias receipt = %#v", receipt) + } +} + +func runSourceV2CLIGit(t *testing.T, root string, arguments ...string) { + t.Helper() + + command := exec.Command("git", arguments...) + command.Dir = root + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", arguments, err, output) + } +} diff --git a/go/internal/hooks/code_intel_enrichment.go b/go/internal/hooks/code_intel_enrichment.go index a7c92d00..1095aa86 100644 --- a/go/internal/hooks/code_intel_enrichment.go +++ b/go/internal/hooks/code_intel_enrichment.go @@ -27,7 +27,7 @@ const ( defaultCodeIntelEnrichmentMaxEdges = 4 defaultCodeIntelEnrichmentMaxFailures = 4 defaultCodeIntelEnrichmentMaxOutputPaths = 12 - codeIntelEnrichmentRefreshCommand = "coding-ethos-code-intel rebuild-index" + codeIntelEnrichmentRefreshCommand = "coding-ethos-code-intel sync --root ." codeIntelTimestampSQLLayout = "2006-01-02 15:04:05" unknownValue = "unknown" codeIntelFreshnessStateDirMode = 0o700 @@ -50,6 +50,7 @@ type codeIntelEnrichmentOptions struct { } type codeIntelEnrichment struct { + V2 *codeintel.SourceStatusReceipt Status string Refresh string Reason string @@ -224,16 +225,23 @@ func buildCodeIntelEnrichment( ) defer cancel() + sourceStatus, sourceStatusErr := codeintel.SourceIndexStatus(ctx, root) + store, err := codeintel.OpenReadOnly( ctx, codeintel.DefaultDBPath(codeintel.ResolveStateRoot(root)), ) if err != nil { - return codeIntelEnrichment{ + enrichment := codeIntelEnrichment{ Status: codeIntelStatusMissingIndex, Refresh: codeIntelEnrichmentRefreshCommand, Reason: "code-intel index is not available", } + if sourceStatusErr == nil { + enrichment.V2 = &sourceStatus + } + + return enrichment } defer store.Close() @@ -241,6 +249,9 @@ func buildCodeIntelEnrichment( Status: codeIntelIndexStatus(ctx, store), Refresh: codeIntelEnrichmentRefreshCommand, } + if sourceStatusErr == nil { + enrichment.V2 = &sourceStatus + } paths := enrichmentTargetPaths(event, root, toolOutput, options.MaxPaths) for _, path := range paths { @@ -904,6 +915,8 @@ func renderCodeIntelEnrichment(enrichment codeIntelEnrichment) string { lines = append(lines, "reason: "+toonCell(enrichment.Reason)) } + lines = appendCodeIntelV2Metadata(lines, enrichment.V2) + lines = appendEnrichmentPaths(lines, enrichment.Paths) lines = appendEnrichmentSymbols(lines, enrichment.Symbols) lines = appendEnrichmentRelated(lines, enrichment.Related) @@ -913,6 +926,27 @@ func renderCodeIntelEnrichment(enrichment codeIntelEnrichment) string { return strings.Join(lines, "\n") } +func appendCodeIntelV2Metadata( + lines []string, + receipt *codeintel.SourceStatusReceipt, +) []string { + if receipt == nil { + return lines + } + + identity := receipt.SourceReadiness.Identity + + return append(lines, + "coding-ethos.code-intel/v2:", + " source_status: "+toonCell(receipt.SourceReadiness.Status), + " repository_id: "+toonCell(string(identity.RepositoryID)), + " source_snapshot_id: "+toonCell(string(identity.SourceSnapshotID)), + " generation_id: "+toonCell(string(identity.GenerationID)), + " vector_status: "+toonCell(receipt.VectorReadiness.Status), + " repair: "+toonCell(receipt.Repair), + ) +} + func appendEnrichmentPaths( lines []string, paths []codeIntelEnrichmentPath, diff --git a/go/internal/hooks/lifecycle_context.go b/go/internal/hooks/lifecycle_context.go index 0bb2a38d..6b9127e9 100644 --- a/go/internal/hooks/lifecycle_context.go +++ b/go/internal/hooks/lifecycle_context.go @@ -216,7 +216,7 @@ func sessionStartStorageUpgradeContext(cwd string) string { feedback.S("storage", "code-intel"), feedback.S("status", "upgrade_failed"), feedback.S("reason", err.Error()), - feedback.S("repair", "Run coding-ethos-code-intel rebuild-index."), + feedback.S("repair", "Run coding-ethos-code-intel rebuild-derived."), }, }, feedback.FormatTOON) } diff --git a/go/internal/hooks/runner_test.go b/go/internal/hooks/runner_test.go index cee364e1..1dc32cef 100644 --- a/go/internal/hooks/runner_test.go +++ b/go/internal/hooks/runner_test.go @@ -5579,8 +5579,10 @@ func TestRunReportsStaleCodeIntelEnrichmentForModifiedReadTarget(t *testing.T) { for _, expected := range []string{ "code_intel_enrichment:", "status: stale", - "refresh: coding-ethos-code-intel rebuild-index", + "refresh: coding-ethos-code-intel sync --root .", "reason: stale code context; refresh required", + "coding-ethos.code-intel/v2:", + " source_status: missing", `code_intel_context_card {"path":"pkg/app.py"}`, } { if !strings.Contains(context, expected) { @@ -5641,11 +5643,22 @@ func TestRunAddsGoldenCodeIntelEnrichmentForGlobTool(t *testing.T) { if result.HookSpecificOutput == nil { t.Fatalf("missing code-intel enrichment output: %#v", result) } + v2, err := codeintel.SourceIndexStatus(context.Background(), repo) + if err != nil { + t.Fatalf("read expected v2 status: %v", err) + } expected := strings.Join([]string{ "code_intel_enrichment:", "status: ready", - "refresh: coding-ethos-code-intel rebuild-index", + "refresh: coding-ethos-code-intel sync --root .", + "coding-ethos.code-intel/v2:", + " source_status: missing", + " repository_id: " + string(v2.SourceReadiness.Identity.RepositoryID), + " source_snapshot_id: " + string(v2.SourceReadiness.Identity.SourceSnapshotID), + " generation_id: ", + " vector_status: not_evaluated", + " repair: coding-ethos-code-intel sync --root .", "paths[1]{path,language,lines,symbols,risk}:", " pkg/app.py,python,3,1,normal", "symbols[1]{path,symbol,line,signature}:", @@ -5734,7 +5747,9 @@ func TestRunReportsMissingCodeIntelIndexForSearchTool(t *testing.T) { for _, expected := range []string{ "code_intel_enrichment:", "status: missing_index", - "refresh: coding-ethos-code-intel rebuild-index", + "refresh: coding-ethos-code-intel sync --root .", + "coding-ethos.code-intel/v2:", + " source_status: missing", } { if !strings.Contains(context, expected) { t.Fatalf("missing-index context lacks %q:\n%s", expected, context) @@ -5860,7 +5875,7 @@ func TestRunDedupesCodeIntelFreshnessNoticeAfterCommit(t *testing.T) { firstContext := first.HookSpecificOutput.AdditionalContext if !strings.Contains(firstContext, "code_intel_freshness:") || !strings.Contains(firstContext, "status: stale") || - !strings.Contains(firstContext, "coding-ethos-code-intel rebuild-index") { + !strings.Contains(firstContext, "coding-ethos-code-intel sync --root .") { t.Fatalf("first commit missing freshness notice:\n%s", firstContext) } diff --git a/go/internal/mcp/code_intel.go b/go/internal/mcp/code_intel.go index b9df31d7..452735dd 100644 --- a/go/internal/mcp/code_intel.go +++ b/go/internal/mcp/code_intel.go @@ -41,20 +41,24 @@ const ( ) type codeIntelTaskMeta struct { - RepoHeadCommit string `json:"repo_head_commit,omitempty"` - IndexAge string `json:"index_age,omitempty"` - IndexedAtUTC string `json:"indexed_at_utc,omitempty"` - StaleWarning string `json:"stale_warning,omitempty"` - Compression string `json:"compression"` - DataSources []string `json:"data_sources"` - ReadyRecords int `json:"ready_records"` - MissingVectors int `json:"missing_vectors"` - IndexedFiles int `json:"indexed_files"` - IndexedChunks int `json:"indexed_chunks"` - SchemaVersion int `json:"schema_version"` - Fresh bool `json:"fresh"` - Truncated bool `json:"truncated"` -} + //nolint:tagliatelle // The public versioned key is frozen. + V2 *sourceStatusReceipt `json:"coding-ethos.code-intel/v2,omitempty"` + IndexAge string `json:"index_age,omitempty"` + IndexedAtUTC string `json:"indexed_at_utc,omitempty"` + StaleWarning string `json:"stale_warning,omitempty"` + Compression string `json:"compression"` + RepoHeadCommit string `json:"repo_head_commit,omitempty"` + DataSources []string `json:"data_sources"` + ReadyRecords int `json:"ready_records"` + IndexedFiles int `json:"indexed_files"` + IndexedChunks int `json:"indexed_chunks"` + SchemaVersion int `json:"schema_version"` + MissingVectors int `json:"missing_vectors"` + Fresh bool `json:"fresh"` + Truncated bool `json:"truncated"` +} + +type sourceStatusReceipt = codeintel.SourceStatusReceipt type codeIntelCitation struct { Kind string `json:"kind"` @@ -450,6 +454,8 @@ func (server Server) codeIntelIndexStatus(args json.RawMessage) (any, error) { return nil, fmt.Errorf("read code intelligence index status: %w", err) } + attachCodeIntelV2Status(ctx, server.codeIntelRoot(), &status) + return status, nil } @@ -1187,6 +1193,16 @@ func (server Server) codeIntelTaskMetaForRoot( IndexedChunks: status.Stats.CodeChunks, SchemaVersion: status.Stats.SchemaVersion, } + + sourceStatus, sourceStatusErr := codeintel.SourceIndexStatus(ctx, root) + if sourceStatusErr == nil { + sourceStatus.VectorReadiness = codeIntelV2VectorReadiness( + status.ReadyRecords, + status.MissingVectors, + ) + meta.V2 = &sourceStatus + } + if !meta.Fresh { meta.StaleWarning = "code-intel vectors are missing for some ready records" } @@ -1194,6 +1210,40 @@ func (server Server) codeIntelTaskMetaForRoot( return meta, nil } +func attachCodeIntelV2Status( + ctx context.Context, + root string, + status *codeintel.IndexStatus, +) { + sourceStatus, err := codeintel.SourceIndexStatus(ctx, root) + if err != nil { + return + } + + sourceStatus.VectorReadiness = codeIntelV2VectorReadiness( + status.ReadyRecords, + status.MissingVectors, + ) + status.V2 = &sourceStatus +} + +func codeIntelV2VectorReadiness( + readyRecords, missingVectors int, +) codeintel.VectorReadiness { + status := codeintel.VectorStatusNotEvaluated + if missingVectors > 0 { + status = codeintel.VectorStatusPartial + } else if readyRecords > 0 { + status = codeintel.VectorStatusReady + } + + return codeintel.VectorReadiness{ + Status: status, + ReadyRecords: readyRecords, + MissingVectors: missingVectors, + } +} + func indexAgeDescription(indexedAt string) string { if strings.TrimSpace(indexedAt) == "" { return "unknown" diff --git a/go/internal/mcp/code_intel_workspace_test.go b/go/internal/mcp/code_intel_workspace_test.go index 70887d38..beb9ca0d 100644 --- a/go/internal/mcp/code_intel_workspace_test.go +++ b/go/internal/mcp/code_intel_workspace_test.go @@ -4,6 +4,7 @@ package mcp import ( + "encoding/json" "errors" "os" "path/filepath" @@ -14,6 +15,59 @@ import ( "blackcat.ca/coding-ethos/go/internal/policy" ) +func TestCodeIntelTaskMetaPublishesAdditiveV2Receipt(t *testing.T) { + t.Parallel() + + meta := codeIntelTaskMeta{ + Compression: "bounded_json", + DataSources: []string{}, + V2: &codeintel.SourceStatusReceipt{ + Contract: "coding-ethos.code-intel/v2", + Kind: "coding-ethos.code-intel.status/v2", + SourceReadiness: codeintel.SourceReadiness{ + Status: codeintel.SourceStatusExact, + }, + }, + } + payload, err := json.Marshal(meta) + if err != nil { + t.Fatalf("marshal task metadata: %v", err) + } + for _, expected := range []string{ + `"coding-ethos.code-intel/v2"`, + `"contract":"coding-ethos.code-intel/v2"`, + `"status":"exact"`, + } { + if !strings.Contains(string(payload), expected) { + t.Fatalf("v2 MCP metadata missing %q: %s", expected, payload) + } + } +} + +func TestCodeIntelV2VectorReadinessKeepsUnevaluatedSeparate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + ready int + missing int + want string + }{ + {name: "no derived records", want: codeintel.VectorStatusNotEvaluated}, + {name: "missing vectors", ready: 2, missing: 1, want: codeintel.VectorStatusPartial}, + {name: "complete vectors", ready: 2, want: codeintel.VectorStatusReady}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := codeIntelV2VectorReadiness(test.ready, test.missing) + if actual.Status != test.want || actual.ReadyRecords != test.ready || + actual.MissingVectors != test.missing { + t.Fatalf("vector readiness = %#v, want status %q", actual, test.want) + } + }) + } +} + func TestCapWorkspaceSearchResultsRespectsLimit(t *testing.T) { t.Parallel() From 05e7aaa7149ce43a2049b4ce2af33da0ab7f506b Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 15:08:28 -0600 Subject: [PATCH 09/17] fix(build): fetch public PurRDF dependency over HTTPS --- rust/purrdf-code-intel-extractor/Cargo.lock | 18 +++++++++--------- rust/purrdf-code-intel-extractor/Cargo.toml | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/rust/purrdf-code-intel-extractor/Cargo.lock b/rust/purrdf-code-intel-extractor/Cargo.lock index 9643a141..8fcd0ec1 100644 --- a/rust/purrdf-code-intel-extractor/Cargo.lock +++ b/rust/purrdf-code-intel-extractor/Cargo.lock @@ -652,7 +652,7 @@ dependencies = [ [[package]] name = "purrdf-core" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" dependencies = [ "ahash", "csv", @@ -667,12 +667,12 @@ dependencies = [ [[package]] name = "purrdf-events" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" [[package]] name = "purrdf-gts" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" dependencies = [ "aes-gcm", "ahash", @@ -694,12 +694,12 @@ dependencies = [ [[package]] name = "purrdf-iri" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" [[package]] name = "purrdf-rdf" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" dependencies = [ "ahash", "ciborium", @@ -727,7 +727,7 @@ dependencies = [ [[package]] name = "purrdf-sparql-algebra" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" dependencies = [ "memchr", "purrdf-iri", @@ -738,7 +738,7 @@ dependencies = [ [[package]] name = "purrdf-sparql-eval" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" dependencies = [ "ahash", "getrandom 0.3.4", @@ -760,7 +760,7 @@ dependencies = [ [[package]] name = "purrdf-sparql-results" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" dependencies = [ "purrdf-core", "purrdf-iri", @@ -769,7 +769,7 @@ dependencies = [ [[package]] name = "purrdf-xsd" version = "0.12.0" -source = "git+ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" +source = "git+https://github.com/Blackcat-Informatics/purrdf.git?rev=3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6#3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" [[package]] name = "quote" diff --git a/rust/purrdf-code-intel-extractor/Cargo.toml b/rust/purrdf-code-intel-extractor/Cargo.toml index 944ab31f..d0ba2dc0 100644 --- a/rust/purrdf-code-intel-extractor/Cargo.toml +++ b/rust/purrdf-code-intel-extractor/Cargo.toml @@ -9,8 +9,8 @@ license = "AGPL-3.0-only" publish = false [dependencies] -purrdf-rdf = { git = "ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git", rev = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" } -purrdf-sparql-algebra = { git = "ssh://git@ssh.github.com:443/Blackcat-Informatics/purrdf.git", rev = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" } +purrdf-rdf = { git = "https://github.com/Blackcat-Informatics/purrdf.git", rev = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" } +purrdf-sparql-algebra = { git = "https://github.com/Blackcat-Informatics/purrdf.git", rev = "3aa4ba514ee9cdbfb6966bd0f1fedb87e0d8a0b6" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" sha2 = "0.10.9" From 2706a3b685ea4dee21c876ecbe7a16ffd33fd8c4 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 15:35:01 -0600 Subject: [PATCH 10/17] test(coverage): exercise integrated feature paths --- go/internal/codeintel/skill_health_test.go | 49 ++++++++ go/internal/codeintel/source_v2_test.go | 80 +++++++++++++ go/internal/tokeneconomy/report_test.go | 125 +++++++++++++++++++++ 3 files changed, 254 insertions(+) diff --git a/go/internal/codeintel/skill_health_test.go b/go/internal/codeintel/skill_health_test.go index 96ee8a4a..4b5acd2e 100644 --- a/go/internal/codeintel/skill_health_test.go +++ b/go/internal/codeintel/skill_health_test.go @@ -173,6 +173,55 @@ func TestStoreRecordsSkillObservationAsUnknownOutcome(t *testing.T) { } } +func TestFormatSkillHealthTOONSerializesSummaryAndEscapesCells(t *testing.T) { + t.Parallel() + + report := SkillHealthReport{ + Kind: "skill health", + GeneratedAtUTC: "2026-08-29T21:00:00Z", + PromotionPolicy: "promote: fixed", + Summary: SkillHealthSummary{ + Known: 7, + Observed: 6, + Unknown: 1, + Unused: 2, + FrequentlyFailing: 3, + Improving: 4, + Stale: 5, + }, + Skills: []SkillHealthRecord{{ + SkillID: "safe-git-workflow", + Status: "healthy", + Trend: "stable_success", + Total: 5, + SourcePath: "skills,safe-git\nworkflow", + Window7: SkillHealthWindow{Fixed: 1, Repeated: 2}, + Window30: SkillHealthWindow{Fixed: 3, Repeated: 4}, + }}, + } + + want := strings.Join([]string{ + "code_intel_skill_health:", + ` kind: "skill health"`, + ` generated_at_utc: "2026-08-29T21:00:00Z"`, + ` promotion_policy: "promote: fixed"`, + " known: 7", + " observed: 6", + " unknown: 1", + " unused: 2", + " frequently_failing: 3", + " improving: 4", + " stale: 5", + " skills[skill_id,status,trend,total,7d_fixed,7d_repeated," + + "30d_fixed,30d_repeated,last_used_utc,source_path]:", + ` safe-git-workflow,healthy,stable_success,5,1,2,3,4,"","skills,safe-git workflow"`, + }, "\n") + "\n" + + if got := FormatSkillHealthTOON(report); got != want { + t.Fatalf("skill health TOON mismatch:\n--- got ---\n%s--- want ---\n%s", got, want) + } +} + func recordSkillHealthOutcome( t *testing.T, ctx context.Context, diff --git a/go/internal/codeintel/source_v2_test.go b/go/internal/codeintel/source_v2_test.go index dfc3fc2b..f0b24fd0 100644 --- a/go/internal/codeintel/source_v2_test.go +++ b/go/internal/codeintel/source_v2_test.go @@ -714,3 +714,83 @@ func TestExternalExtractorResponseRejectsWrongRevision(t *testing.T) { t.Fatalf("wrong extractor revision accepted: %s", payload) } } + +func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { + t.Parallel() + + validTurtle := validExternalExtractorResult(astfacts.LanguageTurtle) + if err := validateExternalExtractorFacts(validTurtle); err != nil { + t.Fatalf("valid Turtle facts: %v", err) + } + validSPARQL := validExternalExtractorResult(astfacts.LanguageSPARQL) + if err := validateExternalExtractorFacts(validSPARQL); err != nil { + t.Fatalf("valid SPARQL facts: %v", err) + } + + unidentified := validExternalExtractorResult(astfacts.LanguageTurtle) + unidentified.Facts[0].ID = "" + duplicate := validExternalExtractorResult(astfacts.LanguageTurtle) + duplicate.Facts = append(duplicate.Facts, duplicate.Facts[0]) + wrongParser := validExternalExtractorResult(astfacts.LanguageTurtle) + wrongParser.Facts[0].Provenance.Parser = "wrong-parser" + wrongFidelity := validExternalExtractorResult(astfacts.LanguageTurtle) + wrongFidelity.Facts[0].Provenance.SpanFidelity = "none" + invalidStart := validExternalExtractorResult(astfacts.LanguageTurtle) + invalidStart.Facts[0].Provenance.Start = &ExternalExtractorPosition{ + ByteOffset: -1, + Line: 0, + Column: 0, + } + spanFreeStart := validExternalExtractorResult(astfacts.LanguageSPARQL) + spanFreeStart.Facts[0].Provenance.Start = &ExternalExtractorPosition{ + Line: 1, + Column: 1, + } + + for name, result := range map[string]ExternalExtractorResult{ + "unidentified": unidentified, + "duplicate": duplicate, + "wrong parser": wrongParser, + "wrong fidelity": wrongFidelity, + "invalid start": invalidStart, + "span-free start": spanFreeStart, + "unknown language": {Language: "python"}, + } { + if err := validateExternalExtractorFacts(result); err == nil { + t.Errorf("%s external facts unexpectedly passed", name) + } + } +} + +func validExternalExtractorResult(language string) ExternalExtractorResult { + parser := "purrdf-rdf" + spanFidelity := "subject_start" + start := &ExternalExtractorPosition{Line: 1, Column: 1} + if language == astfacts.LanguageSPARQL { + parser = "purrdf-sparql-algebra" + spanFidelity = "none" + start = nil + } + + path := "model.ttl" + if language == astfacts.LanguageSPARQL { + path = "query.rq" + } + + return ExternalExtractorResult{ + Path: path, + Language: language, + Facts: []ExternalExtractorFact{{ + ID: "fact:sha256:fixture", + Kind: "semantic", + Provenance: ExternalExtractorProvenance{ + Start: start, + Class: "EXTRACTED", + Parser: parser, + ParserRevision: astfacts.PurrdfExtractorRevision, + SpanFidelity: spanFidelity, + SourcePath: path, + }, + }}, + } +} diff --git a/go/internal/tokeneconomy/report_test.go b/go/internal/tokeneconomy/report_test.go index 3a1bd626..6335b99b 100644 --- a/go/internal/tokeneconomy/report_test.go +++ b/go/internal/tokeneconomy/report_test.go @@ -519,6 +519,131 @@ func TestWriteReportArtifactsIsCreateNewAndVerifiable(t *testing.T) { } } +func TestFormatReportMarkdownIncludesVerifiedContextAndComparisonQuality(t *testing.T) { + t.Parallel() + + report := Report{ + Cohort: "cohort-a", + GeneratedAtUTC: "2026-08-29T21:00:00Z", + Conclusion: ConclusionCausalSavings, + Causal: true, + Historical: &HistoricalMetrics{ + FromUTC: "2026-08-01T00:00:00Z", + ToUTC: "2026-09-01T00:00:00Z", + RawContextTokens: 1000, + DeliveredContextTokens: 600, + AvoidedContextTokens: 400, + GrossReductionPercent: 40, + Sources: []HistoricalSource{{ + Path: "evidence`store.duckdb", + SHA256After: "abc123", + }}, + }, + Comparisons: []Comparison{ + { + TreatmentArm: ArmFull, + ControlArm: ArmOff, + TaskCount: 2, + SavingsPercent: 12.5, + SavingsPercentInterval: Interval{Lower: 10, Upper: 15}, + ConfidenceLevelPercent: 95, + QualityNonInferior: true, + }, + { + TreatmentArm: ArmStatic, + ControlArm: ArmOff, + TaskCount: 1, + SavingsPercent: -2, + SavingsPercentInterval: Interval{Lower: -5, Upper: 1}, + ConfidenceLevelPercent: 90, + }, + }, + Coverage: Coverage{ + TaskCount: 3, + CompleteTaskCount: 2, + CompleteBlockCount: 4, + PartialBlockCount: 1, + RunCount: 9, + AcceptedRunCount: 8, + Reasons: []string{"one incomplete\nblock"}, + }, + } + + markdown := formatReportMarkdown(report) + for _, expected := range []string{ + "## Observational context reduction", + "- `evidence\\`store.duckdb`: `abc123` (unchanged)", + "| full | off | 2 | 12.50% | 10.00% to 15.00% | 95.00% | non-inferior |", + "| static | off | 1 | -2.00% | -5.00% to 1.00% | 90.00% | " + + "not established |", + "- one incomplete block", + } { + if !strings.Contains(markdown, expected) { + t.Fatalf("Markdown report missing %q:\n%s", expected, markdown) + } + } +} + +func TestReadRunMechanismsMeasuresFirstTransformsAndRepeatedAdvice(t *testing.T) { + t.Parallel() + + missing, err := readRunMechanisms( + context.Background(), + filepath.Join(t.TempDir(), "missing.duckdb"), + "session-1", + ) + if err != nil || missing != (MechanismMetrics{}) { + t.Fatalf("missing mechanism store = %#v, %v", missing, err) + } + + path := filepath.Join(t.TempDir(), "mechanisms.duckdb") + database, err := sql.Open("duckdb", path) + if err != nil { + t.Fatalf("open mechanism fixture: %v", err) + } + for _, statement := range []string{ + `CREATE TABLE proxy_events( + event_id TEXT, session_id TEXT, event_kind TEXT, + output_hash TEXT, output_tokens BIGINT + )`, + `CREATE TABLE proxy_transforms( + event_id TEXT, ordinal INTEGER, input_tokens BIGINT + )`, + `INSERT INTO proxy_events VALUES + ('e1', 'session-1', 'payload_injection', 'same', 40), + ('e2', 'session-1', 'payload_injection', 'same', 20), + ('e3', 'session-1', 'response', '', 30), + ('outside', 'session-2', 'payload_injection', 'same', 999)`, + `INSERT INTO proxy_transforms VALUES + ('e1', 0, 100), ('e1', 1, 200), ('e2', 0, 50), + ('e3', 0, 70), ('outside', 0, 999)`, + } { + if _, err = database.Exec(statement); err != nil { + _ = database.Close() + t.Fatalf("create mechanism fixture: %v", err) + } + } + if err = database.Close(); err != nil { + t.Fatalf("close mechanism fixture: %v", err) + } + + metrics, err := readRunMechanisms(context.Background(), path, "session-1") + if err != nil { + t.Fatalf("read mechanism evidence: %v", err) + } + want := MechanismMetrics{ + RawContextTokens: 220, + DeliveredContextTokens: 90, + AvoidedContextTokens: 130, + InjectedGuidanceTokens: 60, + RepeatedAdviceCount: 1, + TransformEventCount: 3, + } + if metrics != want { + t.Fatalf("mechanism metrics = %#v, want %#v", metrics, want) + } +} + func createHistoricalFixture(t *testing.T) string { t.Helper() From f8b1521d8a06c53fca961d9b88d5144dc26c01cf Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 15:56:28 -0600 Subject: [PATCH 11/17] fix(agent-hooks): accept native supervisor block contracts --- docs/PROVIDER_CAPABILITY_MATRIX.md | 2 +- .../agenthooks/provider_capabilities.go | 4 +- go/internal/agenthooks/settings_probe.go | 60 ++++++++++--- go/internal/agenthooks/settings_probe_test.go | 84 +++++++++++++++++++ 4 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 go/internal/agenthooks/settings_probe_test.go diff --git a/docs/PROVIDER_CAPABILITY_MATRIX.md b/docs/PROVIDER_CAPABILITY_MATRIX.md index 3a1c8a09..56c71a06 100644 --- a/docs/PROVIDER_CAPABILITY_MATRIX.md +++ b/docs/PROVIDER_CAPABILITY_MATRIX.md @@ -92,7 +92,7 @@ Safety caveats: - Coverage: partial - Settings target: .codex/config.toml - MCP setup: .codex/config.toml mcp_servers.coding-ethos stdio server -- Block response shape: decision = block plus permissionDecision = deny for PreToolUse +- Block response shape: exit 2 with stderr reason or decision = block plus permissionDecision = deny for PreToolUse - Context/advice shape: additionalContext where native; compact systemMessage otherwise - Memory interception: provider memory imports into .coding-ethos/memories - Memory fallback: memory.centralized denial points at the central memory file diff --git a/go/internal/agenthooks/provider_capabilities.go b/go/internal/agenthooks/provider_capabilities.go index 34014d3a..eae4f368 100644 --- a/go/internal/agenthooks/provider_capabilities.go +++ b/go/internal/agenthooks/provider_capabilities.go @@ -149,8 +149,8 @@ func codexProviderCapability() ProviderCapability { "Stop", }, SettingsTarget: ".codex/config.toml", - BlockResponseShape: "decision = block plus permissionDecision = deny " + - "for PreToolUse", + BlockResponseShape: "exit 2 with stderr reason or decision = block plus " + + "permissionDecision = deny for PreToolUse", ContextAdviceShape: "additionalContext where native; compact " + "systemMessage otherwise", MCPSetup: ".codex/config.toml mcp_servers.coding-ethos stdio server", diff --git a/go/internal/agenthooks/settings_probe.go b/go/internal/agenthooks/settings_probe.go index c2cae800..b1b0ef59 100644 --- a/go/internal/agenthooks/settings_probe.go +++ b/go/internal/agenthooks/settings_probe.go @@ -586,6 +586,18 @@ func validateCodexBlockReason( if result.exitCode == 0 { return apperror.StaticError("codex raw git probe should block") } + // Codex's native command-hook block contract is exit 2 with the reason on + // stderr. Coding Ethos's own hook adapter also emits a structured JSON + // receipt, so verify both valid routes instead of requiring an external + // supervisor to manufacture stdout that Codex ignores on a blocking exit. + if strings.TrimSpace(result.stdout) == "" { + reason := strings.TrimSpace(result.stderr) + if reason == "" { + return apperror.StaticError("missing Codex block reason on stderr") + } + + return validateCodexBlockReasonMarkers(reason, reasonMarkers) + } actual, found := result.payload["decision"].(string) if !found || actual != "block" { @@ -606,15 +618,9 @@ func validateCodexBlockReason( ) } - for _, marker := range reasonMarkers { - if !strings.Contains(reason, marker) { - return apperror.Wrapf( - apperror.StaticError("codex block reason lost marker %q: %s"), - "codex block reason lost marker %q: %s", - marker, - reason, - ) - } + err := validateCodexBlockReasonMarkers(reason, reasonMarkers) + if err != nil { + return err } permissionReason, found := nestedString( @@ -633,6 +639,21 @@ func validateCodexBlockReason( return nil } +func validateCodexBlockReasonMarkers(reason string, reasonMarkers []string) error { + for _, marker := range reasonMarkers { + if !strings.Contains(reason, marker) { + return apperror.Wrapf( + apperror.StaticError("codex block reason lost marker %q: %s"), + "codex block reason lost marker %q: %s", + marker, + reason, + ) + } + } + + return nil +} + func validateClaudeBlockProbe(result hookProbeResult) error { return validateDecisionProbe(result, "block") } @@ -670,7 +691,26 @@ func validateKimiStopContinuationProbe(result hookProbeResult) error { ) } - return validateKimiStructuredDeny(result) + if _, found := nestedString( + result.payload, + "hookSpecificOutput", + "permissionDecision", + ); found { + return validateKimiStructuredDeny(result) + } + // A supervising hook may legitimately allow a clean Stop. Kimi still + // receives a native, decoded response proving the hook ran; only a + // supervisor that finds unfinished work needs to return the deny shape that + // continues the turn. + if _, found := result.payload["message"].(string); !found { + return apperror.Wrapf( + apperror.StaticError("Kimi Stop hook returned no native response: %s"), + "Kimi Stop hook returned no native response: %s", + result.stdout, + ) + } + + return nil } func validateKimiStructuredDeny(result hookProbeResult) error { diff --git a/go/internal/agenthooks/settings_probe_test.go b/go/internal/agenthooks/settings_probe_test.go new file mode 100644 index 00000000..1b8c8eb6 --- /dev/null +++ b/go/internal/agenthooks/settings_probe_test.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +package agenthooks + +import ( + "strings" + "testing" +) + +func TestValidateCodexBlockReasonAcceptsNativeStderr(t *testing.T) { + t.Parallel() + + result := hookProbeResult{ + exitCode: 2, + stderr: "policy_ids: git.wrapper_required\n" + + "Resubmit the command through: cerun -- 'git status --short'\n", + } + + if err := validateCodexBlockProbe(result); err != nil { + t.Fatalf("validate native Codex block: %v", err) + } +} + +func TestValidateCodexBlockReasonRejectsInvalidNativeStderr(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + stderr string + wantMarker string + }{ + { + name: "missing reason", + wantMarker: "missing Codex block reason on stderr", + }, + { + name: "missing policy marker", + stderr: "Resubmit the command through: cerun -- 'git status --short'", + wantMarker: "git.wrapper_required", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := validateCodexBlockProbe(hookProbeResult{ + exitCode: 2, + stderr: test.stderr, + }) + if err == nil || !strings.Contains(err.Error(), test.wantMarker) { + t.Fatalf("error = %v, want marker %q", err, test.wantMarker) + } + }) + } +} + +func TestValidateKimiStopContinuationAcceptsSupervisorAllow(t *testing.T) { + t.Parallel() + + result := hookProbeResult{ + exitCode: 0, + stdout: "{\"message\":\"\"}\n", + payload: map[string]any{"message": ""}, + } + + if err := validateKimiStopContinuationProbe(result); err != nil { + t.Fatalf("validate Kimi supervisor Stop allow: %v", err) + } +} + +func TestValidateKimiStopContinuationRejectsMissingResponse(t *testing.T) { + t.Parallel() + + err := validateKimiStopContinuationProbe(hookProbeResult{ + exitCode: 0, + stdout: "{}\n", + payload: map[string]any{}, + }) + if err == nil || !strings.Contains(err.Error(), "no native response") { + t.Fatalf("error = %v, want missing native response", err) + } +} From 78e2b413043cfacc0812af30963d1131860aab87 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 15:56:57 -0600 Subject: [PATCH 12/17] fix(code-intel): centralize external extractor contracts --- go/internal/codeintel/source_v2_external.go | 22 ++++++++++++++------- go/internal/codeintel/source_v2_test.go | 16 +++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/go/internal/codeintel/source_v2_external.go b/go/internal/codeintel/source_v2_external.go index bc72e238..feb051d8 100644 --- a/go/internal/codeintel/source_v2_external.go +++ b/go/internal/codeintel/source_v2_external.go @@ -22,9 +22,13 @@ import ( ) const ( - externalExtractorStatusError = "error" - externalExtractorStatusOK = "ok" - purrdfExtractorBinary = "coding-ethos-purrdf-extractor" + externalExtractorParserRDF = "purrdf-rdf" + externalExtractorParserSPARQL = "purrdf-sparql-algebra" + externalExtractorSpanFidelityNone = "none" + externalExtractorSpanFidelitySubjectStart = "subject_start" + externalExtractorStatusError = "error" + externalExtractorStatusOK = "ok" + purrdfExtractorBinary = "coding-ethos-purrdf-extractor" ) var errExternalExtractorUnavailable = errors.New( @@ -349,9 +353,11 @@ func externalFactContract(language string) (string, string, error) { astfacts.LanguageTriG, astfacts.LanguageNTriples, astfacts.LanguageNQuads: - return "purrdf-rdf", "subject_start", nil + return externalExtractorParserRDF, + externalExtractorSpanFidelitySubjectStart, + nil case astfacts.LanguageSPARQL: - return "purrdf-sparql-algebra", "none", nil + return externalExtractorParserSPARQL, externalExtractorSpanFidelityNone, nil default: return "", "", fmt.Errorf( "%w: unexpected external extractor language %q", @@ -457,7 +463,8 @@ func validateExternalFactStart( path string, provenance ExternalExtractorProvenance, ) error { - if provenance.SpanFidelity == "none" && provenance.Start != nil { + if provenance.SpanFidelity == externalExtractorSpanFidelityNone && + provenance.Start != nil { return fmt.Errorf( "%w: span-free external fact has a start position for %q", errInvalidExternalExtractorResponse, @@ -465,7 +472,8 @@ func validateExternalFactStart( ) } - if provenance.SpanFidelity == "subject_start" && provenance.Start != nil && + if provenance.SpanFidelity == externalExtractorSpanFidelitySubjectStart && + provenance.Start != nil && (provenance.Start.ByteOffset < 0 || provenance.Start.Line < 1 || provenance.Start.Column < 1) { return fmt.Errorf( diff --git a/go/internal/codeintel/source_v2_test.go b/go/internal/codeintel/source_v2_test.go index f0b24fd0..5ecf578c 100644 --- a/go/internal/codeintel/source_v2_test.go +++ b/go/internal/codeintel/source_v2_test.go @@ -36,7 +36,7 @@ func (extractor *sourceV2FakeExtractor) Extract( results := make([]ExternalExtractorResult, 0, len(files)) for _, file := range files { language := astfacts.LanguageTurtle - parser := "purrdf-rdf" + parser := externalExtractorParserRDF switch filepath.Ext(file.Path) { case ".trig": language = astfacts.LanguageTriG @@ -46,7 +46,7 @@ func (extractor *sourceV2FakeExtractor) Extract( language = astfacts.LanguageNQuads case ".rq", ".sparql": language = astfacts.LanguageSPARQL - parser = "purrdf-sparql-algebra" + parser = externalExtractorParserSPARQL } results = append(results, ExternalExtractorResult{ @@ -62,7 +62,7 @@ func (extractor *sourceV2FakeExtractor) Extract( Class: "EXTRACTED", Parser: parser, ParserRevision: astfacts.PurrdfExtractorRevision, - SpanFidelity: "subject_start", + SpanFidelity: externalExtractorSpanFidelitySubjectStart, SourcePath: file.Path, }, }}, @@ -734,7 +734,7 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { wrongParser := validExternalExtractorResult(astfacts.LanguageTurtle) wrongParser.Facts[0].Provenance.Parser = "wrong-parser" wrongFidelity := validExternalExtractorResult(astfacts.LanguageTurtle) - wrongFidelity.Facts[0].Provenance.SpanFidelity = "none" + wrongFidelity.Facts[0].Provenance.SpanFidelity = externalExtractorSpanFidelityNone invalidStart := validExternalExtractorResult(astfacts.LanguageTurtle) invalidStart.Facts[0].Provenance.Start = &ExternalExtractorPosition{ ByteOffset: -1, @@ -763,12 +763,12 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { } func validExternalExtractorResult(language string) ExternalExtractorResult { - parser := "purrdf-rdf" - spanFidelity := "subject_start" + parser := externalExtractorParserRDF + spanFidelity := externalExtractorSpanFidelitySubjectStart start := &ExternalExtractorPosition{Line: 1, Column: 1} if language == astfacts.LanguageSPARQL { - parser = "purrdf-sparql-algebra" - spanFidelity = "none" + parser = externalExtractorParserSPARQL + spanFidelity = externalExtractorSpanFidelityNone start = nil } From 3851aa48336efd34d615ee443839b61c0902ffc5 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 16:22:11 -0600 Subject: [PATCH 13/17] fix(token-economy): enforce benchmark isolation --- go/internal/tokeneconomy/artifact.go | 35 ++++- .../tokeneconomy/benchmark_manifest.go | 88 +++++++++++-- go/internal/tokeneconomy/benchmark_runner.go | 46 +++++-- go/internal/tokeneconomy/benchmark_test.go | 123 ++++++++++++++++++ go/internal/tokeneconomy/report_test.go | 40 +++++- 5 files changed, 300 insertions(+), 32 deletions(-) diff --git a/go/internal/tokeneconomy/artifact.go b/go/internal/tokeneconomy/artifact.go index 5734b620..0015990d 100644 --- a/go/internal/tokeneconomy/artifact.go +++ b/go/internal/tokeneconomy/artifact.go @@ -166,11 +166,11 @@ func formatReportMarkdown(report Report) string { output.WriteString("\n### Verified sources\n\n") for _, source := range report.Historical.Sources { - output.WriteString("- `") - output.WriteString(strings.ReplaceAll(source.Path, "`", "\\`")) - output.WriteString("`: `") - output.WriteString(source.SHA256After) - output.WriteString("` (unchanged)\n") + output.WriteString("- ") + output.WriteString(markdownCodeSpan(source.Path)) + output.WriteString(": ") + output.WriteString(markdownCodeSpan(source.SHA256After)) + output.WriteString(" (unchanged)\n") } } @@ -217,6 +217,31 @@ func formatReportMarkdown(report Report) string { return output.String() } +func markdownCodeSpan(value string) string { + value = strings.NewReplacer("\r", " ", "\n", " ").Replace(value) + longestRun := 0 + currentRun := 0 + for _, character := range value { + if character != '`' { + currentRun = 0 + + continue + } + + currentRun++ + longestRun = max(longestRun, currentRun) + } + + delimiter := strings.Repeat("`", longestRun+1) + padding := "" + if strings.HasPrefix(value, "`") || strings.HasSuffix(value, "`") || + strings.HasPrefix(value, " ") || strings.HasSuffix(value, " ") { + padding = " " + } + + return delimiter + padding + value + padding + delimiter +} + func writeMarkdownField(output *strings.Builder, label, value string) { output.WriteString("- ") output.WriteString(label) diff --git a/go/internal/tokeneconomy/benchmark_manifest.go b/go/internal/tokeneconomy/benchmark_manifest.go index bb5171c4..a78946f5 100644 --- a/go/internal/tokeneconomy/benchmark_manifest.go +++ b/go/internal/tokeneconomy/benchmark_manifest.go @@ -21,6 +21,7 @@ import ( "strings" "time" + "github.com/pelletier/go-toml/v2" "go.yaml.in/yaml/v3" "blackcat.ca/coding-ethos/go/internal/safeexec" @@ -498,33 +499,92 @@ func validateFullConfigOverrides(overrides []string) error { ) } - forbidden := []string{ - "approval_policy=", - "model=", - "model_reasoning_effort=", - "project_doc_max_bytes=", - "sandbox_mode=", - "sandbox_workspace_write.network_access=", + controlledPaths := [][]string{ + {"approval_policy"}, + {"model"}, + {"model_reasoning_effort"}, + {"project_doc_max_bytes"}, + {"sandbox_mode"}, + {"sandbox_workspace_write", "network_access"}, } - for _, override := range overrides { - cleaned := strings.ReplaceAll(strings.TrimSpace(override), " ", "") - if cleaned == "" || !strings.Contains(cleaned, "=") { - return fmt.Errorf("%w: invalid full config override", errInvalidBenchmarkManifest) + seenPaths := map[string]struct{}{} + for index, override := range overrides { + config := map[string]any{} + if strings.TrimSpace(override) == "" || + toml.Unmarshal([]byte(override), &config) != nil || len(config) == 0 { + return fmt.Errorf( + "%w: full config override %d is invalid TOML", + errInvalidBenchmarkManifest, + index+1, + ) } - for _, prefix := range forbidden { - if strings.HasPrefix(cleaned, prefix) { + + paths, leafPaths := benchmarkConfigPaths(config, nil) + for _, path := range paths { + for _, controlledPath := range controlledPaths { + if !benchmarkConfigPathsRelated(path, controlledPath) { + continue + } + return fmt.Errorf( "%w: full config override changes controlled setting %q", errInvalidBenchmarkManifest, - override, + strings.Join(controlledPath, "."), + ) + } + } + + for _, path := range leafPaths { + identity := strings.Join(path, "\x00") + if _, found := seenPaths[identity]; found { + return fmt.Errorf( + "%w: duplicate full config override %q", + errInvalidBenchmarkManifest, + strings.Join(path, "."), ) } + seenPaths[identity] = struct{}{} } } return nil } +func benchmarkConfigPaths( + config map[string]any, + prefix []string, +) ([][]string, [][]string) { + paths := [][]string{} + leaves := [][]string{} + for key, value := range config { + path := append(slices.Clone(prefix), key) + paths = append(paths, path) + + nested, isTable := value.(map[string]any) + if !isTable || len(nested) == 0 { + leaves = append(leaves, path) + + continue + } + + nestedPaths, nestedLeaves := benchmarkConfigPaths(nested, path) + paths = append(paths, nestedPaths...) + leaves = append(leaves, nestedLeaves...) + } + + return paths, leaves +} + +func benchmarkConfigPathsRelated(left, right []string) bool { + for index := range min(len(left), len(right)) { + if left[index] != right[index] { + return false + } + } + + return true +} + func validateBenchmarkTask(ctx context.Context, task BenchmarkTask) error { if !benchmarkIDPattern.MatchString(task.TaskID) { return fmt.Errorf("%w: invalid task_id %q", errInvalidBenchmarkManifest, task.TaskID) diff --git a/go/internal/tokeneconomy/benchmark_runner.go b/go/internal/tokeneconomy/benchmark_runner.go index f5e1946e..1dc2b7fc 100644 --- a/go/internal/tokeneconomy/benchmark_runner.go +++ b/go/internal/tokeneconomy/benchmark_runner.go @@ -472,6 +472,26 @@ func runCodexBenchmark( spec benchmarkRunSpec, runRoot string, workspace string, +) codexBenchmarkOutcome { + return runCodexBenchmarkWithHomeRemover( + ctx, + manifest, + task, + spec, + runRoot, + workspace, + os.RemoveAll, + ) +} + +func runCodexBenchmarkWithHomeRemover( + ctx context.Context, + manifest BenchmarkManifest, + task BenchmarkTask, + spec benchmarkRunSpec, + runRoot string, + workspace string, + removeHome func(string) error, ) codexBenchmarkOutcome { started := time.Now().UTC() outcome := codexBenchmarkOutcome{ @@ -496,39 +516,41 @@ func runCodexBenchmark( return finishCodexOutcome(outcome, started) } - defer func() { - if removeErr := os.RemoveAll(codexHome); removeErr != nil { - outcome.Errors = append( - outcome.Errors, - "remove isolated Codex home: "+removeErr.Error(), - ) + finish := func() codexBenchmarkOutcome { + removeErr := removeHome(codexHome) + if removeErr != nil { + cleanupErr := fmt.Errorf("remove isolated Codex home: %w", removeErr) + outcome.EvidenceError = errors.Join(outcome.EvidenceError, cleanupErr) + outcome.Errors = append(outcome.Errors, cleanupErr.Error()) } - }() + + return finishCodexOutcome(outcome, started) + } err = populateBenchmarkCodexHome(codexHome, manifest, spec.Arm) if err != nil { outcome.Errors = append(outcome.Errors, err.Error()) - return finishCodexOutcome(outcome, started) + return finish() } prompt, err := os.ReadFile(task.PromptPath) if err != nil { outcome.Errors = append(outcome.Errors, fmt.Sprintf("read benchmark prompt: %v", err)) - return finishCodexOutcome(outcome, started) + return finish() } if len(prompt) > maximumBenchmarkPromptBytes { outcome.Errors = append(outcome.Errors, "benchmark prompt exceeds size limit") - return finishCodexOutcome(outcome, started) + return finish() } stdout, stderr, err := createAgentLogs(runRoot) if err != nil { outcome.Errors = append(outcome.Errors, err.Error()) - return finishCodexOutcome(outcome, started) + return finish() } runContext, cancel := context.WithTimeout(ctx, agentTimeout) @@ -569,7 +591,7 @@ func runCodexBenchmark( outcome.FullStateRoot = filepath.Join(runRoot, "coding-ethos-state") } - return finishCodexOutcome(outcome, started) + return finish() } func finishCodexOutcome( diff --git a/go/internal/tokeneconomy/benchmark_test.go b/go/internal/tokeneconomy/benchmark_test.go index 250c55c2..ef990846 100644 --- a/go/internal/tokeneconomy/benchmark_test.go +++ b/go/internal/tokeneconomy/benchmark_test.go @@ -9,6 +9,7 @@ import ( "context" "crypto/sha256" "encoding/hex" + "errors" "os" "os/exec" "path/filepath" @@ -228,6 +229,117 @@ func TestBenchmarkCodexArgumentsDisableSandboxNetwork(t *testing.T) { } } +func TestValidateFullConfigOverridesParsesTOMLStructure(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + overrides []string + wantError string + }{ + { + name: "legitimate feature", + overrides: []string{" features . code_intel = true "}, + }, + { + name: "quoted controlled key", + overrides: []string{`"sandbox_mode" = "danger-full-access"`}, + wantError: "sandbox_mode", + }, + { + name: "quoted controlled path segment", + overrides: []string{ + `"sandbox_workspace_write" . "network_access" = true`, + }, + wantError: "sandbox_workspace_write.network_access", + }, + { + name: "controlled inline parent table", + overrides: []string{ + `sandbox_workspace_write = { network_access = true }`, + }, + wantError: "sandbox_workspace_write.network_access", + }, + { + name: "duplicate normalized path", + overrides: []string{ + "features.code_intel=true", + `features."code_intel"=false`, + }, + wantError: "duplicate full config override", + }, + { + name: "invalid TOML", + overrides: []string{"features.code_intel=["}, + wantError: "invalid TOML", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := validateFullConfigOverrides(test.overrides) + if test.wantError == "" { + if err != nil { + t.Fatalf("validate legitimate overrides: %v", err) + } + + return + } + if !errors.Is(err, errInvalidBenchmarkManifest) || + !strings.Contains(err.Error(), test.wantError) { + t.Fatalf("error = %v, want invalid manifest containing %q", err, test.wantError) + } + }) + } +} + +func TestRunCodexBenchmarkReportsHomeCleanupFailure(t *testing.T) { + t.Parallel() + + fixture := newBenchmarkFixture(t) + prepared, err := LoadBenchmarkManifest(context.Background(), fixture.manifestPath) + if err != nil { + t.Fatalf("load benchmark manifest: %v", err) + } + + runRoot := filepath.Join(fixture.root, "cleanup-run") + workspace := filepath.Join(fixture.root, "cleanup-workspace") + for _, path := range []string{runRoot, workspace} { + if err = os.Mkdir(path, 0o700); err != nil { + t.Fatalf("create benchmark path %s: %v", path, err) + } + } + + cleanupFailure := errors.New("synthetic cleanup failure") + removedPath := "" + outcome := runCodexBenchmarkWithHomeRemover( + context.Background(), + prepared.Manifest, + prepared.Manifest.Tasks[0], + benchmarkRunSpec{TaskID: "diagnostic-one", RunID: "cleanup", Arm: ArmOff}, + runRoot, + workspace, + func(path string) error { + removedPath = path + + return cleanupFailure + }, + ) + if removedPath == "" || !errors.Is(outcome.EvidenceError, cleanupFailure) || + !strings.Contains(strings.Join(outcome.Errors, "; "), "remove isolated Codex home") { + t.Fatalf( + "cleanup failure was not returned: path=%q outcome=%#v", + removedPath, + outcome, + ) + } + if _, err = os.Stat(filepath.Join(removedPath, "auth.json")); err != nil { + t.Fatalf("cleanup failure fixture did not retain copied auth: %v", err) + } +} + func TestRunBenchmarkUsesIsolatedArmsAndResumesWithoutReplacement(t *testing.T) { fixture := newBenchmarkFixture(t) prepared, err := LoadBenchmarkManifest(context.Background(), fixture.manifestPath) @@ -244,6 +356,17 @@ func TestRunBenchmarkUsesIsolatedArmsAndResumesWithoutReplacement(t *testing.T) if first.NewlyRecorded != 6 || first.PreviouslyRecorded != 0 { t.Fatalf("unexpected first run summary: %#v", first) } + temporaryHomes, err := filepath.Glob(filepath.Join( + stateRoot, + ".coding-ethos", + "token-economy-runs", + fixture.experimentID, + "*", + ".codex-home-*", + )) + if err != nil || len(temporaryHomes) != 0 { + t.Fatalf("temporary Codex homes survived: paths=%v error=%v", temporaryHomes, err) + } store, err := Open(context.Background(), DefaultDBPath(stateRoot)) if err != nil { diff --git a/go/internal/tokeneconomy/report_test.go b/go/internal/tokeneconomy/report_test.go index 6335b99b..ea648d1d 100644 --- a/go/internal/tokeneconomy/report_test.go +++ b/go/internal/tokeneconomy/report_test.go @@ -4,9 +4,11 @@ package tokeneconomy import ( + "bytes" "context" "database/sql" "fmt" + "html" "os" "path/filepath" "reflect" @@ -16,6 +18,7 @@ import ( "time" _ "github.com/duckdb/duckdb-go/v2" + "github.com/yuin/goldmark" ) func TestHistoricalReportLabelsGrossReductionNonCausal(t *testing.T) { @@ -572,7 +575,7 @@ func TestFormatReportMarkdownIncludesVerifiedContextAndComparisonQuality(t *test markdown := formatReportMarkdown(report) for _, expected := range []string{ "## Observational context reduction", - "- `evidence\\`store.duckdb`: `abc123` (unchanged)", + "- ``evidence`store.duckdb``: `abc123` (unchanged)", "| full | off | 2 | 12.50% | 10.00% to 15.00% | 95.00% | non-inferior |", "| static | off | 1 | -2.00% | -5.00% to 1.00% | 90.00% | " + "not established |", @@ -582,6 +585,41 @@ func TestFormatReportMarkdownIncludesVerifiedContextAndComparisonQuality(t *test t.Fatalf("Markdown report missing %q:\n%s", expected, markdown) } } + + var rendered bytes.Buffer + if err := goldmark.Convert([]byte(markdown), &rendered); err != nil { + t.Fatalf("render Markdown report: %v", err) + } + if !strings.Contains(rendered.String(), "evidence`store.duckdb") { + t.Fatalf( + "verified source path did not round trip through Markdown: %s", + rendered.String(), + ) + } +} + +func TestMarkdownCodeSpanRoundTripsDelimiterEdges(t *testing.T) { + t.Parallel() + + for _, value := range []string{ + "plain", + "evidence`store.duckdb", + "evidence``store.duckdb", + "`boundary`", + " leading-space", + "trailing-space ", + } { + var rendered bytes.Buffer + err := goldmark.Convert([]byte(markdownCodeSpan(value)), &rendered) + if err != nil { + t.Fatalf("render code span for %q: %v", value, err) + } + + want := "

" + html.EscapeString(value) + "

\n" + if rendered.String() != want { + t.Fatalf("code span for %q rendered as %q, want %q", value, rendered.String(), want) + } + } } func TestReadRunMechanismsMeasuresFirstTransformsAndRepeatedAdvice(t *testing.T) { From 0200f038ed77396debf56d543ac7c7e8ae09caee Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 16:44:47 -0600 Subject: [PATCH 14/17] fix(review): close integration correctness gaps --- go/cmd/coding-ethos-run/main_test.go | 7 +- go/cmd/coding-ethos-run/runtime_exec.go | 11 +--- go/internal/codeintel/source_v2_external.go | 4 +- go/internal/codeintel/source_v2_test.go | 11 +++- go/internal/codeintelcli/source_v2_test.go | 11 +++- go/internal/mcp/code_intel_workspace_test.go | 2 +- go/internal/sandboxexec/main.go | 11 +--- go/internal/sandboxexec/main_test.go | 34 +++++++++- go/internal/sharedlock/metadata.go | 20 ++++++ go/internal/tokeneconomy/benchmark_source.go | 2 +- go/internal/tokeneconomy/benchmark_test.go | 66 +++++++++++++++++++ .../tokeneconomy/benchmark_validation.go | 1 - go/internal/tokeneconomy/ledger.go | 2 + go/internal/tokeneconomy/ledger_test.go | 22 +++++++ go/internal/tokeneconomy/mechanisms.go | 5 +- go/internal/tokeneconomy/report.go | 8 +-- go/internal/tokeneconomy/report_test.go | 30 ++++++++- rust/purrdf-code-intel-extractor/PROTOCOL.md | 8 ++- 18 files changed, 215 insertions(+), 40 deletions(-) create mode 100644 go/internal/sharedlock/metadata.go diff --git a/go/cmd/coding-ethos-run/main_test.go b/go/cmd/coding-ethos-run/main_test.go index fc331138..f2fbe93e 100644 --- a/go/cmd/coding-ethos-run/main_test.go +++ b/go/cmd/coding-ethos-run/main_test.go @@ -24,6 +24,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/policy" "blackcat.ca/coding-ethos/go/internal/realgit" "blackcat.ca/coding-ethos/go/internal/sandbox" + "blackcat.ca/coding-ethos/go/internal/sharedlock" "blackcat.ca/coding-ethos/go/internal/shellquote" "blackcat.ca/coding-ethos/go/internal/testlock" ) @@ -1935,7 +1936,7 @@ func TestSharedLockDirectoryMetadataRequiresTheExactExternalCapabilityShape( if err != nil { t.Fatalf("inspect shared lock metadata: %v", err) } - if !validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-shared-lock-test", info) { + if !sharedlock.ValidDirectoryMetadata("/var/tmp/coding-ethos-shared-lock-test", info) { t.Fatal("the exact direct-child mode-1777 capability shape was rejected") } @@ -1946,10 +1947,10 @@ func TestSharedLockDirectoryMetadataRequiresTheExactExternalCapabilityShape( if err != nil { t.Fatalf("inspect non-sticky metadata: %v", err) } - if validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-shared-lock-test", info) { + if sharedlock.ValidDirectoryMetadata("/var/tmp/coding-ethos-shared-lock-test", info) { t.Fatal("a non-sticky external directory became a shared lock capability") } - if validSharedLockDirectoryMetadata("/tmp/coding-ethos-shared-lock-test", info) { + if sharedlock.ValidDirectoryMetadata("/tmp/coding-ethos-shared-lock-test", info) { t.Fatal("a path outside /var/tmp became a shared lock capability") } } diff --git a/go/cmd/coding-ethos-run/runtime_exec.go b/go/cmd/coding-ethos-run/runtime_exec.go index 71641f81..10bafdd6 100644 --- a/go/cmd/coding-ethos-run/runtime_exec.go +++ b/go/cmd/coding-ethos-run/runtime_exec.go @@ -37,6 +37,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/realgit" "blackcat.ca/coding-ethos/go/internal/safeexec" "blackcat.ca/coding-ethos/go/internal/sandbox" + "blackcat.ca/coding-ethos/go/internal/sharedlock" "blackcat.ca/coding-ethos/go/internal/shellparse" "blackcat.ca/coding-ethos/go/internal/toolchaincli" "blackcat.ca/coding-ethos/go/internal/toolconfigs" @@ -350,21 +351,13 @@ func validateSharedLockDirectory(path string) error { return fmt.Errorf("inspect shared lock directory %s: %w", path, err) } - if !validSharedLockDirectoryMetadata(path, info) { + if !sharedlock.ValidDirectoryMetadata(path, info) { return fmt.Errorf("%w: %s", errSharedLockDirectoryShape, path) } return nil } -func validSharedLockDirectoryMetadata(path string, info os.FileInfo) bool { - mode := info.Mode() - - return filepath.IsAbs(path) && filepath.Dir(path) == "/var/tmp" && - info.IsDir() && mode&os.ModeSymlink == 0 && - mode.Perm() == 0o777 && mode&os.ModeSticky != 0 -} - // agentShellEnsureWriteDirs creates the agent-shell managed write directories // under the repo root and returns their paths so the caller can fold them into // the sandbox write set. diff --git a/go/internal/codeintel/source_v2_external.go b/go/internal/codeintel/source_v2_external.go index feb051d8..c6030bdc 100644 --- a/go/internal/codeintel/source_v2_external.go +++ b/go/internal/codeintel/source_v2_external.go @@ -473,8 +473,8 @@ func validateExternalFactStart( } if provenance.SpanFidelity == externalExtractorSpanFidelitySubjectStart && - provenance.Start != nil && - (provenance.Start.ByteOffset < 0 || provenance.Start.Line < 1 || + (provenance.Start == nil || provenance.Start.ByteOffset < 0 || + provenance.Start.Line < 1 || provenance.Start.Column < 1) { return fmt.Errorf( "%w: invalid external fact start position for %q", diff --git a/go/internal/codeintel/source_v2_test.go b/go/internal/codeintel/source_v2_test.go index 5ecf578c..37aac638 100644 --- a/go/internal/codeintel/source_v2_test.go +++ b/go/internal/codeintel/source_v2_test.go @@ -575,9 +575,15 @@ func TestRepositoryIDUsesDocumentedLocalFallback(t *testing.T) { if err != nil { t.Fatalf("read root commit: %v", err) } + canonicalCommonDir := layout.commonGitDir + resolvedCommonDir, resolveErr := filepath.EvalSymlinks(layout.commonGitDir) + if resolveErr == nil { + canonicalCommonDir = resolvedCommonDir + } expected := RepositoryID(sourceV2Digest( "repository", - "local-common-dir:"+filepath.ToSlash(layout.commonGitDir)+"\x00"+rootCommit, + "local-common-dir:"+filepath.ToSlash(filepath.Clean(canonicalCommonDir))+ + "\x00"+rootCommit, )) if layout.repositoryID != expected { t.Fatalf("local repository ID = %q, want %q", layout.repositoryID, expected) @@ -735,6 +741,8 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { wrongParser.Facts[0].Provenance.Parser = "wrong-parser" wrongFidelity := validExternalExtractorResult(astfacts.LanguageTurtle) wrongFidelity.Facts[0].Provenance.SpanFidelity = externalExtractorSpanFidelityNone + missingStart := validExternalExtractorResult(astfacts.LanguageTurtle) + missingStart.Facts[0].Provenance.Start = nil invalidStart := validExternalExtractorResult(astfacts.LanguageTurtle) invalidStart.Facts[0].Provenance.Start = &ExternalExtractorPosition{ ByteOffset: -1, @@ -752,6 +760,7 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { "duplicate": duplicate, "wrong parser": wrongParser, "wrong fidelity": wrongFidelity, + "missing start": missingStart, "invalid start": invalidStart, "span-free start": spanFreeStart, "unknown language": {Language: "python"}, diff --git a/go/internal/codeintelcli/source_v2_test.go b/go/internal/codeintelcli/source_v2_test.go index a5dbaeb9..ac91438d 100644 --- a/go/internal/codeintelcli/source_v2_test.go +++ b/go/internal/codeintelcli/source_v2_test.go @@ -27,7 +27,16 @@ func TestSyncAndStatusCommandsEmitTypedV2Receipts(t *testing.T) { t.Fatalf("write source: %v", err) } runSourceV2CLIGit(t, root, "add", "app.go") - runSourceV2CLIGit(t, root, "commit", "-m", "base") + runSourceV2CLIGit( + t, + root, + "-c", + "commit.gpgSign=false", + "commit", + "--no-gpg-sign", + "-m", + "base", + ) var runErr error syncOutput := captureStdout(t, func() { diff --git a/go/internal/mcp/code_intel_workspace_test.go b/go/internal/mcp/code_intel_workspace_test.go index beb9ca0d..15777f18 100644 --- a/go/internal/mcp/code_intel_workspace_test.go +++ b/go/internal/mcp/code_intel_workspace_test.go @@ -34,7 +34,7 @@ func TestCodeIntelTaskMetaPublishesAdditiveV2Receipt(t *testing.T) { t.Fatalf("marshal task metadata: %v", err) } for _, expected := range []string{ - `"coding-ethos.code-intel/v2"`, + `"coding-ethos.code-intel/v2":{`, `"contract":"coding-ethos.code-intel/v2"`, `"status":"exact"`, } { diff --git a/go/internal/sandboxexec/main.go b/go/internal/sandboxexec/main.go index 2356d850..8e45702f 100644 --- a/go/internal/sandboxexec/main.go +++ b/go/internal/sandboxexec/main.go @@ -17,6 +17,7 @@ import ( "blackcat.ca/coding-ethos/go/internal/apperror" "blackcat.ca/coding-ethos/go/internal/execguard" "blackcat.ca/coding-ethos/go/internal/feedback" + "blackcat.ca/coding-ethos/go/internal/sharedlock" ) var errSandboxExecCommand = apperror.StaticError("sandbox exec requires command") @@ -301,15 +302,7 @@ func allowedSharedLockDirectory(path string) bool { return false } - return validSharedLockDirectoryMetadata(path, info) -} - -func validSharedLockDirectoryMetadata(path string, info os.FileInfo) bool { - mode := info.Mode() - - return filepath.IsAbs(path) && filepath.Dir(path) == "/var/tmp" && - info.IsDir() && mode&os.ModeSymlink == 0 && - mode.Perm() == 0o777 && mode&os.ModeSticky != 0 + return sharedlock.ValidDirectoryMetadata(path, info) } func allowedTerminalWritePath(path string) bool { diff --git a/go/internal/sandboxexec/main_test.go b/go/internal/sandboxexec/main_test.go index 6fb4c73d..4b5e08ca 100644 --- a/go/internal/sandboxexec/main_test.go +++ b/go/internal/sandboxexec/main_test.go @@ -14,6 +14,8 @@ import ( "testing" "golang.org/x/sys/unix" + + "blackcat.ca/coding-ethos/go/internal/sharedlock" ) func TestParseOptionsNormalizesPathsAndPreservesCommand(t *testing.T) { @@ -170,7 +172,10 @@ func TestSharedLockDirectoryMetadataAllowsOnlyTheValidatedCapabilityShape( if err != nil { t.Fatalf("inspect shared lock metadata: %v", err) } - if !validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { + if !sharedlock.ValidDirectoryMetadata( + "/var/tmp/coding-ethos-sandbox-lock-test", + info, + ) { t.Fatal("the exact direct-child mode-1777 capability shape was rejected") } @@ -181,12 +186,35 @@ func TestSharedLockDirectoryMetadataAllowsOnlyTheValidatedCapabilityShape( if err != nil { t.Fatalf("inspect non-sticky metadata: %v", err) } - if validSharedLockDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { + if sharedlock.ValidDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { t.Fatal("a non-sticky external directory became a sandbox write capability") } - if validSharedLockDirectoryMetadata("/tmp/coding-ethos-sandbox-lock-test", info) { + if sharedlock.ValidDirectoryMetadata("/tmp/coding-ethos-sandbox-lock-test", info) { t.Fatal("a path outside /var/tmp became a sandbox write capability") } + + if err = os.Chmod(shared, os.ModeSticky|0o755); err != nil { + t.Fatalf("narrow shared lock directory mode: %v", err) + } + info, err = os.Lstat(shared) + if err != nil { + t.Fatalf("inspect mode-1755 metadata: %v", err) + } + if sharedlock.ValidDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { + t.Fatal("a mode-1755 directory became a sandbox write capability") + } + + link := filepath.Join(t.TempDir(), "shared-lock-link") + if err = os.Symlink(shared, link); err != nil { + t.Fatalf("create shared lock symlink: %v", err) + } + info, err = os.Lstat(link) + if err != nil { + t.Fatalf("inspect shared lock symlink: %v", err) + } + if sharedlock.ValidDirectoryMetadata("/var/tmp/coding-ethos-sandbox-lock-test", info) { + t.Fatal("a symlink became a sandbox write capability") + } } func TestCleanPolicyPathAllowsCurrentTerminal(t *testing.T) { diff --git a/go/internal/sharedlock/metadata.go b/go/internal/sharedlock/metadata.go new file mode 100644 index 00000000..bebb058d --- /dev/null +++ b/go/internal/sharedlock/metadata.go @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Blackcat Informatics Inc. +// SPDX-License-Identifier: AGPL-3.0-only + +// Package sharedlock validates the narrow external lock-directory capability. +package sharedlock + +import ( + "os" + "path/filepath" +) + +// ValidDirectoryMetadata reports whether path and info describe the exact +// direct-child, non-symlink, mode-1777 /var/tmp capability used for shared locks. +func ValidDirectoryMetadata(path string, info os.FileInfo) bool { + mode := info.Mode() + + return filepath.IsAbs(path) && filepath.Dir(path) == "/var/tmp" && + info.IsDir() && mode&os.ModeSymlink == 0 && + mode.Perm() == 0o777 && mode&os.ModeSticky != 0 +} diff --git a/go/internal/tokeneconomy/benchmark_source.go b/go/internal/tokeneconomy/benchmark_source.go index ba03d9a7..6b645f08 100644 --- a/go/internal/tokeneconomy/benchmark_source.go +++ b/go/internal/tokeneconomy/benchmark_source.go @@ -66,7 +66,7 @@ func prepareBenchmarkWorkspace( for _, arguments := range [][]string{ {"init", "--quiet", "--initial-branch=benchmark"}, - {"add", "--all"}, + {"add", "--all", "--force"}, { "-c", "user.name=Coding Ethos Benchmark", "-c", "user.email=benchmark@invalid", diff --git a/go/internal/tokeneconomy/benchmark_test.go b/go/internal/tokeneconomy/benchmark_test.go index ef990846..3a982602 100644 --- a/go/internal/tokeneconomy/benchmark_test.go +++ b/go/internal/tokeneconomy/benchmark_test.go @@ -340,6 +340,72 @@ func TestRunCodexBenchmarkReportsHomeCleanupFailure(t *testing.T) { } } +func TestBenchmarkWorkspaceTracksIgnoredFiles(t *testing.T) { + t.Parallel() + + root := t.TempDir() + repository := filepath.Join(root, "source") + if err := os.Mkdir(repository, 0o700); err != nil { + t.Fatalf("create source repository: %v", err) + } + writeTestFile(t, filepath.Join(repository, ".gitignore"), "ignored-*.txt\n", 0o600) + writeTestFile( + t, + filepath.Join(repository, "ignored-baseline.txt"), + "baseline\n", + 0o600, + ) + runTestGit(t, repository, "init", "--quiet", "--initial-branch=main") + runTestGit(t, repository, "add", "--all", "--force") + runTestGit( + t, + repository, + "-c", + "user.name=Benchmark Test", + "-c", + "user.email=benchmark@invalid", + "-c", + "commit.gpgSign=false", + "commit", + "--quiet", + "--no-gpg-sign", + "-m", + "baseline", + ) + commit := strings.TrimSpace(runTestGit(t, repository, "rev-parse", "HEAD")) + archiveSHA, err := benchmarkSourceArchiveSHA256( + context.Background(), + repository, + commit, + ) + if err != nil { + t.Fatalf("hash source archive: %v", err) + } + + workspace, err := prepareBenchmarkWorkspace( + context.Background(), + BenchmarkTask{ + RepositoryPath: repository, + Commit: commit, + SourceArchiveSHA256: archiveSHA, + }, + filepath.Join(root, "workspace"), + ) + if err != nil { + t.Fatalf("prepare benchmark workspace: %v", err) + } + paths, err := benchmarkChangedPaths(context.Background(), workspace) + if err != nil || len(paths) != 0 { + t.Fatalf("frozen ignored baseline appeared changed: paths=%v error=%v", paths, err) + } + + writeTestFile(t, filepath.Join(workspace.Path, "ignored-new.txt"), "new\n", 0o600) + paths, err = benchmarkChangedPaths(context.Background(), workspace) + if err != nil || !slices.Equal(paths, []string{"ignored-new.txt"}) { + t.Fatalf("ignored workspace write was not tracked: paths=%v error=%v", paths, err) + } +} + func TestRunBenchmarkUsesIsolatedArmsAndResumesWithoutReplacement(t *testing.T) { fixture := newBenchmarkFixture(t) prepared, err := LoadBenchmarkManifest(context.Background(), fixture.manifestPath) diff --git a/go/internal/tokeneconomy/benchmark_validation.go b/go/internal/tokeneconomy/benchmark_validation.go index 0509fffa..a372e4ca 100644 --- a/go/internal/tokeneconomy/benchmark_validation.go +++ b/go/internal/tokeneconomy/benchmark_validation.go @@ -137,7 +137,6 @@ func benchmarkChangedPaths( workspace.Path, "ls-files", "--others", - "--exclude-standard", "-z", ) if err != nil { diff --git a/go/internal/tokeneconomy/ledger.go b/go/internal/tokeneconomy/ledger.go index d17c622d..d7f44091 100644 --- a/go/internal/tokeneconomy/ledger.go +++ b/go/internal/tokeneconomy/ledger.go @@ -352,6 +352,8 @@ func validateUsage(usage TokenUsage) error { func usageDecreased(previous, current TokenUsage) bool { return current.InputTokens < previous.InputTokens || current.CachedInputTokens < previous.CachedInputTokens || + current.CacheCreationInputTokens < previous.CacheCreationInputTokens || + current.CacheReadInputTokens < previous.CacheReadInputTokens || current.OutputTokens < previous.OutputTokens || current.ReasoningOutputTokens < previous.ReasoningOutputTokens || current.TotalTokens < previous.TotalTokens diff --git a/go/internal/tokeneconomy/ledger_test.go b/go/internal/tokeneconomy/ledger_test.go index ab6269de..30b138f6 100644 --- a/go/internal/tokeneconomy/ledger_test.go +++ b/go/internal/tokeneconomy/ledger_test.go @@ -54,6 +54,28 @@ func TestParseCodexLedgerRejectsDecreasingCumulativeUsage(t *testing.T) { } } +func TestUsageDecreasedCoversCumulativeCacheCounters(t *testing.T) { + t.Parallel() + + previous := TokenUsage{ + CacheCreationInputTokens: 20, + CacheReadInputTokens: 30, + } + for name, current := range map[string]TokenUsage{ + "cache creation": {CacheCreationInputTokens: 19, CacheReadInputTokens: 30}, + "cache read": {CacheCreationInputTokens: 20, CacheReadInputTokens: 29}, + } { + if !usageDecreased(previous, current) { + t.Errorf( + "%s decrease was accepted: previous=%#v current=%#v", + name, + previous, + current, + ) + } + } +} + func TestParseClaudeLedgerDeduplicatesMessagesAndExcludesSyntheticUsage(t *testing.T) { t.Parallel() diff --git a/go/internal/tokeneconomy/mechanisms.go b/go/internal/tokeneconomy/mechanisms.go index e1f8aa20..aa806a8a 100644 --- a/go/internal/tokeneconomy/mechanisms.go +++ b/go/internal/tokeneconomy/mechanisms.go @@ -88,7 +88,10 @@ func queryRunMechanisms( return MechanismMetrics{}, fmt.Errorf("query run mechanism evidence: %w", err) } - metrics.AvoidedContextTokens = metrics.RawContextTokens - metrics.DeliveredContextTokens + metrics.AvoidedContextTokens = max( + metrics.RawContextTokens-metrics.DeliveredContextTokens, + 0, + ) return metrics, nil } diff --git a/go/internal/tokeneconomy/report.go b/go/internal/tokeneconomy/report.go index a24ef54f..14d6279e 100644 --- a/go/internal/tokeneconomy/report.go +++ b/go/internal/tokeneconomy/report.go @@ -478,6 +478,9 @@ func comparisonFor( } func comparisonConclusion(comparison Comparison) Conclusion { + if !comparison.PrecisionMet { + return ConclusionInconclusive + } if comparison.SavingsPercentInterval.Lower > 0 { if !comparison.QualityNonInferior { return ConclusionQualityTradeoff @@ -488,11 +491,8 @@ func comparisonConclusion(comparison Comparison) Conclusion { if comparison.SavingsPercentInterval.Upper < 0 { return ConclusionTokenRegression } - if comparison.PrecisionMet { - return ConclusionNoDifference - } - return ConclusionInconclusive + return ConclusionNoDifference } func sha256Bytes(payload []byte) [32]byte { diff --git a/go/internal/tokeneconomy/report_test.go b/go/internal/tokeneconomy/report_test.go index ea648d1d..9fc46144 100644 --- a/go/internal/tokeneconomy/report_test.go +++ b/go/internal/tokeneconomy/report_test.go @@ -481,6 +481,25 @@ func TestExperimentReportDoesNotTreatZeroAcceptedRunsAsPrecise(t *testing.T) { } } +func TestComparisonConclusionRequiresPrecisionForDirectionalClaims(t *testing.T) { + t.Parallel() + + for name, interval := range map[string]Interval{ + "savings": {Lower: 0.1, Upper: 100}, + "regression": {Lower: -100, Upper: -0.1}, + } { + comparison := Comparison{ + SavingsPercentInterval: interval, + QualityNonInferior: true, + } + if conclusion := comparisonConclusion( + comparison, + ); conclusion != ConclusionInconclusive { + t.Errorf("imprecise %s conclusion = %q, want inconclusive", name, conclusion) + } + } +} + func TestWriteReportArtifactsIsCreateNewAndVerifiable(t *testing.T) { t.Parallel() @@ -654,7 +673,7 @@ func TestReadRunMechanismsMeasuresFirstTransformsAndRepeatedAdvice(t *testing.T) ('outside', 'session-2', 'payload_injection', 'same', 999)`, `INSERT INTO proxy_transforms VALUES ('e1', 0, 100), ('e1', 1, 200), ('e2', 0, 50), - ('e3', 0, 70), ('outside', 0, 999)`, + ('e3', 0, 70), ('outside', 0, 5)`, } { if _, err = database.Exec(statement); err != nil { _ = database.Close() @@ -680,6 +699,15 @@ func TestReadRunMechanismsMeasuresFirstTransformsAndRepeatedAdvice(t *testing.T) if metrics != want { t.Fatalf("mechanism metrics = %#v, want %#v", metrics, want) } + clamped, err := readRunMechanisms(context.Background(), path, "session-2") + if err != nil || clamped.RawContextTokens != 5 || + clamped.DeliveredContextTokens != 999 || clamped.AvoidedContextTokens != 0 { + t.Fatalf( + "negative avoided-token delta was not clamped: metrics=%#v error=%v", + clamped, + err, + ) + } } func createHistoricalFixture(t *testing.T) string { diff --git a/rust/purrdf-code-intel-extractor/PROTOCOL.md b/rust/purrdf-code-intel-extractor/PROTOCOL.md index 3f3ccd51..172d5795 100644 --- a/rust/purrdf-code-intel-extractor/PROTOCOL.md +++ b/rust/purrdf-code-intel-extractor/PROTOCOL.md @@ -105,9 +105,11 @@ null: Term tags are `iri`, `blank`, `literal`, `quoted_triple`, and `variable`. Literal terms carry `value`, nullable `datatype`, nullable `language`, and -nullable `direction`. A quoted-triple term recursively carries `subject`, an IRI -string `predicate`, and `object`. Blank values include the `_:` prefix; variable -values omit the `?` or `$` sigil. +nullable `direction`. A quoted-triple term recursively carries `subject`, a +string `predicate`, and `object`. The predicate is an IRI when concrete; a +SPARQL variable predicate retains its leading `?` sigil so it cannot be mistaken +for an IRI. Blank values include the `_:` prefix. Values in tagged `variable` +terms omit the `?` or `$` sigil. RDF fact kinds are `rdf_quad`, `rdf_reifier`, and `rdf_annotation`. SPARQL facts describe the parsed public algebra, including document/query form, triple and From 56fae7bc02b9ca0e2185062f4e7b221a8a221e04 Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 17:16:44 -0600 Subject: [PATCH 15/17] fix(review): enforce integration edge contracts --- go/internal/agenthooks/settings_probe.go | 21 +++-- go/internal/agenthooks/settings_probe_test.go | 20 +++++ go/internal/codeintel/source_v2_external.go | 30 +++++-- go/internal/codeintel/source_v2_test.go | 54 +++++++++--- go/internal/tokeneconomy/artifact.go | 14 +++- .../tokeneconomy/benchmark_manifest.go | 84 ++++++++++++++++++- go/internal/tokeneconomy/benchmark_runner.go | 23 +++-- go/internal/tokeneconomy/benchmark_test.go | 27 +++++- go/internal/tokeneconomy/historical.go | 6 +- go/internal/tokeneconomy/mechanisms.go | 11 ++- go/internal/tokeneconomy/report_test.go | 37 ++++++++ 11 files changed, 287 insertions(+), 40 deletions(-) diff --git a/go/internal/agenthooks/settings_probe.go b/go/internal/agenthooks/settings_probe.go index e6add7c3..d829ac80 100644 --- a/go/internal/agenthooks/settings_probe.go +++ b/go/internal/agenthooks/settings_probe.go @@ -699,13 +699,22 @@ func validateKimiStopContinuationProbe(result hookProbeResult) error { ) } - if _, found := nestedString( - result.payload, - "hookSpecificOutput", - "permissionDecision", - ); found { - return validateKimiStructuredDeny(result) + if hookOutput, found := result.payload["hookSpecificOutput"].(map[string]any); found { + if decision, present := hookOutput["permissionDecision"]; present { + if _, valid := decision.(string); !valid { + return apperror.Wrapf( + apperror.StaticError( + "Kimi Stop hook permissionDecision must be a string; stdout=%s", + ), + "Kimi Stop hook permissionDecision must be a string; stdout=%s", + result.stdout, + ) + } + + return validateKimiStructuredDeny(result) + } } + // A supervising hook may legitimately allow a clean Stop. Kimi still // receives a native, decoded response proving the hook ran; only a // supervisor that finds unfinished work needs to return the deny shape that diff --git a/go/internal/agenthooks/settings_probe_test.go b/go/internal/agenthooks/settings_probe_test.go index 9b472210..6b6a411d 100644 --- a/go/internal/agenthooks/settings_probe_test.go +++ b/go/internal/agenthooks/settings_probe_test.go @@ -91,3 +91,23 @@ func TestValidateKimiStopContinuationRejectsMissingResponse(t *testing.T) { t.Fatalf("error = %v, want missing native response", err) } } + +func TestValidateKimiStopContinuationRejectsBooleanPermissionDecision(t *testing.T) { + t.Parallel() + + err := validateKimiStopContinuationProbe(hookProbeResult{ + exitCode: 0, + stdout: "{\"message\":\"\",\"hookSpecificOutput\":" + + "{\"permissionDecision\":true}}\n", + payload: map[string]any{ + "message": "", + "hookSpecificOutput": map[string]any{ + "permissionDecision": true, + }, + }, + }) + if err == nil || + !strings.Contains(err.Error(), "permissionDecision must be a string") { + t.Fatalf("error = %v, want non-string permissionDecision rejection", err) + } +} diff --git a/go/internal/codeintel/source_v2_external.go b/go/internal/codeintel/source_v2_external.go index c6030bdc..d1348f5b 100644 --- a/go/internal/codeintel/source_v2_external.go +++ b/go/internal/codeintel/source_v2_external.go @@ -427,7 +427,7 @@ func validateExternalExtractorFact( return err } - return validateExternalFactStart(result.Path, provenance) + return validateExternalFactStart(result.Path, fact.Subject, provenance) } func validateExternalFactProvenance( @@ -461,6 +461,7 @@ func validateExternalFactProvenance( func validateExternalFactStart( path string, + subject json.RawMessage, provenance ExternalExtractorProvenance, ) error { if provenance.SpanFidelity == externalExtractorSpanFidelityNone && @@ -472,10 +473,16 @@ func validateExternalFactStart( ) } - if provenance.SpanFidelity == externalExtractorSpanFidelitySubjectStart && - (provenance.Start == nil || provenance.Start.ByteOffset < 0 || - provenance.Start.Line < 1 || - provenance.Start.Column < 1) { + if provenance.SpanFidelity != externalExtractorSpanFidelitySubjectStart { + return nil + } + + if provenance.Start == nil && externalFactHasQuotedTripleSubject(subject) { + return nil + } + + if provenance.Start == nil || provenance.Start.ByteOffset < 0 || + provenance.Start.Line < 1 || provenance.Start.Column < 1 { return fmt.Errorf( "%w: invalid external fact start position for %q", errInvalidExternalExtractorResponse, @@ -485,3 +492,16 @@ func validateExternalFactStart( return nil } + +func externalFactHasQuotedTripleSubject(subject json.RawMessage) bool { + var term struct { + Kind string `json:"kind"` + } + + err := json.Unmarshal(subject, &term) + if err != nil { + return false + } + + return term.Kind == "quoted_triple" +} diff --git a/go/internal/codeintel/source_v2_test.go b/go/internal/codeintel/source_v2_test.go index 37aac638..f7dd550d 100644 --- a/go/internal/codeintel/source_v2_test.go +++ b/go/internal/codeintel/source_v2_test.go @@ -36,7 +36,6 @@ func (extractor *sourceV2FakeExtractor) Extract( results := make([]ExternalExtractorResult, 0, len(files)) for _, file := range files { language := astfacts.LanguageTurtle - parser := externalExtractorParserRDF switch filepath.Ext(file.Path) { case ".trig": language = astfacts.LanguageTriG @@ -46,7 +45,14 @@ func (extractor *sourceV2FakeExtractor) Extract( language = astfacts.LanguageNQuads case ".rq", ".sparql": language = astfacts.LanguageSPARQL - parser = externalExtractorParserSPARQL + } + parser, spanFidelity, err := externalFactContract(language) + if err != nil { + return ExternalExtractorResponse{}, err + } + start := &ExternalExtractorPosition{Line: 1, Column: 1} + if spanFidelity == externalExtractorSpanFidelityNone { + start = nil } results = append(results, ExternalExtractorResult{ @@ -58,11 +64,15 @@ func (extractor *sourceV2FakeExtractor) Extract( Facts: []ExternalExtractorFact{{ ID: "fact:sha256:fixture", Kind: "triple", + Subject: json.RawMessage( + `{"kind":"iri","value":"https://example.test/subject"}`, + ), Provenance: ExternalExtractorProvenance{ + Start: start, Class: "EXTRACTED", Parser: parser, ParserRevision: astfacts.PurrdfExtractorRevision, - SpanFidelity: externalExtractorSpanFidelitySubjectStart, + SpanFidelity: spanFidelity, SourcePath: file.Path, }, }}, @@ -105,6 +115,7 @@ func TestSyncSourceIndexBuildsReusableBaseLaneDeltaAndExactQuery(t *testing.T) { "ontology.ttl", "@prefix ex: .\nex:s ex:p ex:o .\n", ) + writeSourceV2TestFile(t, root, "query.rq", "SELECT * WHERE { ?s ?p ?o }\n") legacyPath := filepath.Join(root, ".coding-ethos", "code-intel.duckdb") writeSourceV2TestFile(t, root, ".coding-ethos/code-intel.duckdb", "legacy-sentinel") @@ -139,6 +150,12 @@ func TestSyncSourceIndexBuildsReusableBaseLaneDeltaAndExactQuery(t *testing.T) { got.Indexed != 1 { t.Fatalf("Turtle coverage = %#v", got) } + if got := sourceV2CoverageForLanguage( + first.SourceReadiness.Coverage, + astfacts.LanguageSPARQL, + ); got.Eligible != 1 || got.Indexed != 1 { + t.Fatalf("SPARQL coverage = %#v", got) + } if extractor.calls != 1 || extractor.validations != 1 { t.Fatalf( "external extractor calls = %d validations = %d", @@ -732,6 +749,14 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { if err := validateExternalExtractorFacts(validSPARQL); err != nil { t.Fatalf("valid SPARQL facts: %v", err) } + quotedTriple := validExternalExtractorResult(astfacts.LanguageTurtle) + quotedTriple.Facts[0].Subject = json.RawMessage( + `{"kind":"quoted_triple","subject":{"kind":"iri","value":"https://example.test/s"},"predicate":"https://example.test/p","object":{"kind":"iri","value":"https://example.test/o"}}`, + ) + quotedTriple.Facts[0].Provenance.Start = nil + if err := validateExternalExtractorFacts(quotedTriple); err != nil { + t.Fatalf("valid quoted-triple subject without a start: %v", err) + } unidentified := validExternalExtractorResult(astfacts.LanguageTurtle) unidentified.Facts[0].ID = "" @@ -741,8 +766,8 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { wrongParser.Facts[0].Provenance.Parser = "wrong-parser" wrongFidelity := validExternalExtractorResult(astfacts.LanguageTurtle) wrongFidelity.Facts[0].Provenance.SpanFidelity = externalExtractorSpanFidelityNone - missingStart := validExternalExtractorResult(astfacts.LanguageTurtle) - missingStart.Facts[0].Provenance.Start = nil + nonQuotedMissingStart := validExternalExtractorResult(astfacts.LanguageTurtle) + nonQuotedMissingStart.Facts[0].Provenance.Start = nil invalidStart := validExternalExtractorResult(astfacts.LanguageTurtle) invalidStart.Facts[0].Provenance.Start = &ExternalExtractorPosition{ ByteOffset: -1, @@ -756,14 +781,14 @@ func TestExternalExtractorFactsEnforceSemanticProvenanceAndSpans(t *testing.T) { } for name, result := range map[string]ExternalExtractorResult{ - "unidentified": unidentified, - "duplicate": duplicate, - "wrong parser": wrongParser, - "wrong fidelity": wrongFidelity, - "missing start": missingStart, - "invalid start": invalidStart, - "span-free start": spanFreeStart, - "unknown language": {Language: "python"}, + "unidentified": unidentified, + "duplicate": duplicate, + "wrong parser": wrongParser, + "wrong fidelity": wrongFidelity, + "non-quoted missing start": nonQuotedMissingStart, + "invalid start": invalidStart, + "span-free start": spanFreeStart, + "unknown language": {Language: "python"}, } { if err := validateExternalExtractorFacts(result); err == nil { t.Errorf("%s external facts unexpectedly passed", name) @@ -792,6 +817,9 @@ func validExternalExtractorResult(language string) ExternalExtractorResult { Facts: []ExternalExtractorFact{{ ID: "fact:sha256:fixture", Kind: "semantic", + Subject: json.RawMessage( + `{"kind":"iri","value":"https://example.test/subject"}`, + ), Provenance: ExternalExtractorProvenance{ Start: start, Class: "EXTRACTED", diff --git a/go/internal/tokeneconomy/artifact.go b/go/internal/tokeneconomy/artifact.go index 0015990d..d4c7fde3 100644 --- a/go/internal/tokeneconomy/artifact.go +++ b/go/internal/tokeneconomy/artifact.go @@ -120,10 +120,22 @@ func writeExclusiveArtifact(path string, payload []byte) error { return fmt.Errorf("create token-economy artifact %s: %w", path, err) } + return writeExclusiveArtifactFile(path, file, payload) +} + +func writeExclusiveArtifactFile(path string, file *os.File, payload []byte) error { _, writeErr := file.Write(payload) syncErr := file.Sync() closeErr := file.Close() - if err = errors.Join(writeErr, syncErr, closeErr); err != nil { + if err := errors.Join(writeErr, syncErr, closeErr); err != nil { + removeErr := os.Remove(path) + if removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) { + err = errors.Join( + err, + fmt.Errorf("remove partial token-economy artifact %s: %w", path, removeErr), + ) + } + return fmt.Errorf("write token-economy artifact %s: %w", path, err) } diff --git a/go/internal/tokeneconomy/benchmark_manifest.go b/go/internal/tokeneconomy/benchmark_manifest.go index a78946f5..27d3b31d 100644 --- a/go/internal/tokeneconomy/benchmark_manifest.go +++ b/go/internal/tokeneconomy/benchmark_manifest.go @@ -39,10 +39,12 @@ const ( ) var ( - errInvalidBenchmarkManifest = errors.New("invalid token-economy benchmark manifest") - benchmarkIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) - benchmarkCommitPattern = regexp.MustCompile(`^[a-f0-9]{40}$`) - hexSHA256Pattern = regexp.MustCompile(`^[a-f0-9]{64}$`) + errInvalidBenchmarkManifest = errors.New("invalid token-economy benchmark manifest") + errInvalidFullConfigOverride = errors.New("invalid benchmark full config override") + benchmarkIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) + benchmarkCommitPattern = regexp.MustCompile(`^[a-f0-9]{40}$`) + benchmarkConfigKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) + hexSHA256Pattern = regexp.MustCompile(`^[a-f0-9]{64}$`) ) // BenchmarkManifest is the versioned YAML experiment contract. @@ -533,6 +535,14 @@ func validateFullConfigOverrides(overrides []string) error { ) } } + if _, err := canonicalFullConfigOverride(override); err != nil { + return fmt.Errorf( + "%w: full config override %d: %w", + errInvalidBenchmarkManifest, + index+1, + err, + ) + } for _, path := range leafPaths { identity := strings.Join(path, "\x00") @@ -550,6 +560,72 @@ func validateFullConfigOverrides(overrides []string) error { return nil } +func canonicalFullConfigOverride(override string) (string, error) { + config := map[string]any{} + if strings.TrimSpace(override) == "" || + toml.Unmarshal([]byte(override), &config) != nil || len(config) == 0 { + return "", fmt.Errorf("%w: invalid TOML", errInvalidFullConfigOverride) + } + + _, leafPaths := benchmarkConfigPaths(config, nil) + if len(leafPaths) != 1 { + return "", fmt.Errorf( + "%w: must define exactly one setting", + errInvalidFullConfigOverride, + ) + } + path := leafPaths[0] + for _, segment := range path { + if !benchmarkConfigKeyPattern.MatchString(segment) { + return "", fmt.Errorf( + "%w: uses a key that cannot be forwarded safely", + errInvalidFullConfigOverride, + ) + } + } + + value, found := benchmarkConfigValue(config, path) + if !found { + return "", fmt.Errorf( + "%w: does not resolve to its parsed setting", + errInvalidFullConfigOverride, + ) + } + encoded, err := toml.Marshal(map[string]any{"value": value}) + if err != nil { + return "", fmt.Errorf("cannot encode its parsed value: %w", err) + } + line := strings.TrimSuffix(string(encoded), "\n") + const valuePrefix = "value = " + if !strings.HasPrefix(line, valuePrefix) || strings.Contains(line, "\n") { + return "", fmt.Errorf( + "%w: uses a value that cannot be forwarded safely", + errInvalidFullConfigOverride, + ) + } + + return strings.Join(path, ".") + "=" + strings.TrimPrefix(line, valuePrefix), nil +} + +func benchmarkConfigValue(config map[string]any, path []string) (any, bool) { + current := config + for index, segment := range path { + value, found := current[segment] + if !found { + return nil, false + } + if index == len(path)-1 { + return value, true + } + current, found = value.(map[string]any) + if !found { + return nil, false + } + } + + return nil, false +} + func benchmarkConfigPaths( config map[string]any, prefix []string, diff --git a/go/internal/tokeneconomy/benchmark_runner.go b/go/internal/tokeneconomy/benchmark_runner.go index 1dc2b7fc..30ecfd4b 100644 --- a/go/internal/tokeneconomy/benchmark_runner.go +++ b/go/internal/tokeneconomy/benchmark_runner.go @@ -545,6 +545,12 @@ func runCodexBenchmarkWithHomeRemover( return finish() } + arguments, err := benchmarkCodexArguments(manifest, spec.Arm, workspace) + if err != nil { + outcome.Errors = append(outcome.Errors, err.Error()) + + return finish() + } stdout, stderr, err := createAgentLogs(runRoot) if err != nil { @@ -556,7 +562,6 @@ func runCodexBenchmarkWithHomeRemover( runContext, cancel := context.WithTimeout(ctx, agentTimeout) defer cancel() - arguments := benchmarkCodexArguments(manifest, spec.Arm, workspace) command := safeexec.CommandContext( runContext, manifest.Provider.Executable, @@ -670,7 +675,7 @@ func benchmarkCodexArguments( manifest BenchmarkManifest, arm Arm, workspace string, -) []string { +) ([]string, error) { arguments := []string{ "exec", "--json", @@ -693,12 +698,20 @@ func benchmarkCodexArguments( "sandbox_workspace_write.network_access=false", } if arm == ArmFull { - for _, override := range manifest.FullConfigOverrides { - arguments = append(arguments, codexConfigFlag, override) + for index, override := range manifest.FullConfigOverrides { + canonical, err := canonicalFullConfigOverride(override) + if err != nil { + return nil, fmt.Errorf( + "canonicalize full config override %d: %w", + index+1, + err, + ) + } + arguments = append(arguments, codexConfigFlag, canonical) } } - return append(arguments, "-") + return append(arguments, "-"), nil } func benchmarkAgentEnvironment( diff --git a/go/internal/tokeneconomy/benchmark_test.go b/go/internal/tokeneconomy/benchmark_test.go index 3a982602..c59429d3 100644 --- a/go/internal/tokeneconomy/benchmark_test.go +++ b/go/internal/tokeneconomy/benchmark_test.go @@ -218,17 +218,42 @@ func TestBenchmarkEnvironmentUsesBenignAllowlist(t *testing.T) { func TestBenchmarkCodexArgumentsDisableSandboxNetwork(t *testing.T) { t.Parallel() - arguments := benchmarkCodexArguments(BenchmarkManifest{ + arguments, err := benchmarkCodexArguments(BenchmarkManifest{ Provider: BenchmarkProvider{ Model: "fixed-model", ReasoningEffort: "high", }, }, ArmOff, "/frozen/workspace") + if err != nil { + t.Fatalf("build Codex benchmark arguments: %v", err) + } if !slices.Contains(arguments, "sandbox_workspace_write.network_access=false") { t.Fatalf("Codex benchmark arguments do not disable sandbox network: %#v", arguments) } } +func TestBenchmarkCodexArgumentsCanonicalizeFullOverrides(t *testing.T) { + t.Parallel() + + manifest := BenchmarkManifest{ + Provider: BenchmarkProvider{ + Model: "fixed-model", + ReasoningEffort: "high", + }, + FullConfigOverrides: []string{` features . "code_intel" = true `}, + } + arguments, err := benchmarkCodexArguments(manifest, ArmFull, "/frozen/workspace") + if err != nil { + t.Fatalf("build Codex benchmark arguments: %v", err) + } + if !slices.Contains(arguments, "features.code_intel=true") { + t.Fatalf("Codex benchmark arguments omit canonical override: %#v", arguments) + } + if slices.Contains(arguments, manifest.FullConfigOverrides[0]) { + t.Fatalf("Codex benchmark arguments forwarded raw TOML: %#v", arguments) + } +} + func TestValidateFullConfigOverridesParsesTOMLStructure(t *testing.T) { t.Parallel() diff --git a/go/internal/tokeneconomy/historical.go b/go/internal/tokeneconomy/historical.go index 33e50ee3..41b74d26 100644 --- a/go/internal/tokeneconomy/historical.go +++ b/go/internal/tokeneconomy/historical.go @@ -547,8 +547,10 @@ func (aggregate *historicalAggregate) finish( aggregate.metrics.Sources = sources aggregate.metrics.ProxySessions = len(aggregate.sessionOwners) - aggregate.metrics.AvoidedContextTokens = aggregate.metrics.RawContextTokens - - aggregate.metrics.DeliveredContextTokens + aggregate.metrics.AvoidedContextTokens = avoidedContextTokens( + aggregate.metrics.RawContextTokens, + aggregate.metrics.DeliveredContextTokens, + ) if aggregate.metrics.RawContextTokens > 0 { aggregate.metrics.GrossReductionPercent = percentageScale * diff --git a/go/internal/tokeneconomy/mechanisms.go b/go/internal/tokeneconomy/mechanisms.go index aa806a8a..e7adb350 100644 --- a/go/internal/tokeneconomy/mechanisms.go +++ b/go/internal/tokeneconomy/mechanisms.go @@ -88,10 +88,15 @@ func queryRunMechanisms( return MechanismMetrics{}, fmt.Errorf("query run mechanism evidence: %w", err) } - metrics.AvoidedContextTokens = max( - metrics.RawContextTokens-metrics.DeliveredContextTokens, - 0, + metrics.AvoidedContextTokens = avoidedContextTokens( + metrics.RawContextTokens, + metrics.DeliveredContextTokens, ) return metrics, nil } + +// avoidedContextTokens treats context expansion as zero savings, never negative savings. +func avoidedContextTokens(rawContextTokens, deliveredContextTokens int64) int64 { + return max(rawContextTokens-deliveredContextTokens, 0) +} diff --git a/go/internal/tokeneconomy/report_test.go b/go/internal/tokeneconomy/report_test.go index 9fc46144..75b8685e 100644 --- a/go/internal/tokeneconomy/report_test.go +++ b/go/internal/tokeneconomy/report_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "database/sql" + "errors" "fmt" "html" "os" @@ -541,6 +542,27 @@ func TestWriteReportArtifactsIsCreateNewAndVerifiable(t *testing.T) { } } +func TestWriteExclusiveArtifactRemovesPartialFileAfterWriteFailure(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "partial.json") + file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, storeFileMode) + if err != nil { + t.Fatalf("create partial artifact fixture: %v", err) + } + if err = file.Close(); err != nil { + t.Fatalf("close partial artifact fixture: %v", err) + } + + err = writeExclusiveArtifactFile(path, file, []byte("payload")) + if err == nil { + t.Fatal("write through a closed artifact unexpectedly succeeded") + } + if _, statErr := os.Stat(path); !errors.Is(statErr, os.ErrNotExist) { + t.Fatalf("partial artifact survived failed write: %v", statErr) + } +} + func TestFormatReportMarkdownIncludesVerifiedContextAndComparisonQuality(t *testing.T) { t.Parallel() @@ -710,6 +732,21 @@ func TestReadRunMechanismsMeasuresFirstTransformsAndRepeatedAdvice(t *testing.T) } } +func TestHistoricalAggregateTreatsExpansionAsZeroSavings(t *testing.T) { + t.Parallel() + + aggregate := historicalAggregate{ + metrics: HistoricalMetrics{ + RawContextTokens: 5, + DeliveredContextTokens: 9, + }, + } + metrics, _ := aggregate.finish(historicalWindow{}, nil) + if metrics.AvoidedContextTokens != 0 || metrics.GrossReductionPercent != 0 { + t.Fatalf("historical expansion reported negative savings: %#v", metrics) + } +} + func createHistoricalFixture(t *testing.T) string { t.Helper() From fd78c93d7332b8ea2dcd0b016147fbfd98ebd71a Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 17:23:06 -0600 Subject: [PATCH 16/17] refactor(token-economy): isolate override validation --- .../tokeneconomy/benchmark_manifest.go | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/go/internal/tokeneconomy/benchmark_manifest.go b/go/internal/tokeneconomy/benchmark_manifest.go index 27d3b31d..c8210f71 100644 --- a/go/internal/tokeneconomy/benchmark_manifest.go +++ b/go/internal/tokeneconomy/benchmark_manifest.go @@ -522,18 +522,8 @@ func validateFullConfigOverrides(overrides []string) error { } paths, leafPaths := benchmarkConfigPaths(config, nil) - for _, path := range paths { - for _, controlledPath := range controlledPaths { - if !benchmarkConfigPathsRelated(path, controlledPath) { - continue - } - - return fmt.Errorf( - "%w: full config override changes controlled setting %q", - errInvalidBenchmarkManifest, - strings.Join(controlledPath, "."), - ) - } + if err := validateControlledFullConfigPaths(paths, controlledPaths); err != nil { + return err } if _, err := canonicalFullConfigOverride(override); err != nil { return fmt.Errorf( @@ -560,6 +550,24 @@ func validateFullConfigOverrides(overrides []string) error { return nil } +func validateControlledFullConfigPaths(paths, controlledPaths [][]string) error { + for _, path := range paths { + for _, controlledPath := range controlledPaths { + if !benchmarkConfigPathsRelated(path, controlledPath) { + continue + } + + return fmt.Errorf( + "%w: full config override changes controlled setting %q", + errInvalidBenchmarkManifest, + strings.Join(controlledPath, "."), + ) + } + } + + return nil +} + func canonicalFullConfigOverride(override string) (string, error) { config := map[string]any{} if strings.TrimSpace(override) == "" || From a7e2b946f02f8f778b9f3d4fc87e1c903711841c Mon Sep 17 00:00:00 2001 From: Patrick_Audley Date: Sat, 29 Aug 2026 17:48:24 -0600 Subject: [PATCH 17/17] fix(agent-hooks): reject malformed Kimi hook output --- go/internal/agenthooks/settings_probe.go | 51 ++++++++++++++----- go/internal/agenthooks/settings_probe_test.go | 17 +++++++ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/go/internal/agenthooks/settings_probe.go b/go/internal/agenthooks/settings_probe.go index d829ac80..40ae1f3b 100644 --- a/go/internal/agenthooks/settings_probe.go +++ b/go/internal/agenthooks/settings_probe.go @@ -699,22 +699,45 @@ func validateKimiStopContinuationProbe(result hookProbeResult) error { ) } - if hookOutput, found := result.payload["hookSpecificOutput"].(map[string]any); found { - if decision, present := hookOutput["permissionDecision"]; present { - if _, valid := decision.(string); !valid { - return apperror.Wrapf( - apperror.StaticError( - "Kimi Stop hook permissionDecision must be a string; stdout=%s", - ), - "Kimi Stop hook permissionDecision must be a string; stdout=%s", - result.stdout, - ) - } - - return validateKimiStructuredDeny(result) - } + hookOutputValue, present := result.payload["hookSpecificOutput"] + if present { + return validateKimiStopHookOutput(result, hookOutputValue) + } + + return validateKimiStopMessage(result) +} + +func validateKimiStopHookOutput(result hookProbeResult, value any) error { + hookOutput, valid := value.(map[string]any) + if !valid { + return apperror.Wrapf( + apperror.StaticError( + "Kimi Stop hook hookSpecificOutput must be an object; stdout=%s", + ), + "Kimi Stop hook hookSpecificOutput must be an object; stdout=%s", + result.stdout, + ) + } + + decision, present := hookOutput["permissionDecision"] + if !present { + return validateKimiStopMessage(result) } + if _, valid = decision.(string); !valid { + return apperror.Wrapf( + apperror.StaticError( + "Kimi Stop hook permissionDecision must be a string; stdout=%s", + ), + "Kimi Stop hook permissionDecision must be a string; stdout=%s", + result.stdout, + ) + } + + return validateKimiStructuredDeny(result) +} + +func validateKimiStopMessage(result hookProbeResult) error { // A supervising hook may legitimately allow a clean Stop. Kimi still // receives a native, decoded response proving the hook ran; only a // supervisor that finds unfinished work needs to return the deny shape that diff --git a/go/internal/agenthooks/settings_probe_test.go b/go/internal/agenthooks/settings_probe_test.go index 6b6a411d..4590c9c9 100644 --- a/go/internal/agenthooks/settings_probe_test.go +++ b/go/internal/agenthooks/settings_probe_test.go @@ -111,3 +111,20 @@ func TestValidateKimiStopContinuationRejectsBooleanPermissionDecision(t *testing t.Fatalf("error = %v, want non-string permissionDecision rejection", err) } } + +func TestValidateKimiStopContinuationRejectsNonObjectHookOutput(t *testing.T) { + t.Parallel() + + err := validateKimiStopContinuationProbe(hookProbeResult{ + exitCode: 0, + stdout: "{\"message\":\"\",\"hookSpecificOutput\":true}\n", + payload: map[string]any{ + "message": "", + "hookSpecificOutput": true, + }, + }) + if err == nil || + !strings.Contains(err.Error(), "hookSpecificOutput must be an object") { + t.Fatalf("error = %v, want non-object hookSpecificOutput rejection", err) + } +}