From 456e1e1c28de25a68252d35a282114b6fd054436 Mon Sep 17 00:00:00 2001 From: Curry Date: Mon, 13 Jul 2026 11:00:44 +0800 Subject: [PATCH 01/34] feat: define atomic capability contract v1 --- .github/workflows/ci.yml | 10 + CONTEXT.md | 21 ++ .../0009-atomic-capability-execution-model.md | 50 ++++ docs/atomic-development-model.md | 100 +++++++ docs/code-intel-architecture.md | 4 + orchestration/capability-contract.v1.json | 77 +++++ orchestration/integrations.json | 1 + ...e-intel-capability-envelope.v1.schema.json | 208 ++++++++++++++ test-atomic-capability-contract.ps1 | 267 ++++++++++++++++++ 9 files changed, 738 insertions(+) create mode 100644 docs/adr/0009-atomic-capability-execution-model.md create mode 100644 docs/atomic-development-model.md create mode 100644 orchestration/capability-contract.v1.json create mode 100644 orchestration/schemas/code-intel-capability-envelope.v1.schema.json create mode 100644 test-atomic-capability-contract.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51a9312..0081243 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: "test-code-intel-pipeline.ps1", "test-github-solution-research.ps1", "test-hospital-trust-contract.ps1", + "test-atomic-capability-contract.ps1", "test-project-discovery.ps1", "test-project-management-support.ps1", "test-scoped-repowise-security.ps1", @@ -136,6 +137,10 @@ jobs: shell: pwsh run: .\test-project-management-support.ps1 -RepoPath . + - name: Atomic capability contract tests + shell: pwsh + run: .\test-atomic-capability-contract.ps1 -RepoPath . + - name: Skill development benchmark contract tests shell: pwsh run: .\test-skill-development-benchmark.ps1 -RepoPath . @@ -237,6 +242,7 @@ jobs: "test-code-intel-pipeline.ps1", "test-github-solution-research.ps1", "test-hospital-trust-contract.ps1", + "test-atomic-capability-contract.ps1", "test-project-discovery.ps1", "test-project-management-support.ps1", "test-scoped-repowise-security.ps1", @@ -309,6 +315,10 @@ jobs: shell: pwsh run: ./test-hospital-trust-contract.ps1 + - name: Atomic capability contract tests + shell: pwsh + run: ./test-atomic-capability-contract.ps1 -RepoPath . + - name: Scoped Repowise manifest validator tests run: python ./test-scoped-repowise-validator.py diff --git a/CONTEXT.md b/CONTEXT.md index 1a30c32..bd6c024 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -43,6 +43,27 @@ _Avoid_: Rust scanner, new pipeline, replacement scanner **Artifact Run**: The durable record produced by one scanner execution for one target repository. It is the unit that later tools resume from, classify, or hand to an Agent. _Avoid_: Scan folder, output dump, report batch +**Capability Atom**: One independently executable responsibility with a versioned request, a versioned result, declared effects, and independently verifiable artifacts. +_Avoid_: Function, microservice, tiny script + +**Snapshot Identity**: Portable identity of the repository inputs consumed by a Capability Atom: repository identity, HEAD, working-tree policy, scope, and input digest. +_Avoid_: Timestamp, checkout path, branch name + +**Artifact Ref**: A typed reference carrying its own envelope version, the referenced payload schema, location, SHA-256 content identity, and consumed Snapshot Identity between Capability Atoms. +_Avoid_: File path, inline evidence dump, latest output + +**Effect Boundary**: Pre-execution permission boundary plus post-execution audit for a Capability Atom. Determinism is separate from the allowed and observed effects: repository read, local write, network, or repository mutation. +_Avoid_: Tool type, permission prompt, implementation language + +**Domain Verdict**: Evidence judgment returned by a completed capability: pass, fail, unknown, or not applicable. It is independent of process execution status. +_Avoid_: Exit code, exception, health score + +**Run Commit**: Transactional publication boundary that promotes validated staged artifacts and writes `run-complete.json` last. +_Avoid_: Git commit, timestamp directory, successful subprocess + +**Materialized View**: Rebuildable human or index projection derived from machine artifacts, such as summary Markdown or the cross-repository index. +_Avoid_: Source of truth, artifact producer, mutable task state + **Artifact Data Contract**: The stable meaning and ownership of generated artifact files and fields. _Avoid_: JSON shape, output format, file schema diff --git a/docs/adr/0009-atomic-capability-execution-model.md b/docs/adr/0009-atomic-capability-execution-model.md new file mode 100644 index 0000000..5458f2f --- /dev/null +++ b/docs/adr/0009-atomic-capability-execution-model.md @@ -0,0 +1,50 @@ +# ADR 0009: Adopt an Atomic Capability Execution Model + +- Status: accepted +- Date: 2026-07-13 + +## Context + +The trust-boundary work made Hospital and scoped Repowise fail closed, but real project runs show a second-order problem: the pipeline can collect valid signals while losing identity, causality, or phase boundaries during orchestration. + +Examples include stale graph files treated as current anatomy, Sentrux rule failures normalized only partially, enrichment selecting a surgery target while authoritative diagnosis is untrusted, and Repowise combining successful indexing with optional global hook installation in one process outcome. + +The current system is conceptually layered but operationally monolithic. `run-code-intel.ps1` and `Invoke-SentruxAgentTool.ps1` still combine collection, normalization, policy, rendering, and publication. A full rewrite would mix language migration with boundary repair and would be difficult to verify. + +## Decision + +Adopt the machine contract in `orchestration/capability-contract.v1.json` and the vocabulary in `docs/atomic-development-model.md`. + +Every **Capability Atom** moving across the orchestration boundary will converge on: + +- one versioned request envelope; +- one versioned result envelope; +- Snapshot Identity bound to source inputs; +- Artifact Refs with payload schema, SHA-256 content identity, and consumed Snapshot Identity; +- an Effect Boundary with determinism, pre-execution allowed effects, and post-execution observed effects; +- Domain Verdict separated from process status; +- deterministic cache-key inputs; +- staging plus a final Run Commit marker; +- rebuildable Materialized Views. + +Migration uses a strangler pattern. Existing scripts remain compatibility adapters until each atom has contract tests and parity evidence. Language migration is a separate decision. + +## Immediate Consequence + +ADR 0009 and A01 establish vocabulary, a machine-validatable JSON Schema, legal outcome combinations, and CI drift guards. They do not change current runtime execution, artifact publication, caching, portability, or side-effect enforcement. + +## Target Consequences After Subsequent Atoms + +- Individual capabilities will be rerunnable, cacheable, resumable, and independently testable. +- Cross-device artifacts will be verifiable by content and snapshot identity instead of absolute path. +- Capability declarations and request allowlists will let the orchestrator reject undeclared network, local-write, or repository-mutation effects. +- A tool will be able to complete successfully with a domain `fail` verdict without being mislabeled as a runtime crash. +- Transactional publication and the index reader guard will prevent incomplete runs from entering the artifact index. +- Hermetic execution, content-addressed artifacts, incremental caching, and portable transport remain separate atomic tickets. + +## Rejected Alternatives + +- **Big-bang Rust rewrite**: rejected because it couples language migration to behavioral decomposition. +- **Add a workflow engine**: rejected because the current graph is small and can be expressed in the existing registry. +- **Make every function a process**: rejected because large analysis payloads belong in artifacts, not pipes. +- **Use timestamps as run identity**: retained only as a human navigation view; content and snapshot digests become authority. diff --git a/docs/atomic-development-model.md b/docs/atomic-development-model.md new file mode 100644 index 0000000..66a87f9 --- /dev/null +++ b/docs/atomic-development-model.md @@ -0,0 +1,100 @@ +# Atomic Development Model + +Code Intel Pipeline adopts a Linux-style atomic development model: a capability is not atomic because its source file is small, but because its inputs are identifiable, its output is verifiable, its effects are declared, and its publication is safe. + +The canonical machine contract is `orchestration/capability-contract.v1.json`. + +## Current Maturity + +The pipeline already provides useful repository inventory, Code Evidence, structural checks, scoped Repowise egress controls, fail-closed Hospital routing, and cross-platform installation tests. It is useful today as a repository examination instrument and conservative gate. + +It is not yet an autonomous repair authority: + +- graph presence is not consistently bound to the current Git snapshot; +- Sentrux normalization does not yet cover every authoritative rule kind; +- a partial diagnosis can still compete with enrichment when selecting a surgery target; +- normal runs may invoke tools with undeclared local or global side effects; +- timestamped, machine-local artifact paths are not portable identity; +- there is no proven edit episode from `session_start` through regression rejection to post-op discharge. + +These are contract and composition problems, not a reason for a big-bang rewrite. + +## Seven Owned Concepts + +1. **Capability Atom** — one responsibility with one request and one result contract. +2. **Snapshot Identity** — repository identity, HEAD, working-tree policy, scope, and input digest. +3. **Artifact Ref** — `{schema, artifactSchema, type, path, sha256, consumedSnapshotIdentity}`; `schema` versions the reference envelope, `artifactSchema` identifies payload validation, content digest is identity, and path is location. +4. **Effect Boundary** — determinism is declared separately; `allowedEffects` is fixed before execution and `observedEffects` is audited afterward. Permission effects are `repo_read`, `local_write`, `network`, or `repo_mutation`. +5. **Domain Verdict** — `pass`, `fail`, `unknown`, or `not_applicable`, separate from process execution status. +6. **Run Commit** — validate in staging, promote atomically, then write `run-complete.json` last. +7. **Materialized View** — Markdown and indexes are rebuildable views over machine JSON, never fact authority. + +## Capability Graph + +```text +repo.snapshot +├── inventory.rg +├── memory.repowise +├── graph.understand +└── structure.sentrux.collect + └── structure.sentrux.normalize + └── localization.codenexus + +inventory + memory + graph + structure + localization + │ + ▼ + diagnosis.hospital + │ + ┌───────┴────────┐ + ▼ ▼ + view.render run.publish + │ + artifact.index +``` + +The target graph allows collection atoms to run concurrently. Diagnosis is deterministic from artifact references but its adapter may still declare `local_write` when it persists output. Rendering cannot change a verdict. Indexing will only see committed runs after the corresponding atoms land. + +## Process Contract + +The intended stable entrypoint is: + +```text +code-intel capability exec --request - --out +``` + +- stdin carries one request envelope or `--request` names a file; +- stdout carries exactly one result envelope; +- stderr carries human diagnostics; +- large evidence crosses boundaries by Artifact Ref; +- exit code distinguishes a domain failure from invalid input, unavailable dependency, internal error, or I/O error. + +Existing PowerShell, Python, and Rust implementations remain valid adapters while they converge on this contract. + +The v1 JSON Schema now validates declaration, request, result, and Artifact Ref envelopes, including declaration dependency ids and legal `status × verdict × exitCode` combinations. The contract also defines cross-envelope coherence for identity, implementation, determinism, snapshot, and effect allowlists. This is a control-plane contract only: current runtime adapters do not yet emit these envelopes or enforce those invariants. + +## Target Cache And Reproducibility + +The future cache key will be a SHA-256 over capability id, contract and implementation versions, Snapshot Identity, canonical options, ordered input Artifact Ref digests, and toolchain digests. Timestamps, machine paths, and output directories are attempt metadata and will not enter deterministic identity. + +Network-backed results will also bind provider, model, prompt/config digest, and network policy. They will be explicitly `external_nondeterministic` in provenance even when cached. + +## Atomic Migration + +1. Freeze current behavior with golden artifacts. +2. Land Capability Envelope v1 without moving implementations. +3. Add the atomic artifact writer and Snapshot Identity. +4. Extract `inventory.rg` and pure Hospital diagnosis first. +5. Wrap Repowise, Understand, and Sentrux behind effect-declaring adapters. +6. Add node-level cache, resume, and red-green invalidation. +7. Separate rendering, transactional publish, and indexing. +8. Keep `run-code-intel.ps1` as a compatibility façade until parity is proven. + +## Non-Goals + +- Do not turn every function into a process. +- Do not pass repository contents through stdout. +- Do not introduce a workflow engine, database, message queue, Nix, Bazel, or OPA runtime. +- Do not migrate all PowerShell to Rust before contracts and parity tests exist. +- Do not cache network output without provider and configuration provenance. + +The project absorbs mechanisms from Unix pipes, reproducible builds, content-addressed storage, incremental query systems, SLSA provenance, and policy-as-code. It does not import those systems as mandatory runtime dependencies. diff --git a/docs/code-intel-architecture.md b/docs/code-intel-architecture.md index 3ee79f8..d7f1594 100644 --- a/docs/code-intel-architecture.md +++ b/docs/code-intel-architecture.md @@ -11,6 +11,8 @@ Artifact ownership and reader/writer boundaries are defined in `docs/artifact-da 1. `orchestration/integrations.json` and `code-intel.exe orchestrate` Integration registry and fusion layer. New scanners, memory systems, graph providers, governance strategies, and compatibility shims must be registered here before they are wired into runner scripts. + `orchestration/capability-contract.v1.json` defines the Capability Atom declaration/request/result, Snapshot Identity, Artifact Ref, Effect Boundary, Domain Verdict, Run Commit, Materialized View, cache-key, and transactional publication vocabulary. `orchestration/schemas/code-intel-capability-envelope.v1.schema.json` rejects malformed envelopes and impossible outcome combinations. Existing integrations migrate behind that contract one atom at a time; the registry remains the graph authority. Runtime effect enforcement is not yet implemented. + 2. Rust targets - `crates/code-intel-cli`: compiled `code-intel` CLI for integration orchestration, artifact resume, classify, and artifact doctor contracts. - `crates/code-nexus-lite`: compiled `code-nexus-lite` iii worker for CodeNexus scan/lite/doctor behavior. @@ -189,3 +191,5 @@ Copy the operational shell, not the internal machinery. That is the useful lesson from `gitnexus-stable-ops`. The updated version of this rule is: register integrations first, then adapt or internalize them behind the orchestration layer. + +Atomicity means identifiable inputs, verifiable outputs, declared effects, and safe publication. It does not mean one process per function. See `docs/atomic-development-model.md` and ADR 0009. diff --git a/orchestration/capability-contract.v1.json b/orchestration/capability-contract.v1.json new file mode 100644 index 0000000..c251eb6 --- /dev/null +++ b/orchestration/capability-contract.v1.json @@ -0,0 +1,77 @@ +{ + "schema": "code-intel-capability-contract.v1", + "contractVersion": 1, + "envelopeSchema": "orchestration/schemas/code-intel-capability-envelope.v1.schema.json", + "purpose": "Define the stable control-plane envelope for independently executable Code Intel capabilities.", + "vocabulary": [ + "Capability Atom", + "Snapshot Identity", + "Artifact Ref", + "Effect Boundary", + "Domain Verdict", + "Run Commit", + "Materialized View" + ], + "envelopes": { + "declaration": "code-intel-capability-declaration.v1", + "request": "code-intel-capability-request.v1", + "result": "code-intel-capability-result.v1", + "artifactRef": "code-intel-artifact-ref.v1" + }, + "artifactRef": { + "identity": "sha256 identifies bytes; path is only a location.", + "artifactSchema": "Names the schema of the referenced artifact payload, not the Artifact Ref envelope.", + "consumedSnapshotIdentity": "Required SHA-256 for repository-derived evidence; null only for snapshot-independent control artifacts." + }, + "effectBoundary": { + "determinism": ["deterministic", "external_nondeterministic"], + "effects": ["repo_read", "local_write", "network", "repo_mutation"], + "rule": "A capability declaration and request establish the allowlist before execution; the result reports both declaredEffects and observedEffects, and observed effects must be a subset of the declared allowlist." + }, + "declaration": { + "dependencies": "A stable ordered set of capability ids used by the DAG planner; execution remains a later atom." + }, + "coherenceRules": [ + "request.capability equals declaration.id", + "request.implementation equals declaration.implementation", + "request.effectPolicy.allowedEffects is a subset of declaration.allowedEffects", + "result.capability equals request.capability", + "result.implementation equals request.implementation", + "result.determinism equals declaration.determinism", + "result.declaredEffects equals request.effectPolicy.allowedEffects", + "result.observedEffects is a subset of result.declaredEffects", + "result.snapshotIdentity equals request.snapshot.identity", + "each repository-derived result Artifact Ref consumedSnapshotIdentity equals result.snapshotIdentity" + ], + "result": { + "statuses": ["completed", "blocked", "failed"], + "verdicts": ["pass", "fail", "unknown", "not_applicable"] + }, + "exitCodes": [ + { "code": 0, "status": "completed", "verdicts": ["pass", "not_applicable"], "meaning": "capability completed without a domain rejection" }, + { "code": 10, "status": "completed", "verdicts": ["fail"], "meaning": "capability completed with a domain fail verdict" }, + { "code": 20, "status": "blocked", "verdicts": ["unknown"], "meaning": "capability requires manual evidence or is otherwise blocked" }, + { "code": 64, "status": "failed", "verdicts": ["unknown"], "meaning": "invalid request or command usage" }, + { "code": 65, "status": "failed", "verdicts": ["unknown"], "meaning": "input artifact, schema, digest, or trust validation failed" }, + { "code": 69, "status": "failed", "verdicts": ["unknown"], "meaning": "declared dependency or provider unavailable" }, + { "code": 70, "status": "failed", "verdicts": ["unknown"], "meaning": "internal capability failure" }, + { "code": 74, "status": "failed", "verdicts": ["unknown"], "meaning": "artifact input or output failure" } + ], + "cacheKey": { + "algorithm": "sha256", + "orderedComponents": ["capability", "contractVersion", "implementationVersion", "snapshotIdentity", "canonicalOptions", "orderedInputArtifactDigests", "toolchainDigests"], + "forbiddenComponents": ["generatedAt", "attemptStartedAt", "attemptFinishedAt", "artifactOutputPath", "machineAbsolutePath"] + }, + "publication": { + "stagingSuffix": ".staging-", + "completionMarker": "run-complete.json", + "rule": "Write and validate artifacts in staging, promote atomically, then write the completion marker last.", + "indexRequiresCompletionMarker": true + }, + "streamContract": { + "stdin": "one code-intel-capability-request.v1 JSON document or an explicit request file", + "stdout": "exactly one code-intel-capability-result.v1 JSON document", + "stderr": "human-readable diagnostics only", + "largePayloadRule": "Large evidence remains in artifact files and crosses capability boundaries by artifact reference." + } +} diff --git a/orchestration/integrations.json b/orchestration/integrations.json index 6d1f97f..b04b1f2 100644 --- a/orchestration/integrations.json +++ b/orchestration/integrations.json @@ -2,6 +2,7 @@ "schemaVersion": 1, "policy": { "name": "code-intel-integration-orchestration", + "capabilityContract": "orchestration/capability-contract.v1.json", "normalOperation": "self-contained", "rule": "Normal code-intel runs must use integrations registered here. Separately installed project-intelligence CLIs are allowed only as compatibility shims or optional accelerators.", "extensionRule": "Add new projects, scanners, memory systems, graph providers, or governance strategies here before wiring them into pipeline scripts." diff --git a/orchestration/schemas/code-intel-capability-envelope.v1.schema.json b/orchestration/schemas/code-intel-capability-envelope.v1.schema.json new file mode 100644 index 0000000..78dcc6b --- /dev/null +++ b/orchestration/schemas/code-intel-capability-envelope.v1.schema.json @@ -0,0 +1,208 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://code-intel.local/schemas/code-intel-capability-envelope.v1.schema.json", + "title": "Code Intel Capability Envelope v1", + "oneOf": [ + { "$ref": "#/definitions/capabilityDeclaration" }, + { "$ref": "#/definitions/capabilityRequest" }, + { "$ref": "#/definitions/capabilityResult" }, + { "$ref": "#/definitions/artifactRef" } + ], + "definitions": { + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "effect": { + "type": "string", + "enum": ["repo_read", "local_write", "network", "repo_mutation"] + }, + "effectSet": { + "type": "array", + "items": { "$ref": "#/definitions/effect" }, + "uniqueItems": true + }, + "implementation": { + "type": "object", + "additionalProperties": false, + "required": ["id", "version", "toolchainDigests"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "minLength": 1 }, + "toolchainDigests": { + "type": "array", + "items": { "$ref": "#/definitions/sha256" }, + "uniqueItems": true + } + } + }, + "snapshot": { + "type": "object", + "additionalProperties": false, + "required": ["identity", "repoIdentity", "head", "workingTreePolicy", "scope", "inputDigest"], + "properties": { + "identity": { "$ref": "#/definitions/sha256" }, + "repoIdentity": { "type": "string", "minLength": 1 }, + "head": { "type": "string", "minLength": 1 }, + "workingTreePolicy": { "enum": ["head_only", "explicit_overlay"] }, + "scope": { + "type": "array", + "items": { "type": "string", "minLength": 1 }, + "uniqueItems": true + }, + "inputDigest": { "$ref": "#/definitions/sha256" } + } + }, + "artifactRef": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "artifactSchema", "type", "path", "sha256", "consumedSnapshotIdentity"], + "properties": { + "schema": { "const": "code-intel-artifact-ref.v1" }, + "artifactSchema": { "type": "string", "minLength": 1 }, + "type": { "type": "string", "minLength": 1 }, + "path": { "type": "string", "minLength": 1 }, + "sha256": { "$ref": "#/definitions/sha256" }, + "consumedSnapshotIdentity": { + "oneOf": [ + { "$ref": "#/definitions/sha256" }, + { "type": "null" } + ] + } + } + }, + "capabilityDeclaration": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "id", "contractVersion", "implementation", "determinism", "allowedEffects", "dependencies"], + "properties": { + "schema": { "const": "code-intel-capability-declaration.v1" }, + "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" }, + "contractVersion": { "const": 1 }, + "implementation": { "$ref": "#/definitions/implementation" }, + "determinism": { "enum": ["deterministic", "external_nondeterministic"] }, + "allowedEffects": { "$ref": "#/definitions/effectSet" }, + "dependencies": { + "type": "array", + "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" }, + "uniqueItems": true + } + } + }, + "capabilityRequest": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "capability", "contractVersion", "implementation", "snapshot", "options", "inputs", "effectPolicy"], + "properties": { + "schema": { "const": "code-intel-capability-request.v1" }, + "capability": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" }, + "contractVersion": { "const": 1 }, + "implementation": { "$ref": "#/definitions/implementation" }, + "snapshot": { "$ref": "#/definitions/snapshot" }, + "options": { "type": "object" }, + "inputs": { + "type": "array", + "items": { "$ref": "#/definitions/artifactRef" } + }, + "effectPolicy": { + "type": "object", + "additionalProperties": false, + "required": ["allowedEffects"], + "properties": { + "allowedEffects": { "$ref": "#/definitions/effectSet" } + } + } + } + }, + "cache": { + "type": "object", + "additionalProperties": false, + "required": ["key", "hit"], + "properties": { + "key": { + "oneOf": [ + { "$ref": "#/definitions/sha256" }, + { "type": "null" } + ] + }, + "hit": { "type": "boolean" } + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["attemptId", "generatedAt"], + "properties": { + "attemptId": { "type": "string", "minLength": 1 }, + "generatedAt": { "type": "string", "format": "date-time" }, + "provider": { "type": "string", "minLength": 1 }, + "model": { "type": "string", "minLength": 1 }, + "configurationDigest": { "$ref": "#/definitions/sha256" } + } + }, + "capabilityResultBase": { + "type": "object", + "additionalProperties": false, + "required": ["schema", "capability", "implementation", "snapshotIdentity", "status", "verdict", "exitCode", "determinism", "declaredEffects", "observedEffects", "cache", "artifacts", "diagnostics", "provenance"], + "properties": { + "schema": { "const": "code-intel-capability-result.v1" }, + "capability": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]*$" }, + "implementation": { "$ref": "#/definitions/implementation" }, + "snapshotIdentity": { "$ref": "#/definitions/sha256" }, + "status": { "enum": ["completed", "blocked", "failed"] }, + "verdict": { "enum": ["pass", "fail", "unknown", "not_applicable"] }, + "exitCode": { "enum": [0, 10, 20, 64, 65, 69, 70, 74] }, + "determinism": { "enum": ["deterministic", "external_nondeterministic"] }, + "declaredEffects": { "$ref": "#/definitions/effectSet" }, + "observedEffects": { "$ref": "#/definitions/effectSet" }, + "cache": { "$ref": "#/definitions/cache" }, + "artifacts": { + "type": "array", + "items": { "$ref": "#/definitions/artifactRef" } + }, + "diagnostics": { + "type": "array", + "items": { "type": "string" } + }, + "provenance": { "$ref": "#/definitions/provenance" } + } + }, + "capabilityResult": { + "allOf": [ + { "$ref": "#/definitions/capabilityResultBase" }, + { + "oneOf": [ + { + "properties": { + "status": { "const": "completed" }, + "verdict": { "enum": ["pass", "not_applicable"] }, + "exitCode": { "const": 0 } + } + }, + { + "properties": { + "status": { "const": "completed" }, + "verdict": { "const": "fail" }, + "exitCode": { "const": 10 } + } + }, + { + "properties": { + "status": { "const": "blocked" }, + "verdict": { "const": "unknown" }, + "exitCode": { "const": 20 } + } + }, + { + "properties": { + "status": { "const": "failed" }, + "verdict": { "const": "unknown" }, + "exitCode": { "enum": [64, 65, 69, 70, 74] } + } + } + ] + } + ] + } + } +} diff --git a/test-atomic-capability-contract.ps1 b/test-atomic-capability-contract.ps1 new file mode 100644 index 0000000..ee5311f --- /dev/null +++ b/test-atomic-capability-contract.ps1 @@ -0,0 +1,267 @@ +param( + [string]$RepoPath = $PSScriptRoot +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Assert-Contract { + param([bool]$Condition, [string]$Message) + if (-not $Condition) { throw $Message } +} + +function ConvertTo-CanonicalFixtureJson { + param([Parameter(Mandatory = $true)]$Value) + return ($Value | ConvertTo-Json -Depth 20 -Compress) +} + +function Assert-SchemaValid { + param([string]$Json, [string]$SchemaPath, [string]$Name) + $valid = Test-Json -Json $Json -SchemaFile $SchemaPath -ErrorAction SilentlyContinue + Assert-Contract $valid "$Name must satisfy the capability envelope schema." +} + +function Assert-SchemaInvalid { + param([string]$Json, [string]$SchemaPath, [string]$Name) + $valid = Test-Json -Json $Json -SchemaFile $SchemaPath -ErrorAction SilentlyContinue + Assert-Contract (-not $valid) "$Name must be rejected by the capability envelope schema." +} + +function Assert-ObservedEffectsDeclared { + param([string[]]$DeclaredEffects, [string[]]$ObservedEffects, [string]$Name) + $undeclared = @($ObservedEffects | Where-Object { $_ -notin $DeclaredEffects }) + Assert-Contract ($undeclared.Count -eq 0) "$Name observed undeclared effects: $($undeclared -join ', ')" +} + +function Assert-SetEqual { + param([string[]]$Expected, [string[]]$Actual, [string]$Name) + $missing = @($Expected | Where-Object { $_ -notin $Actual }) + $unexpected = @($Actual | Where-Object { $_ -notin $Expected }) + Assert-Contract ($missing.Count -eq 0 -and $unexpected.Count -eq 0) "$Name differs: missing=[$($missing -join ', ')], unexpected=[$($unexpected -join ', ')]" +} + +function Assert-EnvelopesCoherent { + param($Declaration, $Request, $Result, [string]$Name) + Assert-Contract ($Request.capability -eq $Declaration.id) "$Name request capability differs from declaration id." + Assert-Contract ((ConvertTo-CanonicalFixtureJson $Request.implementation) -eq (ConvertTo-CanonicalFixtureJson $Declaration.implementation)) "$Name request implementation differs from declaration." + $disallowed = @($Request.effectPolicy.allowedEffects | Where-Object { $_ -notin $Declaration.allowedEffects }) + Assert-Contract ($disallowed.Count -eq 0) "$Name request asks for effects outside declaration: $($disallowed -join ', ')" + Assert-Contract ($Result.capability -eq $Request.capability) "$Name result capability differs from request." + Assert-Contract ((ConvertTo-CanonicalFixtureJson $Result.implementation) -eq (ConvertTo-CanonicalFixtureJson $Request.implementation)) "$Name result implementation differs from request." + Assert-Contract ($Result.determinism -eq $Declaration.determinism) "$Name result determinism differs from declaration." + Assert-SetEqual $Request.effectPolicy.allowedEffects $Result.declaredEffects "$Name result declared effects" + Assert-ObservedEffectsDeclared $Result.declaredEffects $Result.observedEffects $Name + Assert-Contract ($Result.snapshotIdentity -eq $Request.snapshot.identity) "$Name result snapshot identity differs from request." + foreach ($artifactRef in @($Result.artifacts)) { + if ($null -ne $artifactRef.consumedSnapshotIdentity) { + Assert-Contract ($artifactRef.consumedSnapshotIdentity -eq $Result.snapshotIdentity) "$Name output artifact consumed snapshot differs from result." + } + } +} + +$root = (Resolve-Path -LiteralPath $RepoPath).Path +$registryPath = Join-Path $root "orchestration\integrations.json" +$contractPath = Join-Path $root "orchestration\capability-contract.v1.json" +$schemaPath = Join-Path $root "orchestration\schemas\code-intel-capability-envelope.v1.schema.json" +$documentationPaths = @( + (Join-Path $root "CONTEXT.md"), + (Join-Path $root "docs\atomic-development-model.md"), + (Join-Path $root "docs\adr\0009-atomic-capability-execution-model.md"), + (Join-Path $root "docs\code-intel-architecture.md") +) + +foreach ($path in @($registryPath, $contractPath, $schemaPath) + $documentationPaths) { + Assert-Contract (Test-Path -LiteralPath $path -PathType Leaf) "Required atomic capability contract file is missing: $path" +} + +$registry = Get-Content -LiteralPath $registryPath -Raw | ConvertFrom-Json -ErrorAction Stop +$contract = Get-Content -LiteralPath $contractPath -Raw | ConvertFrom-Json -ErrorAction Stop +$null = Get-Content -LiteralPath $schemaPath -Raw | ConvertFrom-Json -ErrorAction Stop + +Assert-Contract ($contract.schema -eq "code-intel-capability-contract.v1") "Unexpected capability contract schema." +Assert-Contract ($contract.contractVersion -eq 1) "Capability contract version must be 1." +Assert-Contract ($contract.envelopeSchema -eq "orchestration/schemas/code-intel-capability-envelope.v1.schema.json") "Contract must bind the canonical JSON Schema." +Assert-Contract ($registry.policy.capabilityContract -eq "orchestration/capability-contract.v1.json") "Integration registry must bind the canonical capability contract." + +$expectedVocabulary = @("Capability Atom", "Snapshot Identity", "Artifact Ref", "Effect Boundary", "Domain Verdict", "Run Commit", "Materialized View") +$expectedVerdicts = @("pass", "fail", "unknown", "not_applicable") +$expectedEffects = @("repo_read", "local_write", "network", "repo_mutation") +$expectedExitCodes = @(0, 10, 20, 64, 65, 69, 70, 74) + +Assert-Contract ((@($contract.vocabulary) -join "|") -eq ($expectedVocabulary -join "|")) "Atomic vocabulary changed without a contract version bump." +Assert-Contract ((@($contract.result.verdicts) -join "|") -eq ($expectedVerdicts -join "|")) "Verdict lattice changed without a contract version bump." +Assert-Contract ((@($contract.effectBoundary.effects) -join "|") -eq ($expectedEffects -join "|")) "Effect allowlist changed without a contract version bump." +Assert-Contract ((@($contract.exitCodes.code) -join "|") -eq ($expectedExitCodes -join "|")) "Exit-code contract changed without a version bump." +Assert-Contract (-not (@($contract.effectBoundary.effects) -contains "pure")) "Purity/determinism must not be encoded as a permission effect." + +$exitCodeDuplicates = @($contract.exitCodes.code | Group-Object | Where-Object Count -gt 1) +Assert-Contract ($exitCodeDuplicates.Count -eq 0) "Exit codes must be unique." +Assert-Contract ($contract.cacheKey.algorithm -eq "sha256") "Capability cache keys must use SHA-256." +Assert-Contract (@($contract.cacheKey.orderedComponents) -contains "snapshotIdentity") "Cache key must bind the repository snapshot." +Assert-Contract (@($contract.cacheKey.orderedComponents) -contains "orderedInputArtifactDigests") "Cache key must bind input artifact digests." +Assert-Contract (@($contract.cacheKey.forbiddenComponents) -contains "generatedAt") "Wall-clock time must not enter the deterministic cache key." +Assert-Contract ($contract.publication.completionMarker -eq "run-complete.json") "Transactional publication requires the canonical completion marker." + +foreach ($path in $documentationPaths) { + $text = Get-Content -LiteralPath $path -Raw + foreach ($term in $expectedVocabulary) { + Assert-Contract ($text.Contains($term)) "$path is missing canonical atomic vocabulary: $term" + } +} + +$digestA = "a" * 64 +$digestB = "b" * 64 +$implementation = [ordered]@{ + id = "inventory.rg" + version = "1.0.0" + toolchainDigests = @($digestA) +} +$artifact = [ordered]@{ + schema = "code-intel-artifact-ref.v1" + artifactSchema = "code-intel-file-inventory.v1" + type = "inventory.files" + path = "artifacts/inventory.json" + sha256 = $digestB + consumedSnapshotIdentity = $digestA +} +$declaration = [ordered]@{ + schema = "code-intel-capability-declaration.v1" + id = "inventory.rg" + contractVersion = 1 + implementation = $implementation + determinism = "deterministic" + allowedEffects = @("repo_read", "local_write") + dependencies = @() +} +$request = [ordered]@{ + schema = "code-intel-capability-request.v1" + capability = "inventory.rg" + contractVersion = 1 + implementation = $implementation + snapshot = [ordered]@{ + identity = $digestA + repoIdentity = "github.com/2233admin/code-intel-pipeline" + head = "0123456789abcdef0123456789abcdef01234567" + workingTreePolicy = "head_only" + scope = @(".") + inputDigest = $digestA + } + options = [ordered]@{} + inputs = @($artifact) + effectPolicy = [ordered]@{ allowedEffects = @("repo_read", "local_write") } +} +$result = [ordered]@{ + schema = "code-intel-capability-result.v1" + capability = "inventory.rg" + implementation = $implementation + snapshotIdentity = $digestA + status = "completed" + verdict = "pass" + exitCode = 0 + determinism = "deterministic" + declaredEffects = @("repo_read", "local_write") + observedEffects = @("repo_read", "local_write") + cache = [ordered]@{ key = $digestB; hit = $false } + artifacts = @($artifact) + diagnostics = @() + provenance = [ordered]@{ + attemptId = "fixture-1" + generatedAt = "2026-07-13T00:00:00Z" + } +} + +Assert-SchemaValid (ConvertTo-CanonicalFixtureJson $artifact) $schemaPath "valid artifact ref" +Assert-SchemaValid (ConvertTo-CanonicalFixtureJson $declaration) $schemaPath "valid capability declaration" +Assert-SchemaValid (ConvertTo-CanonicalFixtureJson $request) $schemaPath "valid capability request" +Assert-SchemaValid (ConvertTo-CanonicalFixtureJson $result) $schemaPath "valid completed result" +Assert-EnvelopesCoherent $declaration $request $result "valid envelope chain" + +$domainFail = ConvertTo-CanonicalFixtureJson $result | ConvertFrom-Json +$domainFail.status = "completed" +$domainFail.verdict = "fail" +$domainFail.exitCode = 10 +Assert-SchemaValid (ConvertTo-CanonicalFixtureJson $domainFail) $schemaPath "valid domain-fail result" + +$blocked = ConvertTo-CanonicalFixtureJson $result | ConvertFrom-Json +$blocked.status = "blocked" +$blocked.verdict = "unknown" +$blocked.exitCode = 20 +Assert-SchemaValid (ConvertTo-CanonicalFixtureJson $blocked) $schemaPath "valid blocked result" + +$outcomeMatrixCases = 0 +foreach ($status in @($contract.result.statuses)) { + foreach ($verdict in @($contract.result.verdicts)) { + foreach ($exitCode in @($contract.exitCodes.code)) { + $candidate = ConvertTo-CanonicalFixtureJson $result | ConvertFrom-Json + $candidate.status = $status + $candidate.verdict = $verdict + $candidate.exitCode = $exitCode + $actualValid = Test-Json -Json (ConvertTo-CanonicalFixtureJson $candidate) -SchemaFile $schemaPath -ErrorAction SilentlyContinue + $mapping = @($contract.exitCodes | Where-Object { $_.code -eq $exitCode })[0] + $expectedValid = ($status -eq $mapping.status -and $verdict -in @($mapping.verdicts)) + Assert-Contract ($actualValid -eq $expectedValid) "Outcome matrix drift for status=$status verdict=$verdict exitCode=$exitCode." + $outcomeMatrixCases++ + } + } +} + +$invalidOutcome = ConvertTo-CanonicalFixtureJson $result | ConvertFrom-Json +$invalidOutcome.status = "failed" +$invalidOutcome.verdict = "pass" +$invalidOutcome.exitCode = 70 +Assert-SchemaInvalid (ConvertTo-CanonicalFixtureJson $invalidOutcome) $schemaPath "failed/pass outcome" + +$invalidEffectType = ConvertTo-CanonicalFixtureJson $result | ConvertFrom-Json +$invalidEffectType.observedEffects = "repo_read" +Assert-SchemaInvalid (ConvertTo-CanonicalFixtureJson $invalidEffectType) $schemaPath "string effect set" + +$unknownField = ConvertTo-CanonicalFixtureJson $request | ConvertFrom-Json +$unknownField | Add-Member -NotePropertyName surprise -NotePropertyValue $true +Assert-SchemaInvalid (ConvertTo-CanonicalFixtureJson $unknownField) $schemaPath "request with unknown field" + +$badDigest = ConvertTo-CanonicalFixtureJson $artifact | ConvertFrom-Json +$badDigest.sha256 = "not-a-digest" +Assert-SchemaInvalid (ConvertTo-CanonicalFixtureJson $badDigest) $schemaPath "artifact with invalid digest" + +$missingPayloadSchema = ConvertTo-CanonicalFixtureJson $artifact | ConvertFrom-Json +$missingPayloadSchema.PSObject.Properties.Remove("artifactSchema") +Assert-SchemaInvalid (ConvertTo-CanonicalFixtureJson $missingPayloadSchema) $schemaPath "artifact without payload schema" + +$missingConsumedSnapshot = ConvertTo-CanonicalFixtureJson $artifact | ConvertFrom-Json +$missingConsumedSnapshot.PSObject.Properties.Remove("consumedSnapshotIdentity") +Assert-SchemaInvalid (ConvertTo-CanonicalFixtureJson $missingConsumedSnapshot) $schemaPath "artifact without consumed snapshot identity" + +$coherenceMutators = @( + @{ name = "request capability mismatch"; apply = { param($d, $q, $r) $q.capability = "other.capability" } }, + @{ name = "request implementation mismatch"; apply = { param($d, $q, $r) $q.implementation.version = "2.0.0" } }, + @{ name = "request effect outside declaration"; apply = { param($d, $q, $r) $q.effectPolicy.allowedEffects = @("repo_read", "network") } }, + @{ name = "result capability mismatch"; apply = { param($d, $q, $r) $r.capability = "other.capability" } }, + @{ name = "result implementation mismatch"; apply = { param($d, $q, $r) $r.implementation.version = "2.0.0" } }, + @{ name = "result determinism mismatch"; apply = { param($d, $q, $r) $r.determinism = "external_nondeterministic" } }, + @{ name = "result declared effect drift"; apply = { param($d, $q, $r) $r.declaredEffects = @("repo_read") } }, + @{ name = "observed undeclared effect"; apply = { param($d, $q, $r) $r.observedEffects = @("repo_read", "network") } }, + @{ name = "result snapshot mismatch"; apply = { param($d, $q, $r) $r.snapshotIdentity = "c" * 64 } } + @{ name = "output artifact snapshot mismatch"; apply = { param($d, $q, $r) $r.artifacts[0].consumedSnapshotIdentity = "c" * 64 } } +) + +foreach ($case in $coherenceMutators) { + $badDeclaration = ConvertTo-CanonicalFixtureJson $declaration | ConvertFrom-Json + $badRequest = ConvertTo-CanonicalFixtureJson $request | ConvertFrom-Json + $badResult = ConvertTo-CanonicalFixtureJson $result | ConvertFrom-Json + & $case.apply $badDeclaration $badRequest $badResult + $rejected = $false + try { Assert-EnvelopesCoherent $badDeclaration $badRequest $badResult $case.name } + catch { $rejected = $true } + Assert-Contract $rejected "$($case.name) must be rejected by cross-envelope coherence rules." +} + +[ordered]@{ + ok = $true + schema = $contract.schema + envelopeSchema = $contract.envelopeSchema + outcomeMatrixCases = $outcomeMatrixCases + rejectedSchemaFixtures = 6 + rejectedCoherenceFixtures = $coherenceMutators.Count + vocabularyTerms = $expectedVocabulary.Count + completionMarker = $contract.publication.completionMarker +} | ConvertTo-Json -Depth 4 From 3bc551e868a20e3d89640fc7dca35fbaae6fd206 Mon Sep 17 00:00:00 2001 From: Curry Date: Wed, 15 Jul 2026 22:58:51 +0800 Subject: [PATCH 02/34] feat: move Sentrux DSM analysis to Rust --- crates/code-intel-cli/src/main.rs | 3 +- crates/code-intel-cli/src/sentrux.rs | 6 + crates/code-intel-cli/src/sentrux_analysis.rs | 1186 +++++++++++++++++ .../code-intel-cli/tests/sentrux_analysis.rs | 125 ++ .../sentrux-rust-analysis-kernel-idea.md | 37 + orchestration/integrations.json | 1 + run-code-intel.ps1 | 63 +- 7 files changed, 1412 insertions(+), 9 deletions(-) create mode 100644 crates/code-intel-cli/src/sentrux_analysis.rs create mode 100644 crates/code-intel-cli/tests/sentrux_analysis.rs create mode 100644 docs/plans/sentrux-rust-analysis-kernel-idea.md diff --git a/crates/code-intel-cli/src/main.rs b/crates/code-intel-cli/src/main.rs index eee206f..5b1d926 100644 --- a/crates/code-intel-cli/src/main.rs +++ b/crates/code-intel-cli/src/main.rs @@ -12,6 +12,7 @@ mod orchestration; mod providers; mod routes; mod sentrux; +mod sentrux_analysis; type Result = std::result::Result>; @@ -1104,7 +1105,7 @@ fn print_help() { println!(" graph --repo [--language zh] [--full] [--write] [--json]"); println!(" provider [--action List|Plan|Validate|Invoke] [--provider repowise|understand] [--operation ] [--repo ] [--language zh] [--write] [--json]"); println!(" route [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json]"); - println!(" sentrux "); + println!(" sentrux "); println!(" orchestrate [--action Validate|List|Plan] [--repo ] [--mode lite|normal|full] [--capability ] [--manifest ] [--json]"); } diff --git a/crates/code-intel-cli/src/sentrux.rs b/crates/code-intel-cli/src/sentrux.rs index 9f0983f..1a3fccf 100644 --- a/crates/code-intel-cli/src/sentrux.rs +++ b/crates/code-intel-cli/src/sentrux.rs @@ -1,3 +1,4 @@ +use crate::sentrux_analysis; use crate::Result; use std::path::Path; use std::process::{Command, Stdio}; @@ -11,6 +12,11 @@ pub fn run(options: &Options<'_>) -> Result<()> { let operation = options.operation.ok_or("sentrux requires an operation")?; let repo = options.repo.ok_or("sentrux requires a repo/path")?; let repo = repo.canonicalize()?; + if operation == "dsm" { + let snapshot = sentrux_analysis::analyze(&repo)?; + println!("{}", serde_json::to_string(&snapshot)?); + return Ok(()); + } let repo_cli = cli_path(&repo); let mut args = Vec::new(); diff --git a/crates/code-intel-cli/src/sentrux_analysis.rs b/crates/code-intel-cli/src/sentrux_analysis.rs new file mode 100644 index 0000000..a69ca29 --- /dev/null +++ b/crates/code-intel-cli/src/sentrux_analysis.rs @@ -0,0 +1,1186 @@ +use serde_json::{json, Value}; +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fs; +use std::path::Path; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +const SOURCE_EXTENSIONS: [&str; 14] = [ + ".ps1", ".psm1", ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".go", ".java", + ".cs", ".v", +]; + +#[derive(Clone, Default)] +struct GitSignal { + status: &'static str, + dirty: bool, + untracked: bool, + age_days: Option, + churn: i64, + last_commit_unix: Option, +} + +#[derive(Clone, Default)] +struct ModuleMetrics { + files: i64, + source_files: i64, + test_files: i64, + test_gap: i64, + avg_age_days: Option, + max_age_days: Option, + churn: i64, + dirty_files: i64, + untracked_files: i64, + git_files: i64, + inbound_edges: i64, + outbound_edges: i64, + coupling: i64, + exec_depth: i64, + blast_radius: i64, + risk: i64, +} + +struct Inventory { + files: Vec, + scope: Value, +} + +pub fn analyze(target: &Path) -> Result { + let target = target + .canonicalize() + .map_err(|error| format!("canonicalize {}: {error}", target.display()))?; + if !target.is_dir() { + return Err(format!( + "DSM target is not a directory: {}", + target.display() + )); + } + + let inventory = source_inventory(&target)?; + let git = git_signals(&target, &inventory.files); + let mut file_details = Vec::with_capacity(inventory.files.len()); + let mut contents = BTreeMap::new(); + let mut modules: BTreeMap = BTreeMap::new(); + let mut module_files: BTreeMap> = BTreeMap::new(); + + for relative in &inventory.files { + let content = fs::read_to_string(target.join(relative)).unwrap_or_default(); + let module = module_name(relative); + let signal = git.get(relative).cloned().unwrap_or_else(untracked_signal); + let metrics = modules.entry(module.clone()).or_default(); + metrics.files += 1; + if is_test_file(relative) { + metrics.test_files += 1; + } else { + metrics.source_files += 1; + } + metrics.churn += signal.churn; + metrics.dirty_files += i64::from(signal.dirty); + metrics.untracked_files += i64::from(signal.untracked); + metrics.git_files += i64::from(signal.dirty || signal.untracked); + module_files + .entry(module) + .or_default() + .push(relative.clone()); + file_details.push(file_detail(relative, &content, &signal)); + contents.insert(relative.clone(), content); + } + + let edges = dsm_edges(&inventory.files, &contents, &modules); + derive_module_metrics(&mut modules, &module_files, &git, &edges); + score_risk(&mut modules); + + file_details.sort_by(|left, right| { + integer(right, "max_complexity") + .cmp(&integer(left, "max_complexity")) + .then_with(|| string(left, "path").cmp(string(right, "path"))) + }); + + Ok(json!({ + "tool": "dsm", + "path": cli_path(&target), + "scope": inventory.scope, + "default_color_mode": "Risk", + "color_modes": color_modes(), + "modules": module_output(&modules), + "file_details": file_details, + "edges": edge_output(&edges), + "note": "Lightweight DSM with 9 color modes. Git-derived modes depend on local git history; use Sentrux/CodeNexus for authoritative graph detail." + })) +} + +fn source_inventory(target: &Path) -> Result { + let listed = Command::new("rg") + .arg("--files") + .current_dir(target) + .output() + .ok() + .filter(|output| output.status.success()) + .map(|output| { + String::from_utf8_lossy(&output.stdout) + .lines() + .map(normalize_path) + .collect::>() + }) + .unwrap_or_else(|| recursive_files(target)); + + let mut included = Vec::new(); + let mut excluded: BTreeMap)> = BTreeMap::new(); + for relative in listed { + let extension = extension(&relative); + if !SOURCE_EXTENSIONS.contains(&extension.as_str()) { + continue; + } + let mut reason = excluded_reason(&relative); + if reason.is_none() + && matches!(extension.as_str(), ".js" | ".jsx" | ".mjs" | ".cjs") + && fs::metadata(target.join(&relative)) + .map(|metadata| metadata.len() > 2_097_152) + .unwrap_or(false) + { + reason = Some("oversized_generated_or_bundle".to_string()); + } + if let Some(reason) = reason { + let entry = excluded.entry(reason).or_default(); + entry.0 += 1; + if entry.1.len() < 8 { + entry.1.push(relative); + } + } else { + included.push(relative); + } + } + included.sort(); + included.dedup(); + let excluded_total = excluded.values().map(|entry| entry.0).sum::(); + let mut excluded_by_reason = excluded + .into_iter() + .map(|(reason, (files, samples))| json!({"reason": reason, "files": files, "samples": samples})) + .collect::>(); + excluded_by_reason.sort_by(|left, right| { + integer(right, "files") + .cmp(&integer(left, "files")) + .then_with(|| string(left, "reason").cmp(string(right, "reason"))) + }); + let included_count = included.len(); + Ok(Inventory { + files: included, + scope: json!({ + "mode": "auto_governed_source", + "included_files": included_count, + "excluded_files": excluded_total, + "excluded_by_reason": excluded_by_reason, + "source_extensions": SOURCE_EXTENSIONS, + "note": "Root paths are allowed. Dependency, build-output, cache, and bundled static-asset code is excluded from governed source metrics." + }), + }) +} + +fn recursive_files(root: &Path) -> Vec { + fn visit(root: &Path, current: &Path, out: &mut Vec) { + let Ok(entries) = fs::read_dir(current) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + if entry.file_name() != ".git" { + visit(root, &path, out); + } + } else if let Ok(relative) = path.strip_prefix(root) { + out.push(normalize_path(&relative.to_string_lossy())); + } + } + } + let mut out = Vec::new(); + visit(root, root, &mut out); + out +} + +fn excluded_reason(relative: &str) -> Option { + let lower = relative.to_ascii_lowercase(); + let parts = lower + .split('/') + .filter(|part| !part.is_empty()) + .collect::>(); + if let Some(top) = parts.first() { + if ["tools", "vendor", "third_party", "external"].contains(top) { + return Some(format!("external_tooling_dir:{top}")); + } + } + let excluded = [ + ".git", + ".repowise", + ".understand-anything", + ".sentrux", + "node_modules", + ".pnpm", + ".yarn", + "target", + "dist", + "build", + "out", + "coverage", + ".venv", + "venv", + "env", + ".tox", + "__pycache__", + ".next", + ".nuxt", + ".turbo", + ".cache", + ]; + if let Some(part) = parts.iter().find(|part| excluded.contains(part)) { + return Some(format!("excluded_dir:{part}")); + } + if lower.starts_with("static/assets/") + || lower.starts_with("public/assets/") + || lower.starts_with("wwwroot/assets/") + { + return Some("bundled_static_assets".to_string()); + } + let leaf = relative.rsplit('/').next().unwrap_or(relative); + let leaf_lower = leaf.to_ascii_lowercase(); + if [ + ".min.js", + ".bundle.js", + ".min.jsx", + ".bundle.jsx", + ".min.mjs", + ".bundle.mjs", + ".min.cjs", + ".bundle.cjs", + ] + .iter() + .any(|suffix| leaf_lower.ends_with(suffix)) + { + return Some("bundled_or_minified_file".to_string()); + } + None +} + +fn git_signals(target: &Path, files: &[String]) -> BTreeMap { + let mut signals = files + .iter() + .map(|file| (file.clone(), untracked_signal())) + .collect::>(); + if files.is_empty() || !git_ok(target, &["rev-parse", "--is-inside-work-tree"]) { + return signals; + } + + for batch in files.chunks(80) { + for tracked in git_lines(target, "ls-files", &[], batch) { + if let Some(signal) = resolve_git_key(&tracked, &mut signals) { + *signal = GitSignal { + status: "clean", + ..GitSignal::default() + }; + } + } + for modified in git_lines(target, "ls-files", &["--modified"], batch) { + if let Some(signal) = resolve_git_key(&modified, &mut signals) { + signal.status = "dirty"; + signal.dirty = true; + signal.untracked = false; + } + } + for untracked in git_lines( + target, + "ls-files", + &["--others", "--exclude-standard"], + batch, + ) { + if let Some(signal) = resolve_git_key(&untracked, &mut signals) { + *signal = untracked_signal(); + } + } + apply_git_log(target, batch, &mut signals); + } + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs() as i64) + .unwrap_or(0); + for signal in signals.values_mut() { + signal.age_days = signal + .last_commit_unix + .map(|timestamp| (now - timestamp).max(0) / 86_400); + } + signals +} + +fn untracked_signal() -> GitSignal { + GitSignal { + status: "untracked", + untracked: true, + ..GitSignal::default() + } +} + +fn git_ok(target: &Path, args: &[&str]) -> bool { + Command::new("git") + .arg("-C") + .arg(target) + .args(args) + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} + +fn git_lines(target: &Path, command: &str, extra: &[&str], files: &[String]) -> Vec { + let Ok(output) = Command::new("git") + .arg("-C") + .arg(target) + .arg(command) + .args(extra) + .arg("--") + .args(files) + .output() + else { + return Vec::new(); + }; + if !output.status.success() { + return Vec::new(); + } + String::from_utf8_lossy(&output.stdout) + .lines() + .map(normalize_path) + .filter(|line| !line.is_empty()) + .collect() +} + +fn apply_git_log(target: &Path, files: &[String], signals: &mut BTreeMap) { + let Ok(output) = Command::new("git") + .arg("-C") + .arg(target) + .args(["log", "--format=__SENTRUX_COMMIT__%ct", "--name-only", "--"]) + .args(files) + .output() + else { + return; + }; + if !output.status.success() { + return; + } + let mut commit = None; + for line in String::from_utf8_lossy(&output.stdout).lines() { + let line = line.trim(); + if let Some(timestamp) = line.strip_prefix("__SENTRUX_COMMIT__") { + commit = timestamp.parse::().ok(); + continue; + } + if line.is_empty() { + continue; + } + if let (Some(timestamp), Some(signal)) = (commit, resolve_git_key(line, signals)) { + signal.churn += 1; + signal.last_commit_unix = Some(signal.last_commit_unix.unwrap_or(0).max(timestamp)); + } + } +} + +fn resolve_git_key<'a>( + path: &str, + signals: &'a mut BTreeMap, +) -> Option<&'a mut GitSignal> { + let candidate = normalize_path(path); + if signals.contains_key(&candidate) { + return signals.get_mut(&candidate); + } + let key = signals + .keys() + .find(|key| { + candidate.ends_with(&format!("/{key}")) || key.ends_with(&format!("/{candidate}")) + })? + .clone(); + signals.get_mut(&key) +} + +fn file_detail(relative: &str, content: &str, signal: &GitSignal) -> Value { + let lines = split_lines(content); + let language = language(relative); + let mut functions = functions(language, &lines); + for function in &mut functions { + let name = string(function, "name").to_string(); + let start = integer(function, "start_line"); + let end = integer(function, "end_line"); + if let Some(object) = function.as_object_mut() { + object.insert( + "id".to_string(), + json!(stable_id(&format!( + "function:{relative}:{name}:{start}:{end}" + ))), + ); + object.insert("source_anchor".to_string(), source_anchor(relative, start)); + } + } + functions.sort_by(|left, right| integer(right, "complexity").cmp(&integer(left, "complexity"))); + let (lines_count, loc, blank, comments) = line_stats(&lines); + let total_complexity = functions + .iter() + .map(|function| integer(function, "complexity")) + .sum::(); + let max_complexity = functions + .iter() + .map(|function| integer(function, "complexity")) + .max() + .unwrap_or(0); + let average = if functions.is_empty() { + 0.0 + } else { + round2(total_complexity as f64 / functions.len() as f64) + }; + json!({ + "id": stable_id(&format!("file:{relative}")), + "path": relative, + "module": module_name(relative), + "language": language, + "source_anchor": source_anchor(relative, 1), + "lines": lines_count, + "loc": loc, + "blank_lines": blank, + "comment_lines": comments, + "function_count": functions.len(), + "max_complexity": max_complexity, + "avg_complexity": average, + "total_complexity": total_complexity, + "git": {"status": signal.status, "dirty": signal.dirty, "untracked": signal.untracked, "age_days": signal.age_days, "churn": signal.churn}, + "functions": functions + }) +} + +fn functions(language: &str, lines: &[String]) -> Vec { + let mut out = Vec::new(); + let mut index = 0; + while index < lines.len() { + let line = &lines[index]; + let parsed = match language { + "python" => parse_python_signature(line), + "rust" => parse_c_signature(line, "fn ", true), + "vlang" => parse_c_signature(line, "fn ", false), + "typescript" | "javascript" => parse_javascript_signature(line), + "powershell" => parse_powershell_signature(line), + _ => None, + }; + if let Some((name, mut params, is_async, is_public)) = parsed { + let end = if language == "python" { + python_end(lines, index) + } else { + c_like_end(lines, index) + }; + let body = &lines[index..=end.min(lines.len().saturating_sub(1))]; + if language == "powershell" && params.is_empty() { + params = body + .iter() + .find_map(|line| { + let trimmed = line.trim_start(); + trimmed + .to_ascii_lowercase() + .starts_with("param(") + .then(|| trimmed[6..].to_string()) + }) + .unwrap_or_default(); + } + let (count, loc, _, _) = line_stats(body); + out.push(json!({ + "name": name, "kind": "function", "start_line": index + 1, "end_line": end + 1, + "lines": count, "loc": loc, "complexity": complexity(body), "params": param_count(¶ms), + "async": is_async, "public": is_public + })); + } + index += 1; + } + out +} + +fn parse_python_signature(line: &str) -> Option<(String, String, bool, bool)> { + let trimmed = line.trim_start(); + let (rest, is_async) = if let Some(rest) = trimmed.strip_prefix("async def ") { + (rest, true) + } else { + (trimmed.strip_prefix("def ")?, false) + }; + let open = rest.find('(')?; + let close = rest[open + 1..] + .find(')') + .map(|offset| offset + open + 1) + .unwrap_or(rest.len()); + let name = identifier(&rest[..open])?; + if rest[..open].trim() != name { + return None; + } + Some(( + name.to_string(), + rest[open + 1..close].to_string(), + is_async, + false, + )) +} + +fn parse_c_signature( + line: &str, + _marker: &str, + rust: bool, +) -> Option<(String, String, bool, bool)> { + let trimmed = line.trim_start(); + let (rest, is_async, is_public) = if let Some(rest) = trimmed.strip_prefix("fn ") { + (rest, false, false) + } else if rust { + if let Some(rest) = trimmed.strip_prefix("async fn ") { + (rest, true, false) + } else if let Some(rest) = trimmed.strip_prefix("pub fn ") { + (rest, false, true) + } else if let Some(rest) = trimmed.strip_prefix("pub async fn ") { + (rest, true, true) + } else if let Some(scoped) = trimmed.strip_prefix("pub(") { + let after_scope = scoped.split_once(") ")?.1; + if let Some(rest) = after_scope.strip_prefix("fn ") { + (rest, false, true) + } else { + (after_scope.strip_prefix("async fn ")?, true, true) + } + } else { + return None; + } + } else { + (trimmed.strip_prefix("pub fn ")?, false, true) + }; + let open = rest.find('(')?; + let close = rest[open + 1..] + .find(')') + .map(|offset| offset + open + 1) + .unwrap_or(rest.len()); + let name = identifier(&rest[..open])?; + if rest[..open].trim() != name { + return None; + } + Some(( + name.to_string(), + rest[open + 1..close].to_string(), + is_async, + is_public, + )) +} + +fn parse_javascript_signature(line: &str) -> Option<(String, String, bool, bool)> { + let trimmed = line.trim_start(); + let is_public = trimmed.starts_with("export "); + let is_async = trimmed.contains("async function ") || trimmed.contains("= async "); + if let Some(position) = trimmed.find("function ") { + let rest = &trimmed[position + 9..]; + let open = rest.find('(')?; + let close = rest[open + 1..] + .find(')') + .map(|offset| offset + open + 1) + .unwrap_or(rest.len()); + let name = identifier(&rest[..open])?; + if rest[..open].trim() != name { + return None; + } + return Some(( + name.to_string(), + rest[open + 1..close].to_string(), + is_async, + is_public, + )); + } + let assignment = trimmed.find("=>")?; + let before = trimmed[..assignment].trim(); + let equals = before.rfind('=')?; + let binding = before[..equals].split_whitespace().last()?; + let params = before[equals + 1..] + .trim() + .trim_start_matches("async") + .trim() + .trim_matches(['(', ')']); + Some(( + identifier(binding)?.to_string(), + params.to_string(), + is_async, + is_public, + )) +} + +fn parse_powershell_signature(line: &str) -> Option<(String, String, bool, bool)> { + let trimmed = line.trim_start(); + let lower = trimmed.to_ascii_lowercase(); + let rest = trimmed + .get(9..) + .filter(|_| lower.starts_with("function "))?; + let name = rest + .split(|ch: char| ch.is_whitespace() || ch == '{' || ch == '(') + .next()?; + (!name.is_empty()).then(|| (name.to_string(), String::new(), false, true)) +} + +fn python_end(lines: &[String], start: usize) -> usize { + let indent = leading_whitespace(&lines[start]); + for (index, line) in lines.iter().enumerate().skip(start + 1) { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('@') { + continue; + } + if leading_whitespace(line) <= indent { + return index.saturating_sub(1).max(start); + } + } + lines.len().saturating_sub(1).max(start) +} + +fn c_like_end(lines: &[String], start: usize) -> usize { + let mut depth = 0i64; + let mut seen = false; + for (index, line) in lines.iter().enumerate().skip(start) { + for ch in line.chars() { + if ch == '{' { + depth += 1; + seen = true + } + if ch == '}' { + depth -= 1; + if seen && depth <= 0 { + return index; + } + } + } + } + start +} + +fn complexity(lines: &[String]) -> i64 { + let keywords = [ + "if", "elif", "for", "while", "except", "case", "catch", "match", "guard", "when", "with", + ]; + let mut score = 1; + for line in lines { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") { + continue; + } + score += keywords + .iter() + .map(|word| count_word(trimmed, word)) + .sum::(); + score += trimmed.matches("&&").count() as i64 + trimmed.matches("||").count() as i64; + score += i64::from(trimmed.contains(" => ")); + } + score +} + +fn dsm_edges( + files: &[String], + contents: &BTreeMap, + modules: &BTreeMap, +) -> BTreeMap<(String, String), i64> { + let mut edges = BTreeMap::new(); + for file in files { + let ext = extension(file); + if !matches!(ext.as_str(), ".py" | ".rs" | ".v") { + continue; + } + let from = module_name(file); + let Some(content) = contents.get(file) else { + continue; + }; + for target in import_targets(&ext, content) { + if target != from && modules.contains_key(&target) { + edges.insert((from.clone(), target), 1); + } + } + } + edges +} + +fn import_targets(extension: &str, content: &str) -> Vec { + let mut targets = Vec::new(); + for line in content.lines() { + let trimmed = line.trim_start(); + match extension { + ".py" => { + let token = trimmed + .strip_prefix("from ") + .or_else(|| trimmed.strip_prefix("import ")) + .and_then(|rest| rest.split_whitespace().next()); + if let Some(token) = token { + targets.push(module_name(&token.replace('.', "/"))); + } + } + ".rs" => { + if let Some(rest) = trimmed.strip_prefix("mod ") { + if let Some(name) = identifier(rest) { + targets.push(module_name(&format!("src/{name}.rs"))); + } + } + if let Some(rest) = trimmed.strip_prefix("use crate::") { + if let Some(name) = identifier(rest) { + targets.push(module_name(&format!("src/{name}.rs"))); + } + } + } + ".v" => { + if let Some(rest) = trimmed.strip_prefix("import ") { + if let Some(token) = rest.split_whitespace().next() { + let root = token.split('.').next().unwrap_or(token); + targets.extend([ + module_name(root), + module_name(&format!("{root}.v")), + module_name(&token.replace('.', "/")), + ]); + } + } + } + _ => {} + } + } + targets +} + +fn derive_module_metrics( + modules: &mut BTreeMap, + module_files: &BTreeMap>, + git: &BTreeMap, + edges: &BTreeMap<(String, String), i64>, +) { + let mut adjacency: BTreeMap> = BTreeMap::new(); + let mut reverse: BTreeMap> = BTreeMap::new(); + for ((from, to), count) in edges { + adjacency + .entry(from.clone()) + .or_default() + .insert(to.clone()); + reverse.entry(to.clone()).or_default().insert(from.clone()); + if let Some(module) = modules.get_mut(from) { + module.outbound_edges += count + } + if let Some(module) = modules.get_mut(to) { + module.inbound_edges += count + } + } + let mut depths = modules + .keys() + .map(|name| (name.clone(), 0i64)) + .collect::>(); + for _ in 0..modules.len().max(1) { + let mut changed = false; + for ((from, to), _) in edges { + let candidate = depths.get(to).copied().unwrap_or(0) + 1; + if candidate > depths.get(from).copied().unwrap_or(0) { + depths.insert(from.clone(), candidate.min(99)); + changed = true; + } + } + if !changed { + break; + } + } + for (name, module) in modules.iter_mut() { + let ages = module_files + .get(name) + .into_iter() + .flatten() + .filter_map(|file| git.get(file)?.age_days) + .collect::>(); + if !ages.is_empty() { + module.avg_age_days = + Some((ages.iter().sum::() as f64 / ages.len() as f64).round() as i64); + module.max_age_days = ages.iter().max().copied(); + } + module.test_gap = (module.source_files - module.test_files).max(0); + module.coupling = module.inbound_edges + module.outbound_edges; + module.exec_depth = depths.get(name).copied().unwrap_or(0); + module.blast_radius = reachable(name, &reverse) as i64 + module.coupling; + } +} + +fn reachable(start: &str, reverse: &BTreeMap>) -> usize { + let mut seen = BTreeSet::new(); + let mut queue = VecDeque::from([start.to_string()]); + while let Some(node) = queue.pop_front() { + if let Some(next) = reverse.get(&node) { + for item in next { + if item != start && seen.insert(item.clone()) { + queue.push_back(item.clone()); + } + } + } + } + seen.len() +} + +fn score_risk(modules: &mut BTreeMap) { + let max = maximums(modules); + for module in modules.values_mut() { + module.risk = (heat(module.coupling, max.coupling) * 0.18 + + heat(module.blast_radius, max.blast_radius) * 0.18 + + heat(module.exec_depth, max.exec_depth) * 0.14 + + heat(module.churn, max.churn) * 0.14 + + heat(module.git_files, max.git_files) * 0.12 + + heat(module.test_gap, max.test_gap) * 0.12 + + heat( + module.avg_age_days.unwrap_or(0), + max.avg_age_days.unwrap_or(0), + ) * 0.07 + + heat(module.files, max.files) * 0.05) + .round() as i64; + } +} + +fn module_output(modules: &BTreeMap) -> Vec { + let max = maximums(modules); + let mut out = modules.iter().map(|(name, module)| { + let metrics = metrics_json(module); + json!({ + "id": stable_id(&format!("module:{name}")), "name": name, "files": module.files, + "metrics": metrics, + "colors": { + "Size": color(module.files, heat(module.files, max.files)), + "Coupling": color(module.coupling, heat(module.coupling, max.coupling)), + "TestGap": color(module.test_gap, heat(module.test_gap, max.test_gap)), + "Age": color_optional(module.avg_age_days, heat(module.avg_age_days.unwrap_or(0), max.avg_age_days.unwrap_or(0))), + "Churn": color(module.churn, heat(module.churn, max.churn)), + "Risk": color(module.risk, module.risk as f64), + "Git": color(module.git_files, heat(module.git_files, max.git_files)), + "ExecDepth": color(module.exec_depth, heat(module.exec_depth, max.exec_depth)), + "BlastRadius": color(module.blast_radius, heat(module.blast_radius, max.blast_radius)) + } + }) + }).collect::>(); + out.sort_by(|left, right| { + integer(&right["colors"]["Risk"], "score") + .cmp(&integer(&left["colors"]["Risk"], "score")) + .then_with(|| string(left, "name").cmp(string(right, "name"))) + }); + out +} + +fn metrics_json(module: &ModuleMetrics) -> Value { + json!({ + "files": module.files, "source_files": module.source_files, "test_files": module.test_files, + "test_gap": module.test_gap, "avg_age_days": module.avg_age_days, "max_age_days": module.max_age_days, + "churn": module.churn, "dirty_files": module.dirty_files, "untracked_files": module.untracked_files, + "git_files": module.git_files, "inbound_edges": module.inbound_edges, "outbound_edges": module.outbound_edges, + "coupling": module.coupling, "exec_depth": module.exec_depth, "blast_radius": module.blast_radius, "risk": module.risk + }) +} + +fn edge_output(edges: &BTreeMap<(String, String), i64>) -> Vec { + edges.iter().map(|((from, to), count)| json!({ + "id": stable_id(&format!("edge:{from}->{to}")), "from": from, "to": to, "count": count + })).collect() +} + +fn color_modes() -> Value { + json!([ + {"name":"Size","key":"size","metric":"files","meaning":"module file count"}, + {"name":"Coupling","key":"coupling","metric":"coupling","meaning":"incoming plus outgoing dependency edges"}, + {"name":"TestGap","key":"test_gap","metric":"test_gap","meaning":"source files without matching test density"}, + {"name":"Age","key":"age","metric":"avg_age_days","meaning":"average days since last git commit touching files in the module"}, + {"name":"Churn","key":"churn","metric":"churn","meaning":"git commit touches for files in the module"}, + {"name":"Risk","key":"risk","metric":"risk","meaning":"composite score from coupling, blast radius, execution depth, churn, git dirtiness, test gap, age, and size"}, + {"name":"Git","key":"git","metric":"git_files","meaning":"dirty or untracked files in the module"}, + {"name":"ExecDepth","key":"exec_depth","metric":"exec_depth","meaning":"approximate dependency-chain depth from this module"}, + {"name":"BlastRadius","key":"blast_radius","metric":"blast_radius","meaning":"reachable dependents plus incident dependency edges"} + ]) +} + +fn maximums(modules: &BTreeMap) -> ModuleMetrics { + let mut max = ModuleMetrics::default(); + for module in modules.values() { + max.files = max.files.max(module.files); + max.coupling = max.coupling.max(module.coupling); + max.test_gap = max.test_gap.max(module.test_gap); + max.avg_age_days = Some( + max.avg_age_days + .unwrap_or(0) + .max(module.avg_age_days.unwrap_or(0)), + ); + max.churn = max.churn.max(module.churn); + max.git_files = max.git_files.max(module.git_files); + max.exec_depth = max.exec_depth.max(module.exec_depth); + max.blast_radius = max.blast_radius.max(module.blast_radius); + } + max +} + +fn color(value: i64, score: f64) -> Value { + json!({"value": value, "score": score.round() as i64, "color": heat_color(score)}) +} +fn color_optional(value: Option, score: f64) -> Value { + json!({"value": value, "score": score.round() as i64, "color": heat_color(score)}) +} +fn heat(value: i64, max: i64) -> f64 { + if max <= 0 { + 0.0 + } else { + ((value as f64 / max as f64) * 100.0) + .clamp(0.0, 100.0) + .round() + } +} + +fn heat_color(score: f64) -> String { + let bounded = score.clamp(0.0, 100.0); + let (r, g, b) = if bounded <= 50.0 { + let t = bounded / 50.0; + ( + 34.0 + (245.0 - 34.0) * t, + 197.0 + (158.0 - 197.0) * t, + 94.0 + (11.0 - 94.0) * t, + ) + } else { + let t = (bounded - 50.0) / 50.0; + ( + 245.0 + (239.0 - 245.0) * t, + 158.0 + (68.0 - 158.0) * t, + 11.0 + (68.0 - 11.0) * t, + ) + }; + format!( + "#{:02X}{:02X}{:02X}", + r.round() as u8, + g.round() as u8, + b.round() as u8 + ) +} + +fn module_name(relative: &str) -> String { + let normalized = normalize_path(relative); + let mut parts = normalized + .split('/') + .filter(|part| !part.is_empty()) + .collect::>(); + if let Some(last) = parts.last_mut() { + if let Some(rest) = last.strip_prefix("test_") { + *last = rest + } + } + match parts.as_slice() { + [] => "root".to_string(), + [one] => (*one).to_string(), + [first, _] if ["app", "src", "tests"].contains(first) && parts[1].contains('.') => { + (*first).to_string() + } + ["backend", second, third, ..] if ["app", "src", "tests"].contains(second) => { + format!("backend/{second}/{third}") + } + ["backend", second, ..] => format!("backend/{second}"), + [first, second, third, ..] if ["app", "src", "tests"].contains(first) => { + format!("{first}/{second}/{third}") + } + [first, _] + if [ + "frontend", + "integrations", + "research", + "scripts", + "services", + ] + .contains(first) + && parts.len() == 2 => + { + (*first).to_string() + } + [first, second, ..] + if [ + "frontend", + "integrations", + "research", + "scripts", + "services", + ] + .contains(first) => + { + format!("{first}/{second}") + } + [first, ..] => (*first).to_string(), + } +} + +fn is_test_file(relative: &str) -> bool { + let lower = relative.to_ascii_lowercase(); + let leaf = lower.rsplit('/').next().unwrap_or(&lower); + lower.starts_with("test/") + || lower.starts_with("tests/") + || lower.contains("/test/") + || lower.contains("/tests/") + || lower.contains("/__tests__/") + || leaf.starts_with("test_") + || leaf.contains(".test.") + || leaf.contains(".spec.") +} + +fn split_lines(content: &str) -> Vec { + if content.is_empty() { + return Vec::new(); + } + content + .split('\n') + .map(|line| line.strip_suffix('\r').unwrap_or(line).to_string()) + .collect() +} + +fn line_stats(lines: &[String]) -> (usize, usize, usize, usize) { + let mut blank = 0; + let mut comments = 0; + for line in lines { + let trimmed = line.trim(); + if trimmed.is_empty() { + blank += 1 + } else if trimmed.starts_with('#') + || trimmed.starts_with("//") + || trimmed.starts_with("/*") + || trimmed.starts_with('*') + { + comments += 1 + } + } + ( + lines.len(), + lines.len().saturating_sub(blank + comments), + blank, + comments, + ) +} + +fn count_word(text: &str, word: &str) -> i64 { + text.split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_')) + .filter(|token| *token == word) + .count() as i64 +} + +fn param_count(params: &str) -> usize { + params + .split(',') + .map(str::trim) + .filter(|param| { + !param.is_empty() && !["self", "&self", "mut self", "&mut self", "cls"].contains(param) + }) + .count() +} + +fn leading_whitespace(line: &str) -> usize { + line.chars().take_while(|ch| ch.is_whitespace()).count() +} +fn identifier(text: &str) -> Option<&str> { + let trimmed = text.trim(); + let end = trimmed + .char_indices() + .take_while(|(_, ch)| ch.is_ascii_alphanumeric() || *ch == '_' || *ch == ':' || *ch == '-') + .last() + .map(|(index, ch)| index + ch.len_utf8())?; + let value = &trimmed[..end]; + value + .chars() + .next() + .filter(|ch| ch.is_ascii_alphabetic() || *ch == '_') + .map(|_| value) +} + +fn language(relative: &str) -> &'static str { + match extension(relative).as_str() { + ".py" => "python", + ".rs" => "rust", + ".ts" | ".tsx" => "typescript", + ".js" | ".jsx" | ".mjs" | ".cjs" => "javascript", + ".ps1" | ".psm1" => "powershell", + ".go" => "go", + ".java" => "java", + ".cs" => "csharp", + ".v" => "vlang", + _ => "unknown", + } +} + +fn extension(relative: &str) -> String { + Path::new(relative) + .extension() + .map(|ext| format!(".{}", ext.to_string_lossy().to_ascii_lowercase())) + .unwrap_or_default() +} +fn normalize_path(path: &str) -> String { + path.replace('\\', "/") + .trim() + .trim_start_matches("./") + .to_string() +} +fn source_anchor(path: &str, line: i64) -> Value { + json!({"path": path, "line": line, "label": format!("{path}:{line}")}) +} +fn round2(value: f64) -> f64 { + (value * 100.0).round() / 100.0 +} +fn integer(value: &Value, key: &str) -> i64 { + value.get(key).and_then(Value::as_i64).unwrap_or(0) +} +fn string<'a>(value: &'a Value, key: &str) -> &'a str { + value.get(key).and_then(Value::as_str).unwrap_or("") +} +fn cli_path(path: &Path) -> String { + path.to_string_lossy() + .strip_prefix(r"\\?\") + .unwrap_or(&path.to_string_lossy()) + .to_string() +} + +fn stable_id(text: &str) -> String { + sha1(text.as_bytes())[..8] + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn sha1(input: &[u8]) -> [u8; 20] { + let mut message = input.to_vec(); + let bit_len = (message.len() as u64) * 8; + message.push(0x80); + while message.len() % 64 != 56 { + message.push(0) + } + message.extend_from_slice(&bit_len.to_be_bytes()); + let mut h = [ + 0x67452301u32, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476, + 0xC3D2E1F0, + ]; + for chunk in message.chunks_exact(64) { + let mut w = [0u32; 80]; + for (index, word) in chunk.chunks_exact(4).enumerate() { + w[index] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]); + } + for index in 16..80 { + w[index] = (w[index - 3] ^ w[index - 8] ^ w[index - 14] ^ w[index - 16]).rotate_left(1) + } + let (mut a, mut b, mut c, mut d, mut e) = (h[0], h[1], h[2], h[3], h[4]); + for (index, word) in w.iter().enumerate() { + let (f, k) = match index { + 0..=19 => ((b & c) | ((!b) & d), 0x5A827999), + 20..=39 => (b ^ c ^ d, 0x6ED9EBA1), + 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), + _ => (b ^ c ^ d, 0xCA62C1D6), + }; + let temp = a + .rotate_left(5) + .wrapping_add(f) + .wrapping_add(e) + .wrapping_add(k) + .wrapping_add(*word); + e = d; + d = c; + c = b.rotate_left(30); + b = a; + a = temp; + } + h[0] = h[0].wrapping_add(a); + h[1] = h[1].wrapping_add(b); + h[2] = h[2].wrapping_add(c); + h[3] = h[3].wrapping_add(d); + h[4] = h[4].wrapping_add(e); + } + let mut out = [0u8; 20]; + for (index, word) in h.iter().enumerate() { + out[index * 4..index * 4 + 4].copy_from_slice(&word.to_be_bytes()) + } + out +} diff --git a/crates/code-intel-cli/tests/sentrux_analysis.rs b/crates/code-intel-cli/tests/sentrux_analysis.rs new file mode 100644 index 0000000..e319277 --- /dev/null +++ b/crates/code-intel-cli/tests/sentrux_analysis.rs @@ -0,0 +1,125 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[path = "../src/sentrux_analysis.rs"] +mod sentrux_analysis; + +struct Fixture { + root: PathBuf, +} + +impl Fixture { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let root = std::env::temp_dir().join(format!("code-intel-sentrux-analysis-{nonce}")); + fs::create_dir_all(&root).expect("create fixture root"); + Self { root } + } + + fn write(&self, relative: &str, content: &str) { + let path = self.root.join(relative); + fs::create_dir_all(path.parent().expect("fixture parent")).expect("create fixture dir"); + fs::write(path, content).expect("write fixture"); + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.root); + } +} + +fn file<'a>(snapshot: &'a serde_json::Value, path: &str) -> &'a serde_json::Value { + snapshot["file_details"] + .as_array() + .expect("file_details array") + .iter() + .find(|file| file["path"] == path) + .unwrap_or_else(|| panic!("missing {path}")) +} + +#[test] +fn dsm_snapshot_preserves_contract_and_excludes_non_governed_source() { + let fixture = Fixture::new(); + fixture.write( + "src/lib.rs", + "// choice\npub async fn choose(value: i32) -> i32 {\n if value > 0 && value < 10 { value } else { 0 }\n}\n", + ); + fixture.write( + "tests/test_lib.rs", + "fn choose_contract() { assert_eq!(1, 1); }\n", + ); + fixture.write( + "tools/ignored.ps1", + "function Invoke-Ignored { if ($true) { 1 } }\n", + ); + fixture.write("target/ignored.rs", "fn ignored() {}\n"); + fixture.write("README.md", "not source\n"); + + let snapshot = sentrux_analysis::analyze(&fixture.root).expect("native DSM analysis"); + + assert_eq!(snapshot["tool"], "dsm"); + assert_eq!(snapshot["default_color_mode"], "Risk"); + assert_eq!(snapshot["color_modes"].as_array().unwrap().len(), 9); + assert_eq!(snapshot["scope"]["included_files"], 2); + assert_eq!(snapshot["scope"]["excluded_files"], 2); + assert_eq!(snapshot["file_details"].as_array().unwrap().len(), 2); + assert!(snapshot.get("modules").is_some()); + assert!(snapshot.get("edges").is_some()); + assert!(snapshot.get("note").is_some()); + + let rust = file(&snapshot, "src/lib.rs"); + assert_eq!(rust["id"], "1ec5a09a3e51f785"); + assert_eq!(rust["language"], "rust"); + assert_eq!(rust["function_count"], 1); + assert_eq!(rust["functions"][0]["name"], "choose"); + assert_eq!(rust["functions"][0]["async"], true); + assert_eq!(rust["functions"][0]["public"], true); + assert_eq!(rust["functions"][0]["params"], 1); + assert_eq!(rust["functions"][0]["complexity"], 3); + assert_eq!(rust["source_anchor"]["label"], "src/lib.rs:1"); +} + +#[test] +fn dsm_snapshot_builds_cross_module_edges_and_risk_colors() { + let fixture = Fixture::new(); + fixture.write( + "foo/a.py", + "from bar import b\n\ndef alpha(x):\n if x:\n return b(x)\n return 0\n", + ); + fixture.write("bar/b.py", "def b(x):\n return x\n"); + + let snapshot = + sentrux_analysis::analyze(Path::new(&fixture.root)).expect("native DSM analysis"); + let edges = snapshot["edges"].as_array().expect("edge array"); + assert!(edges + .iter() + .any(|edge| edge["from"] == "foo" && edge["to"] == "bar")); + + let modules = snapshot["modules"].as_array().expect("module array"); + let foo = modules + .iter() + .find(|module| module["name"] == "foo") + .expect("foo module"); + assert_eq!(foo["metrics"]["outbound_edges"], 1); + assert_eq!(foo["metrics"]["coupling"], 1); + assert!(foo["colors"]["Risk"]["score"].is_number()); + assert!(foo["colors"]["Risk"]["color"] + .as_str() + .expect("risk color") + .starts_with('#')); +} + +#[test] +fn production_pipeline_prefers_rust_dsm_with_explicit_powershell_rollback() { + let pipeline = include_str!("../../../run-code-intel.ps1"); + assert!(pipeline.contains("CODE_INTEL_SENTRUX_DSM_PROVIDER")); + assert!(pipeline.contains("CODE_INTEL_RUST_CLI")); + assert!(pipeline.contains("& $sentruxDsmRustCli sentrux dsm $sentruxTargetPath")); + assert!(pipeline.contains("& $sentruxAgentTool dsm $sentruxTargetPath")); + assert!(pipeline.contains("Sentrux DSM provider: $sentruxDsmProvider")); +} diff --git a/docs/plans/sentrux-rust-analysis-kernel-idea.md b/docs/plans/sentrux-rust-analysis-kernel-idea.md new file mode 100644 index 0000000..6579877 --- /dev/null +++ b/docs/plans/sentrux-rust-analysis-kernel-idea.md @@ -0,0 +1,37 @@ +# Sentrux Rust Analysis Kernel — Idea Record + +## Outcome + +Move the performance-sensitive DSM analysis path from `Invoke-SentruxAgentTool.ps1` into the +`code-intel` Rust CLI without changing the artifact contract consumed by `run-code-intel.ps1`. + +## First vertical slice + +- Add `code-intel sentrux dsm ` as a native Rust operation. +- Port governed-source inventory, file/function metrics, stable IDs, module aggregation, + dependency edges, derived risk, and the nine DSM color modes. +- Preserve the existing root JSON fields and nested field names, including `file_details`. +- Prefer the Rust command in production pipeline execution. +- Keep the PowerShell DSM implementation only as an explicit compatibility fallback when the + Rust executable is unavailable or fails. + +## Non-goals for this slice + +- Reimplement Sentrux `check`, `gate`, or parser/plugin ownership. +- Port `evolution`, `what_if`, or session persistence yet; they will consume the shared Rust + snapshot in later slices. +- Add a new runtime dependency or change admission policy. + +## Acceptance + +1. Fixture tests lock inventory exclusions, stable IDs, function metrics, module metrics, and JSON + shape. +2. The production pipeline records Rust as the DSM provider on a successful native invocation and + records an explicit fallback note otherwise. +3. Rust tests, targeted PowerShell tests, and a fresh normal pipeline pass. +4. Native DSM is measurably faster than the prior PowerShell DSM on this repository. + +## Rollback + +Set `CODE_INTEL_SENTRUX_DSM_PROVIDER=powershell` to select the compatibility path while retaining +the same downstream artifact contract. diff --git a/orchestration/integrations.json b/orchestration/integrations.json index b04b1f2..8db197a 100644 --- a/orchestration/integrations.json +++ b/orchestration/integrations.json @@ -261,6 +261,7 @@ ], "commands": { "rustTarget": "target/debug/code-intel.exe sentrux ", + "rustDsm": "target/debug/code-intel.exe sentrux dsm ", "scan": "Invoke-SentruxAgentTool.ps1 scan ", "sessionStart": "Invoke-SentruxAgentTool.ps1 session_start ", "sessionEnd": "Invoke-SentruxAgentTool.ps1 session_end " diff --git a/run-code-intel.ps1 b/run-code-intel.ps1 index 8d6c122..fc9ba1b 100644 --- a/run-code-intel.ps1 +++ b/run-code-intel.ps1 @@ -4011,16 +4011,63 @@ $codeNexusContextSummary = $null $sentruxAgentTool = Join-Path $PSScriptRoot "Invoke-SentruxAgentTool.ps1" if (-not [string]::IsNullOrWhiteSpace($sentruxTargetPath) -and (Test-Path -LiteralPath $sentruxAgentTool -PathType Leaf)) { try { - $previousErrorActionPreference = $ErrorActionPreference - try { - $ErrorActionPreference = "Continue" - $dsmRaw = & $sentruxAgentTool dsm $sentruxTargetPath 2>&1 + $sentruxDsmPreference = if ([string]::IsNullOrWhiteSpace($env:CODE_INTEL_SENTRUX_DSM_PROVIDER)) { + "rust" + } else { + $env:CODE_INTEL_SENTRUX_DSM_PROVIDER.Trim().ToLowerInvariant() } - finally { - $ErrorActionPreference = $previousErrorActionPreference + if ($sentruxDsmPreference -notin @("rust", "powershell")) { + throw "CODE_INTEL_SENTRUX_DSM_PROVIDER must be 'rust' or 'powershell'" + } + $sentruxDsmRustCli = if ([string]::IsNullOrWhiteSpace($env:CODE_INTEL_RUST_CLI)) { + Join-Path $PSScriptRoot "target\debug\code-intel.exe" + } else { + [IO.Path]::GetFullPath($env:CODE_INTEL_RUST_CLI) + } + $dsmObject = $null + $sentruxDsmProvider = "" + if ($sentruxDsmPreference -eq "rust" -and (Test-Path -LiteralPath $sentruxDsmRustCli -PathType Leaf)) { + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $global:LASTEXITCODE = 0 + $dsmRaw = & $sentruxDsmRustCli sentrux dsm $sentruxTargetPath 2>&1 + $dsmExitCode = $global:LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($dsmExitCode -eq 0) { + $dsmText = ($dsmRaw | ForEach-Object { $_.ToString() } | Out-String).Trim() + try { + $dsmObject = $dsmText | ConvertFrom-Json + $sentruxDsmProvider = "rust" + } + catch { + $notes.Add("Rust Sentrux DSM returned invalid JSON; using the explicit PowerShell compatibility fallback.") + } + } + else { + $notes.Add("Rust Sentrux DSM exited with code $dsmExitCode; using the explicit PowerShell compatibility fallback.") + } + } + elseif ($sentruxDsmPreference -eq "rust") { + $notes.Add("Rust Sentrux DSM binary was unavailable at $sentruxDsmRustCli; using the explicit PowerShell compatibility fallback.") + } + if ($null -eq $dsmObject) { + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $dsmRaw = & $sentruxAgentTool dsm $sentruxTargetPath 2>&1 + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + $dsmText = ($dsmRaw | ForEach-Object { $_.ToString() } | Out-String).Trim() + $dsmObject = $dsmText | ConvertFrom-Json + $sentruxDsmProvider = "powershell_compatibility" } - $dsmText = ($dsmRaw | ForEach-Object { $_.ToString() } | Out-String).Trim() - $dsmObject = $dsmText | ConvertFrom-Json + $notes.Add("Sentrux DSM provider: $sentruxDsmProvider") $fileDetails = @($dsmObject.file_details) $dsmObject.PSObject.Properties.Remove("file_details") $dsmObject | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $sentruxDsmPath -Encoding UTF8 From fd13b071f61896e3c48ac8229aeb38187af70041 Mon Sep 17 00:00:00 2001 From: Curry Date: Wed, 15 Jul 2026 23:44:53 +0800 Subject: [PATCH 03/34] fix: harden Rust Sentrux analysis --- Invoke-SentruxAgentTool.ps1 | 260 +++++++++-- crates/code-intel-cli/src/sentrux_analysis.rs | 438 ++++++++++++++---- .../code-intel-cli/tests/sentrux_analysis.rs | 90 ++++ run-code-intel.ps1 | 25 +- 4 files changed, 692 insertions(+), 121 deletions(-) diff --git a/Invoke-SentruxAgentTool.ps1 b/Invoke-SentruxAgentTool.ps1 index 1524168..e5cd2f1 100644 --- a/Invoke-SentruxAgentTool.ps1 +++ b/Invoke-SentruxAgentTool.ps1 @@ -887,6 +887,10 @@ function Get-ModuleName { return "backend/$($parts[1])" } + if ($parts[0] -in @("crates", "packages")) { + return "$($parts[0])/$($parts[1])" + } + if ($parts[0] -in @("app", "src", "tests")) { if ($parts.Count -ge 4 -and $parts[1] -eq "markets") { if ($parts.Count -eq 4) { @@ -1277,7 +1281,7 @@ function Add-DsmEdge { if (-not $Edges.ContainsKey($key)) { $Edges[$key] = [ordered]@{ from = $From; to = $To; count = 0 } } - $Edges[$key]["count"] = 1 + $Edges[$key]["count"] = [int]$Edges[$key]["count"] + 1 } function ConvertTo-HeatColor { @@ -1399,7 +1403,10 @@ function Get-ParamCount { } function Measure-FunctionComplexity { - param([string[]]$Lines) + param( + [string[]]$Lines, + [switch]$IgnoreArrowSyntax + ) $complexity = 1 foreach ($line in $Lines) { @@ -1408,7 +1415,7 @@ function Measure-FunctionComplexity { if ($trimmed.StartsWith("#") -or $trimmed.StartsWith("//")) { continue } $complexity += [regex]::Matches($trimmed, "\b(if|elif|for|while|except|case|catch|match|guard|when|with)\b").Count $complexity += [regex]::Matches($trimmed, "&&|\|\|").Count - if ($trimmed -match "\s=>\s") { $complexity++ } + if (-not $IgnoreArrowSyntax -and $trimmed -match "\s=>\s") { $complexity++ } } return $complexity } @@ -1444,7 +1451,8 @@ function New-FunctionMetric { [string[]]$BodyLines, [string]$Params = "", [bool]$IsAsync = $false, - [bool]$IsPublic = $false + [bool]$IsPublic = $false, + [switch]$IgnoreArrowSyntax ) $stats = Measure-LineStats $BodyLines @@ -1455,13 +1463,79 @@ function New-FunctionMetric { end_line = $EndLine lines = $stats["lines"] loc = $stats["loc"] - complexity = Measure-FunctionComplexity $BodyLines + complexity = Measure-FunctionComplexity -Lines $BodyLines -IgnoreArrowSyntax:$IgnoreArrowSyntax params = Get-ParamCount $Params async = $IsAsync public = $IsPublic } } +function Get-CodeStructuralLine { + param( + [string]$Line, + [ref]$InBlockComment + ) + + $result = [ordered]@{ opens = 0; closes = 0; semicolon = $false; arrow_expression = $false } + $chars = @($Line.ToCharArray()) + $quote = 0 + $escaped = $false + for ($index = 0; $index -lt $chars.Count; $index++) { + $code = [int]$chars[$index] + $nextCode = if ($index + 1 -lt $chars.Count) { [int]$chars[$index + 1] } else { -1 } + if ($InBlockComment.Value) { + if ($code -eq 42 -and $nextCode -eq 47) { + $InBlockComment.Value = $false + $index++ + } + continue + } + if ($quote -ne 0) { + if ($escaped) { + $escaped = $false + } + elseif ($code -eq 92 -or ($quote -eq 96 -and $code -eq 96)) { + $escaped = $true + } + elseif ($code -eq $quote) { + $quote = 0 + } + continue + } + if ($code -eq 47 -and $nextCode -eq 47) { break } + if ($code -eq 47 -and $nextCode -eq 42) { + $InBlockComment.Value = $true + $index++ + continue + } + if ($code -eq 35 -and $Line.Substring(0, $index).Trim().Length -eq 0) { break } + $isRustLifetime = $false + if ($code -eq 39 -and $nextCode -ge 0 -and ([char]$nextCode -match "[A-Za-z_]")) { + $isRustLifetime = -not (@($chars[($index + 1)..($chars.Count - 1)] | Where-Object { [int]$_ -eq 39 }).Count -gt 0) + } + if (($code -in @(34, 39, 96)) -and -not $isRustLifetime) { + $quote = $code + continue + } + switch ($code) { + 123 { $result["opens"] = [int]$result["opens"] + 1 } + 125 { $result["closes"] = [int]$result["closes"] + 1 } + 59 { $result["semicolon"] = $true } + 61 { if ($nextCode -eq 62) { $result["arrow_expression"] = $true } } + } + } + return $result +} + +function Test-IsCLikeFunctionDeclaration { + param([string]$Line) + + return $Line -match "^\s*(?:(?:pub(?:\([^)]*\))?|export)\s+)?(?:(?:async)\s+)?fn\s+[A-Za-z_][A-Za-z0-9_]*\s*\(" -or + $Line -match "^\s*(?:export\s+)?(?:async\s+)?function\s+[A-Za-z_][A-Za-z0-9_]*\s*\(" -or + $Line -match "^\s*(?:export\s+)?(?:const|let|var)\s+[A-Za-z_][A-Za-z0-9_]*\s*=.*=>" -or + $Line -match "^\s*function\s+[A-Za-z_][A-Za-z0-9_:-]*" +} + function Get-CLikeFunctionEndLine { param( [string[]]$Lines, @@ -1470,19 +1544,20 @@ function Get-CLikeFunctionEndLine { $braceDepth = 0 $seenBrace = $false + $inBlockComment = $false for ($i = $StartIndex; $i -lt $Lines.Count; $i++) { $line = $Lines[$i] - foreach ($char in $line.ToCharArray()) { - if ($char -eq "{") { - $braceDepth++ - $seenBrace = $true - } - elseif ($char -eq "}") { - $braceDepth-- - if ($seenBrace -and $braceDepth -le 0) { - return $i - } - } + $structure = Get-CodeStructuralLine -Line $line -InBlockComment ([ref]$inBlockComment) + $braceDepth += [int]$structure["opens"] - [int]$structure["closes"] + if ([int]$structure["opens"] -gt 0) { $seenBrace = $true } + if ($seenBrace -and $braceDepth -le 0) { + return $i + } + if (-not $seenBrace -and ($structure["semicolon"] -or $structure["arrow_expression"])) { + return $i + } + if ($i -gt $StartIndex -and (Test-IsCLikeFunctionDeclaration $line)) { + return [math]::Max($StartIndex, $i - 1) } } return $StartIndex @@ -1501,7 +1576,14 @@ function Get-FunctionsFromPython { for ($j = $i + 1; $j -lt $Lines.Count; $j++) { $candidate = $Lines[$j] $trimmed = $candidate.Trim() - if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith("#") -or $trimmed.StartsWith("@")) { continue } + if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith("#")) { continue } + if ($trimmed.StartsWith("@")) { + if ((Get-LeadingWhitespaceCount $candidate) -le $indent) { + $end = [math]::Max($i, $j - 1) + break + } + continue + } if ((Get-LeadingWhitespaceCount $candidate) -le $indent) { $end = [math]::Max($i, $j - 1) break @@ -1586,7 +1668,8 @@ function Get-FunctionsFromJavaScriptLike { -BodyLines $body ` -Params $match.Groups[4].Value ` -IsAsync (-not [string]::IsNullOrWhiteSpace($match.Groups[3].Value)) ` - -IsPublic (-not [string]::IsNullOrWhiteSpace($match.Groups[1].Value)) + -IsPublic (-not [string]::IsNullOrWhiteSpace($match.Groups[1].Value)) ` + -IgnoreArrowSyntax } continue } @@ -1600,7 +1683,8 @@ function Get-FunctionsFromJavaScriptLike { -BodyLines $body ` -Params $match.Groups[4].Value ` -IsAsync (-not [string]::IsNullOrWhiteSpace($match.Groups[2].Value)) ` - -IsPublic (-not [string]::IsNullOrWhiteSpace($match.Groups[1].Value)) + -IsPublic (-not [string]::IsNullOrWhiteSpace($match.Groups[1].Value)) ` + -IgnoreArrowSyntax } return $functions } @@ -1814,29 +1898,63 @@ function New-DsmModuleState { function Get-DsmImportTargetsForFile { param( [string]$Extension, - [string]$Content + [string]$Content, + [string]$SourcePath, + [object]$Modules ) $targets = New-Object System.Collections.Generic.List[string] switch ($Extension) { ".py" { - foreach ($match in [regex]::Matches($Content, "(?m)^\s*(?:from|import)\s+([A-Za-z_][A-Za-z0-9_\.]*)")) { - $targets.Add((Get-ModuleName ($match.Groups[1].Value -replace "\.", "/"))) + foreach ($match in [regex]::Matches($Content, "(?m)^\s*(?:from|import)\s+([\.]*[A-Za-z_][A-Za-z0-9_\.]*)")) { + $token = $match.Groups[1].Value + $leadingDots = ([regex]::Match($token, "^\.*")).Value.Length + $parts = New-Object System.Collections.Generic.List[string] + if ($leadingDots -gt 0) { + foreach ($part in @((Normalize-RelativeFilePath $SourcePath) -split "/" | Select-Object -SkipLast 1)) { + $parts.Add($part) + } + for ($level = 1; $level -lt $leadingDots -and $parts.Count -gt 0; $level++) { + $parts.RemoveAt($parts.Count - 1) + } + } + foreach ($part in @(($token.Substring($leadingDots)) -split "\." | Where-Object { $_ })) { + $parts.Add($part) + } + $importPath = $parts -join "/" + foreach ($candidate in @( + (Get-ModuleName "$importPath.py"), + (Get-ModuleName "$importPath/__init__.py"), + (Get-ModuleName $importPath) + )) { + if ($Modules.Contains($candidate)) { + $targets.Add($candidate) + break + } + } } } ".rs" { - foreach ($match in [regex]::Matches($Content, "(?m)^\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;")) { - $targets.Add("src/$($match.Groups[1].Value).rs") - } - foreach ($match in [regex]::Matches($Content, "use\s+crate::([A-Za-z_][A-Za-z0-9_]*)")) { - $targets.Add("src/$($match.Groups[1].Value).rs") + foreach ($match in [regex]::Matches($Content, "(?m)^\s*use\s+([A-Za-z_][A-Za-z0-9_]*)::")) { + $name = $match.Groups[1].Value + if ($name -in @("crate", "self", "super", "std", "core", "alloc")) { continue } + foreach ($candidate in @("crates/$name", "packages/$name", $name)) { + if ($Modules.Contains($candidate)) { + $targets.Add($candidate) + break + } + } } } ".v" { foreach ($match in [regex]::Matches($Content, "(?m)^\s*import\s+([A-Za-z_][A-Za-z0-9_\.]*)")) { $root = ($match.Groups[1].Value -split "\.")[0] foreach ($target in @($root, "$root.v", ($match.Groups[1].Value -replace "\.", "/"))) { - $targets.Add($target) + $candidate = Get-ModuleName $target + if ($Modules.Contains($candidate)) { + $targets.Add($candidate) + break + } } } } @@ -1876,7 +1994,7 @@ function Get-DsmEdgesForFiles { if ([string]::IsNullOrWhiteSpace($content)) { continue } $from = Get-ModuleName $file - foreach ($target in @(Get-DsmImportTargetsForFile -Extension $ext -Content $content)) { + foreach ($target in @(Get-DsmImportTargetsForFile -Extension $ext -Content $content -SourcePath $file -Modules $Modules)) { Add-DsmEdgeIfAllowed ` -Edges $edges ` -Modules $Modules ` @@ -1924,28 +2042,92 @@ function New-DsmGraphState { } } +function Get-DsmReachableModules { + param( + [string]$Start, + [object]$Adjacency + ) + + $seen = @{} + $queue = New-Object System.Collections.Generic.Queue[string] + if ($Adjacency.ContainsKey($Start)) { + foreach ($target in $Adjacency[$Start]) { $queue.Enqueue($target) } + } + while ($queue.Count -gt 0) { + $current = $queue.Dequeue() + if ($seen.ContainsKey($current)) { continue } + $seen[$current] = $true + if ($Adjacency.ContainsKey($current)) { + foreach ($target in $Adjacency[$current]) { + if (-not $seen.ContainsKey($target)) { $queue.Enqueue($target) } + } + } + } + return $seen +} + function Get-DsmExecutionDepths { param( [object]$Modules, [object]$Edges ) - $depths = @{} - foreach ($name in $Modules.Keys) { $depths[$name] = 0 } - for ($i = 0; $i -lt [math]::Max(1, $Modules.Keys.Count); $i++) { + $names = @($Modules.Keys) + $adjacency = @{} + foreach ($name in $names) { $adjacency[$name] = New-Object System.Collections.Generic.List[string] } + foreach ($edge in $Edges.Values) { + $from = [string]$edge["from"] + $to = [string]$edge["to"] + if ($adjacency.ContainsKey($from) -and $adjacency.ContainsKey($to) -and -not $adjacency[$from].Contains($to)) { + $adjacency[$from].Add($to) + } + } + + $reachable = @{} + foreach ($name in $names) { + $reachable[$name] = Get-DsmReachableModules -Start $name -Adjacency $adjacency + } + $componentByModule = @{} + $componentCount = 0 + foreach ($name in $names) { + if ($componentByModule.ContainsKey($name)) { continue } + foreach ($candidate in $names) { + if ($componentByModule.ContainsKey($candidate)) { continue } + if ($candidate -eq $name -or + ($reachable[$name].ContainsKey($candidate) -and $reachable[$candidate].ContainsKey($name))) { + $componentByModule[$candidate] = $componentCount + } + } + $componentCount++ + } + + $componentEdges = @{} + foreach ($edge in $Edges.Values) { + $fromComponent = [int]$componentByModule[[string]$edge["from"]] + $toComponent = [int]$componentByModule[[string]$edge["to"]] + if ($fromComponent -ne $toComponent) { + $componentEdges["$fromComponent->$toComponent"] = [ordered]@{ from = $fromComponent; to = $toComponent } + } + } + $componentDepths = @{} + for ($component = 0; $component -lt $componentCount; $component++) { $componentDepths[$component] = 0 } + for ($i = 0; $i -lt [math]::Max(1, $componentCount); $i++) { $changed = $false - foreach ($edge in $Edges.Values) { - $from = [string]$edge["from"] - $to = [string]$edge["to"] - if (-not $depths.ContainsKey($from) -or -not $depths.ContainsKey($to)) { continue } - $candidate = [int]$depths[$to] + 1 - if ($candidate -gt [int]$depths[$from]) { - $depths[$from] = [math]::Min(99, $candidate) + foreach ($edge in $componentEdges.Values) { + $from = [int]$edge["from"] + $to = [int]$edge["to"] + $candidate = [int]$componentDepths[$to] + 1 + if ($candidate -gt [int]$componentDepths[$from]) { + $componentDepths[$from] = [math]::Min(99, $candidate) $changed = $true } } if (-not $changed) { break } } + $depths = @{} + foreach ($name in $names) { + $depths[$name] = [int]$componentDepths[[int]$componentByModule[$name]] + } return $depths } diff --git a/crates/code-intel-cli/src/sentrux_analysis.rs b/crates/code-intel-cli/src/sentrux_analysis.rs index a69ca29..f56f338 100644 --- a/crates/code-intel-cli/src/sentrux_analysis.rs +++ b/crates/code-intel-cli/src/sentrux_analysis.rs @@ -64,7 +64,9 @@ pub fn analyze(target: &Path) -> Result { let mut module_files: BTreeMap> = BTreeMap::new(); for relative in &inventory.files { - let content = fs::read_to_string(target.join(relative)).unwrap_or_default(); + let source_path = target.join(relative); + let content = fs::read_to_string(&source_path) + .map_err(|error| format!("read source {}: {error}", source_path.display()))?; let module = module_name(relative); let signal = git.get(relative).cloned().unwrap_or_else(untracked_signal); let metrics = modules.entry(module.clone()).or_default(); @@ -110,19 +112,22 @@ pub fn analyze(target: &Path) -> Result { } fn source_inventory(target: &Path) -> Result { - let listed = Command::new("rg") + let output = Command::new("rg") .arg("--files") .current_dir(target) .output() - .ok() - .filter(|output| output.status.success()) - .map(|output| { - String::from_utf8_lossy(&output.stdout) - .lines() - .map(normalize_path) - .collect::>() - }) - .unwrap_or_else(|| recursive_files(target)); + .map_err(|error| format!("run rg --files in {}: {error}", target.display()))?; + if !output.status.success() { + return Err(format!( + "rg --files failed in {}: {}", + target.display(), + String::from_utf8_lossy(&output.stderr).trim() + )); + } + let listed = String::from_utf8_lossy(&output.stdout) + .lines() + .map(normalize_path) + .collect::>(); let mut included = Vec::new(); let mut excluded: BTreeMap)> = BTreeMap::new(); @@ -176,27 +181,6 @@ fn source_inventory(target: &Path) -> Result { }) } -fn recursive_files(root: &Path) -> Vec { - fn visit(root: &Path, current: &Path, out: &mut Vec) { - let Ok(entries) = fs::read_dir(current) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - if entry.file_name() != ".git" { - visit(root, &path, out); - } - } else if let Ok(relative) = path.strip_prefix(root) { - out.push(normalize_path(&relative.to_string_lossy())); - } - } - } - let mut out = Vec::new(); - visit(root, root, &mut out); - out -} - fn excluded_reason(relative: &str) -> Option { let lower = relative.to_ascii_lowercase(); let parts = lower @@ -390,7 +374,12 @@ fn resolve_git_key<'a>( let key = signals .keys() .find(|key| { - candidate.ends_with(&format!("/{key}")) || key.ends_with(&format!("/{candidate}")) + candidate + .strip_suffix(key.as_str()) + .is_some_and(|prefix| prefix.ends_with('/')) + || key + .strip_suffix(candidate.as_str()) + .is_some_and(|prefix| prefix.ends_with('/')) })? .clone(); signals.get_mut(&key) @@ -484,7 +473,7 @@ fn functions(language: &str, lines: &[String]) -> Vec { let (count, loc, _, _) = line_stats(body); out.push(json!({ "name": name, "kind": "function", "start_line": index + 1, "end_line": end + 1, - "lines": count, "loc": loc, "complexity": complexity(body), "params": param_count(¶ms), + "lines": count, "loc": loc, "complexity": complexity(language, body), "params": param_count(¶ms), "async": is_async, "public": is_public })); } @@ -606,7 +595,8 @@ fn parse_powershell_signature(line: &str) -> Option<(String, String, bool, bool) let lower = trimmed.to_ascii_lowercase(); let rest = trimmed .get(9..) - .filter(|_| lower.starts_with("function "))?; + .filter(|_| lower.starts_with("function "))? + .trim_start(); let name = rest .split(|ch: char| ch.is_whitespace() || ch == '{' || ch == '(') .next()?; @@ -617,7 +607,13 @@ fn python_end(lines: &[String], start: usize) -> usize { let indent = leading_whitespace(&lines[start]); for (index, line) in lines.iter().enumerate().skip(start + 1) { let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with('@') { + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + if trimmed.starts_with('@') { + if leading_whitespace(line) <= indent { + return index.saturating_sub(1).max(start); + } continue; } if leading_whitespace(line) <= indent { @@ -629,25 +625,102 @@ fn python_end(lines: &[String], start: usize) -> usize { fn c_like_end(lines: &[String], start: usize) -> usize { let mut depth = 0i64; - let mut seen = false; + let mut seen_brace = false; + let mut block_comment = false; for (index, line) in lines.iter().enumerate().skip(start) { - for ch in line.chars() { - if ch == '{' { - depth += 1; - seen = true + let structure = structural_line(line, &mut block_comment); + depth += structure.opens - structure.closes; + seen_brace |= structure.opens > 0; + if seen_brace && depth <= 0 { + return index; + } + if !seen_brace && (structure.semicolon || structure.arrow_expression) { + return index; + } + if index > start && looks_like_function_declaration(line) { + return index.saturating_sub(1).max(start); + } + } + start +} + +#[derive(Default)] +struct StructuralLine { + opens: i64, + closes: i64, + semicolon: bool, + arrow_expression: bool, +} + +fn structural_line(line: &str, block_comment: &mut bool) -> StructuralLine { + let chars = line.chars().collect::>(); + let mut result = StructuralLine::default(); + let mut quote = None; + let mut escaped = false; + let mut index = 0; + while index < chars.len() { + let ch = chars[index]; + let next = chars.get(index + 1).copied(); + if *block_comment { + if ch == '*' && next == Some('/') { + *block_comment = false; + index += 2; + } else { + index += 1; } - if ch == '}' { - depth -= 1; - if seen && depth <= 0 { - return index; - } + continue; + } + if let Some(delimiter) = quote { + if escaped { + escaped = false; + } else if ch == '\\' { + escaped = true; + } else if ch == delimiter { + quote = None; } + index += 1; + continue; + } + if ch == '/' && next == Some('/') { + break; + } + if ch == '/' && next == Some('*') { + *block_comment = true; + index += 2; + continue; + } + if ch == '#' && chars[..index].iter().all(|value| value.is_whitespace()) { + break; + } + let is_rust_lifetime = ch == '\'' + && next.is_some_and(|value| value.is_ascii_alphabetic() || value == '_') + && !chars[index + 1..].contains(&'\''); + if matches!(ch, '\'' | '"' | '`') && !is_rust_lifetime { + quote = Some(ch); + index += 1; + continue; + } + match ch { + '{' => result.opens += 1, + '}' => result.closes += 1, + ';' => result.semicolon = true, + '=' if next == Some('>') => result.arrow_expression = true, + _ => {} } + index += 1; } - start + result +} + +fn looks_like_function_declaration(line: &str) -> bool { + parse_python_signature(line).is_some() + || parse_c_signature(line, "fn ", true).is_some() + || parse_c_signature(line, "fn ", false).is_some() + || parse_javascript_signature(line).is_some() + || parse_powershell_signature(line).is_some() } -fn complexity(lines: &[String]) -> i64 { +fn complexity(language: &str, lines: &[String]) -> i64 { let keywords = [ "if", "elif", "for", "while", "except", "case", "catch", "match", "guard", "when", "with", ]; @@ -662,7 +735,9 @@ fn complexity(lines: &[String]) -> i64 { .map(|word| count_word(trimmed, word)) .sum::(); score += trimmed.matches("&&").count() as i64 + trimmed.matches("||").count() as i64; - score += i64::from(trimmed.contains(" => ")); + if !matches!(language, "javascript" | "typescript") { + score += i64::from(trimmed.contains(" => ")); + } } score } @@ -682,16 +757,22 @@ fn dsm_edges( let Some(content) = contents.get(file) else { continue; }; - for target in import_targets(&ext, content) { + for target in import_targets(&ext, file, content, files, modules) { if target != from && modules.contains_key(&target) { - edges.insert((from.clone(), target), 1); + *edges.entry((from.clone(), target)).or_default() += 1; } } } edges } -fn import_targets(extension: &str, content: &str) -> Vec { +fn import_targets( + extension: &str, + source: &str, + content: &str, + files: &[String], + modules: &BTreeMap, +) -> Vec { let mut targets = Vec::new(); for line in content.lines() { let trimmed = line.trim_start(); @@ -702,18 +783,32 @@ fn import_targets(extension: &str, content: &str) -> Vec { .or_else(|| trimmed.strip_prefix("import ")) .and_then(|rest| rest.split_whitespace().next()); if let Some(token) = token { - targets.push(module_name(&token.replace('.', "/"))); + if let Some(target) = resolve_python_import(source, token, files) { + targets.push(target); + } } } ".rs" => { if let Some(rest) = trimmed.strip_prefix("mod ") { if let Some(name) = identifier(rest) { - targets.push(module_name(&format!("src/{name}.rs"))); + if let Some(target) = resolve_relative_source(source, name, ".rs", files) { + targets.push(module_name(target)); + } } } if let Some(rest) = trimmed.strip_prefix("use crate::") { - if let Some(name) = identifier(rest) { - targets.push(module_name(&format!("src/{name}.rs"))); + if let Some(name) = identifier(rest).and_then(|path| path.split("::").next()) { + if let Some(target) = resolve_crate_source(source, name, files) { + targets.push(module_name(target)); + } + } + } else if let Some(rest) = trimmed.strip_prefix("use ") { + if let Some(name) = identifier(rest).and_then(|path| path.split("::").next()) { + if !matches!(name, "crate" | "self" | "super" | "std" | "core" | "alloc") { + if let Some(target) = resolve_module_token(name, modules) { + targets.push(target); + } + } } } } @@ -721,11 +816,15 @@ fn import_targets(extension: &str, content: &str) -> Vec { if let Some(rest) = trimmed.strip_prefix("import ") { if let Some(token) = rest.split_whitespace().next() { let root = token.split('.').next().unwrap_or(token); - targets.extend([ - module_name(root), - module_name(&format!("{root}.v")), - module_name(&token.replace('.', "/")), - ]); + for candidate in [ + root.to_string(), + format!("{root}.v"), + token.replace('.', "/"), + ] { + if let Some(target) = resolve_module_token(&candidate, modules) { + targets.push(target); + } + } } } } @@ -735,6 +834,101 @@ fn import_targets(extension: &str, content: &str) -> Vec { targets } +fn resolve_python_import(source: &str, token: &str, files: &[String]) -> Option { + let leading_dots = token.chars().take_while(|ch| *ch == '.').count(); + let mut parts = source + .rsplit_once('/') + .map(|(directory, _)| directory.split('/').collect::>()) + .unwrap_or_default(); + if leading_dots == 0 { + parts.clear(); + } else { + for _ in 1..leading_dots { + parts.pop(); + } + } + parts.extend( + token[leading_dots..] + .split('.') + .filter(|part| !part.is_empty()), + ); + if parts.is_empty() { + return None; + } + let base = parts.join("/"); + let file_candidate = format!("{base}.py"); + let init_candidate = format!("{base}/__init__.py"); + files + .iter() + .find(|file| *file == &file_candidate || *file == &init_candidate) + .or_else(|| { + let prefix = format!("{base}/"); + files.iter().find(|file| file.starts_with(&prefix)) + }) + .map(|file| module_name(file)) +} + +fn resolve_relative_source<'a>( + source: &str, + name: &str, + extension: &str, + files: &'a [String], +) -> Option<&'a str> { + let directory = source.rsplit_once('/').map(|(path, _)| path).unwrap_or(""); + let prefix = if directory.is_empty() { + name.to_string() + } else { + format!("{directory}/{name}") + }; + let direct = format!("{prefix}{extension}"); + let nested = format!("{prefix}/mod{extension}"); + files + .iter() + .find(|file| *file == &direct || *file == &nested) + .map(String::as_str) +} + +fn resolve_crate_source<'a>(source: &str, name: &str, files: &'a [String]) -> Option<&'a str> { + let crate_root = source.find("/src/").map(|index| &source[..index + 4]); + let prefix = crate_root.unwrap_or_else(|| { + source + .rsplit_once('/') + .map(|(directory, _)| directory) + .unwrap_or("") + }); + let candidate_source = if prefix.is_empty() { + format!("{name}.rs") + } else { + format!("{prefix}/{name}.rs") + }; + let candidate_module = if prefix.is_empty() { + format!("{name}/mod.rs") + } else { + format!("{prefix}/{name}/mod.rs") + }; + files + .iter() + .find(|file| *file == &candidate_source || *file == &candidate_module) + .map(String::as_str) +} + +fn resolve_module_token(token: &str, modules: &BTreeMap) -> Option { + let normalized = token.trim_matches(';').replace('.', "/"); + if modules.contains_key(&normalized) { + return Some(normalized); + } + let comparable = normalized.replace('_', "-"); + modules + .keys() + .find(|module| { + module + .rsplit('/') + .next() + .is_some_and(|leaf| leaf.replace('_', "-") == comparable) + }) + .cloned() +} + fn derive_module_metrics( modules: &mut BTreeMap, module_files: &BTreeMap>, @@ -756,23 +950,7 @@ fn derive_module_metrics( module.inbound_edges += count } } - let mut depths = modules - .keys() - .map(|name| (name.clone(), 0i64)) - .collect::>(); - for _ in 0..modules.len().max(1) { - let mut changed = false; - for ((from, to), _) in edges { - let candidate = depths.get(to).copied().unwrap_or(0) + 1; - if candidate > depths.get(from).copied().unwrap_or(0) { - depths.insert(from.clone(), candidate.min(99)); - changed = true; - } - } - if !changed { - break; - } - } + let depths = execution_depths(modules.keys(), &adjacency, &reverse); for (name, module) in modules.iter_mut() { let ages = module_files .get(name) @@ -792,6 +970,107 @@ fn derive_module_metrics( } } +fn execution_depths<'a>( + modules: impl Iterator, + adjacency: &BTreeMap>, + reverse: &BTreeMap>, +) -> BTreeMap { + let names = modules.cloned().collect::>(); + let mut visited = BTreeSet::new(); + let mut finish_order = Vec::with_capacity(names.len()); + for start in &names { + if visited.contains(start) { + continue; + } + let mut stack = vec![(start.clone(), false)]; + while let Some((node, expanded)) = stack.pop() { + if expanded { + finish_order.push(node); + continue; + } + if !visited.insert(node.clone()) { + continue; + } + stack.push((node.clone(), true)); + if let Some(next) = adjacency.get(&node) { + for dependency in next.iter().rev() { + if !visited.contains(dependency) { + stack.push((dependency.clone(), false)); + } + } + } + } + } + + let mut components = BTreeMap::new(); + let mut component_count = 0usize; + for start in finish_order.into_iter().rev() { + if components.contains_key(&start) { + continue; + } + let mut stack = vec![start]; + while let Some(node) = stack.pop() { + if components.insert(node.clone(), component_count).is_some() { + continue; + } + if let Some(next) = reverse.get(&node) { + for dependent in next { + if !components.contains_key(dependent) { + stack.push(dependent.clone()); + } + } + } + } + component_count += 1; + } + + let mut dependencies = vec![BTreeSet::new(); component_count]; + let mut dependents = vec![BTreeSet::new(); component_count]; + for (from, targets) in adjacency { + let Some(&from_component) = components.get(from) else { + continue; + }; + for target in targets { + let Some(&to_component) = components.get(target) else { + continue; + }; + if from_component != to_component && dependencies[from_component].insert(to_component) { + dependents[to_component].insert(from_component); + } + } + } + + let mut remaining = dependencies.iter().map(BTreeSet::len).collect::>(); + let mut component_depths = vec![0i64; component_count]; + let mut queue = VecDeque::new(); + for (component, count) in remaining.iter().enumerate() { + if *count == 0 { + queue.push_back(component); + } + } + while let Some(dependency) = queue.pop_front() { + for &dependent in &dependents[dependency] { + component_depths[dependent] = component_depths[dependent] + .max(component_depths[dependency].saturating_add(1).min(99)); + remaining[dependent] -= 1; + if remaining[dependent] == 0 { + queue.push_back(dependent); + } + } + } + + names + .into_iter() + .map(|name| { + let depth = components + .get(&name) + .map(|component| component_depths[*component]) + .unwrap_or(0); + (name, depth) + }) + .collect() +} + fn reachable(start: &str, reverse: &BTreeMap>) -> usize { let mut seen = BTreeSet::new(); let mut queue = VecDeque::from([start.to_string()]); @@ -964,6 +1243,9 @@ fn module_name(relative: &str) -> String { format!("backend/{second}/{third}") } ["backend", second, ..] => format!("backend/{second}"), + [first, second, ..] if ["crates", "packages"].contains(first) => { + format!("{first}/{second}") + } [first, second, third, ..] if ["app", "src", "tests"].contains(first) => { format!("{first}/{second}/{third}") } diff --git a/crates/code-intel-cli/tests/sentrux_analysis.rs b/crates/code-intel-cli/tests/sentrux_analysis.rs index e319277..6308d29 100644 --- a/crates/code-intel-cli/tests/sentrux_analysis.rs +++ b/crates/code-intel-cli/tests/sentrux_analysis.rs @@ -25,6 +25,12 @@ impl Fixture { fs::create_dir_all(path.parent().expect("fixture parent")).expect("create fixture dir"); fs::write(path, content).expect("write fixture"); } + + fn write_bytes(&self, relative: &str, content: &[u8]) { + let path = self.root.join(relative); + fs::create_dir_all(path.parent().expect("fixture parent")).expect("create fixture dir"); + fs::write(path, content).expect("write fixture bytes"); + } } impl Drop for Fixture { @@ -42,6 +48,15 @@ fn file<'a>(snapshot: &'a serde_json::Value, path: &str) -> &'a serde_json::Valu .unwrap_or_else(|| panic!("missing {path}")) } +fn module<'a>(snapshot: &'a serde_json::Value, name: &str) -> &'a serde_json::Value { + snapshot["modules"] + .as_array() + .expect("modules array") + .iter() + .find(|module| module["name"] == name) + .unwrap_or_else(|| panic!("missing module {name}")) +} + #[test] fn dsm_snapshot_preserves_contract_and_excludes_non_governed_source() { let fixture = Fixture::new(); @@ -114,6 +129,79 @@ fn dsm_snapshot_builds_cross_module_edges_and_risk_colors() { .starts_with('#')); } +#[test] +fn dsm_dependency_graph_accumulates_imports_and_resolves_workspace_crates() { + let fixture = Fixture::new(); + fixture.write("a.py", "import b\nimport b\n"); + fixture.write("b.py", "VALUE = 1\n"); + fixture.write("crates/foo/src/lib.rs", "use bar::Thing;\n"); + fixture.write("crates/bar/src/lib.rs", "pub struct Thing;\n"); + + let snapshot = sentrux_analysis::analyze(&fixture.root).expect("native DSM analysis"); + let edges = snapshot["edges"].as_array().expect("edge array"); + let python = edges + .iter() + .find(|edge| edge["from"] == "a.py" && edge["to"] == "b.py") + .expect("flat Python import edge"); + assert_eq!(python["count"], 2); + assert!(edges + .iter() + .any(|edge| edge["from"] == "crates/foo" && edge["to"] == "crates/bar")); +} + +#[test] +fn dsm_execution_depth_collapses_dependency_cycles() { + let fixture = Fixture::new(); + fixture.write("a.py", "import b\n"); + fixture.write("b.py", "import a\n"); + fixture.write("c.py", "import a\n"); + + let snapshot = sentrux_analysis::analyze(&fixture.root).expect("native DSM analysis"); + assert_eq!(module(&snapshot, "a.py")["metrics"]["exec_depth"], 0); + assert_eq!(module(&snapshot, "b.py")["metrics"]["exec_depth"], 0); + assert_eq!(module(&snapshot, "c.py")["metrics"]["exec_depth"], 1); +} + +#[test] +fn dsm_function_boundaries_ignore_sibling_decorators_and_structural_braces() { + let fixture = Fixture::new(); + fixture.write( + "decorated.py", + "def first():\n return 1\n@decorate\ndef second():\n return 2\n", + ); + fixture.write( + "src/lib.rs", + "trait Contract {\n fn declaration(&self);\n}\npub fn real() -> &'static str {\n let marker = \"}\"; // }\n \"ok\"\n}\n", + ); + fixture.write("script.ps1", "function Invoke-Contract { 1 }\n"); + fixture.write( + "frontend/main.js", + "export const project = (items) => items.map((item) => item + 1);\n", + ); + + let snapshot = sentrux_analysis::analyze(&fixture.root).expect("native DSM analysis"); + let python = file(&snapshot, "decorated.py"); + assert_eq!(python["functions"][0]["end_line"], 2); + let rust = file(&snapshot, "src/lib.rs"); + assert_eq!(rust["functions"][0]["end_line"], 2); + assert_eq!(rust["functions"][1]["end_line"], 7); + assert_eq!(file(&snapshot, "script.ps1")["function_count"], 1); + assert_eq!( + file(&snapshot, "frontend/main.js")["functions"][0]["complexity"], + 1 + ); +} + +#[test] +fn dsm_analysis_reports_unreadable_source_content() { + let fixture = Fixture::new(); + fixture.write_bytes("src/bad.rs", &[0xff, 0xfe]); + + let error = sentrux_analysis::analyze(&fixture.root).expect_err("invalid UTF-8 must fail"); + assert!(error.contains("read source")); + assert!(error.contains("bad.rs")); +} + #[test] fn production_pipeline_prefers_rust_dsm_with_explicit_powershell_rollback() { let pipeline = include_str!("../../../run-code-intel.ps1"); @@ -121,5 +209,7 @@ fn production_pipeline_prefers_rust_dsm_with_explicit_powershell_rollback() { assert!(pipeline.contains("CODE_INTEL_RUST_CLI")); assert!(pipeline.contains("& $sentruxDsmRustCli sentrux dsm $sentruxTargetPath")); assert!(pipeline.contains("& $sentruxAgentTool dsm $sentruxTargetPath")); + assert!(pipeline.contains("RuntimeInformation]::IsOSPlatform")); + assert!(pipeline.contains("$dsmLaunchError = $_.Exception.Message")); assert!(pipeline.contains("Sentrux DSM provider: $sentruxDsmProvider")); } diff --git a/run-code-intel.ps1 b/run-code-intel.ps1 index fc9ba1b..b369e62 100644 --- a/run-code-intel.ps1 +++ b/run-code-intel.ps1 @@ -4020,7 +4020,13 @@ if (-not [string]::IsNullOrWhiteSpace($sentruxTargetPath) -and (Test-Path -Liter throw "CODE_INTEL_SENTRUX_DSM_PROVIDER must be 'rust' or 'powershell'" } $sentruxDsmRustCli = if ([string]::IsNullOrWhiteSpace($env:CODE_INTEL_RUST_CLI)) { - Join-Path $PSScriptRoot "target\debug\code-intel.exe" + $rustExecutableName = if ([Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([Runtime.InteropServices.OSPlatform]::Windows)) { + "code-intel.exe" + } + else { + "code-intel" + } + Join-Path $PSScriptRoot (Join-Path "target/debug" $rustExecutableName) } else { [IO.Path]::GetFullPath($env:CODE_INTEL_RUST_CLI) } @@ -4028,16 +4034,27 @@ if (-not [string]::IsNullOrWhiteSpace($sentruxTargetPath) -and (Test-Path -Liter $sentruxDsmProvider = "" if ($sentruxDsmPreference -eq "rust" -and (Test-Path -LiteralPath $sentruxDsmRustCli -PathType Leaf)) { $previousErrorActionPreference = $ErrorActionPreference + $dsmRaw = @() + $dsmExitCode = $null + $dsmLaunchError = $null try { $ErrorActionPreference = "Continue" $global:LASTEXITCODE = 0 - $dsmRaw = & $sentruxDsmRustCli sentrux dsm $sentruxTargetPath 2>&1 - $dsmExitCode = $global:LASTEXITCODE + try { + $dsmRaw = & $sentruxDsmRustCli sentrux dsm $sentruxTargetPath 2>&1 + $dsmExitCode = $global:LASTEXITCODE + } + catch { + $dsmLaunchError = $_.Exception.Message + } } finally { $ErrorActionPreference = $previousErrorActionPreference } - if ($dsmExitCode -eq 0) { + if (-not [string]::IsNullOrWhiteSpace($dsmLaunchError)) { + $notes.Add("Rust Sentrux DSM could not be launched ($dsmLaunchError); using the explicit PowerShell compatibility fallback.") + } + elseif ($dsmExitCode -eq 0) { $dsmText = ($dsmRaw | ForEach-Object { $_.ToString() } | Out-String).Trim() try { $dsmObject = $dsmText | ConvertFrom-Json From 1b11ce0ff5133215afe4167e84af1c12ab20e430 Mon Sep 17 00:00:00 2001 From: Curry Date: Wed, 15 Jul 2026 23:48:11 +0800 Subject: [PATCH 04/34] fix: make Rust inventory self-contained --- crates/code-intel-cli/src/sentrux_analysis.rs | 53 +++++++++++++------ .../code-intel-cli/tests/sentrux_analysis.rs | 21 ++++++++ 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/crates/code-intel-cli/src/sentrux_analysis.rs b/crates/code-intel-cli/src/sentrux_analysis.rs index f56f338..e5d74a8 100644 --- a/crates/code-intel-cli/src/sentrux_analysis.rs +++ b/crates/code-intel-cli/src/sentrux_analysis.rs @@ -112,22 +112,7 @@ pub fn analyze(target: &Path) -> Result { } fn source_inventory(target: &Path) -> Result { - let output = Command::new("rg") - .arg("--files") - .current_dir(target) - .output() - .map_err(|error| format!("run rg --files in {}: {error}", target.display()))?; - if !output.status.success() { - return Err(format!( - "rg --files failed in {}: {}", - target.display(), - String::from_utf8_lossy(&output.stderr).trim() - )); - } - let listed = String::from_utf8_lossy(&output.stdout) - .lines() - .map(normalize_path) - .collect::>(); + let listed = inventory_files(target)?; let mut included = Vec::new(); let mut excluded: BTreeMap)> = BTreeMap::new(); @@ -181,6 +166,42 @@ fn source_inventory(target: &Path) -> Result { }) } +fn inventory_files(root: &Path) -> Result, String> { + fn visit(root: &Path, current: &Path, out: &mut Vec) -> Result<(), String> { + let entries = fs::read_dir(current) + .map_err(|error| format!("read inventory directory {}: {error}", current.display()))?; + for entry in entries { + let entry = entry.map_err(|error| { + format!("read inventory entry in {}: {error}", current.display()) + })?; + let file_type = entry + .file_type() + .map_err(|error| format!("read file type {}: {error}", entry.path().display()))?; + if file_type.is_symlink() { + continue; + } + let path = entry.path(); + if file_type.is_dir() { + if entry.file_name() != ".git" { + visit(root, &path, out)?; + } + } else if file_type.is_file() { + let relative = path + .strip_prefix(root) + .map_err(|error| format!("relativize {}: {error}", path.display()))?; + out.push(normalize_path(&relative.to_string_lossy())); + } + } + Ok(()) + } + + let mut files = Vec::new(); + visit(root, root, &mut files)?; + files.sort(); + files.dedup(); + Ok(files) +} + fn excluded_reason(relative: &str) -> Option { let lower = relative.to_ascii_lowercase(); let parts = lower diff --git a/crates/code-intel-cli/tests/sentrux_analysis.rs b/crates/code-intel-cli/tests/sentrux_analysis.rs index 6308d29..ccca342 100644 --- a/crates/code-intel-cli/tests/sentrux_analysis.rs +++ b/crates/code-intel-cli/tests/sentrux_analysis.rs @@ -202,6 +202,27 @@ fn dsm_analysis_reports_unreadable_source_content() { assert!(error.contains("bad.rs")); } +#[cfg(unix)] +#[test] +fn dsm_inventory_does_not_traverse_directory_symlinks() { + use std::os::unix::fs::symlink; + + let external = Fixture::new(); + external.write("outside.py", "raise RuntimeError\n"); + let fixture = Fixture::new(); + fixture.write("inside.py", "VALUE = 1\n"); + symlink(&external.root, fixture.root.join("linked")).expect("create fixture symlink"); + + let snapshot = sentrux_analysis::analyze(&fixture.root).expect("native DSM analysis"); + assert_eq!(snapshot["scope"]["included_files"], 1); + assert_eq!(file(&snapshot, "inside.py")["path"], "inside.py"); + assert!(snapshot["file_details"] + .as_array() + .unwrap() + .iter() + .all(|detail| detail["path"] != "linked/outside.py")); +} + #[test] fn production_pipeline_prefers_rust_dsm_with_explicit_powershell_rollback() { let pipeline = include_str!("../../../run-code-intel.ps1"); From 798dc980cd8924513f9299f509e0be4956390c58 Mon Sep 17 00:00:00 2001 From: Curry Date: Wed, 15 Jul 2026 23:52:12 +0800 Subject: [PATCH 05/34] test: isolate concurrent DSM fixtures --- crates/code-intel-cli/tests/sentrux_analysis.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/code-intel-cli/tests/sentrux_analysis.rs b/crates/code-intel-cli/tests/sentrux_analysis.rs index ccca342..11a9f96 100644 --- a/crates/code-intel-cli/tests/sentrux_analysis.rs +++ b/crates/code-intel-cli/tests/sentrux_analysis.rs @@ -1,5 +1,6 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; #[path = "../src/sentrux_analysis.rs"] @@ -9,13 +10,19 @@ struct Fixture { root: PathBuf, } +static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0); + impl Fixture { fn new() -> Self { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock") .as_nanos(); - let root = std::env::temp_dir().join(format!("code-intel-sentrux-analysis-{nonce}")); + let sequence = FIXTURE_COUNTER.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "code-intel-sentrux-analysis-{}-{nonce}-{sequence}", + std::process::id() + )); fs::create_dir_all(&root).expect("create fixture root"); Self { root } } From e6e73e4f720ab2ae2bca531a07ed638f55fecd1d Mon Sep 17 00:00:00 2001 From: Curry Date: Thu, 16 Jul 2026 00:25:17 +0800 Subject: [PATCH 06/34] chore: prepare v0.3.0-beta.1 prerelease --- .github/workflows/release.yml | 27 ++++++++++++++++++++- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++ Cargo.lock | 2 +- crates/code-intel-cli/Cargo.toml | 2 +- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5df1fb4..bf41700 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,24 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Validate release tag and package version + shell: pwsh + run: | + $tag = "${{ github.ref_name }}" + if ($tag -notmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$') { + throw "Unsupported release tag: $tag" + } + $tagVersion = $Matches.version + $manifest = Get-Content -Raw -LiteralPath crates\code-intel-cli\Cargo.toml + $manifestMatch = [regex]::Match($manifest, '(?m)^version\s*=\s*"([^"]+)"') + if (-not $manifestMatch.Success) { + throw "Could not read the code-intel package version." + } + $packageVersion = $manifestMatch.Groups[1].Value + if ($packageVersion -ne $tagVersion) { + throw "Release tag $tag does not match Cargo package version $packageVersion." + } + - name: Setup Python uses: actions/setup-python@v5 with: @@ -108,5 +126,12 @@ jobs: gh release upload $tag $asset --clobber } else { - gh release create $tag $asset --title $tag --notes "Code Intel Pipeline $tag" + $releaseArgs = @( + "--title", $tag, + "--notes", "Code Intel Pipeline $tag. Pre-release builds are intended for integration testing before stable promotion." + ) + if ($tag.Contains("-")) { + $releaseArgs += "--prerelease" + } + gh release create $tag $asset @releaseArgs } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e7518f..5d16575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.0-beta.1] — 2026-07-16 + +Pre-release for the Rust-first Code Intel control plane. This build is intended +for integration testing before the `0.3.0` stable release. + +### Added + +- **Rust Sentrux DSM analysis kernel** — repository inventory, dependency + structure, complexity, health, rules, gaps, and evolution analysis now run in + the Rust CLI, with the PowerShell path retained as a compatibility fallback. +- **Atomic capability contract v1** — defines the execution envelope used to + coordinate capability ownership, effects, dependencies, and artifacts. +- **Trust-boundary hardening** — Hospital and scoped Repowise paths fail closed + at repository and artifact boundaries. + +### Changed + +- Rust DSM executable discovery is cross-platform and accepts an explicit + `CODE_INTEL_RUST_CLI` override. +- File inventory is self-contained when Git metadata is absent, and symlinked + directories are not followed during recursive traversal. +- Concurrent DSM integration fixtures are isolated to prevent cross-test + interference on Windows. + +### Verified + +- Rust unit and integration suites pass locally. +- Windows package/build and Windows, Ubuntu, and macOS smoke jobs pass in CI. +- Rust and PowerShell DSM providers produce matching core repository and module + metrics on the release candidate repository. + +### Beta limitations + +- GitHub Release packaging currently publishes a Windows ZIP only. +- The PowerShell DSM provider remains the automatic fallback when the Rust CLI + cannot be located or executed. +- Complexity scoring is intentionally heuristic and may count keywords inside + trailing comments; naive comment stripping was rejected because it corrupts + strings and URLs. + ## [0.2.0] — 2026-07-02 The "understand any repo, cheaply" release. Docs generation now runs on any diff --git a/Cargo.lock b/Cargo.lock index cb93fe7..ef64889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "code-intel" -version = "0.2.0" +version = "0.3.0-beta.1" dependencies = [ "serde_json", ] diff --git a/crates/code-intel-cli/Cargo.toml b/crates/code-intel-cli/Cargo.toml index b227dfc..f4964de 100644 --- a/crates/code-intel-cli/Cargo.toml +++ b/crates/code-intel-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "code-intel" -version = "0.2.0" +version = "0.3.0-beta.1" edition = "2021" description = "Local Code Intel Pipeline CLI for artifact resume and contract checks" license = "Apache-2.0" From 42de0635bece8fe0e4d062504ea5edab93070a82 Mon Sep 17 00:00:00 2001 From: Curry Date: Thu, 23 Jul 2026 15:20:40 +0800 Subject: [PATCH 07/34] feat: complete model-independent code intel pipeline --- .githooks/pre-push | 45 + .github/workflows/ci.yml | 80 +- .gitignore | 2 + .sentrux/baseline.json | 8 +- AGENTS.md | 15 + CHANGELOG.md | 50 + CONTEXT.md | 103 +- Cargo.lock | 2 +- Install-MultiAgentMergeQueue.ps1 | 62 + Invoke-CodeIntelAcceptance.ps1 | 299 ++ Invoke-CodeIntelAutomaticPullRequest.ps1 | 8 + Invoke-CodeIntelAutomaticPullRequestFlow.ps1 | 8 + Invoke-CodeNexusLite.ps1 | 126 +- Invoke-CompatibilityFacadeFinalize.ps1 | 226 + Invoke-CompeteProjectScore.ps1 | 110 + Invoke-GitHubSolutionResearch.ps1 | 30 +- Invoke-ModelChannelDelegate.ps1 | 367 ++ Invoke-MultiAgentMergeQueue.ps1 | 238 + Invoke-MultiAgentWorkspacePreflight.ps1 | 342 ++ Invoke-ProviderRuntimeInventory.ps1 | 451 ++ Invoke-RepowiseProviderProbe.ps1 | 222 + Invoke-ScopedRepowise.ps1 | 47 +- Invoke-WorkflowRecommendation.ps1 | 26 + LICENSE | 21 + New-ModelAdapterRequest.ps1 | 100 + New-ModelExecutableHandle.ps1 | 48 + OpenSpec-Detector.ps1 | 61 +- README.md | 66 +- Run-ScopedRepowiseDocs.py | 23 +- Test-CodeIntelProjectConformance.ps1 | 189 + Test-LanguageAdapterAcceptance.ps1 | 202 + Test-Python314PonCompatibility.ps1 | 216 + bootstrap-new-machine.ps1 | 4 +- check-code-intel-tools.ps1 | 2 + claude-code-merge-queue.config.mjs | 11 + crates/code-intel-cli/Cargo.toml | 4 +- crates/code-intel-cli/src/admissibility.rs | 415 ++ crates/code-intel-cli/src/artifact_index.rs | 496 ++ crates/code-intel-cli/src/artifact_ref.rs | 3205 ++++++++++++ .../src/assistance_discovery.rs | 282 + crates/code-intel-cli/src/authority.rs | 530 ++ .../src/builtin_provider_evidence.rs | 443 ++ crates/code-intel-cli/src/capability.rs | 1257 +++++ .../src/capability_inventory.rs | 1267 +++++ crates/code-intel-cli/src/change_impact.rs | 449 ++ .../code-intel-cli/src/codenexus_adapter.rs | 386 ++ .../code-intel-cli/src/committed_evidence.rs | 118 + .../src/compatibility_retirement_gate.rs | 889 ++++ .../src/compatibility_retirement_ticket.rs | 437 ++ crates/code-intel-cli/src/dag_coordinator.rs | 1058 ++++ crates/code-intel-cli/src/dag_run.rs | 755 +++ crates/code-intel-cli/src/decision_gap.rs | 375 ++ crates/code-intel-cli/src/decision_port.rs | 898 ++++ crates/code-intel-cli/src/decision_record.rs | 1042 ++++ .../src/delivery_light_speed.rs | 717 +++ crates/code-intel-cli/src/doctor_adapter.rs | 512 ++ crates/code-intel-cli/src/evidence_query.rs | 288 ++ crates/code-intel-cli/src/file_boundary.rs | 295 ++ crates/code-intel-cli/src/graph.rs | 21 +- crates/code-intel-cli/src/graph_adapter.rs | 273 + .../code-intel-cli/src/hospital_diagnosis.rs | 409 ++ .../src/internalization_record.rs | 785 +++ crates/code-intel-cli/src/main.rs | 318 ++ crates/code-intel-cli/src/method_catalog.rs | 536 ++ crates/code-intel-cli/src/method_select.rs | 528 ++ crates/code-intel-cli/src/model_channels.rs | 735 +++ .../src/native_code_evidence.rs | 749 +++ crates/code-intel-cli/src/orchestration.rs | 722 +++ crates/code-intel-cli/src/ponytail_gate.rs | 589 +++ .../code-intel-cli/src/project_orientation.rs | 405 ++ .../src/project_orientation_benchmark.rs | 759 +++ crates/code-intel-cli/src/providers.rs | 1256 ++++- crates/code-intel-cli/src/repowise_adapter.rs | 328 ++ crates/code-intel-cli/src/run_commit.rs | 923 ++++ .../code-intel-cli/src/runtime_ci_evidence.rs | 466 ++ crates/code-intel-cli/src/sentrux_adapter.rs | 351 ++ crates/code-intel-cli/src/sentrux_analysis.rs | 43 + crates/code-intel-cli/src/session_evidence.rs | 1021 ++++ crates/code-intel-cli/src/snapshot.rs | 1464 ++++++ crates/code-intel-cli/src/stable_artifact.rs | 539 ++ crates/code-intel-cli/src/staged_artifact.rs | 1240 +++++ crates/code-intel-cli/src/survival_scan.rs | 248 + .../src/understanding_quadrant.rs | 440 ++ crates/code-intel-cli/tests/artifact_index.rs | 396 ++ crates/code-intel-cli/tests/artifact_ref.rs | 540 ++ .../tests/assistance_discovery.rs | 169 + .../tests/authority_transition.rs | 467 ++ .../code-intel-cli/tests/capability_exec.rs | 1746 +++++++ .../code-intel-cli/tests/codenexus_adapter.rs | 493 ++ .../tests/compatibility_retirement_gate.rs | 671 +++ ...ompatibility_retirement_ticket_template.rs | 99 + .../code-intel-cli/tests/dag_coordinator.rs | 522 ++ crates/code-intel-cli/tests/dag_run.rs | 555 ++ crates/code-intel-cli/tests/decision_gap.rs | 188 + crates/code-intel-cli/tests/decision_port.rs | 577 +++ .../code-intel-cli/tests/decision_record.rs | 549 ++ .../tests/delivery_light_speed.rs | 717 +++ .../code-intel-cli/tests/doctor_envelope.rs | 290 ++ .../tests/evidence_admissibility.rs | 343 ++ crates/code-intel-cli/tests/file_boundary.rs | 180 + .../codenexus-adapter/full-current.json | 9 + .../codenexus-adapter/lite-current.json | 9 + .../codenexus-adapter/unavailable.json | 9 + .../graph-adapter/external-current.json | 11 + .../graph-adapter/internal-current.json | 7 + .../graph-adapter/internal-missing.json | 7 + .../graph-adapter/internal-partial.json | 7 + .../method-select/dependency-delay.json | 15 + .../method-select/insufficient-evidence.json | 10 + .../fixtures/model-routing/ready-local.json | 37 + .../project-orientation/missing-purpose.json | 92 + .../fixtures/sentrux-adapter/complete.json | 12 + .../fixtures/sentrux-adapter/crashed.json | 5 + .../fixtures/sentrux-adapter/partial.json | 8 + .../sentrux-adapter/unknown-kind.json | 7 + .../unavailable-expectations.json | 6 + crates/code-intel-cli/tests/graph_adapter.rs | 455 ++ .../tests/hospital_diagnosis.rs | 929 ++++ .../tests/internalization_record.rs | 1637 ++++++ crates/code-intel-cli/tests/method_catalog.rs | 260 + crates/code-intel-cli/tests/method_select.rs | 364 ++ .../tests/mindwalk_internalization.rs | 50 + .../tests/native_code_evidence.rs | 538 ++ .../tests/pon_multilanguage_mapping.rs | 195 + crates/code-intel-cli/tests/ponytail_gate.rs | 458 ++ .../tests/project_orientation.rs | 275 + .../tests/project_orientation_benchmark.rs | 416 ++ .../code-intel-cli/tests/repowise_adapter.rs | 164 + crates/code-intel-cli/tests/repowise_route.rs | 186 + crates/code-intel-cli/tests/run_commit.rs | 282 + .../tests/runtime_ci_evidence.rs | 259 + .../code-intel-cli/tests/schema_lifecycle.rs | 250 + .../code-intel-cli/tests/sentrux_adapter.rs | 248 + .../code-intel-cli/tests/sentrux_analysis.rs | 9 +- .../code-intel-cli/tests/session_evidence.rs | 228 + .../code-intel-cli/tests/snapshot_identity.rs | 517 ++ .../code-intel-cli/tests/staged_artifact.rs | 380 ++ crates/code-intel-cli/tests/survival_scan.rs | 276 + .../tests/understanding_quadrant.rs | 320 ++ ...l-neutral-engineering-intelligence-core.md | 20 + docs/advisory-workflow-recommendation.md | 11 + docs/architecture/reference-capability-map.md | 64 + docs/artifact-data-contract.md | 46 + docs/artifact-ref-verifier.md | 39 + docs/assistance-discovery.md | 7 + docs/atomic-development-model.md | 6 +- docs/authority-transition-gate.md | 20 + docs/automatic-pull-request-beta.md | 71 + docs/change-impact.md | 16 + docs/code-intel-architecture.md | 32 +- docs/code-intel-project-conformance.md | 52 + docs/code-intel-three-stage-acceptance.md | 103 + docs/codenexus-provider-adapter.md | 17 + docs/committed-run-index.md | 26 + docs/compatibility-facade-finalize.md | 32 + ...tibility-retire-codenexus-direct-branch.md | 34 + ...patibility-retire-doctor-wrapper-branch.md | 31 + docs/compatibility-retire-hospital-branch.md | 24 + docs/compatibility-retire-index-branch.md | 24 + ...compatibility-retire-native-code-branch.md | 28 + ...bility-retire-provider-preflight-branch.md | 42 + ...compatibility-retire-publication-branch.md | 23 + ...compatibility-retire-recommender-branch.md | 37 + docs/compatibility-retirement-gate.md | 41 + ...ompatibility-retirement-ticket-template.md | 34 + docs/compete-project-score.md | 28 + docs/dag-coordinator.md | 65 + docs/decision-gap-detector.md | 36 + docs/decision-request-response-port.md | 48 + docs/delivery-light-speed-measurement.md | 30 + docs/doctor-envelope.md | 24 + docs/evidence-admissibility.md | 9 + docs/evidence-query.md | 20 + docs/file-boundary-observation.md | 26 + docs/final-commitment-reconciliation-usage.md | 33 + docs/final-commitment-reconciliation.md | 90 + docs/follow-up-automation.md | 65 + docs/frontier-quality-loop.md | 65 + docs/graph-provider-adapter.md | 41 + docs/hospital-diagnosis.md | 52 + docs/internalization-advisory-candidates.md | 33 + docs/internalization-r09-r26-records.md | 31 + docs/internalization-record-engine.md | 45 + docs/inventory-rg-capability-contract.md | 56 + docs/language-adapter-acceptance-standard.md | 61 + docs/method-catalog.md | 26 + docs/method-selection.md | 40 + docs/model-channel-routing.md | 37 + docs/model-executable-handle.md | 9 + docs/multi-agent-merge-queue.md | 64 + docs/multi-agent-workspace-governance.md | 43 + docs/native-code-evidence.md | 22 + docs/plans/adr-0010-execution-plan.md | 935 ++++ ...matic-pr-one-command-orchestration-idea.md | 46 + docs/plans/compete-project-score-idea.md | 36 + docs/plans/four-blind-spots-closure-idea.md | 42 + ...nguage-adapter-acceptance-standard-idea.md | 41 + ...el-independent-pipeline-completion-idea.md | 154 + docs/plans/multi-agent-merge-queue-idea.md | 52 + .../multi-agent-workspace-governance-idea.md | 47 + .../pon-multilanguage-code-evidence-idea.md | 40 + docs/plans/pon-parity-floor-idea.md | 45 + .../pon-project-conformance-mapping-idea.md | 41 + .../python314-pon-development-lane-idea.md | 34 + docs/plans/session-evidence-adapter-idea.md | 63 + .../three-stage-project-acceptance-idea.md | 38 + .../understanding-quadrant-complexity-idea.md | 23 + docs/pon-conformance-ratchet.md | 55 + docs/pon-multilanguage-code-evidence.md | 36 + docs/ponytail-gain-ledger.md | 6 + docs/ponytail-governance-gate.md | 64 + docs/project-orientation-benchmark.md | 26 + docs/project-orientation.md | 9 + docs/public-beta.md | 65 + docs/python314-pon-development.md | 40 + docs/repository-snapshot-identity.md | 43 + docs/repository-survival-scan.md | 15 + docs/repowise-adapter.md | 40 + docs/run-commit.md | 11 + docs/runtime-ci-evidence.md | 23 + docs/schema-lifecycle.md | 18 + docs/sentrux-provider-adapter.md | 7 + docs/session-evidence-adapter.md | 79 + docs/staged-artifact-writer.md | 81 + docs/understanding-quadrant.md | 45 + install-code-intel-pipeline.ps1 | 64 + invoke-code-intel.ps1 | 285 +- .../acceptance/known-divergences.v1.json | 4 + .../native-code-evidence-candidate.json | 83 + .../sentrux-baseline-migration-20260723.json | 42 + .../authority-transition-policy.v1.json | 40 + .../code-intel-acceptance-policy.v1.json | 43 + ...e-intel-project-conformance-policy.v1.json | 134 + orchestration/decision-gap-rules.v1.json | 13 + ...clean-machine-verifier-attestation.v1.json | 82 + .../final-commitment-reconciliation.json | 988 ++++ ...retirement-ticket-template.v1.example.json | 26 + .../facade-finalize-fixtures/doctor.json | 1 + .../facade-finalize-fixtures/full.json | 1 + .../facade-finalize-fixtures/lite.json | 1 + .../facade-finalize-fixtures/normal.json | 1 + orchestration/facade-finalize-policy.v1.json | 37 + orchestration/integrations.json | 1309 ++++- .../internalization/agent-loops.json | 22 + .../c03-r05-r12-measurements.json | 111 + .../claude-code-merge-queue.json | 155 + orchestration/internalization/cocoindex.json | 14 + orchestration/internalization/codenexus.json | 28 + .../github-solution-research.json | 101 + .../github-solution-research.md | 24 + .../pon-multilanguage-code-evidence.json | 74 + .../fixtures/r06-native-labeled-corpus.json | 18 + .../fixtures/r10-alternate-vcs-port.json | 26 + orchestration/internalization/frontier.json | 95 + orchestration/internalization/git.json | 18 + .../internalization/github-research.json | 14 + orchestration/internalization/graph.json | 31 + orchestration/internalization/greenfield.json | 14 + orchestration/internalization/gstack.json | 22 + orchestration/internalization/linear.json | 8 + orchestration/internalization/llm-wiki.json | 187 + orchestration/internalization/matt-flow.json | 22 + .../internalization/mattpocock-skills.json | 27 + .../internalization/metaharness.json | 22 + orchestration/internalization/mindwalk.json | 217 + .../internalization/my-code-machine.json | 204 + .../internalization/native-code-evidence.json | 15 + orchestration/internalization/obsidian.json | 186 + orchestration/internalization/openspec.json | 32 + .../internalization/pon-multilanguage.json | 215 + .../pon-project-conformance.json | 106 + .../pon-python314-development.json | 107 + orchestration/internalization/pon.json | 94 + orchestration/internalization/ponytail.json | 13 + .../internalization/qiaomu-goal.json | 22 + orchestration/internalization/repomix.json | 14 + orchestration/internalization/repowise.json | 28 + orchestration/internalization/rg.json | 20 + orchestration/internalization/sentrux.json | 38 + orchestration/internalization/spec-kit.json | 27 + .../internalization/tree-sitter-v.json | 13 + .../internalization/yao-meta-skill.json | 22 + ...language-adapter-acceptance-policy.v1.json | 69 + orchestration/method-selection-rules.v1.json | 77 + .../methods/cards/contract-testing.v1.json | 37 + .../methods/cards/critical-path-pert.v1.json | 37 + .../methods/cards/fault-tree-analysis.v1.json | 39 + orchestration/methods/cards/fmea.v1.json | 39 + orchestration/methods/cards/pdca.v1.json | 39 + .../methods/cards/root-cause-analysis.v1.json | 39 + orchestration/methods/cards/spc.v1.json | 39 + .../methods/cards/strangler-migration.v1.json | 39 + .../cards/value-stream-queue-delay.v1.json | 39 + orchestration/methods/catalog.v1.json | 16 + .../multi-agent-merge-queue-policy.v1.json | 35 + .../multi-agent-workspace-policy.v1.json | 29 + orchestration/ponytail-gate-policy.v1.json | 40 + .../python314-pon-development-policy.v1.json | 36 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../e02-recommender/e00-request.json | 1 + .../e02-recommender/e01-request.json | 1 + .../e02-recommender/e01-stderr.txt | 2 + .../evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../dependency-d02-clean-machine.json | 1 + .../evidence/dependency-repo-snapshot.json | 1 + .../evidence/effect-parity.json | 1 + .../evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../retirements/e02-recommender/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../e03-provider-preflight/e00-request.json | 1 + .../e03-provider-preflight/e01-request.json | 1 + .../e03-provider-preflight/e01-stderr.txt | 2 + .../evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../dependency-a04-admissibility.json | 1 + .../evidence/dependency-repo-snapshot.json | 1 + .../evidence/effect-parity.json | 1 + .../evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../e03-provider-preflight/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../e04-codenexus-direct/e00-request.json | 1 + .../e04-codenexus-direct/e01-request.json | 1 + .../e04-codenexus-direct/e01-stderr.txt | 2 + .../evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../evidence/dependency-b05.json | 1 + .../evidence/effect-parity.json | 1 + .../evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../rollback-rehearsal/run-code-intel.ps1 | 4529 +++++++++++++++++ .../e04-codenexus-direct/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../e05-publication/e00-request.json | 1 + .../e05-publication/e01-request.json | 1 + .../e05-publication/e01-stderr.txt | 2 + .../evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../evidence/dependency-a06-stage.json | 1 + .../evidence/dependency-a09.json | 1 + .../evidence/effect-parity.json | 1 + .../evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../retirements/e05-publication/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../e07-native-code/e00-request.json | 1 + .../e07-native-code/e01-request.json | 1 + .../e07-native-code/e01-stderr.txt | 2 + .../evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../evidence/dependency-inventory.json | 1 + .../evidence/dependency-snapshot.json | 1 + .../evidence/effect-parity.json | 1 + .../evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../rollback-rehearsal/run-code-intel.ps1 | 4529 +++++++++++++++++ .../retirements/e07-native-code/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../retirements/e08-hospital/e00-request.json | 1 + .../retirements/e08-hospital/e01-request.json | 1 + .../retirements/e08-hospital/e01-stderr.txt | 2 + .../e08-hospital/evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../e08-hospital/evidence/dependency-a04.json | 1 + .../e08-hospital/evidence/dependency-b07.json | 1 + .../e08-hospital/evidence/effect-parity.json | 1 + .../e08-hospital/evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../retirements/e08-hospital/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../e09-doctor-wrapper/e00-request.json | 1 + .../e09-doctor-wrapper/e01-request.json | 1 + .../e09-doctor-wrapper/e01-stderr.txt | 2 + .../evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../evidence/contract-parity.json | 1 + .../evidence/dependency-snapshot.json | 1 + .../evidence/effect-parity.json | 1 + .../evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../rollback-rehearsal/invoke-code-intel.ps1 | 193 + .../e09-doctor-wrapper/status.json | 1 + ...ompatibility-retirement-deletion-diff.json | 1 + .../compatibility-retirement-manifest.json | 1 + .../compatibility-retirement-ticket.json | 1 + .../retirements/e10-index/e00-request.json | 1 + .../retirements/e10-index/e01-request.json | 1 + .../retirements/e10-index/e01-stderr.txt | 2 + .../e10-index/evidence/c00-necessity.json | 1 + .../evidence/compatibility-window.json | 1 + .../e10-index/evidence/contract-parity.json | 1 + .../e10-index/evidence/dependency-e05.json | 1 + .../e10-index/evidence/effect-parity.json | 1 + .../e10-index/evidence/golden-parity.json | 1 + .../evidence/independent-approval.json | 1 + .../evidence/registry-reconciliation.json | 1 + .../e10-index/evidence/replacement-atom.json | 1 + .../evidence/rollback-execution.json | 1 + .../e10-index/evidence/usage-observation.json | 1 + .../compatibility-retirement-decision.json | 1 + .../retirements/e10-index/status.json | 1 + orchestration/schema-lifecycle.v1.json | 176 + ...e-evidence-native-artifacts.v1.schema.json | 107 + ...ode-intel-acceptance-result.v1.schema.json | 34 + ...ory-workflow-recommendation.v1.schema.json | 65 + ...tel-architecture-graph-port.v1.schema.json | 63 + .../code-intel-artifact-index.v1.schema.json | 74 + ...ssistance-discovery-request.v1.schema.json | 30 + ...assistance-discovery-result.v1.schema.json | 21 + ...de-intel-assistance-dossier.v1.schema.json | 69 + ...-intel-authority-transition.v1.schema.json | 100 + ...intel-auto-pr-authorization.v1.schema.json | 52 + ...l-auto-pr-execution-receipt.v1.schema.json | 43 + ...el-auto-pr-execution-result.v1.schema.json | 50 + ...e-intel-auto-pr-flow-result.v1.schema.json | 16 + ...code-intel-auto-pr-proposal.v1.schema.json | 23 + ...e-intel-capability-envelope.v1.schema.json | 9 +- .../code-intel-change-impact.v1.schema.json | 74 + ...achine-verifier-attestation.v1.schema.json | 78 + .../code-intel-codenexus-port.v1.schema.json | 101 + ...ntel-codenexus-route-result.v1.schema.json | 67 + ...mpatibility-facade-finalize.v1.schema.json | 51 + ...ibility-retirement-decision.v1.schema.json | 18 + ...ty-retirement-deletion-diff.v1.schema.json | 8 + ...ibility-retirement-evidence.v1.schema.json | 16 + ...ibility-retirement-manifest.v1.schema.json | 34 + ...-retirement-ticket-template.v1.schema.json | 26 + ...intel-decision-cancellation.v1.schema.json | 25 + ...el-decision-exchange-result.v1.schema.json | 127 + ...cision-gap-detection-result.v1.schema.json | 79 + .../code-intel-decision-gap.v1.schema.json | 74 + .../code-intel-decision-record.v1.schema.json | 74 + ...code-intel-decision-request.v1.schema.json | 86 + ...ode-intel-decision-response.v1.schema.json | 46 + ...-intel-delivery-light-speed.v1.schema.json | 182 + ...de-intel-doctor-observation.v1.schema.json | 116 + ...idence-admissibility-result.v1.schema.json | 17 + ...code-intel-evidence-payload.v1.schema.json | 11 + ...ntel-evidence-provider-port.v1.schema.json | 74 + .../code-intel-evidence-query.v1.schema.json | 78 + ...ntel-file-boundary-document.v1.schema.json | 66 + ...intel-file-boundary-request.v1.schema.json | 23 + ...-intel-file-boundary-result.v1.schema.json | 80 + ...l-commitment-reconciliation.v1.schema.json | 81 + ...-intel-follow-up-automation.v1.schema.json | 64 + ...de-intel-graph-route-result.v1.schema.json | 67 + .../code-intel-hospital.v1.schema.json | 93 + ...ntel-internalization-record.v1.schema.json | 167 + ...language-adapter-acceptance.v1.schema.json | 142 + .../code-intel-method-card.v1.schema.json | 135 + ...code-intel-method-selection.v1.schema.json | 125 + ...intel-model-adapter-request.v1.schema.json | 76 + ...intel-model-adapter-request.v2.schema.json | 46 + ...-intel-model-adapter-result.v1.schema.json | 62 + ...el-model-assistance-dossier.v1.schema.json | 64 + ...l-channel-inventory-request.v1.schema.json | 38 + ...el-channel-inventory-result.v1.schema.json | 60 + ...tel-model-executable-handle.v1.schema.json | 19 + ...intel-model-routing-request.v1.schema.json | 55 + ...-intel-model-routing-result.v1.schema.json | 78 + ...ti-agent-merge-queue-status.v1.schema.json | 36 + ...ode-intel-notice-provenance.v1.schema.json | 30 + .../code-intel-ponytail-gate.v1.schema.json | 148 + ...tion-benchmark-observations.v1.schema.json | 58 + ...oject-orientation-benchmark.v1.schema.json | 82 + ...e-intel-project-orientation.v1.schema.json | 149 + ...e-intel-repository-snapshot.v1.schema.json | 57 + ...itory-survival-scan-request.v1.schema.json | 36 + ...sitory-survival-scan-result.v1.schema.json | 63 + ...intel-repowise-route-result.v1.schema.json | 26 + .../code-intel-reuse-record.v1.schema.json | 84 + .../code-intel-run-commit.v1.schema.json | 13 + .../schemas/code-intel-run-dag.v1.schema.json | 40 + .../code-intel-run-manifest.v1.schema.json | 81 + .../code-intel-run-state.v1.schema.json | 79 + ...ode-intel-run-timing-events.v1.schema.json | 65 + ...l-runtime-ci-ingest-request.v1.schema.json | 31 + ...ntel-runtime-ci-observation.v1.schema.json | 76 + ...de-intel-runtime-ci-summary.v1.schema.json | 54 + ...code-intel-schema-lifecycle.v1.schema.json | 49 + ...-intel-sentrux-route-result.v1.schema.json | 15 + ...code-intel-session-evidence.v1.schema.json | 149 + ...e-intel-staged-artifact-set.v1.schema.json | 70 + ...el-structural-evidence-port.v1.schema.json | 25 + ...ntel-understanding-quadrant.v1.schema.json | 109 + package-lock.json | 28 + package.json | 8 + pipeline.config.example.json | 9 + prototypes/session-observability/.gitignore | 1 + prototypes/session-observability/Cargo.lock | 105 + prototypes/session-observability/Cargo.toml | 11 + prototypes/session-observability/IDEA.md | 60 + prototypes/session-observability/README.md | 35 + prototypes/session-observability/src/main.rs | 158 + prototypes/session-observability/src/model.rs | 266 + run-code-intel.ps1 | 1290 +++-- skill/SKILL.md | 13 +- templates/understanding-report.md | 2 + test-artifact-index-contract.ps1 | 48 + test-code-evidence-layer.ps1 | 23 +- test-code-intel-acceptance.ps1 | 188 + test-code-intel-project-conformance.ps1 | 61 + test-code-intel-provider.ps1 | 239 +- test-codenexus-adapter-contract.ps1 | 137 + test-codenexus-generated-path-filter.ps1 | 104 + test-compatibility-facade-finalize.ps1 | 92 + test-compete-project-score.ps1 | 45 + test-dag-facade.ps1 | 62 + test-github-solution-research.ps1 | 41 +- test-greenfield-integration.ps1 | 42 +- test-integration-orchestration.ps1 | 222 +- test-language-adapter-acceptance.ps1 | 90 + test-model-request-synthesis-and-handle.ps1 | 65 + test-multi-agent-merge-queue.ps1 | 137 + test-multi-agent-workspace-preflight.ps1 | 119 + test-parity-baseline.ps1 | 215 + test-parity-floor.ps1 | 108 + test-ponytail-gate-contract.ps1 | 92 + test-provider-runtime-inventory.ps1 | 113 + test-python314-pon-compatibility.ps1 | 87 + test-regression-fixes.ps1 | 20 +- test-repowise-adapter-contract.ps1 | 71 + ...repowise-provider-probe-classification.ps1 | 73 + test-run-commit-contract.ps1 | 89 + test-runtime-ci-hospital-pet.ps1 | 44 + test-scoped-repowise-security.ps1 | 33 + test-scoped-repowise-worktree.ps1 | 1 + test-sentrux-failure-normalization.ps1 | 13 +- test-skill-development-benchmark.ps1 | 4 +- test-stable-wrapper-e2e.ps1 | 154 + test-survival-scan-contract.ps1 | 31 + test-transactional-publication.ps1 | 142 + test-workflow-recommendation-brief.ps1 | 52 +- tests/fixtures/decision-gap/missing-fact.json | 24 + .../decision-gap/risk-acceptance.json | 29 + .../fixtures/decision-port/cancellation.json | 8 + tests/fixtures/decision-port/request.json | 37 + .../decision-port/stale-response.json | 8 + .../decision-port/valid-response.json | 15 + .../decision-port/wrong-actor-response.json | 8 + .../decision-port/wrong-gap-response.json | 8 + .../evidence-admissibility/good/payload.json | 1 + .../evidence-admissibility/good/request.json | 1 + tests/fixtures/internalization/complete.json | 67 + .../fixtures/parity/clean/fixture-review.json | 5 + .../parity/clean/machine-artifacts.json | 47 + tests/fixtures/parity/clean/normalized.json | 108 + .../fixtures/parity/dirty/fixture-review.json | 5 + .../parity/dirty/machine-artifacts.json | 46 + tests/fixtures/parity/dirty/normalized.json | 102 + .../parity/domain-fail/fixture-review.json | 5 + .../parity/domain-fail/machine-artifacts.json | 44 + .../parity/domain-fail/normalized.json | 89 + tests/fixtures/parity/parity-floor.json | 19 + .../partial-evidence/fixture-review.json | 5 + .../partial-evidence/machine-artifacts.json | 46 + .../parity/partial-evidence/normalized.json | 105 + .../provider-unavailable/fixture-review.json | 5 + .../machine-artifacts.json | 46 + .../provider-unavailable/normalized.json | 101 + .../ponytail/c00-necessity-trace.json | 116 + .../python314-compat/core_language.py | 18 + .../exceptions_and_objects.py | 18 + .../python314-compat/manifest.v1.json | 29 + .../python314_template_strings.py | 6 + .../repowise-adapter/docs-payload.json | 1 + .../fixtures/repowise-adapter/index-only.json | 15 + .../repowise-adapter/index-payload.json | 1 + .../partial-docs-payload.json | 1 + tests/fixtures/repowise-adapter/quota.json | 19 + tests/fixtures/repowise-adapter/success.json | 20 + tests/test-automatic-pull-request-flow.ps1 | 103 + tests/test-automatic-pull-request.ps1 | 195 + tests/test-follow-up-automation.ps1 | 50 + .../test-model-channel-degraded-pipeline.ps1 | 55 + tests/test-model-channel-delegate.ps1 | 204 + .../Invoke-CodeIntelAutomaticPullRequest.ps1 | 389 ++ ...voke-CodeIntelAutomaticPullRequestFlow.ps1 | 239 + tools/New-BetaPackage.ps1 | 211 + tools/Test-BetaPackage.ps1 | 181 + tools/Test-FinalCommitmentReconciliation.ps1 | 139 + tools/code-intel-follow-up-automation.psm1 | 187 + .../New-CodeNexusDirectRetirementPacket.ps1 | 272 + .../New-DoctorWrapperRetirementPacket.ps1 | 27 + .../New-HospitalRetirementPacket.ps1 | 36 + .../New-IndexRetirementPacket.ps1 | 28 + .../New-NativeCodeRetirementPacket.ps1 | 272 + .../New-ProviderPreflightRetirementPacket.ps1 | 151 + .../New-PublicationRetirementPacket.ps1 | 50 + .../New-RecommenderRetirementPacket.ps1 | 231 + .../Restore-CodeNexusDirectBranch.ps1 | 38 + .../Restore-DoctorWrapperBranch.ps1 | 13 + .../Restore-HospitalLegacyBranch.ps1 | 10 + .../Restore-IndexLegacyBranch.ps1 | 5 + .../Restore-NativeCodeEmbeddedBranch.ps1 | 40 + .../Restore-ProviderPreflightLegacyBranch.ps1 | 52 + .../Restore-PublicationLegacyBranch.ps1 | 19 + .../Restore-RecommenderLegacyBranch.ps1 | 74 + .../Test-CodeNexusDirectRetirementPacket.ps1 | 91 + .../Test-DoctorWrapperRetirementPacket.ps1 | 159 + .../Test-HospitalRetirementBoundary.ps1 | 7 + .../Test-HospitalRetirementPacket.ps1 | 28 + .../Test-IndexRetirementBoundary.ps1 | 7 + .../Test-IndexRetirementPacket.ps1 | 11 + .../Test-NativeCodeRetirementPacket.ps1 | 119 + ...st-ProviderPreflightRetirementBoundary.ps1 | 38 + ...Test-ProviderPreflightRetirementPacket.ps1 | 21 + .../Test-PublicationRetirementBoundary.ps1 | 22 + .../Test-PublicationRetirementPacket.ps1 | 11 + .../Test-RecommenderRetirementPacket.ps1 | 101 + tools/normalize_compete_score.py | 86 + update-code-intel-index.ps1 | 141 +- 669 files changed, 87211 insertions(+), 1162 deletions(-) create mode 100644 .githooks/pre-push create mode 100644 AGENTS.md create mode 100644 Install-MultiAgentMergeQueue.ps1 create mode 100644 Invoke-CodeIntelAcceptance.ps1 create mode 100644 Invoke-CodeIntelAutomaticPullRequest.ps1 create mode 100644 Invoke-CodeIntelAutomaticPullRequestFlow.ps1 create mode 100644 Invoke-CompatibilityFacadeFinalize.ps1 create mode 100644 Invoke-CompeteProjectScore.ps1 create mode 100644 Invoke-ModelChannelDelegate.ps1 create mode 100644 Invoke-MultiAgentMergeQueue.ps1 create mode 100644 Invoke-MultiAgentWorkspacePreflight.ps1 create mode 100644 Invoke-ProviderRuntimeInventory.ps1 create mode 100644 Invoke-RepowiseProviderProbe.ps1 create mode 100644 Invoke-WorkflowRecommendation.ps1 create mode 100644 LICENSE create mode 100644 New-ModelAdapterRequest.ps1 create mode 100644 New-ModelExecutableHandle.ps1 create mode 100644 Test-CodeIntelProjectConformance.ps1 create mode 100644 Test-LanguageAdapterAcceptance.ps1 create mode 100644 Test-Python314PonCompatibility.ps1 create mode 100644 claude-code-merge-queue.config.mjs create mode 100644 crates/code-intel-cli/src/admissibility.rs create mode 100644 crates/code-intel-cli/src/artifact_index.rs create mode 100644 crates/code-intel-cli/src/artifact_ref.rs create mode 100644 crates/code-intel-cli/src/assistance_discovery.rs create mode 100644 crates/code-intel-cli/src/authority.rs create mode 100644 crates/code-intel-cli/src/builtin_provider_evidence.rs create mode 100644 crates/code-intel-cli/src/capability.rs create mode 100644 crates/code-intel-cli/src/capability_inventory.rs create mode 100644 crates/code-intel-cli/src/change_impact.rs create mode 100644 crates/code-intel-cli/src/codenexus_adapter.rs create mode 100644 crates/code-intel-cli/src/committed_evidence.rs create mode 100644 crates/code-intel-cli/src/compatibility_retirement_gate.rs create mode 100644 crates/code-intel-cli/src/compatibility_retirement_ticket.rs create mode 100644 crates/code-intel-cli/src/dag_coordinator.rs create mode 100644 crates/code-intel-cli/src/dag_run.rs create mode 100644 crates/code-intel-cli/src/decision_gap.rs create mode 100644 crates/code-intel-cli/src/decision_port.rs create mode 100644 crates/code-intel-cli/src/decision_record.rs create mode 100644 crates/code-intel-cli/src/delivery_light_speed.rs create mode 100644 crates/code-intel-cli/src/doctor_adapter.rs create mode 100644 crates/code-intel-cli/src/evidence_query.rs create mode 100644 crates/code-intel-cli/src/file_boundary.rs create mode 100644 crates/code-intel-cli/src/graph_adapter.rs create mode 100644 crates/code-intel-cli/src/hospital_diagnosis.rs create mode 100644 crates/code-intel-cli/src/internalization_record.rs create mode 100644 crates/code-intel-cli/src/method_catalog.rs create mode 100644 crates/code-intel-cli/src/method_select.rs create mode 100644 crates/code-intel-cli/src/model_channels.rs create mode 100644 crates/code-intel-cli/src/native_code_evidence.rs create mode 100644 crates/code-intel-cli/src/ponytail_gate.rs create mode 100644 crates/code-intel-cli/src/project_orientation.rs create mode 100644 crates/code-intel-cli/src/project_orientation_benchmark.rs create mode 100644 crates/code-intel-cli/src/repowise_adapter.rs create mode 100644 crates/code-intel-cli/src/run_commit.rs create mode 100644 crates/code-intel-cli/src/runtime_ci_evidence.rs create mode 100644 crates/code-intel-cli/src/sentrux_adapter.rs create mode 100644 crates/code-intel-cli/src/session_evidence.rs create mode 100644 crates/code-intel-cli/src/snapshot.rs create mode 100644 crates/code-intel-cli/src/stable_artifact.rs create mode 100644 crates/code-intel-cli/src/staged_artifact.rs create mode 100644 crates/code-intel-cli/src/survival_scan.rs create mode 100644 crates/code-intel-cli/src/understanding_quadrant.rs create mode 100644 crates/code-intel-cli/tests/artifact_index.rs create mode 100644 crates/code-intel-cli/tests/artifact_ref.rs create mode 100644 crates/code-intel-cli/tests/assistance_discovery.rs create mode 100644 crates/code-intel-cli/tests/authority_transition.rs create mode 100644 crates/code-intel-cli/tests/capability_exec.rs create mode 100644 crates/code-intel-cli/tests/codenexus_adapter.rs create mode 100644 crates/code-intel-cli/tests/compatibility_retirement_gate.rs create mode 100644 crates/code-intel-cli/tests/compatibility_retirement_ticket_template.rs create mode 100644 crates/code-intel-cli/tests/dag_coordinator.rs create mode 100644 crates/code-intel-cli/tests/dag_run.rs create mode 100644 crates/code-intel-cli/tests/decision_gap.rs create mode 100644 crates/code-intel-cli/tests/decision_port.rs create mode 100644 crates/code-intel-cli/tests/decision_record.rs create mode 100644 crates/code-intel-cli/tests/delivery_light_speed.rs create mode 100644 crates/code-intel-cli/tests/doctor_envelope.rs create mode 100644 crates/code-intel-cli/tests/evidence_admissibility.rs create mode 100644 crates/code-intel-cli/tests/file_boundary.rs create mode 100644 crates/code-intel-cli/tests/fixtures/codenexus-adapter/full-current.json create mode 100644 crates/code-intel-cli/tests/fixtures/codenexus-adapter/lite-current.json create mode 100644 crates/code-intel-cli/tests/fixtures/codenexus-adapter/unavailable.json create mode 100644 crates/code-intel-cli/tests/fixtures/graph-adapter/external-current.json create mode 100644 crates/code-intel-cli/tests/fixtures/graph-adapter/internal-current.json create mode 100644 crates/code-intel-cli/tests/fixtures/graph-adapter/internal-missing.json create mode 100644 crates/code-intel-cli/tests/fixtures/graph-adapter/internal-partial.json create mode 100644 crates/code-intel-cli/tests/fixtures/method-select/dependency-delay.json create mode 100644 crates/code-intel-cli/tests/fixtures/method-select/insufficient-evidence.json create mode 100644 crates/code-intel-cli/tests/fixtures/model-routing/ready-local.json create mode 100644 crates/code-intel-cli/tests/fixtures/project-orientation/missing-purpose.json create mode 100644 crates/code-intel-cli/tests/fixtures/sentrux-adapter/complete.json create mode 100644 crates/code-intel-cli/tests/fixtures/sentrux-adapter/crashed.json create mode 100644 crates/code-intel-cli/tests/fixtures/sentrux-adapter/partial.json create mode 100644 crates/code-intel-cli/tests/fixtures/sentrux-adapter/unknown-kind.json create mode 100644 crates/code-intel-cli/tests/fixtures/survival-scan/unavailable-expectations.json create mode 100644 crates/code-intel-cli/tests/graph_adapter.rs create mode 100644 crates/code-intel-cli/tests/hospital_diagnosis.rs create mode 100644 crates/code-intel-cli/tests/internalization_record.rs create mode 100644 crates/code-intel-cli/tests/method_catalog.rs create mode 100644 crates/code-intel-cli/tests/method_select.rs create mode 100644 crates/code-intel-cli/tests/mindwalk_internalization.rs create mode 100644 crates/code-intel-cli/tests/native_code_evidence.rs create mode 100644 crates/code-intel-cli/tests/pon_multilanguage_mapping.rs create mode 100644 crates/code-intel-cli/tests/ponytail_gate.rs create mode 100644 crates/code-intel-cli/tests/project_orientation.rs create mode 100644 crates/code-intel-cli/tests/project_orientation_benchmark.rs create mode 100644 crates/code-intel-cli/tests/repowise_adapter.rs create mode 100644 crates/code-intel-cli/tests/repowise_route.rs create mode 100644 crates/code-intel-cli/tests/run_commit.rs create mode 100644 crates/code-intel-cli/tests/runtime_ci_evidence.rs create mode 100644 crates/code-intel-cli/tests/schema_lifecycle.rs create mode 100644 crates/code-intel-cli/tests/sentrux_adapter.rs create mode 100644 crates/code-intel-cli/tests/session_evidence.rs create mode 100644 crates/code-intel-cli/tests/snapshot_identity.rs create mode 100644 crates/code-intel-cli/tests/staged_artifact.rs create mode 100644 crates/code-intel-cli/tests/survival_scan.rs create mode 100644 crates/code-intel-cli/tests/understanding_quadrant.rs create mode 100644 docs/adr/0010-tool-neutral-engineering-intelligence-core.md create mode 100644 docs/advisory-workflow-recommendation.md create mode 100644 docs/architecture/reference-capability-map.md create mode 100644 docs/artifact-ref-verifier.md create mode 100644 docs/assistance-discovery.md create mode 100644 docs/authority-transition-gate.md create mode 100644 docs/automatic-pull-request-beta.md create mode 100644 docs/change-impact.md create mode 100644 docs/code-intel-project-conformance.md create mode 100644 docs/code-intel-three-stage-acceptance.md create mode 100644 docs/codenexus-provider-adapter.md create mode 100644 docs/committed-run-index.md create mode 100644 docs/compatibility-facade-finalize.md create mode 100644 docs/compatibility-retire-codenexus-direct-branch.md create mode 100644 docs/compatibility-retire-doctor-wrapper-branch.md create mode 100644 docs/compatibility-retire-hospital-branch.md create mode 100644 docs/compatibility-retire-index-branch.md create mode 100644 docs/compatibility-retire-native-code-branch.md create mode 100644 docs/compatibility-retire-provider-preflight-branch.md create mode 100644 docs/compatibility-retire-publication-branch.md create mode 100644 docs/compatibility-retire-recommender-branch.md create mode 100644 docs/compatibility-retirement-gate.md create mode 100644 docs/compatibility-retirement-ticket-template.md create mode 100644 docs/compete-project-score.md create mode 100644 docs/dag-coordinator.md create mode 100644 docs/decision-gap-detector.md create mode 100644 docs/decision-request-response-port.md create mode 100644 docs/delivery-light-speed-measurement.md create mode 100644 docs/doctor-envelope.md create mode 100644 docs/evidence-admissibility.md create mode 100644 docs/evidence-query.md create mode 100644 docs/file-boundary-observation.md create mode 100644 docs/final-commitment-reconciliation-usage.md create mode 100644 docs/final-commitment-reconciliation.md create mode 100644 docs/follow-up-automation.md create mode 100644 docs/frontier-quality-loop.md create mode 100644 docs/graph-provider-adapter.md create mode 100644 docs/hospital-diagnosis.md create mode 100644 docs/internalization-advisory-candidates.md create mode 100644 docs/internalization-r09-r26-records.md create mode 100644 docs/internalization-record-engine.md create mode 100644 docs/inventory-rg-capability-contract.md create mode 100644 docs/language-adapter-acceptance-standard.md create mode 100644 docs/method-catalog.md create mode 100644 docs/method-selection.md create mode 100644 docs/model-channel-routing.md create mode 100644 docs/model-executable-handle.md create mode 100644 docs/multi-agent-merge-queue.md create mode 100644 docs/multi-agent-workspace-governance.md create mode 100644 docs/native-code-evidence.md create mode 100644 docs/plans/adr-0010-execution-plan.md create mode 100644 docs/plans/automatic-pr-one-command-orchestration-idea.md create mode 100644 docs/plans/compete-project-score-idea.md create mode 100644 docs/plans/four-blind-spots-closure-idea.md create mode 100644 docs/plans/language-adapter-acceptance-standard-idea.md create mode 100644 docs/plans/model-independent-pipeline-completion-idea.md create mode 100644 docs/plans/multi-agent-merge-queue-idea.md create mode 100644 docs/plans/multi-agent-workspace-governance-idea.md create mode 100644 docs/plans/pon-multilanguage-code-evidence-idea.md create mode 100644 docs/plans/pon-parity-floor-idea.md create mode 100644 docs/plans/pon-project-conformance-mapping-idea.md create mode 100644 docs/plans/python314-pon-development-lane-idea.md create mode 100644 docs/plans/session-evidence-adapter-idea.md create mode 100644 docs/plans/three-stage-project-acceptance-idea.md create mode 100644 docs/plans/understanding-quadrant-complexity-idea.md create mode 100644 docs/pon-conformance-ratchet.md create mode 100644 docs/pon-multilanguage-code-evidence.md create mode 100644 docs/ponytail-governance-gate.md create mode 100644 docs/project-orientation-benchmark.md create mode 100644 docs/project-orientation.md create mode 100644 docs/public-beta.md create mode 100644 docs/python314-pon-development.md create mode 100644 docs/repository-snapshot-identity.md create mode 100644 docs/repository-survival-scan.md create mode 100644 docs/repowise-adapter.md create mode 100644 docs/run-commit.md create mode 100644 docs/runtime-ci-evidence.md create mode 100644 docs/schema-lifecycle.md create mode 100644 docs/sentrux-provider-adapter.md create mode 100644 docs/session-evidence-adapter.md create mode 100644 docs/staged-artifact-writer.md create mode 100644 docs/understanding-quadrant.md create mode 100644 orchestration/acceptance/known-divergences.v1.json create mode 100644 orchestration/acceptance/native-code-evidence-candidate.json create mode 100644 orchestration/acceptance/sentrux-baseline-migration-20260723.json create mode 100644 orchestration/authority-transition-policy.v1.json create mode 100644 orchestration/code-intel-acceptance-policy.v1.json create mode 100644 orchestration/code-intel-project-conformance-policy.v1.json create mode 100644 orchestration/decision-gap-rules.v1.json create mode 100644 orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json create mode 100644 orchestration/evidence/final-commitment-reconciliation.json create mode 100644 orchestration/examples/compatibility-retirement-ticket-template.v1.example.json create mode 100644 orchestration/facade-finalize-fixtures/doctor.json create mode 100644 orchestration/facade-finalize-fixtures/full.json create mode 100644 orchestration/facade-finalize-fixtures/lite.json create mode 100644 orchestration/facade-finalize-fixtures/normal.json create mode 100644 orchestration/facade-finalize-policy.v1.json create mode 100644 orchestration/internalization/agent-loops.json create mode 100644 orchestration/internalization/c03-r05-r12-measurements.json create mode 100644 orchestration/internalization/claude-code-merge-queue.json create mode 100644 orchestration/internalization/cocoindex.json create mode 100644 orchestration/internalization/codenexus.json create mode 100644 orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json create mode 100644 orchestration/internalization/evidence/r08-live-20260714/github-solution-research.md create mode 100644 orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json create mode 100644 orchestration/internalization/fixtures/r06-native-labeled-corpus.json create mode 100644 orchestration/internalization/fixtures/r10-alternate-vcs-port.json create mode 100644 orchestration/internalization/frontier.json create mode 100644 orchestration/internalization/git.json create mode 100644 orchestration/internalization/github-research.json create mode 100644 orchestration/internalization/graph.json create mode 100644 orchestration/internalization/greenfield.json create mode 100644 orchestration/internalization/gstack.json create mode 100644 orchestration/internalization/linear.json create mode 100644 orchestration/internalization/llm-wiki.json create mode 100644 orchestration/internalization/matt-flow.json create mode 100644 orchestration/internalization/mattpocock-skills.json create mode 100644 orchestration/internalization/metaharness.json create mode 100644 orchestration/internalization/mindwalk.json create mode 100644 orchestration/internalization/my-code-machine.json create mode 100644 orchestration/internalization/native-code-evidence.json create mode 100644 orchestration/internalization/obsidian.json create mode 100644 orchestration/internalization/openspec.json create mode 100644 orchestration/internalization/pon-multilanguage.json create mode 100644 orchestration/internalization/pon-project-conformance.json create mode 100644 orchestration/internalization/pon-python314-development.json create mode 100644 orchestration/internalization/pon.json create mode 100644 orchestration/internalization/ponytail.json create mode 100644 orchestration/internalization/qiaomu-goal.json create mode 100644 orchestration/internalization/repomix.json create mode 100644 orchestration/internalization/repowise.json create mode 100644 orchestration/internalization/rg.json create mode 100644 orchestration/internalization/sentrux.json create mode 100644 orchestration/internalization/spec-kit.json create mode 100644 orchestration/internalization/tree-sitter-v.json create mode 100644 orchestration/internalization/yao-meta-skill.json create mode 100644 orchestration/language-adapter-acceptance-policy.v1.json create mode 100644 orchestration/method-selection-rules.v1.json create mode 100644 orchestration/methods/cards/contract-testing.v1.json create mode 100644 orchestration/methods/cards/critical-path-pert.v1.json create mode 100644 orchestration/methods/cards/fault-tree-analysis.v1.json create mode 100644 orchestration/methods/cards/fmea.v1.json create mode 100644 orchestration/methods/cards/pdca.v1.json create mode 100644 orchestration/methods/cards/root-cause-analysis.v1.json create mode 100644 orchestration/methods/cards/spc.v1.json create mode 100644 orchestration/methods/cards/strangler-migration.v1.json create mode 100644 orchestration/methods/cards/value-stream-queue-delay.v1.json create mode 100644 orchestration/methods/catalog.v1.json create mode 100644 orchestration/multi-agent-merge-queue-policy.v1.json create mode 100644 orchestration/multi-agent-workspace-policy.v1.json create mode 100644 orchestration/ponytail-gate-policy.v1.json create mode 100644 orchestration/python314-pon-development-policy.v1.json create mode 100644 orchestration/retirements/e02-recommender/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e02-recommender/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e02-recommender/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e02-recommender/e00-request.json create mode 100644 orchestration/retirements/e02-recommender/e01-request.json create mode 100644 orchestration/retirements/e02-recommender/e01-stderr.txt create mode 100644 orchestration/retirements/e02-recommender/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e02-recommender/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e02-recommender/evidence/contract-parity.json create mode 100644 orchestration/retirements/e02-recommender/evidence/dependency-d02-clean-machine.json create mode 100644 orchestration/retirements/e02-recommender/evidence/dependency-repo-snapshot.json create mode 100644 orchestration/retirements/e02-recommender/evidence/effect-parity.json create mode 100644 orchestration/retirements/e02-recommender/evidence/golden-parity.json create mode 100644 orchestration/retirements/e02-recommender/evidence/independent-approval.json create mode 100644 orchestration/retirements/e02-recommender/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e02-recommender/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e02-recommender/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e02-recommender/evidence/usage-observation.json create mode 100644 orchestration/retirements/e02-recommender/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e02-recommender/status.json create mode 100644 orchestration/retirements/e03-provider-preflight/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e03-provider-preflight/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e03-provider-preflight/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e03-provider-preflight/e00-request.json create mode 100644 orchestration/retirements/e03-provider-preflight/e01-request.json create mode 100644 orchestration/retirements/e03-provider-preflight/e01-stderr.txt create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/contract-parity.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/dependency-a04-admissibility.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/dependency-repo-snapshot.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/effect-parity.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/golden-parity.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/independent-approval.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e03-provider-preflight/evidence/usage-observation.json create mode 100644 orchestration/retirements/e03-provider-preflight/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e03-provider-preflight/status.json create mode 100644 orchestration/retirements/e04-codenexus-direct/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e04-codenexus-direct/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e04-codenexus-direct/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e04-codenexus-direct/e00-request.json create mode 100644 orchestration/retirements/e04-codenexus-direct/e01-request.json create mode 100644 orchestration/retirements/e04-codenexus-direct/e01-stderr.txt create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/contract-parity.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/dependency-b05.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/effect-parity.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/golden-parity.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/independent-approval.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e04-codenexus-direct/evidence/usage-observation.json create mode 100644 orchestration/retirements/e04-codenexus-direct/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e04-codenexus-direct/rollback-rehearsal/run-code-intel.ps1 create mode 100644 orchestration/retirements/e04-codenexus-direct/status.json create mode 100644 orchestration/retirements/e05-publication/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e05-publication/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e05-publication/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e05-publication/e00-request.json create mode 100644 orchestration/retirements/e05-publication/e01-request.json create mode 100644 orchestration/retirements/e05-publication/e01-stderr.txt create mode 100644 orchestration/retirements/e05-publication/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e05-publication/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e05-publication/evidence/contract-parity.json create mode 100644 orchestration/retirements/e05-publication/evidence/dependency-a06-stage.json create mode 100644 orchestration/retirements/e05-publication/evidence/dependency-a09.json create mode 100644 orchestration/retirements/e05-publication/evidence/effect-parity.json create mode 100644 orchestration/retirements/e05-publication/evidence/golden-parity.json create mode 100644 orchestration/retirements/e05-publication/evidence/independent-approval.json create mode 100644 orchestration/retirements/e05-publication/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e05-publication/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e05-publication/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e05-publication/evidence/usage-observation.json create mode 100644 orchestration/retirements/e05-publication/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e05-publication/status.json create mode 100644 orchestration/retirements/e07-native-code/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e07-native-code/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e07-native-code/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e07-native-code/e00-request.json create mode 100644 orchestration/retirements/e07-native-code/e01-request.json create mode 100644 orchestration/retirements/e07-native-code/e01-stderr.txt create mode 100644 orchestration/retirements/e07-native-code/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e07-native-code/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e07-native-code/evidence/contract-parity.json create mode 100644 orchestration/retirements/e07-native-code/evidence/dependency-inventory.json create mode 100644 orchestration/retirements/e07-native-code/evidence/dependency-snapshot.json create mode 100644 orchestration/retirements/e07-native-code/evidence/effect-parity.json create mode 100644 orchestration/retirements/e07-native-code/evidence/golden-parity.json create mode 100644 orchestration/retirements/e07-native-code/evidence/independent-approval.json create mode 100644 orchestration/retirements/e07-native-code/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e07-native-code/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e07-native-code/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e07-native-code/evidence/usage-observation.json create mode 100644 orchestration/retirements/e07-native-code/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e07-native-code/rollback-rehearsal/run-code-intel.ps1 create mode 100644 orchestration/retirements/e07-native-code/status.json create mode 100644 orchestration/retirements/e08-hospital/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e08-hospital/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e08-hospital/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e08-hospital/e00-request.json create mode 100644 orchestration/retirements/e08-hospital/e01-request.json create mode 100644 orchestration/retirements/e08-hospital/e01-stderr.txt create mode 100644 orchestration/retirements/e08-hospital/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e08-hospital/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e08-hospital/evidence/contract-parity.json create mode 100644 orchestration/retirements/e08-hospital/evidence/dependency-a04.json create mode 100644 orchestration/retirements/e08-hospital/evidence/dependency-b07.json create mode 100644 orchestration/retirements/e08-hospital/evidence/effect-parity.json create mode 100644 orchestration/retirements/e08-hospital/evidence/golden-parity.json create mode 100644 orchestration/retirements/e08-hospital/evidence/independent-approval.json create mode 100644 orchestration/retirements/e08-hospital/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e08-hospital/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e08-hospital/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e08-hospital/evidence/usage-observation.json create mode 100644 orchestration/retirements/e08-hospital/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e08-hospital/status.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/e00-request.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/e01-request.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/e01-stderr.txt create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/contract-parity.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/dependency-snapshot.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/effect-parity.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/golden-parity.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/independent-approval.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/evidence/usage-observation.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e09-doctor-wrapper/rollback-rehearsal/invoke-code-intel.ps1 create mode 100644 orchestration/retirements/e09-doctor-wrapper/status.json create mode 100644 orchestration/retirements/e10-index/compatibility-retirement-deletion-diff.json create mode 100644 orchestration/retirements/e10-index/compatibility-retirement-manifest.json create mode 100644 orchestration/retirements/e10-index/compatibility-retirement-ticket.json create mode 100644 orchestration/retirements/e10-index/e00-request.json create mode 100644 orchestration/retirements/e10-index/e01-request.json create mode 100644 orchestration/retirements/e10-index/e01-stderr.txt create mode 100644 orchestration/retirements/e10-index/evidence/c00-necessity.json create mode 100644 orchestration/retirements/e10-index/evidence/compatibility-window.json create mode 100644 orchestration/retirements/e10-index/evidence/contract-parity.json create mode 100644 orchestration/retirements/e10-index/evidence/dependency-e05.json create mode 100644 orchestration/retirements/e10-index/evidence/effect-parity.json create mode 100644 orchestration/retirements/e10-index/evidence/golden-parity.json create mode 100644 orchestration/retirements/e10-index/evidence/independent-approval.json create mode 100644 orchestration/retirements/e10-index/evidence/registry-reconciliation.json create mode 100644 orchestration/retirements/e10-index/evidence/replacement-atom.json create mode 100644 orchestration/retirements/e10-index/evidence/rollback-execution.json create mode 100644 orchestration/retirements/e10-index/evidence/usage-observation.json create mode 100644 orchestration/retirements/e10-index/gate-out/compatibility-retirement-decision.json create mode 100644 orchestration/retirements/e10-index/status.json create mode 100644 orchestration/schema-lifecycle.v1.json create mode 100644 orchestration/schemas/code-evidence-native-artifacts.v1.schema.json create mode 100644 orchestration/schemas/code-intel-acceptance-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-advisory-workflow-recommendation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-architecture-graph-port.v1.schema.json create mode 100644 orchestration/schemas/code-intel-artifact-index.v1.schema.json create mode 100644 orchestration/schemas/code-intel-assistance-discovery-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-assistance-discovery-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-assistance-dossier.v1.schema.json create mode 100644 orchestration/schemas/code-intel-authority-transition.v1.schema.json create mode 100644 orchestration/schemas/code-intel-auto-pr-authorization.v1.schema.json create mode 100644 orchestration/schemas/code-intel-auto-pr-execution-receipt.v1.schema.json create mode 100644 orchestration/schemas/code-intel-auto-pr-execution-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-auto-pr-flow-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-auto-pr-proposal.v1.schema.json create mode 100644 orchestration/schemas/code-intel-change-impact.v1.schema.json create mode 100644 orchestration/schemas/code-intel-clean-machine-verifier-attestation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-codenexus-port.v1.schema.json create mode 100644 orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-compatibility-facade-finalize.v1.schema.json create mode 100644 orchestration/schemas/code-intel-compatibility-retirement-decision.v1.schema.json create mode 100644 orchestration/schemas/code-intel-compatibility-retirement-deletion-diff.v1.schema.json create mode 100644 orchestration/schemas/code-intel-compatibility-retirement-evidence.v1.schema.json create mode 100644 orchestration/schemas/code-intel-compatibility-retirement-manifest.v1.schema.json create mode 100644 orchestration/schemas/code-intel-compatibility-retirement-ticket-template.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-cancellation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-exchange-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-gap-detection-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-gap.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-record.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-decision-response.v1.schema.json create mode 100644 orchestration/schemas/code-intel-delivery-light-speed.v1.schema.json create mode 100644 orchestration/schemas/code-intel-doctor-observation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-evidence-payload.v1.schema.json create mode 100644 orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json create mode 100644 orchestration/schemas/code-intel-evidence-query.v1.schema.json create mode 100644 orchestration/schemas/code-intel-file-boundary-document.v1.schema.json create mode 100644 orchestration/schemas/code-intel-file-boundary-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-file-boundary-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-final-commitment-reconciliation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-follow-up-automation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-graph-route-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-hospital.v1.schema.json create mode 100644 orchestration/schemas/code-intel-internalization-record.v1.schema.json create mode 100644 orchestration/schemas/code-intel-language-adapter-acceptance.v1.schema.json create mode 100644 orchestration/schemas/code-intel-method-card.v1.schema.json create mode 100644 orchestration/schemas/code-intel-method-selection.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-adapter-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-adapter-request.v2.schema.json create mode 100644 orchestration/schemas/code-intel-model-adapter-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-assistance-dossier.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-channel-inventory-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-channel-inventory-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-executable-handle.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-routing-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-model-routing-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-multi-agent-merge-queue-status.v1.schema.json create mode 100644 orchestration/schemas/code-intel-notice-provenance.v1.schema.json create mode 100644 orchestration/schemas/code-intel-ponytail-gate.v1.schema.json create mode 100644 orchestration/schemas/code-intel-project-orientation-benchmark-observations.v1.schema.json create mode 100644 orchestration/schemas/code-intel-project-orientation-benchmark.v1.schema.json create mode 100644 orchestration/schemas/code-intel-project-orientation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-repository-snapshot.v1.schema.json create mode 100644 orchestration/schemas/code-intel-repository-survival-scan-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-repository-survival-scan-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-repowise-route-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-reuse-record.v1.schema.json create mode 100644 orchestration/schemas/code-intel-run-commit.v1.schema.json create mode 100644 orchestration/schemas/code-intel-run-dag.v1.schema.json create mode 100644 orchestration/schemas/code-intel-run-manifest.v1.schema.json create mode 100644 orchestration/schemas/code-intel-run-state.v1.schema.json create mode 100644 orchestration/schemas/code-intel-run-timing-events.v1.schema.json create mode 100644 orchestration/schemas/code-intel-runtime-ci-ingest-request.v1.schema.json create mode 100644 orchestration/schemas/code-intel-runtime-ci-observation.v1.schema.json create mode 100644 orchestration/schemas/code-intel-runtime-ci-summary.v1.schema.json create mode 100644 orchestration/schemas/code-intel-schema-lifecycle.v1.schema.json create mode 100644 orchestration/schemas/code-intel-sentrux-route-result.v1.schema.json create mode 100644 orchestration/schemas/code-intel-session-evidence.v1.schema.json create mode 100644 orchestration/schemas/code-intel-staged-artifact-set.v1.schema.json create mode 100644 orchestration/schemas/code-intel-structural-evidence-port.v1.schema.json create mode 100644 orchestration/schemas/code-intel-understanding-quadrant.v1.schema.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prototypes/session-observability/.gitignore create mode 100644 prototypes/session-observability/Cargo.lock create mode 100644 prototypes/session-observability/Cargo.toml create mode 100644 prototypes/session-observability/IDEA.md create mode 100644 prototypes/session-observability/README.md create mode 100644 prototypes/session-observability/src/main.rs create mode 100644 prototypes/session-observability/src/model.rs create mode 100644 test-artifact-index-contract.ps1 create mode 100644 test-code-intel-acceptance.ps1 create mode 100644 test-code-intel-project-conformance.ps1 create mode 100644 test-codenexus-adapter-contract.ps1 create mode 100644 test-codenexus-generated-path-filter.ps1 create mode 100644 test-compatibility-facade-finalize.ps1 create mode 100644 test-compete-project-score.ps1 create mode 100644 test-dag-facade.ps1 create mode 100644 test-language-adapter-acceptance.ps1 create mode 100644 test-model-request-synthesis-and-handle.ps1 create mode 100644 test-multi-agent-merge-queue.ps1 create mode 100644 test-multi-agent-workspace-preflight.ps1 create mode 100644 test-parity-baseline.ps1 create mode 100644 test-parity-floor.ps1 create mode 100644 test-ponytail-gate-contract.ps1 create mode 100644 test-provider-runtime-inventory.ps1 create mode 100644 test-python314-pon-compatibility.ps1 create mode 100644 test-repowise-adapter-contract.ps1 create mode 100644 test-repowise-provider-probe-classification.ps1 create mode 100644 test-run-commit-contract.ps1 create mode 100644 test-runtime-ci-hospital-pet.ps1 create mode 100644 test-stable-wrapper-e2e.ps1 create mode 100644 test-survival-scan-contract.ps1 create mode 100644 test-transactional-publication.ps1 create mode 100644 tests/fixtures/decision-gap/missing-fact.json create mode 100644 tests/fixtures/decision-gap/risk-acceptance.json create mode 100644 tests/fixtures/decision-port/cancellation.json create mode 100644 tests/fixtures/decision-port/request.json create mode 100644 tests/fixtures/decision-port/stale-response.json create mode 100644 tests/fixtures/decision-port/valid-response.json create mode 100644 tests/fixtures/decision-port/wrong-actor-response.json create mode 100644 tests/fixtures/decision-port/wrong-gap-response.json create mode 100644 tests/fixtures/evidence-admissibility/good/payload.json create mode 100644 tests/fixtures/evidence-admissibility/good/request.json create mode 100644 tests/fixtures/internalization/complete.json create mode 100644 tests/fixtures/parity/clean/fixture-review.json create mode 100644 tests/fixtures/parity/clean/machine-artifacts.json create mode 100644 tests/fixtures/parity/clean/normalized.json create mode 100644 tests/fixtures/parity/dirty/fixture-review.json create mode 100644 tests/fixtures/parity/dirty/machine-artifacts.json create mode 100644 tests/fixtures/parity/dirty/normalized.json create mode 100644 tests/fixtures/parity/domain-fail/fixture-review.json create mode 100644 tests/fixtures/parity/domain-fail/machine-artifacts.json create mode 100644 tests/fixtures/parity/domain-fail/normalized.json create mode 100644 tests/fixtures/parity/parity-floor.json create mode 100644 tests/fixtures/parity/partial-evidence/fixture-review.json create mode 100644 tests/fixtures/parity/partial-evidence/machine-artifacts.json create mode 100644 tests/fixtures/parity/partial-evidence/normalized.json create mode 100644 tests/fixtures/parity/provider-unavailable/fixture-review.json create mode 100644 tests/fixtures/parity/provider-unavailable/machine-artifacts.json create mode 100644 tests/fixtures/parity/provider-unavailable/normalized.json create mode 100644 tests/fixtures/ponytail/c00-necessity-trace.json create mode 100644 tests/fixtures/python314-compat/core_language.py create mode 100644 tests/fixtures/python314-compat/exceptions_and_objects.py create mode 100644 tests/fixtures/python314-compat/manifest.v1.json create mode 100644 tests/fixtures/python314-compat/python314_template_strings.py create mode 100644 tests/fixtures/repowise-adapter/docs-payload.json create mode 100644 tests/fixtures/repowise-adapter/index-only.json create mode 100644 tests/fixtures/repowise-adapter/index-payload.json create mode 100644 tests/fixtures/repowise-adapter/partial-docs-payload.json create mode 100644 tests/fixtures/repowise-adapter/quota.json create mode 100644 tests/fixtures/repowise-adapter/success.json create mode 100644 tests/test-automatic-pull-request-flow.ps1 create mode 100644 tests/test-automatic-pull-request.ps1 create mode 100644 tests/test-follow-up-automation.ps1 create mode 100644 tests/test-model-channel-degraded-pipeline.ps1 create mode 100644 tests/test-model-channel-delegate.ps1 create mode 100644 tools/Invoke-CodeIntelAutomaticPullRequest.ps1 create mode 100644 tools/Invoke-CodeIntelAutomaticPullRequestFlow.ps1 create mode 100644 tools/New-BetaPackage.ps1 create mode 100644 tools/Test-BetaPackage.ps1 create mode 100644 tools/Test-FinalCommitmentReconciliation.ps1 create mode 100644 tools/code-intel-follow-up-automation.psm1 create mode 100644 tools/compatibility/New-CodeNexusDirectRetirementPacket.ps1 create mode 100644 tools/compatibility/New-DoctorWrapperRetirementPacket.ps1 create mode 100644 tools/compatibility/New-HospitalRetirementPacket.ps1 create mode 100644 tools/compatibility/New-IndexRetirementPacket.ps1 create mode 100644 tools/compatibility/New-NativeCodeRetirementPacket.ps1 create mode 100644 tools/compatibility/New-ProviderPreflightRetirementPacket.ps1 create mode 100644 tools/compatibility/New-PublicationRetirementPacket.ps1 create mode 100644 tools/compatibility/New-RecommenderRetirementPacket.ps1 create mode 100644 tools/compatibility/Restore-CodeNexusDirectBranch.ps1 create mode 100644 tools/compatibility/Restore-DoctorWrapperBranch.ps1 create mode 100644 tools/compatibility/Restore-HospitalLegacyBranch.ps1 create mode 100644 tools/compatibility/Restore-IndexLegacyBranch.ps1 create mode 100644 tools/compatibility/Restore-NativeCodeEmbeddedBranch.ps1 create mode 100644 tools/compatibility/Restore-ProviderPreflightLegacyBranch.ps1 create mode 100644 tools/compatibility/Restore-PublicationLegacyBranch.ps1 create mode 100644 tools/compatibility/Restore-RecommenderLegacyBranch.ps1 create mode 100644 tools/compatibility/Test-CodeNexusDirectRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-DoctorWrapperRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-HospitalRetirementBoundary.ps1 create mode 100644 tools/compatibility/Test-HospitalRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-IndexRetirementBoundary.ps1 create mode 100644 tools/compatibility/Test-IndexRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-NativeCodeRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-ProviderPreflightRetirementBoundary.ps1 create mode 100644 tools/compatibility/Test-ProviderPreflightRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-PublicationRetirementBoundary.ps1 create mode 100644 tools/compatibility/Test-PublicationRetirementPacket.ps1 create mode 100644 tools/compatibility/Test-RecommenderRetirementPacket.ps1 create mode 100644 tools/normalize_compete_score.py diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..ed342c4 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,45 @@ +#!/usr/bin/env sh +set -eu + +repo_root="$(git rev-parse --show-toplevel)" +queue_command="$repo_root/node_modules/.bin/claude-code-merge-queue" + +# Git for Windows can prepend user shim directories ahead of the Windows Python +# launcher. Keep the native launcher discoverable so the CPython 3.14 acceptance +# lane behaves the same in pre-push as it does in PowerShell. +if [ -n "${LOCALAPPDATA:-}" ] && command -v cygpath >/dev/null 2>&1; then + windows_apps="$(cygpath -u "$LOCALAPPDATA")/Microsoft/WindowsApps" + PATH="$windows_apps:$PATH" + export PATH +fi + +if [ ! -f "$queue_command" ]; then + printf '%s\n' 'pre-push: repository-local claude-code-merge-queue is missing; run Install-MultiAgentMergeQueue.ps1.' >&2 + exit 1 +fi + +payload="$(mktemp "${TMPDIR:-/tmp}/code-intel-pre-push.XXXXXX")" +trap 'rm -f "$payload"' EXIT HUP INT TERM +cat >"$payload" + +# Required landing guard: claude-code-merge-queue check-push +if ! "$queue_command" check-push "$@" <"$payload"; then + exit 1 +fi + +previous_hooks_path="$(git config --local --get codeIntel.mergeQueue.previousHooksPath || true)" +if [ -n "$previous_hooks_path" ]; then + case "$previous_hooks_path" in + /* | [A-Za-z]:/*) previous_hook="$previous_hooks_path/pre-push" ;; + *) previous_hook="$repo_root/$previous_hooks_path/pre-push" ;; + esac + + current_hook="$repo_root/.githooks/pre-push" + if [ -f "$previous_hook" ] && [ "$previous_hook" != "$current_hook" ]; then + if [ -x /usr/bin/bash ]; then + /usr/bin/bash "$previous_hook" "$@" <"$payload" + else + sh "$previous_hook" "$@" <"$payload" + fi + fi +fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0081243..b4e9d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.14" - name: Setup Rust shell: pwsh @@ -35,10 +35,14 @@ jobs: shell: pwsh run: cargo check - - name: Rust tests + - name: Rust tests and core cross-contract gate shell: pwsh run: cargo test -p code-intel + - name: Representative project orientation benchmark + shell: pwsh + run: cargo test -p code-intel --test project_orientation_benchmark -- --nocapture + - name: Build Rust CLI shell: pwsh run: cargo build -p code-intel --release @@ -47,19 +51,30 @@ jobs: shell: pwsh run: | $files = @( + "invoke-code-intel.ps1", "run-code-intel.ps1", "Invoke-ScopedRepowise.ps1", "Find-CodeIntelProjects.ps1", "Invoke-GitHubSolutionResearch.ps1", "test-code-intel-pipeline.ps1", + "test-stable-wrapper-e2e.ps1", "test-github-solution-research.ps1", "test-hospital-trust-contract.ps1", "test-atomic-capability-contract.ps1", "test-project-discovery.ps1", "test-project-management-support.ps1", + "Test-CodeIntelProjectConformance.ps1", + "test-code-intel-project-conformance.ps1", + "Invoke-MultiAgentMergeQueue.ps1", + "test-multi-agent-merge-queue.ps1", + "test-integration-orchestration.ps1", + "Test-Python314PonCompatibility.ps1", + "test-python314-pon-compatibility.ps1", "test-scoped-repowise-security.ps1", "test-scoped-repowise-worktree.ps1", - "test-skill-development-benchmark.ps1" + "test-skill-development-benchmark.ps1", + "tools/New-BetaPackage.ps1", + "tools/Test-BetaPackage.ps1" ) foreach ($file in $files) { $tokens = $null @@ -162,31 +177,37 @@ jobs: shell: pwsh run: .\test-regression-fixes.ps1 + - name: Python 3.14 and project conformance + shell: pwsh + run: | + .\test-python314-pon-compatibility.ps1 + .\test-integration-orchestration.ps1 + .\test-code-intel-project-conformance.ps1 + - name: Pipeline smoke shell: pwsh run: .\test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -SkipSentruxGate -SkipGitHubResearch -Mode normal + - name: Stable wrapper authoritative outcome E2E + shell: pwsh + run: .\test-stable-wrapper-e2e.ps1 + - name: Package shell: pwsh - run: | - New-Item -ItemType Directory -Force dist\code-intel-pipeline\bin | Out-Null - $files = git ls-files - foreach ($file in $files) { - if ($file -match "^\.gitignore$") { - continue - } - $target = Join-Path "dist\code-intel-pipeline" $file - New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null - Copy-Item -LiteralPath $file -Destination $target -Force - } - Copy-Item -LiteralPath target\release\code-intel.exe -Destination dist\code-intel-pipeline\bin\code-intel.exe -Force - Compress-Archive -Path dist\code-intel-pipeline -DestinationPath dist\code-intel-pipeline-windows.zip -Force + run: ./tools/New-BetaPackage.ps1 -ExecutablePath ./target/release/code-intel.exe -OutputDirectory ./dist -PackageName code-intel-pipeline-windows-beta + + - name: Verify beta package + shell: pwsh + run: ./tools/Test-BetaPackage.ps1 -ZipPath ./dist/code-intel-pipeline-windows-beta.zip -Json - name: Upload package artifact uses: actions/upload-artifact@v4 with: name: code-intel-pipeline-windows - path: dist/code-intel-pipeline-windows.zip + path: | + dist/code-intel-pipeline-windows-beta.zip + dist/code-intel-pipeline-windows-beta.zip.sha256 + dist/code-intel-pipeline-windows-beta.release-manifest.json cross-platform-smoke: permissions: @@ -209,7 +230,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.14" - name: Setup Rust shell: pwsh @@ -223,7 +244,7 @@ jobs: shell: pwsh run: cargo check - - name: Rust tests + - name: Rust tests and core cross-contract gate shell: pwsh run: cargo test -p code-intel @@ -235,19 +256,30 @@ jobs: shell: pwsh run: | $files = @( + "invoke-code-intel.ps1", "run-code-intel.ps1", "Invoke-ScopedRepowise.ps1", "Find-CodeIntelProjects.ps1", "Invoke-GitHubSolutionResearch.ps1", "test-code-intel-pipeline.ps1", + "test-stable-wrapper-e2e.ps1", "test-github-solution-research.ps1", "test-hospital-trust-contract.ps1", "test-atomic-capability-contract.ps1", "test-project-discovery.ps1", "test-project-management-support.ps1", + "Test-CodeIntelProjectConformance.ps1", + "test-code-intel-project-conformance.ps1", + "Invoke-MultiAgentMergeQueue.ps1", + "test-multi-agent-merge-queue.ps1", + "test-integration-orchestration.ps1", + "Test-Python314PonCompatibility.ps1", + "test-python314-pon-compatibility.ps1", "test-scoped-repowise-security.ps1", "test-scoped-repowise-worktree.ps1", - "test-skill-development-benchmark.ps1" + "test-skill-development-benchmark.ps1", + "tools/New-BetaPackage.ps1", + "tools/Test-BetaPackage.ps1" ) foreach ($file in $files) { $tokens = $null @@ -322,6 +354,14 @@ jobs: - name: Scoped Repowise manifest validator tests run: python ./test-scoped-repowise-validator.py + - name: Python 3.14 compatibility lane + shell: pwsh + run: ./test-python314-pon-compatibility.ps1 + + - name: Multi-agent merge queue contract + shell: pwsh + run: ./test-multi-agent-merge-queue.ps1 + - name: Pipeline smoke shell: pwsh run: ./test-code-intel-pipeline.ps1 -RepoPath . -SkipRepowise -AllowGraphMissing -SkipSentruxCheck -SkipSentruxGate -SkipGitHubResearch -Mode normal diff --git a/.gitignore b/.gitignore index de0dcf3..4312b66 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ __pycache__/ artifacts/ cache/ dist/ +work/ +node_modules/ # Rust build artifacts (CodeNexus Lite + future crates) target/ diff --git a/.sentrux/baseline.json b/.sentrux/baseline.json index 1707a78..ba72a67 100644 --- a/.sentrux/baseline.json +++ b/.sentrux/baseline.json @@ -1,12 +1,12 @@ { - "timestamp": 1783162417.7382956, - "quality_signal": 0.4725541988551525, + "timestamp": 1784121873.0427084, + "quality_signal": 0.5469818460724194, "coupling_score": 0.0, "cycle_count": 0, "god_file_count": 0, "hotspot_count": 0, - "complex_fn_count": 20, + "complex_fn_count": 21, "max_depth": 2, "total_import_edges": 2, "cross_module_edges": 0 -} \ No newline at end of file +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fe4aae7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Code Intel Pipeline agent rules + +## Language direction + +- Do not add new PowerShell scripts or new product behavior to existing `.ps1` files. +- Treat current PowerShell entry points as legacy compatibility surfaces only. Limit edits to critical fixes or thin forwarding shims while their Rust replacements are being delivered. +- Implement production CLI, orchestration, artifact, policy, and provider-boundary work in Rust by default. +- MoonBit is an approved experimental language for small, isolated components. Keep experiments outside the production path until they prove artifact-contract parity, cross-platform builds, tests, and a measured advantage over the Rust implementation. +- Do not perform a big-bang PowerShell deletion. Retire each compatibility entry point only after its Rust or promoted MoonBit replacement passes the existing contract tests and release packaging checks. + +## Verification + +- Rust changes require focused `cargo test` coverage plus the relevant integration-contract checks. +- MoonBit experiments require `moon test` and parity fixtures against the current artifact contract before promotion. +- New documentation and command examples should lead with the compiled `code-intel` CLI. Mention PowerShell only when documenting an existing compatibility path. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d16575..94fa4bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.0] — 2026-07-23 + +### Added + +- One manifest-bound normal-run spine for snapshot, doctor, inventory, native code evidence, + architecture graph, real Sentrux `gate`/`check` observations, Hospital diagnosis, atomic + publication, committed-only indexing, evidence query, freshness, and conservative change impact. +- Optional, research-stage Mindwalk trace normalization for privacy-reduced session review; it is + advisory-only and absent from default scans. +- Representative benchmark gates for deterministic replay, artifact size, unresolved coverage, and + unsupported-file coverage. + +- One-command automatic draft-PR orchestration from exact proposal through structured user decision, + C07 record/replay, and the existing fail-closed executor. +- Zero-effect proactive `/investigate` suggestions for actionable Pipeline failures, + plus a branch-local user decision request before any automatic draft-PR path. +- Public-beta package verification, including clean extracted-ZIP smoke coverage + and release checksums. +- Runtime/CI and file-boundary evidence providers, transactional artifact + contracts, model request synthesis, executable handles, and compatibility + retirement approval evidence. + +### Changed + +- Non-completed runs are retained as audit diagnostics and can never replace the latest completed + authority. Domain-failed nodes retain their verified evidence without becoming authoritative. +- The native seven-language adapter is explicitly graded `candidate + structural`; semantic, + behavioral, and production claims remain unsupported. +- Project license metadata, README, and root license text are now consistently MIT. + +- Repowise semantic memory remains in the default orchestration plan but is now + explicitly optional and non-blocking for the beta core. +- CodeNexus context remains an optional compatibility adapter; generated + `work/` paths are excluded from repository evidence. +- Sentrux debt normalization treats an improving quality signal as + informational while structural metric increases remain blocking. +- The stable wrapper resolves the packaged `bin/code-intel.exe` before any + development-tree or Cargo fallback. + +### Security + +- Production model delegation uses synthesized requests and validated + executable handles; legacy raw CLI execution is rejected by default. + +### Known limits + +- The public beta package is Windows-only. +- The incubated `crates/code-nexus-lite` Rust worker is not a shipped workspace + binary; CodeNexus indexing is not a beta-core dependency. + ## [0.3.0-beta.1] — 2026-07-16 Pre-release for the Rust-first Code Intel control plane. This build is intended diff --git a/CONTEXT.md b/CONTEXT.md index bd6c024..4da87d5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,11 +1,11 @@ # Code Intel Pipeline -Code Intel Pipeline is a local repository-intelligence workflow. Its language distinguishes the component that produces fresh evidence from the component that resumes from evidence already written. +Code Intel Pipeline is an independent engineering-intelligence domain. It turns repository and delivery evidence into deterministic engineering facts, derived views, diagnoses, and plans that other products may consume without owning its language or runtime. ## Language -**Code Intel Pipeline**: The workflow that turns a target repository into repository intelligence, diagnosis, and the next recommended protocol. -_Avoid_: Analyzer, crawler, generic scanner +**Code Intel Pipeline**: The independent engineering-intelligence system that turns a Target Repository and its delivery evidence into deterministic engineering facts, derived views, diagnoses, and plans. OpenCLI Admin and other systems are consumers, not owners or internal modules of this domain. +_Avoid_: OpenCLI Admin feature, analyzer, crawler, generic scanner **Agent Goal Intake**: The pre-scan task-contract layer that turns vague work into a bounded goal, verification evidence, constraints, iteration policy, stop conditions, and pause conditions. _Avoid_: Scanner, prompt template, backlog item @@ -19,8 +19,11 @@ _Avoid_: Scanner, Agent Goal Intake, runtime dependency **Skill Development Benchmark**: A quality baseline for creating and hardening reusable skills, including trigger design, evaluation evidence, portability, release gates, failure cases, and review artifacts. _Avoid_: Runtime dependency, scanner contract, prompt style guide -**Implementation Minimalism Benchmark**: An implementation-choice baseline requiring Agent code work to choose the smallest sufficient option before writing code: do nothing, reuse this repository, standard library, platform native capability, already-installed dependency, one-liner, then smallest local implementation. -_Avoid_: Runtime dependency, scanner contract, permission to skip evidence or safety +**Ponytail Value Filter**: The Agent-output discipline that first rejects code, abstractions, dependencies, files, tests, documentation, or process that does not need to exist, then chooses the first sufficient solution rung for work that remains necessary. It is never permission to skip understanding, evidence, safety, or verification. +_Avoid_: Code formatter, shortest-code contest, indiscriminate deletion, permission to under-build + +**Necessity Trace**: The explicit link from an Agent-produced artifact to a current value source: an operator-requested outcome, Committed Engineering Plan deliverable, verified defect or risk, required contract or gate, evidence-closing spike, or approved debt reduction. Output without a Necessity Trace is rejected by the Ponytail Value Filter. +_Avoid_: Generic justification, future possibility, author preference **Project Management Support**: agent-intake layer turning repository evidence into trackable work and durable project knowledge through issue tracker selection, triage labels, domain docs, and optional wiki surfaces. _Avoid_: Scanner runtime, artifact producer, hidden credential store @@ -34,6 +37,90 @@ _Avoid_: Artifact authority, source of truth, replacement for ADRs or scanner ev **Target Repository**: The repository being examined by a pipeline run. _Avoid_: Project, workspace, source tree +**Engineering Evidence Set**: The provenance-bearing, point-in-time collection of available engineering evidence for one analysis scope, anchored by a Target Repository and optionally including specifications, decisions, issue records, CI results, releases, runtime evidence, incidents, and ownership records. Every source declares freshness and availability so missing evidence remains unknown rather than being interpreted as absence. +_Avoid_: Repository snapshot, context dump, all project data, implicit complete picture + +**Evidence Provider**: A replaceable external or built-in source that supplies Observed Evidence for one or more declared engineering capabilities. A provider does not become authoritative merely because it is installed or returns successfully. +_Avoid_: Pipeline dependency, source of truth, hardcoded tool + +**Evidence Provider Port**: The Pipeline-owned contract describing the evidence, provenance, snapshot, freshness, completeness, and failure semantics required from an Evidence Provider. Providers remain independent and do not share Pipeline internals. +_Avoid_: Provider-native API, shared internal model, common database + +**Provider Adapter**: The replaceable translation boundary that maps one provider's native protocol and evidence into an Evidence Provider Port without requiring either repository to depend on the other's internal implementation. +_Avoid_: Shared library coupling, database integration, provider fork + +**Observed Evidence**: Provenance-bearing output captured from an Evidence Provider before Pipeline validation establishes its schema, snapshot, freshness, completeness, and admissibility as an Engineering Fact. +_Avoid_: Engineering Fact, trusted result, provider opinion + +**Selective Internalization**: The policy of reusing mature Evidence Providers by default while bringing only indispensable engineering semantics, trust boundaries, and minimum survival capabilities under Pipeline ownership. Internalization is justified by authority, verifiability, portability, supply-chain, or shared-semantics needs rather than by a desire to eliminate dependencies. +_Avoid_: Reimplement every dependency, vendor everything, dependency minimization as a goal + +**Engineering Capability Gap**: A required engineering ability that the current project, Pipeline, or available Evidence Providers cannot satisfy with admissible evidence and acceptable constraints. +_Avoid_: Missing package, implementation task, vague problem + +**Engineering Assistance Discovery**: The evidence-driven process of finding internal atomic projects, external tools, established methods, and documentation sources that may resolve an Engineering Capability Gap. +_Avoid_: Package search, GitHub browsing, automatic dependency installation + +**Solution Candidate Dossier**: A comparable evidence package for one possible response to an Engineering Capability Gap, covering fit, provenance, version, maintenance, license, security, compatibility, integration cost, reversibility, and validation status. +_Avoid_: Recommendation list, star ranking, LLM summary + +**Adoption Decision**: The explicit decision to build, reuse, adapt, selectively internalize, reject, or defer a candidate after its Solution Candidate Dossier and validation evidence have been reviewed. +_Avoid_: Automatic install, tool recommendation, dependency detection + +**Decision Gap**: A missing choice about intent, trade-offs, authority, priority, resources, or risk acceptance that cannot be resolved from Engineering Facts. It blocks only the decisions and plan branches that depend on it while unrelated deterministic analysis continues. +_Avoid_: Missing fact, generic question, reason to stop all work + +**Decision Interview**: The one-question-at-a-time process by which an Agent closes a Decision Gap with a recommended answer, evidence, and consequences while retrieving discoverable facts instead of asking the operator for them. +_Avoid_: Questionnaire, requirements dump, asking the user to inspect the repository + +**Decision Record**: The provenance-bearing result of a resolved Decision Gap, recording the accepted choice and enough context to prevent the same decision from being repeatedly reopened without new evidence. +_Avoid_: Chat transcript, inferred preference, undocumented approval + +**Progressive Project Understanding**: The staged construction of a provenance-bearing project model, beginning with repository bootstrap and a fast Project Orientation, then deepening into structural and task-specific understanding only as needed. +_Avoid_: Full rescan before every task, LLM repository summary, one-shot context dump + +**Project Orientation**: The first actionable view of a project, covering identity, purpose, language, major boundaries, entry points, commands, active change, available evidence, known risks, unknowns, and confidence. A typical repository should produce this view within sixty seconds without requiring an LLM. +_Avoid_: README summary, architecture deep dive, complete project understanding + +**Project Understanding Quadrant**: The prioritization view that classifies project knowledge by System Criticality and Evidence Confidence into Known Core, Critical Unknown, Supporting Context, and Deferred Unknown. It directs limited understanding effort toward the project skeleton and the most consequential uncertainty first. +_Avoid_: Equal-depth repository reading, urgent-important task matrix, hidden unknowns + +**Light-Speed Baseline**: The shortest credible path from an engineering problem to verified completion after preserving irreducible technical work, correctness, safety, and evidence gates. It exposes avoidable delay from queues, handoffs, repeated understanding, rework, and unnecessary coordination rather than rewarding speed that bypasses verification. +_Avoid_: Move fast and break things, shortest implementation time, skipping gates, raw Agent throughput + +**Engineering Method Catalog**: The authoritative catalog of established engineering methods that Pipeline can select and apply from declared problem signals, required evidence, assumptions, deterministic steps, outputs, confidence rules, and execution cost. A method may have built-in or external implementations without changing its engineering meaning. +_Avoid_: Methodology wiki, prompt library, list of diagrams, LLM best practices + +**Method Implementation**: A replaceable built-in or external implementation of an Engineering Method that declares compatibility, provenance, effects, and output conformance while leaving method selection and result authority with Pipeline. +_Avoid_: Method definition, hardcoded dependency, trusted tool by name + +**Open-Source Reuse Ladder**: The ordered preference for adopting an open-source capability through invoke, adapt, depend, vendor, fork, port, or reimplementation, choosing the lowest ownership burden that satisfies the required trust and capability boundary. +_Avoid_: Copy first, dependency avoidance, default fork, language rewrite + +**Reuse Record**: The provenance-bearing decision record for one reused capability, including source revision, license obligations, adoption mode, compatibility, maintenance and security evidence, owned modifications, and upgrade or exit strategy. +_Avoid_: Dependency entry, bookmark, attribution-only note + +**Internalization Standard**: The tool-neutral governance lifecycle by which an external project, method, or idea becomes a Design Reference, Evidence Provider, Method Implementation, adapted capability, or selectively owned implementation. Every case uses the same evidence, adoption, contract, conformance, measurement, update, and retirement rules; OpenSpec, spec-kit, and creators may implement the lifecycle but never define its meaning. +_Avoid_: One policy per tool, OpenSpec-owned semantics, copy-and-document workflow, integration-specific standards + +**Advisory Atom**: An independently executable capability that consumes Engineering Facts and Derived Engineering Models and returns a non-authoritative recommendation with evidence, confidence, and alternatives. It cannot create an Adoption Decision or Committed Engineering Plan. +_Avoid_: Policy gate, automatic installer, project authority, LLM opinion without evidence + +**Workflow Recommender**: The tool-neutral Advisory Atom that evaluates project evidence and recommends applicable delivery, quality, specification, or engineering-method workflows. OpenSpec, spec-kit, gstack, and creator tools are candidates it may recommend, not dependencies embedded in the scanner core. +_Avoid_: OpenSpec detector, workflow installer, main runner logic, hardcoded preferred stack + +**Engineering Fact**: An authoritative observation captured from a named evidence source, such as repository state, test output, CI state, runtime telemetry, or an approved project record. It carries provenance and is not created by interpretation alone. +_Avoid_: LLM conclusion, inferred opinion, undocumented assumption + +**Derived Engineering Model**: A reproducible model computed from Engineering Facts, such as a dependency graph, critical path, traceability matrix, compatibility result, or risk metric. The same inputs and toolchain must produce the same model. +_Avoid_: Generated narrative, planning opinion, manually maintained diagram + +**Engineering Plan Proposal**: A non-authoritative candidate ordering of engineering work, milestones, trade-offs, and verification conditions derived from Engineering Facts and Derived Engineering Models. Rules, tools, operators, or an LLM may propose it, but it creates no delivery commitment. +_Avoid_: Project commitment, approved roadmap, generated backlog as fact + +**Committed Engineering Plan**: An explicitly approved engineering plan with accountable ownership, accepted priorities, dependencies, and verifiable completion conditions. It may originate from an Engineering Plan Proposal, but approval is the boundary that makes it authoritative. +_Avoid_: LLM plan, draft roadmap, inferred priority + **Scanner**: The authoritative producer of a new artifact run for a target repository. The scanner gathers current evidence and writes the reports that other components read. _Avoid_: PowerShell runner, Rust scanner, artifact reader @@ -73,6 +160,12 @@ _Avoid_: Scanner, producer, runner **Code Evidence Layer**: Optional post-scan evidence layer that produces measurable code-structure artifacts such as symbols, chunks, relationships, and metrics. It supplements an Artifact Run but does not own scanner success semantics. _Avoid_: Scanner, AST platform, semantic search tool +**CodeNexus Evidence Provider**: The independent code-and-Git intelligence provider that owns structural perception, indexing, retrieval, and impact relationships for source repositories. Code Intel Pipeline consumes its provenance-bearing evidence without owning its storage, runtime, or internal graph implementation. +_Avoid_: Pipeline module, shared database, embedded scanner, Pipeline source dependency + +**Survival Scanner**: The smallest built-in repository scanner needed to identify a Target Repository, capture basic evidence, and diagnose unavailable Evidence Providers. It preserves Pipeline operability but does not compete with a specialized provider such as CodeNexus. +_Avoid_: Full CodeNexus replacement, duplicate intelligence platform, preferred scanner + **Agent Code Slice**: Curated, task-oriented view of Code Evidence Layer artifacts for Agent consumption. It points into full evidence dumps instead of replacing them. _Avoid_: Full AST dump, summary, report diff --git a/Cargo.lock b/Cargo.lock index ef64889..945cdf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "code-intel" -version = "0.3.0-beta.1" +version = "0.3.0" dependencies = [ "serde_json", ] diff --git a/Install-MultiAgentMergeQueue.ps1 b/Install-MultiAgentMergeQueue.ps1 new file mode 100644 index 0000000..91c74ac --- /dev/null +++ b/Install-MultiAgentMergeQueue.ps1 @@ -0,0 +1,62 @@ +#requires -Version 7.2 + +param( + [string]$RepoPath = $PSScriptRoot, + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$repo = [System.IO.Path]::GetFullPath($RepoPath) +$adapter = Join-Path $repo "Invoke-MultiAgentMergeQueue.ps1" +$hook = Join-Path $repo ".githooks\pre-push" +$lockFile = Join-Path $repo "package-lock.json" + +foreach ($required in @($adapter, $hook, $lockFile)) { + if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { + throw "Merge queue activation requires repository file: $required" + } +} + +& git -C $repo rev-parse --is-inside-work-tree *> $null +if ($LASTEXITCODE -ne 0) { throw "Not a Git repository: $repo" } + +$npmName = if ($IsWindows) { "npm.cmd" } else { "npm" } +$npm = Get-Command $npmName -CommandType Application -ErrorAction Stop | Select-Object -First 1 +Push-Location $repo +try { + & $npm.Source ci --ignore-scripts --no-audit --no-fund + if ($LASTEXITCODE -ne 0) { throw "npm ci failed with exit code $LASTEXITCODE" } +} finally { + Pop-Location +} + +$localHooksPath = @(& git -C $repo config --local --get core.hooksPath 2>$null) | Select-Object -Last 1 +if ([string]::IsNullOrWhiteSpace($localHooksPath) -or $localHooksPath -ne ".githooks") { + $effectiveHooksPath = @(& git -C $repo config --get core.hooksPath 2>$null) | Select-Object -Last 1 + if (-not [string]::IsNullOrWhiteSpace($effectiveHooksPath) -and $effectiveHooksPath -ne ".githooks") { + & git -C $repo config --local codeIntel.mergeQueue.previousHooksPath $effectiveHooksPath + if ($LASTEXITCODE -ne 0) { throw "Could not preserve the previous hooks path." } + } + & git -C $repo config --local core.hooksPath .githooks + if ($LASTEXITCODE -ne 0) { throw "Could not activate the repository hooks path." } +} + +if (-not $IsWindows) { + & chmod +x $hook + if ($LASTEXITCODE -ne 0) { throw "Could not make the pre-push hook executable." } +} + +$statusJson = @(& pwsh -NoProfile -File $adapter -Action validate -RepoPath $repo -Json 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0) { throw "Merge queue validation failed: $statusJson" } +$status = $statusJson | ConvertFrom-Json +if (-not [bool]$status.ready) { throw "Merge queue activation did not reach readiness." } + +if ($Json) { + $status | ConvertTo-Json -Depth 10 +} else { + Write-Host "Multi-Agent Merge Queue activated: readiness $(@($status.gates | Where-Object passed).Count)/$(@($status.gates).Count)" + Write-Host "Provider: $($status.command)" + Write-Host "Hooks: .githooks (previous path preserved in local Git config when present)" +} diff --git a/Invoke-CodeIntelAcceptance.ps1 b/Invoke-CodeIntelAcceptance.ps1 new file mode 100644 index 0000000..35e2886 --- /dev/null +++ b/Invoke-CodeIntelAcceptance.ps1 @@ -0,0 +1,299 @@ +#requires -Version 7.2 + +param( + [Parameter(Mandatory)] + [ValidateSet("agent", "land", "promote")] + [string]$Stage, + + [string[]]$TargetCheckJson = @(), + + [string]$SkipTargetedChecksReason = "", + + [string]$Policy = (Join-Path $PSScriptRoot "orchestration\code-intel-acceptance-policy.v1.json"), + + [string]$ProjectConformanceScript = (Join-Path $PSScriptRoot "Test-CodeIntelProjectConformance.ps1"), + + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$root = [System.IO.Path]::GetFullPath($PSScriptRoot) +$profile = if ($Stage -eq "promote") { "full" } else { "fast" } +$checks = [System.Collections.Generic.List[object]]::new() +$validatedTargets = [System.Collections.Generic.List[object]]::new() + +function Test-ExactSet { + param([object[]]$Actual, [object[]]$Expected) + $actualItems = @($Actual | ForEach-Object { [string]$_ } | Sort-Object) + $expectedItems = @($Expected | ForEach-Object { [string]$_ } | Sort-Object) + return $actualItems.Count -eq $expectedItems.Count -and @(Compare-Object $actualItems $expectedItems).Count -eq 0 +} + +function Resolve-RepoBoundFile { + param([string]$Path, [string]$Purpose) + + if ([string]::IsNullOrWhiteSpace($Path)) { + throw "$Purpose path is required" + } + $candidate = if ([System.IO.Path]::IsPathRooted($Path)) { $Path } else { Join-Path $root $Path } + $resolved = [System.IO.Path]::GetFullPath($candidate) + $prefix = [System.IO.Path]::TrimEndingDirectorySeparator($root) + [System.IO.Path]::DirectorySeparatorChar + if (-not $resolved.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "$Purpose path escapes repository: $Path" + } + if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { + throw "$Purpose file is missing: $Path" + } + return $resolved +} + +function Add-Check { + param( + [string]$Id, + [ValidateSet("targeted", "project-conformance")] + [string]$Kind, + [ValidateSet("pass", "fail", "blocked", "skipped-with-reason")] + [string]$Status, + [AllowNull()][Nullable[int]]$ExitCode, + [string]$Reason, + [string]$OutputTail = "" + ) + + if ($OutputTail.Length -gt 4000) { + $OutputTail = $OutputTail.Substring($OutputTail.Length - 4000) + } + $checks.Add([pscustomobject][ordered]@{ + id = $Id + kind = $Kind + status = $Status + exitCode = $ExitCode + reason = $Reason + outputTail = $OutputTail + }) +} + +function Complete-Result { + $status = if (@($checks | Where-Object status -eq "blocked").Count -gt 0) { + "blocked" + } elseif (@($checks | Where-Object status -eq "fail").Count -gt 0) { + "fail" + } elseif (@($checks | Where-Object status -eq "skipped-with-reason").Count -gt 0) { + "skipped-with-reason" + } else { + "pass" + } + $nonPassing = @($checks | Where-Object status -ne "pass") + $reason = if ($nonPassing.Count -eq 0) { + "all required acceptance checks passed" + } else { + ($nonPassing | ForEach-Object { "$($_.id): $($_.reason)" }) -join "; " + } + $result = [ordered]@{ + schema = "code-intel-acceptance-result.v1" + stage = $Stage + profile = $profile + status = $status + reason = $reason + checks = @($checks) + } + + if ($Json) { + $result | ConvertTo-Json -Depth 12 + } else { + Write-Host "Code Intel acceptance: $status stage=$Stage profile=$profile" + foreach ($check in $checks) { + Write-Host "$($check.status.ToUpperInvariant()) $($check.kind)/$($check.id): $($check.reason)" + } + } + + switch ($status) { + "pass" { exit 0 } + "fail" { exit 1 } + "blocked" { exit 2 } + "skipped-with-reason" { exit 3 } + } +} + +function Test-PolicyContract { + param([object]$Document) + + try { + $stageNames = @($Document.stages.PSObject.Properties.Name) + $allowedExecutables = @($Document.targetedChecks.allowedProcessExecutables | ForEach-Object { [string]$_ }) + $intrinsicAllowed = @("cargo", "dotnet", "go", "node", "npm", "pnpm", "python", "python3", "pytest", "yarn") + $prohibitedShells = @("bash", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe", "sh", "wsl", "zsh") + return [string]$Document.schema -eq "code-intel-acceptance-policy.v1" -and + (Test-ExactSet @($Document.statuses) @("pass", "fail", "blocked", "skipped-with-reason")) -and + (Test-ExactSet $stageNames @("agent", "land", "promote")) -and + [string]$Document.stages.agent.projectProfile -eq "fast" -and + [string]$Document.stages.agent.targetedChecks -eq "required" -and + [string]$Document.stages.land.projectProfile -eq "fast" -and + [string]$Document.stages.land.targetedChecks -eq "forbidden" -and + [string]$Document.stages.promote.projectProfile -eq "full" -and + [string]$Document.stages.promote.targetedChecks -eq "forbidden" -and + [int]$Document.targetedChecks.maximumCount -ge 1 -and + [int]$Document.targetedChecks.maximumCount -le 16 -and + (Test-ExactSet @($Document.targetedChecks.allowedKinds) @("pwsh", "process")) -and + @($allowedExecutables | Where-Object { $_ -notin $intrinsicAllowed }).Count -eq 0 -and + @($allowedExecutables | Where-Object { $_ -in $prohibitedShells }).Count -eq 0 -and + [string]$Document.targetedChecks.forbiddenShellTokenPattern -eq '[;&|<>`]|\$\(|[\r\n]' + } catch { + return $false + } +} + +function ConvertTo-ValidatedTarget { + param([string]$Raw, [object]$PolicyDocument) + + try { + $document = $Raw | ConvertFrom-Json -Depth 12 + } catch { + throw "targeted check JSON is malformed: $($_.Exception.Message)" + } + if ($null -eq $document -or $document -is [array]) { + throw "each targeted check must be one JSON object" + } + + $id = [string]$document.id + $kind = [string]$document.kind + if ($id -notmatch '^[a-z0-9][a-z0-9._-]{0,63}$') { + throw "targeted check id is invalid: $id" + } + if ($kind -notin @($PolicyDocument.targetedChecks.allowedKinds)) { + throw "targeted check kind is not allowed: $kind" + } + + $allowedProperties = if ($kind -eq "pwsh") { @("id", "kind", "file", "args") } else { @("id", "kind", "executable", "args") } + $actualProperties = @($document.PSObject.Properties.Name) + if (@($actualProperties | Where-Object { $_ -notin $allowedProperties }).Count -gt 0) { + throw "targeted check $id contains unsupported properties" + } + + $arguments = @($document.args | ForEach-Object { [string]$_ }) + $shellPattern = [string]$PolicyDocument.targetedChecks.forbiddenShellTokenPattern + foreach ($argument in $arguments) { + if ($argument -match $shellPattern -or $argument.IndexOf([char]0) -ge 0) { + throw "targeted check $id contains forbidden shell syntax" + } + } + + if ($kind -eq "pwsh") { + $file = Resolve-RepoBoundFile -Path ([string]$document.file) -Purpose "targeted PowerShell check" + return [pscustomobject]@{ id = $id; kind = $kind; file = $file; executable = ""; args = $arguments } + } + + $executable = [string]$document.executable + if ([string]::IsNullOrWhiteSpace($executable) -or $executable -match '[/\\:]') { + throw "targeted check $id process executable must be an allowed command name" + } + if ($executable -notin @($PolicyDocument.targetedChecks.allowedProcessExecutables)) { + throw "targeted check $id process executable is not allowed: $executable" + } + + $intrinsicForbiddenLeading = @{ + node = @("-e", "--eval", "-p", "--print") + npm = @("exec", "x") + pnpm = @("dlx", "exec") + python = @("-c") + python3 = @("-c") + yarn = @("dlx", "exec") + } + if ($arguments.Count -gt 0 -and $intrinsicForbiddenLeading.ContainsKey($executable) -and + $arguments[0] -in @($intrinsicForbiddenLeading[$executable])) { + throw "targeted check $id uses a forbidden inline-execution argument" + } + return [pscustomobject]@{ id = $id; kind = $kind; file = ""; executable = $executable; args = $arguments } +} + +try { + $policyPath = Resolve-RepoBoundFile -Path $Policy -Purpose "acceptance policy" + $policyDocument = Get-Content -Raw -LiteralPath $policyPath | ConvertFrom-Json -Depth 30 +} catch { + Add-Check -Id "request" -Kind "project-conformance" -Status "blocked" -ExitCode $null -Reason $_.Exception.Message + Complete-Result +} + +if (-not (Test-PolicyContract -Document $policyDocument)) { + Add-Check -Id "policy-contract" -Kind "project-conformance" -Status "blocked" -ExitCode $null -Reason "acceptance policy is malformed or weakens the fixed stage contract" + Complete-Result +} +$profile = [string]$policyDocument.stages.PSObject.Properties[$Stage].Value.projectProfile + +$hasTargets = @($TargetCheckJson).Count -gt 0 +$hasSkipReason = -not [string]::IsNullOrWhiteSpace($SkipTargetedChecksReason) +if ($Stage -eq "agent") { + if ($hasTargets -and $hasSkipReason) { + Add-Check -Id "targeted-checks" -Kind "targeted" -Status "blocked" -ExitCode $null -Reason "targeted checks and an explicit skip reason are mutually exclusive" + Complete-Result + } + if (-not $hasTargets -and -not $hasSkipReason) { + Add-Check -Id "targeted-checks" -Kind "targeted" -Status "blocked" -ExitCode $null -Reason "agent acceptance requires explicit targeted checks or an explicit skip reason" + Complete-Result + } +} elseif ($hasTargets -or $hasSkipReason) { + Add-Check -Id "targeted-checks" -Kind "targeted" -Status "blocked" -ExitCode $null -Reason "$Stage acceptance forbids targeted-check overrides" + Complete-Result +} + +if (@($TargetCheckJson).Count -gt [int]$policyDocument.targetedChecks.maximumCount) { + Add-Check -Id "targeted-checks" -Kind "targeted" -Status "blocked" -ExitCode $null -Reason "targeted check count exceeds policy maximum" + Complete-Result +} + +try { + foreach ($raw in @($TargetCheckJson)) { + $validatedTargets.Add((ConvertTo-ValidatedTarget -Raw $raw -PolicyDocument $policyDocument)) + } + $ids = @($validatedTargets | ForEach-Object id) + if (@($ids | Sort-Object -Unique).Count -ne $ids.Count) { + throw "targeted check ids must be unique" + } + $conformancePath = Resolve-RepoBoundFile -Path $ProjectConformanceScript -Purpose "project conformance" +} catch { + Add-Check -Id "targeted-checks" -Kind "targeted" -Status "blocked" -ExitCode $null -Reason $_.Exception.Message + Complete-Result +} + +if ($hasSkipReason) { + Add-Check -Id "targeted-checks" -Kind "targeted" -Status "skipped-with-reason" -ExitCode $null -Reason $SkipTargetedChecksReason +} else { + foreach ($target in $validatedTargets) { + try { + $output = if ($target.kind -eq "pwsh") { + @(& pwsh -NoProfile -File $target.file @($target.args) 2>&1 | ForEach-Object { $_.ToString() }) + } else { + @(& $target.executable @($target.args) 2>&1 | ForEach-Object { $_.ToString() }) + } + $exitCode = $LASTEXITCODE + $targetStatus = if ($exitCode -eq 0) { "pass" } else { "fail" } + $targetReason = if ($exitCode -eq 0) { "targeted check passed" } else { "targeted check exited nonzero" } + Add-Check -Id $target.id -Kind "targeted" -Status $targetStatus -ExitCode $exitCode -Reason $targetReason -OutputTail ($output -join "`n") + } catch { + Add-Check -Id $target.id -Kind "targeted" -Status "blocked" -ExitCode $null -Reason $_.Exception.Message + } + } +} + +try { + $projectOutput = @(& pwsh -NoProfile -File $conformancePath -Profile $profile -Json 2>&1 | ForEach-Object { $_.ToString() }) + $projectExitCode = $LASTEXITCODE + $projectText = $projectOutput -join "`n" + $projectResult = $projectText | ConvertFrom-Json -Depth 30 + $projectVerdict = [string]$projectResult.verdict + $projectProfile = [string]$projectResult.profile + if ($projectProfile -ne $profile -or $projectVerdict -notin @("pass", "fail")) { + throw "project conformance returned a mismatched or malformed result" + } + if ($projectExitCode -eq 0 -and $projectVerdict -eq "pass") { + Add-Check -Id "project-$profile" -Kind "project-conformance" -Status "pass" -ExitCode $projectExitCode -Reason "$profile project conformance passed" -OutputTail $projectText + } elseif ($projectExitCode -eq 1 -and $projectVerdict -eq "fail") { + Add-Check -Id "project-$profile" -Kind "project-conformance" -Status "fail" -ExitCode $projectExitCode -Reason "$profile project conformance failed" -OutputTail $projectText + } else { + Add-Check -Id "project-$profile" -Kind "project-conformance" -Status "blocked" -ExitCode $projectExitCode -Reason "project conformance exit code and verdict disagree" -OutputTail $projectText + } +} catch { + Add-Check -Id "project-$profile" -Kind "project-conformance" -Status "blocked" -ExitCode $null -Reason $_.Exception.Message +} + +Complete-Result diff --git a/Invoke-CodeIntelAutomaticPullRequest.ps1 b/Invoke-CodeIntelAutomaticPullRequest.ps1 new file mode 100644 index 0000000..e79b45d --- /dev/null +++ b/Invoke-CodeIntelAutomaticPullRequest.ps1 @@ -0,0 +1,8 @@ +#requires -Version 7.2 + +$implementation = Join-Path (Join-Path $PSScriptRoot "tools") "Invoke-CodeIntelAutomaticPullRequest.ps1" +if (-not (Test-Path -LiteralPath $implementation -PathType Leaf)) { + throw "Automatic PR implementation is missing: $implementation" +} +& $implementation @args +exit $LASTEXITCODE diff --git a/Invoke-CodeIntelAutomaticPullRequestFlow.ps1 b/Invoke-CodeIntelAutomaticPullRequestFlow.ps1 new file mode 100644 index 0000000..8c6c17c --- /dev/null +++ b/Invoke-CodeIntelAutomaticPullRequestFlow.ps1 @@ -0,0 +1,8 @@ +#requires -Version 7.2 + +$implementation = Join-Path (Join-Path $PSScriptRoot "tools") "Invoke-CodeIntelAutomaticPullRequestFlow.ps1" +if (-not (Test-Path -LiteralPath $implementation -PathType Leaf)) { + throw "Automatic PR flow implementation is missing: $implementation" +} +& $implementation @args +exit $LASTEXITCODE diff --git a/Invoke-CodeNexusLite.ps1 b/Invoke-CodeNexusLite.ps1 index 46af83d..f627e26 100644 --- a/Invoke-CodeNexusLite.ps1 +++ b/Invoke-CodeNexusLite.ps1 @@ -12,6 +12,13 @@ param( [int]$MaxFiles = 8, [int]$MaxReferencesPerFile = 12, [int]$MaxCommitsPerFile = 0, +[string]$AdapterRequestPath = "", +[string]$ExpectedSnapshotIdentity = "", +[string]$SourceSnapshotIdentity = "", +[string]$SourceRevision = "", +[long]$ObservedAt = 0, +[ValidateSet("explicit_fallback", "legacy_rollback")] +[string]$AdapterActivation = "explicit_fallback", [switch]$Quiet ) @@ -89,6 +96,17 @@ function Invoke-TextCommand { } } +function Test-CodeNexusGeneratedPath { + param([string]$Path) + + if ([string]::IsNullOrWhiteSpace($Path)) { return $false } + $normalized = $Path.Replace('\', '/') + while ($normalized.StartsWith('./', [System.StringComparison]::Ordinal)) { + $normalized = $normalized.Substring(2) + } + return $normalized -match '(?i)(^|/)(work|artifact|artifacts|staging|\.code-intel|\.git|node_modules|target|dist|build|\.venv|__pycache__)(/|$)' +} + function Select-HotspotFiles { param( [string]$RepoPath, @@ -102,9 +120,10 @@ function Select-HotspotFiles { $items = New-Object System.Collections.Generic.List[object] if ($null -ne $Hotspots -and $null -ne $Hotspots.files) { - foreach ($file in @($Hotspots.files | Select-Object -First $MaxFiles)) { + foreach ($file in @($Hotspots.files)) { + if ($items.Count -ge $MaxFiles) { break } $path = [string]$file.path - if ([string]::IsNullOrWhiteSpace($path) -or $seen.ContainsKey($path)) { continue } + if ([string]::IsNullOrWhiteSpace($path) -or (Test-CodeNexusGeneratedPath $path) -or $seen.ContainsKey($path)) { continue } $seen[$path] = $true $items.Add([pscustomobject][ordered]@{ path = $path @@ -121,7 +140,7 @@ function Select-HotspotFiles { foreach ($path in @($module.files)) { if ($items.Count -ge $MaxFiles) { break } $pathText = [string]$path - if ([string]::IsNullOrWhiteSpace($pathText) -or $seen.ContainsKey($pathText)) { continue } + if ([string]::IsNullOrWhiteSpace($pathText) -or (Test-CodeNexusGeneratedPath $pathText) -or $seen.ContainsKey($pathText)) { continue } $seen[$pathText] = $true $items.Add([pscustomobject][ordered]@{ path = $pathText @@ -139,8 +158,9 @@ function Select-HotspotFiles { $root = if ([string]::IsNullOrWhiteSpace($TargetPath)) { $RepoPath } else { $TargetPath } $fallbackFiles = Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue | Where-Object { + $relativePath = Get-RelativePathSafe $RepoPath $_.FullName $_.Length -le 1048576 -and - $_.FullName -notmatch "[\\/](\.git|node_modules|target|dist|build|\.venv|__pycache__)[\\/]" -and + -not (Test-CodeNexusGeneratedPath $relativePath) -and $_.Extension.ToLowerInvariant() -in @(".ps1", ".py", ".rs", ".go", ".ts", ".tsx", ".js", ".jsx", ".java", ".cs") } | Sort-Object Length -Descending | @@ -192,7 +212,22 @@ function Get-References { return ,@() } - $lines = Invoke-TextCommand { rg -n -m $Limit --hidden -g "!**/.git/**" -g "!**/node_modules/**" -g "!**/target/**" -g "!**/dist/**" -g "!**/build/**" --fixed-strings $stem $RepoPath } + $lines = Invoke-TextCommand { + rg -n -m $Limit --hidden ` + -g "!**/work/**" ` + -g "!**/artifact/**" ` + -g "!**/artifacts/**" ` + -g "!**/staging/**" ` + -g "!**/.code-intel/**" ` + -g "!**/.git/**" ` + -g "!**/node_modules/**" ` + -g "!**/target/**" ` + -g "!**/dist/**" ` + -g "!**/build/**" ` + -g "!**/.venv/**" ` + -g "!**/__pycache__/**" ` + --fixed-strings $stem $RepoPath + } return ,@($lines | Select-Object -First $Limit) } @@ -289,6 +324,87 @@ $payload = [ordered]@{ } $payload | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 + +if (-not [string]::IsNullOrWhiteSpace($AdapterRequestPath)) { + foreach ($identity in @($ExpectedSnapshotIdentity, $SourceSnapshotIdentity)) { + if ($identity -notmatch '^[0-9a-f]{64}$') { + throw "CodeNexus adapter snapshot identities must be lowercase SHA-256 values" + } + } + if ([string]::IsNullOrWhiteSpace($SourceRevision)) { + throw "CodeNexus adapter source revision is required" + } + if ($ObservedAt -le 0) { + $ObservedAt = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + } + $evidencePayloadPath = Join-Path $RunDir "codenexus-evidence-payload.json" + $evidencePayload = [ordered]@{ + schema = "code-intel-evidence-payload.v1" + data = [ordered]@{ + codenexus = [ordered]@{ + schema = "code-intel-codenexus-evidence.v1" + snapshotIdentity = $SourceSnapshotIdentity + provider = [ordered]@{ + mode = "lite" + providerId = "codenexus.lite-compat" + implementationId = "invoke-codenexus-lite.ps1" + activation = $AdapterActivation + } + provenance = [ordered]@{ + sourceRevision = $SourceRevision + observedAt = $ObservedAt + } + completeness = "complete" + availability = "available" + providerData = $payload + } + } + } + [System.IO.File]::WriteAllText( + $evidencePayloadPath, + ($evidencePayload | ConvertTo-Json -Depth 20 -Compress), + [System.Text.UTF8Encoding]::new($false) + ) + $payloadDigest = (Get-FileHash -LiteralPath $evidencePayloadPath -Algorithm SHA256).Hash.ToLowerInvariant() + $implementationDigest = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant() + $artifactRelativePath = [System.IO.Path]::GetRelativePath($RunDir, $evidencePayloadPath).Replace('\', '/') + $adapterRequest = [ordered]@{ + schema = "code-intel-codenexus-native-result.v1" + providerMode = "lite" + status = "current" + providerId = "codenexus.lite-compat" + implementation = [ordered]@{ + id = "invoke-codenexus-lite.ps1" + version = "compat-v1" + digest = $implementationDigest + } + sourceRevision = $SourceRevision + expectedSnapshotIdentity = $ExpectedSnapshotIdentity + sourceSnapshotIdentity = $SourceSnapshotIdentity + collectedAt = $ObservedAt + observedAt = $ObservedAt + payload = [ordered]@{ + schema = "code-intel-artifact-ref.v1" + artifactSchema = "code-intel-evidence-payload.v1" + type = "observed.evidence.payload" + path = $artifactRelativePath + sha256 = $payloadDigest + consumedSnapshotIdentity = $SourceSnapshotIdentity + } + activation = $AdapterActivation + effects = @( + "read_repository", + "read_git_history", + "read_sentrux_artifacts", + "write_compatibility_artifact" + ) + } + [System.IO.File]::WriteAllText( + $AdapterRequestPath, + ($adapterRequest | ConvertTo-Json -Depth 20 -Compress), + [System.Text.UTF8Encoding]::new($false) + ) +} if (-not $Quiet) { $payload | ConvertTo-Json -Depth 8 } diff --git a/Invoke-CompatibilityFacadeFinalize.ps1 b/Invoke-CompatibilityFacadeFinalize.ps1 new file mode 100644 index 0000000..d6637dc --- /dev/null +++ b/Invoke-CompatibilityFacadeFinalize.ps1 @@ -0,0 +1,226 @@ +#requires -Version 7.2 + +[CmdletBinding()] +param( + [string]$RepoRoot = $PSScriptRoot, + [string]$PolicyPath = "orchestration/facade-finalize-policy.v1.json", + [string]$FixtureRoot = "orchestration/facade-finalize-fixtures", + [long]$EvaluatedAt = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds(), + [string]$OutFile = "", + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Resolve-RepoFile { + param([string]$Path) + if ([System.IO.Path]::IsPathRooted($Path)) { return $Path } + return Join-Path $RepoRoot ($Path -replace '/', [System.IO.Path]::DirectorySeparatorChar) +} + +function Read-JsonFile { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null } + return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json +} + +function Assert-ExactProperties { + param([object]$Value, [string[]]$Names, [string]$Context) + if ($null -eq $Value) { throw "$Context is missing" } + $actual = @($Value.PSObject.Properties.Name | Sort-Object) + $expected = @($Names | Sort-Object) + if (($actual -join "`n") -ne ($expected -join "`n")) { + throw "$Context must have exactly [$($expected -join ', ')], got [$($actual -join ', ')]" + } +} + +function Add-Unsupported { + param( + [System.Collections.Generic.List[object]]$List, + [string]$Code, + [string]$Subject, + [string]$Dependency, + [string]$Detail + ) + $List.Add([pscustomobject][ordered]@{ + code = $Code + subject = $Subject + dependency = $Dependency + detail = $Detail + }) +} + +if ($EvaluatedAt -le 0) { throw "EvaluatedAt must be a positive Unix timestamp" } +$RepoRoot = (Get-Item -LiteralPath $RepoRoot -ErrorAction Stop).FullName +$policy = Read-JsonFile (Resolve-RepoFile $PolicyPath) +Assert-ExactProperties $policy @("schema", "ticketId", "dependencyTickets", "retainedPowerShell", "modeMatrix", "parity") "policy" +if ([string]$policy.schema -ne "code-intel-compatibility-facade-finalize-policy.v1" -or [string]$policy.ticketId -ne "E06") { + throw "policy identity mismatch" +} +Assert-ExactProperties $policy.parity @("a00Evidence", "rollbackTickets") "policy parity" + +$unsupported = [System.Collections.Generic.List[object]]::new() +$dependencies = [System.Collections.Generic.List[object]]::new() +$ticketIds = @($policy.dependencyTickets | ForEach-Object { [string]$_.ticketId }) +if ($ticketIds.Count -ne 8 -or @($ticketIds | Sort-Object -Unique).Count -ne $ticketIds.Count) { + throw "E06 policy must contain exactly eight unique dependency tickets" +} + +foreach ($ticket in @($policy.dependencyTickets)) { + Assert-ExactProperties $ticket @("ticketId", "directory", "branchId", "path") "dependency ticket" + $ticketId = [string]$ticket.ticketId + $packetRoot = Resolve-RepoFile ([string]$ticket.directory) + $statusPath = Join-Path $packetRoot "status.json" + $manifestPath = Join-Path $packetRoot "compatibility-retirement-manifest.json" + $status = Read-JsonFile $statusPath + $manifest = Read-JsonFile $manifestPath + $packetBlockers = @() + $observedDecision = "missing" + $retired = $false + $deletionExecuted = $false + $owner = "" + + if ($null -eq $status -or $null -eq $manifest) { + Add-Unsupported $unsupported "retirement_packet_missing" ([string]$ticket.branchId) $ticketId "status.json or compatibility-retirement-manifest.json is missing" + } + else { + $observedDecision = [string]$status.decision + $retired = [bool]$status.retired + $deletionExecuted = [bool]$status.deletionExecuted + $packetBlockers = @($status.blockers | ForEach-Object { [string]$_ }) + $owner = [string]$manifest.approvalSubject.legacyBranch.owner + if ([string]$manifest.approvalSubject.legacyBranch.branchId -ne [string]$ticket.branchId) { + Add-Unsupported $unsupported "retirement_subject_mismatch" ([string]$ticket.branchId) $ticketId "packet branchId does not match the frozen E06 subject" + } + if ($observedDecision -ne "approved" -or -not $retired -or -not $deletionExecuted) { + $detail = if ($packetBlockers.Count -gt 0) { $packetBlockers -join ',' } else { "decision=$observedDecision;retired=$retired;deletionExecuted=$deletionExecuted" } + Add-Unsupported $unsupported "retirement_not_completed" ([string]$ticket.branchId) $ticketId $detail + } + } + + $parityOutcomes = [ordered]@{} + foreach ($name in @("golden-parity", "contract-parity", "effect-parity", "rollback-execution", "compatibility-window")) { + $evidence = Read-JsonFile (Join-Path $packetRoot "evidence/$name.json") + $outcome = if ($null -eq $evidence) { "missing" } else { [string]$evidence.details.outcome } + $parityOutcomes[$name] = $outcome + if ($outcome -ne "passed") { + $code = if ($name -eq "rollback-execution") { "rollback_window_unproven" } elseif ($name -eq "compatibility-window") { "compatibility_window_unproven" } else { "a00_parity_unproven" } + Add-Unsupported $unsupported $code ([string]$ticket.branchId) $ticketId "$name outcome=$outcome" + } + } + + $dependencies.Add([pscustomobject][ordered]@{ + ticketId = $ticketId + branchId = [string]$ticket.branchId + path = [string]$ticket.path + packetRoot = ([string]$ticket.directory -replace '\\', '/') + owner = $owner + decision = $observedDecision + retired = $retired + deletionExecuted = $deletionExecuted + blockers = @($packetBlockers) + evidenceOutcomes = [pscustomobject]$parityOutcomes + }) +} + +$registry = Read-JsonFile (Resolve-RepoFile "orchestration/integrations.json") +if ($null -eq $registry) { throw "orchestration/integrations.json is missing" } +$registered = @($registry.integrations | ForEach-Object { [string]$_.id }) +$retained = [System.Collections.Generic.List[object]]::new() +$surfaceIds = @($policy.retainedPowerShell | ForEach-Object { [string]$_.surfaceId }) +if ($surfaceIds.Count -ne 11 -or @($surfaceIds | Sort-Object -Unique).Count -ne $surfaceIds.Count) { + throw "E06 policy must contain exactly eleven unique retained PowerShell surfaces" +} +foreach ($surface in @($policy.retainedPowerShell)) { + Assert-ExactProperties $surface @("surfaceId", "path", "owner", "registryParticipantId", "expiresAt", "classification") "retained PowerShell surface" + $pathExists = Test-Path -LiteralPath (Resolve-RepoFile ([string]$surface.path)) -PathType Leaf + $registryBacked = $registered -contains [string]$surface.registryParticipantId + $ownerPresent = -not [string]::IsNullOrWhiteSpace([string]$surface.owner) + $expiry = if ($null -eq $surface.expiresAt) { $null } else { [long]$surface.expiresAt } + $expiryCurrent = $null -ne $expiry -and $expiry -gt $EvaluatedAt + if (-not $pathExists) { Add-Unsupported $unsupported "retained_surface_missing" ([string]$surface.surfaceId) "E06" ([string]$surface.path) } + if (-not $registryBacked) { Add-Unsupported $unsupported "retained_surface_unregistered" ([string]$surface.surfaceId) "B07" ([string]$surface.registryParticipantId) } + if (-not $ownerPresent) { Add-Unsupported $unsupported "retained_surface_owner_missing" ([string]$surface.surfaceId) "E06" ([string]$surface.path) } + if (-not $expiryCurrent) { Add-Unsupported $unsupported "retained_surface_expiry_missing_or_elapsed" ([string]$surface.surfaceId) "E06" "expiresAt must be greater than EvaluatedAt" } + $retained.Add([pscustomobject][ordered]@{ + surfaceId = [string]$surface.surfaceId + path = [string]$surface.path + classification = [string]$surface.classification + owner = [string]$surface.owner + registryParticipantId = [string]$surface.registryParticipantId + registryBacked = $registryBacked + expiresAt = $expiry + expiryCurrent = $expiryCurrent + }) +} + +$modes = [System.Collections.Generic.List[object]]::new() +foreach ($modePolicy in @($policy.modeMatrix)) { + Assert-ExactProperties $modePolicy @("mode", "fixture", "requiredCapabilities") "mode policy" + $mode = [string]$modePolicy.mode + $fixture = Read-JsonFile (Resolve-RepoFile (Join-Path $FixtureRoot ([string]$modePolicy.fixture))) + if ($null -ne $fixture) { + Assert-ExactProperties $fixture @("schema", "mode", "available", "reason", "nodes") "mode fixture" + if ([string]$fixture.schema -ne "code-intel-facade-mode-audit-fixture.v1" -or [string]$fixture.mode -ne $mode) { throw "mode fixture identity mismatch: $mode" } + } + $available = $null -ne $fixture -and [bool]$fixture.available + $reason = if ($null -eq $fixture) { "fixture missing" } else { [string]$fixture.reason } + $nodeResults = [System.Collections.Generic.List[object]]::new() + if (-not $available) { + Add-Unsupported $unsupported "mode_smoke_unavailable" $mode "E06" $reason + } + else { + foreach ($capability in @($modePolicy.requiredCapabilities)) { + $matches = @($fixture.nodes | Where-Object { [string]$_.capabilityId -eq [string]$capability }) + if ($matches.Count -ne 1) { + Add-Unsupported $unsupported "mode_node_missing" $mode ([string]$capability) "required node must appear exactly once" + continue + } + $node = $matches[0] + Assert-ExactProperties $node @("capabilityId", "registryBacked", "enveloped", "admission", "committed", "indexed") "mode node" + $nodeOk = [bool]$node.registryBacked -and [bool]$node.enveloped -and @("admitted", "not_applicable") -contains [string]$node.admission + if ($mode -ne "doctor" -and [string]$capability -eq "run.commit") { $nodeOk = $nodeOk -and [bool]$node.committed } + if ($mode -ne "doctor" -and [string]$capability -eq "artifact.index-committed-only") { $nodeOk = $nodeOk -and [bool]$node.indexed } + if (-not $nodeOk) { Add-Unsupported $unsupported "mode_node_contract_failed" $mode ([string]$capability) "registry/envelope/admission/commit/index contract failed" } + $nodeResults.Add($node) + } + } + $modes.Add([pscustomobject][ordered]@{ mode = $mode; available = $available; reason = $reason; nodes = @($nodeResults) }) +} + +$a00Path = Resolve-RepoFile ([string]$policy.parity.a00Evidence) +$a00Present = Test-Path -LiteralPath $a00Path -PathType Leaf +if (-not $a00Present) { Add-Unsupported $unsupported "a00_final_parity_missing" "public-mode-matrix" "A00" ([string]$policy.parity.a00Evidence) } +foreach ($ticketId in @($policy.parity.rollbackTickets)) { + if ($ticketIds -notcontains [string]$ticketId) { throw "unknown rollback ticket in policy: $ticketId" } +} +if ((@($policy.parity.rollbackTickets | ForEach-Object { [string]$_ }) -join ',') -ne ($ticketIds -join ',')) { + throw "E06 rollback ticket set/order must match dependency tickets" +} + +Add-Unsupported $unsupported "independent_approval_missing" "E06-final-manifest" "independent-verifier" "E06 implementer cannot sign final approval" +$sortedUnsupported = @($unsupported | Sort-Object code, subject, dependency, detail -Unique) +$result = [pscustomobject][ordered]@{ + schema = "code-intel-compatibility-facade-finalize.v1" + evaluatedAt = $EvaluatedAt + status = "blocked" + approvalEligible = $false + independentApproval = $null + dependencies = @($dependencies) + retainedPowerShell = @($retained) + modeMatrix = @($modes) + parity = [pscustomobject][ordered]@{ a00Evidence = [string]$policy.parity.a00Evidence; present = $a00Present } + rollbackWindows = @($policy.parity.rollbackTickets | ForEach-Object { [string]$_ }) + unsupportedBranches = $sortedUnsupported +} + +$serialized = $result | ConvertTo-Json -Depth 20 +if (-not [string]::IsNullOrWhiteSpace($OutFile)) { + $resolvedOut = if ([System.IO.Path]::IsPathRooted($OutFile)) { $OutFile } else { Join-Path $RepoRoot $OutFile } + $parent = Split-Path -Parent $resolvedOut + if (-not [string]::IsNullOrWhiteSpace($parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } + [System.IO.File]::WriteAllText($resolvedOut, $serialized + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false)) +} +if ($Json) { $serialized } else { $result } +if ($sortedUnsupported.Count -gt 0) { exit 2 } diff --git a/Invoke-CompeteProjectScore.ps1 b/Invoke-CompeteProjectScore.ps1 new file mode 100644 index 0000000..74c0d86 --- /dev/null +++ b/Invoke-CompeteProjectScore.ps1 @@ -0,0 +1,110 @@ +#requires -Version 7.2 + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateSet("prepare", "score")] + [string]$Action, + [Parameter(Mandatory = $true)] + [string]$RepoPath, + [Parameter(Mandatory = $true)] + [string]$ArtifactRoot, + [string]$CompeteRoot = "", + [string]$CompeteDataPath = "" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Resolve-CompeteRoot { + param([string]$RequestedRoot) + + $candidates = @( + $RequestedRoot, + $env:COMPETE_HOME, + (Join-Path $HOME ".claude/plugins/compete"), + (Join-Path $HOME ".claude/skills/compete") + ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + + foreach ($candidate in $candidates) { + $full = [IO.Path]::GetFullPath($candidate) + if (Test-Path -LiteralPath (Join-Path $full "skills/compete/scripts/build_report.py") -PathType Leaf) { return $full } + if (Test-Path -LiteralPath (Join-Path $full "scripts/build_report.py") -PathType Leaf) { return $full } + } + return $(if ([string]::IsNullOrWhiteSpace($RequestedRoot)) { "" } else { [IO.Path]::GetFullPath($RequestedRoot) }) +} + +function Get-CompeteScriptRoot { + param([string]$Root) + $pluginScripts = Join-Path $Root "skills/compete/scripts" + if (Test-Path -LiteralPath (Join-Path $pluginScripts "build_report.py") -PathType Leaf) { return $pluginScripts } + $skillScripts = Join-Path $Root "scripts" + if (Test-Path -LiteralPath (Join-Path $skillScripts "build_report.py") -PathType Leaf) { return $skillScripts } + throw "compete build_report.py is missing under: $Root" +} + +$repo = (Resolve-Path -LiteralPath $RepoPath -ErrorAction Stop).Path +$artifactDirectory = [IO.Path]::GetFullPath($ArtifactRoot) +New-Item -ItemType Directory -Force -Path $artifactDirectory | Out-Null +$resolvedCompeteRoot = Resolve-CompeteRoot -RequestedRoot $CompeteRoot + +if ($Action -eq "prepare") { + $dataPath = if ([string]::IsNullOrWhiteSpace($CompeteDataPath)) { Join-Path $artifactDirectory "compete-data" } else { [IO.Path]::GetFullPath($CompeteDataPath) } + $promptPath = Join-Path $artifactDirectory "competitive-intelligence-prompt.md" + $requestPath = Join-Path $artifactDirectory "competitive-intelligence-request.json" + $scorePath = Join-Path $artifactDirectory "competitive-score.json" + $adapterPath = $PSCommandPath + $competeLabel = if ([string]::IsNullOrWhiteSpace($resolvedCompeteRoot)) { "" } else { $resolvedCompeteRoot } + + $prompt = @" +# Competitive project score task + +Analyze $repo with the upstream compete skill at $competeLabel. + +- Keep every generated dataset and report under $dataPath; do not write generated files into the repository. +- Run product analysis against $repo, then perform competitor discovery and intelligence collection with web research. +- Preserve compete confidence/provenance fields and prefer unknown over guessing. +- Produce the normal compete JSON datasets and InsightKit report. +- When complete, normalize the score with: + +~~~powershell +& '$adapterPath' -Action score -RepoPath '$repo' -ArtifactRoot '$artifactDirectory' -CompeteRoot '$competeLabel' -CompeteDataPath '$dataPath' +~~~ + +The resulting score is advisory market/product intelligence. It must not change Code Intel structural gates or hospital discharge state. +"@ + [IO.File]::WriteAllText($promptPath, $prompt, [Text.UTF8Encoding]::new($false)) + + $request = [ordered]@{ + schema = "code-intel-competitive-intelligence-request.v1" + status = "prepared" + authority = "advisory" + repoPath = $repo + artifactRoot = $artifactDirectory + competeRoot = $(if ([string]::IsNullOrWhiteSpace($resolvedCompeteRoot)) { $null } else { $resolvedCompeteRoot }) + source = "https://github.com/lbj96347/compete" + prompt = $promptPath + expectedDataPath = $dataPath + scoreArtifact = $scorePath + dispatch = [ordered]@{ + coordinator = "orca" + available = $null -ne (Get-Command orca -ErrorAction SilentlyContinue) + instruction = "Open an Agent terminal for the target worktree and send the prompt file contents. Agent/web usage remains explicit and non-blocking." + } + } + [IO.File]::WriteAllText($requestPath, ($request | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false)) + $request | ConvertTo-Json -Depth 8 -Compress + exit 0 +} + +if ([string]::IsNullOrWhiteSpace($resolvedCompeteRoot)) { throw "-CompeteRoot or COMPETE_HOME is required for score" } +if ([string]::IsNullOrWhiteSpace($CompeteDataPath)) { throw "-CompeteDataPath is required for score" } +$dataDirectory = (Resolve-Path -LiteralPath $CompeteDataPath -ErrorAction Stop).Path +$competeScripts = Get-CompeteScriptRoot -Root $resolvedCompeteRoot +$normalizer = Join-Path $PSScriptRoot "tools/normalize_compete_score.py" +if (-not (Test-Path -LiteralPath $normalizer -PathType Leaf)) { throw "score normalizer is missing: $normalizer" } +$outputPath = Join-Path $artifactDirectory "competitive-score.json" + +& python $normalizer --repo $repo --compete-scripts $competeScripts --data-dir $dataDirectory --output $outputPath +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +Get-Content -LiteralPath $outputPath -Raw diff --git a/Invoke-GitHubSolutionResearch.ps1 b/Invoke-GitHubSolutionResearch.ps1 index b493d53..37f6aad 100644 --- a/Invoke-GitHubSolutionResearch.ps1 +++ b/Invoke-GitHubSolutionResearch.ps1 @@ -52,7 +52,8 @@ function New-ManualResult { [object[]]$FailedSteps, [object[]]$FailureClassifications, [object]$SentruxFailures = $null, -[string[]]$EvidenceLinks = @() +[string[]]$EvidenceLinks = @(), +[object[]]$Invocations = @() ) return [ordered]@{ @@ -65,6 +66,7 @@ function New-ManualResult { queries = $Queries candidates = @() evidenceLinks = $EvidenceLinks + invocations = $Invocations failedSteps = $FailedSteps failureClassifications = $FailureClassifications sentruxFailures = $SentruxFailures @@ -99,11 +101,22 @@ function Invoke-GhJson { } } +function Protect-GitHubResearchText { + param([string]$Text) + + if ([string]::IsNullOrEmpty($Text)) { return $Text } + $redacted = [regex]::Replace($Text, '(?i)gh[pousr]_[a-z0-9_]{8,}', '[REDACTED_GITHUB_TOKEN]') + $redacted = [regex]::Replace($redacted, '(?i)(authorization\s*:\s*(?:bearer|token)\s+)[^\s"'']+', '$1[REDACTED]') + $redacted = [regex]::Replace($redacted, '(?i)(token\s*[=:]\s*)[^\s,;"'']+', '$1[REDACTED]') + return $redacted +} + New-Item -ItemType Directory -Force -Path $ArtifactDir | Out-Null $jsonPath = Join-Path $ArtifactDir "github-solution-research.json" $markdownPath = Join-Path $ArtifactDir "github-solution-research.md" $queries = [System.Collections.Generic.List[object]]::new() +$invocations = [System.Collections.Generic.List[object]]::new() if ($null -ne $SentruxFailures -and $null -ne $SentruxFailures.primary) { $primary = $SentruxFailures.primary $targetText = if ($null -ne $primary.target -and [string]$primary.target.status -eq "resolved") { @@ -170,6 +183,12 @@ $result = New-ManualResult -Reason "GitHub research skipped by -SkipGitHubResear } $search = Invoke-GhJson $args + $invocations.Add([ordered]@{ + query = $query + surface = $surface + argv = @($args) + exitCode = [int]$search.exitCode + }) if ($search.exitCode -ne 0) { $failureReason = $search.text if ($failureReason -match "(?i)403|429|rate.?limit|api rate limit|authentication|not logged in") { @@ -208,6 +227,7 @@ $result = New-ManualResult -Reason "GitHub research skipped by -SkipGitHubResear $itemLanguage = Get-JsonProperty $item "language" $itemUpdatedAt = Get-JsonProperty $item "updatedAt" $itemPushedAt = Get-JsonProperty $item "pushedAt" + $itemRevision = Get-JsonProperty $item "sha" $candidates.Add([ordered]@{ surface = $surface query = $query @@ -218,6 +238,7 @@ $result = New-ManualResult -Reason "GitHub research skipped by -SkipGitHubResear stars = if ($null -ne $itemStars) { [int]$itemStars } else { $null } language = if ($null -ne $itemLanguage) { [string]$itemLanguage } else { "" } updatedAt = if ($null -ne $itemUpdatedAt) { [string]$itemUpdatedAt } elseif ($null -ne $itemPushedAt) { [string]$itemPushedAt } else { "" } + sourceRevision = if ($null -ne $itemRevision) { [string]$itemRevision } else { "" } }) } } @@ -229,7 +250,7 @@ $result = New-ManualResult -Reason "GitHub research skipped by -SkipGitHubResear if ($candidates.Count -eq 0) { $reason = if ([string]::IsNullOrWhiteSpace($failureReason)) { "No strong GitHub evidence returned for the generated blocker queries." } else { $failureReason } - $result = New-ManualResult -Reason $reason -Queries @($queries) -FailedSteps $FailedSteps -FailureClassifications $FailureClassifications -SentruxFailures $SentruxFailures + $result = New-ManualResult -Reason $reason -Queries @($queries) -FailedSteps $FailedSteps -FailureClassifications $FailureClassifications -SentruxFailures $SentruxFailures -Invocations @($invocations) } else { $result = [ordered]@{ schema = "github-solution-research.v1" @@ -241,6 +262,7 @@ $result = New-ManualResult -Reason "GitHub research skipped by -SkipGitHubResear queries = @($queries) candidates = @($candidates) evidenceLinks = @($evidenceLinks) + invocations = @($invocations) failedSteps = $FailedSteps failureClassifications = $FailureClassifications sentruxFailures = $SentruxFailures @@ -253,7 +275,9 @@ $result = New-ManualResult -Reason "GitHub research skipped by -SkipGitHubResear } } -$result | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $jsonPath -Encoding UTF8 +$sanitizedJson = Protect-GitHubResearchText ($result | ConvertTo-Json -Depth 8) +$result = $sanitizedJson | ConvertFrom-Json +$sanitizedJson | Set-Content -LiteralPath $jsonPath -Encoding UTF8 $candidateLines = @() foreach ($candidate in @($result.candidates | Select-Object -First 10)) { diff --git a/Invoke-ModelChannelDelegate.ps1 b/Invoke-ModelChannelDelegate.ps1 new file mode 100644 index 0000000..2d0b1b2 --- /dev/null +++ b/Invoke-ModelChannelDelegate.ps1 @@ -0,0 +1,367 @@ +#requires -Version 7.2 + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Request, + [Parameter(Mandatory = $true)] + [string]$ArtifactRoot, + [switch]$AllowLegacyRawExecutable +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Get-RequiredProperty { + param([object]$Object, [string]$Name) + $property = $Object.PSObject.Properties[$Name] + if ($null -eq $property) { throw "delegate request is missing '$Name'" } + return $property.Value +} + +function Test-ConsentGranted { + param([object]$Consent, [string]$Name) + if ($null -eq $Consent) { return $false } + $property = $Consent.PSObject.Properties[$Name] + return ($null -ne $property -and [string]$property.Value -eq "granted") +} + +function Write-Result { + param([hashtable]$Value, [string]$Path) + [IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 12), [Text.UTF8Encoding]::new($false)) + $Value | ConvertTo-Json -Depth 12 -Compress +} + +function Get-FailureCategory { + param([string]$Text) + if ($Text -match '(?i)quota|rate.?limit|too many requests|\b429\b') { return "provider_quota" } + if ($Text -match '(?i)model.?not.?found|provider.?unavailable|service.?unavailable|\b404\b|\b503\b') { return "provider_unavailable" } + if ($Text -match '(?i)unauthori[sz]ed|forbidden|invalid.?api.?key|authentication|\b401\b|\b403\b') { return "config_error" } + return "local_tool_error" +} + +function Test-StructuredOutput { + param([string]$Text, [string]$Format) + if ([string]::IsNullOrWhiteSpace($Text)) { return $false } + if ($Format -eq "json") { + try { [void]($Text | ConvertFrom-Json -ErrorAction Stop); return $true } catch { return $false } + } + foreach ($line in @($Text -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) { + try { [void]($line | ConvertFrom-Json -ErrorAction Stop) } catch { return $false } + } + return $true +} + +function Assert-NoDuplicateJsonKeys { + param([System.Text.Json.JsonElement]$Element, [string]$Path = '$') + if ($Element.ValueKind -eq [System.Text.Json.JsonValueKind]::Object) { + $names = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($property in $Element.EnumerateObject()) { + if (-not $names.Add($property.Name)) { throw "duplicate JSON key at $Path.$($property.Name)" } + Assert-NoDuplicateJsonKeys -Element $property.Value -Path "$Path.$($property.Name)" + } + } + elseif ($Element.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { + $index = 0 + foreach ($item in $Element.EnumerateArray()) { + Assert-NoDuplicateJsonKeys -Element $item -Path "$Path[$index]" + $index++ + } + } +} + +function Resolve-VerifiedExecutableHandle { + param([string]$HandlePath, [string]$ExpectedAdapter) + try { $resolvedHandle = (Resolve-Path -LiteralPath $HandlePath -ErrorAction Stop).Path } + catch { throw "cannot read executable handle" } + $raw = Get-Content -LiteralPath $resolvedHandle -Raw + try { $document = [System.Text.Json.JsonDocument]::Parse($raw) } + catch { throw "executable handle is not valid JSON" } + try { + Assert-NoDuplicateJsonKeys -Element $document.RootElement + $observedText = $document.RootElement.GetProperty("observedAt").GetString() + $expiresText = $document.RootElement.GetProperty("expiresAt").GetString() + } + finally { $document.Dispose() } + $handle = $raw | ConvertFrom-Json -ErrorAction Stop + $fields = @("schema", "adapter", "executablePath", "sha256", "length", "lastWriteTimeUtcTicks", "observedAt", "expiresAt", "identity") + if ((@($handle.PSObject.Properties.Name | Sort-Object) -join "`n") -ne (($fields | Sort-Object) -join "`n")) { throw "executable handle has an unsupported shape" } + if ([string]$handle.schema -ne "code-intel-model-executable-handle.v1") { throw "executable handle schema is unsupported" } + if ([string]$handle.adapter -ne $ExpectedAdapter) { throw "executable handle adapter mismatch" } + $observedAt = [DateTimeOffset]::MinValue + $expiresAt = [DateTimeOffset]::MinValue + if (-not [DateTimeOffset]::TryParseExact($observedText, "O", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$observedAt) -or + -not [DateTimeOffset]::TryParseExact($expiresText, "O", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$expiresAt)) { throw "executable handle time is invalid" } + if ($expiresAt -le $observedAt -or [DateTimeOffset]::UtcNow -gt $expiresAt) { throw "executable handle is expired" } + $path = (Resolve-Path -LiteralPath ([string]$handle.executablePath) -ErrorAction Stop).Path + $item = Get-Item -LiteralPath $path -ErrorAction Stop + if ($item.PSIsContainer) { throw "executable handle does not name a file" } + $sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() + if ($sha256 -ne [string]$handle.sha256 -or [long]$item.Length -ne [long]$handle.length -or [long]$item.LastWriteTimeUtc.Ticks -ne [long]$handle.lastWriteTimeUtcTicks) { throw "executable handle content binding is stale" } + $material = @( + [string]$handle.schema, [string]$handle.adapter, $path, $sha256, + [string][long]$item.Length, [string][long]$item.LastWriteTimeUtc.Ticks, + $observedText, $expiresText + ) -join "`n" + $computed = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($material))).ToLowerInvariant() + if ($computed -ne [string]$handle.identity) { throw "executable handle identity mismatch" } + return $path +} + +try { $requestPath = (Resolve-Path -LiteralPath $Request -ErrorAction Stop).Path } +catch { [Console]::Error.WriteLine("cannot read model adapter request"); exit 74 } +try { $requestRaw = Get-Content -LiteralPath $requestPath -Raw } +catch { [Console]::Error.WriteLine("cannot read model adapter request"); exit 74 } +try { $document = [System.Text.Json.JsonDocument]::Parse($requestRaw) } +catch { [Console]::Error.WriteLine("model adapter request is not valid JSON"); exit 64 } +try { Assert-NoDuplicateJsonKeys -Element $document.RootElement } +catch { [Console]::Error.WriteLine("model adapter request contains duplicate JSON keys"); exit 65 } +finally { $document.Dispose() } +try { $requestObject = $requestRaw | ConvertFrom-Json -ErrorAction Stop } +catch { [Console]::Error.WriteLine("model adapter request is not valid JSON"); exit 64 } +trap { + [Console]::Error.WriteLine($_.Exception.Message) + exit 65 +} +$isV2 = [string]$requestObject.schema -eq "code-intel-model-adapter-request.v2" +$allowedTopLevel = if ($isV2) { + @("schema", "adapter", "executableHandle", "endpoint", "protocol", "credentialEnvName", "model", "promptFile", "timeoutSeconds", "consent", "costScope", "externalData", "responseFormat") +} else { + @("schema", "adapter", "executable", "endpoint", "protocol", "credentialEnvName", "model", "promptFile", "timeoutSeconds", "consent", "costScope", "externalData", "responseFormat") +} +$actualTopLevel = @($requestObject.PSObject.Properties.Name | Sort-Object) +if (($actualTopLevel -join "`n") -ne (($allowedTopLevel | Sort-Object) -join "`n")) { throw "delegate request must contain exactly: $($allowedTopLevel -join ', ')" } +if ([string]$requestObject.schema -notin @("code-intel-model-adapter-request.v1", "code-intel-model-adapter-request.v2")) { throw "delegate request schema is unsupported" } +if ($null -eq $requestObject.consent) { throw "delegate request consent must be an object" } +$consentFields = @("consumption", "localCompute", "paidSpend", "externalData") +if ((@($requestObject.consent.PSObject.Properties.Name | Sort-Object) -join "`n") -ne (($consentFields | Sort-Object) -join "`n")) { throw "delegate consent must contain exactly: $($consentFields -join ', ')" } +foreach ($field in $consentFields) { + if ([string]$requestObject.consent.$field -notin @("unanswered", "granted", "denied")) { throw "delegate consent '$field' is outside the closed vocabulary" } +} + +$adapter = [string](Get-RequiredProperty $requestObject "adapter") +if ($adapter -notin @("claude_cli", "opencode_cli", "codex_cli", "ollama", "local_compatible")) { throw "unsupported delegate adapter '$adapter'" } +$executable = if ($isV2) { "" } elseif ($null -eq $requestObject.executable) { "" } else { [string]$requestObject.executable } +$endpoint = if ($null -eq $requestObject.endpoint) { "" } else { [string]$requestObject.endpoint } +$protocol = if ($null -eq $requestObject.protocol) { "" } else { [string]$requestObject.protocol } +$credentialEnvName = if ($null -eq $requestObject.credentialEnvName) { "" } else { [string]$requestObject.credentialEnvName } +$isHttpAdapter = $adapter -in @("ollama", "local_compatible") +if (-not $isV2 -and -not $isHttpAdapter -and -not $AllowLegacyRawExecutable) { + throw "legacy raw executable requests are disabled; synthesize a v2 request with a verified executable handle" +} +$model = [string](Get-RequiredProperty $requestObject "model") +if ([string]::IsNullOrWhiteSpace($model)) { throw "delegate request model must be non-empty" } +if ($isHttpAdapter -and [string]::IsNullOrWhiteSpace($endpoint)) { throw "HTTP model adapters require endpoint" } +if ($adapter -eq "ollama" -and $protocol -ne "ollama") { throw "ollama adapter requires protocol=ollama" } +if ($adapter -eq "local_compatible" -and $protocol -notin @("openai", "anthropic")) { throw "local_compatible requires protocol=openai or anthropic" } +if (-not $isV2 -and -not $isHttpAdapter -and [string]::IsNullOrWhiteSpace($executable)) { throw "CLI model adapters require executable" } +if ($isHttpAdapter -and -not [string]::IsNullOrWhiteSpace($executable)) { throw "HTTP model adapters require executable=null" } +if (-not $isHttpAdapter -and (-not [string]::IsNullOrWhiteSpace($endpoint) -or -not [string]::IsNullOrWhiteSpace($protocol) -or -not [string]::IsNullOrWhiteSpace($credentialEnvName))) { throw "CLI model adapters require endpoint, protocol, and credentialEnvName to be null" } +if ($adapter -eq "ollama" -and -not [string]::IsNullOrWhiteSpace($credentialEnvName)) { throw "ollama adapter does not accept a credential environment variable" } +if (-not [string]::IsNullOrWhiteSpace($credentialEnvName) -and $credentialEnvName -notmatch '^[A-Za-z_][A-Za-z0-9_]{0,127}$') { throw "credentialEnvName is invalid" } +$promptFile = [string](Get-RequiredProperty $requestObject "promptFile") +$promptPath = (Resolve-Path -LiteralPath $promptFile -ErrorAction Stop).Path +$timeoutSeconds = if ($requestObject.PSObject.Properties["timeoutSeconds"]) { [int]$requestObject.timeoutSeconds } else { 300 } +if ($timeoutSeconds -lt 1 -or $timeoutSeconds -gt 3600) { throw "timeoutSeconds must be between 1 and 3600" } +$costScope = [string](Get-RequiredProperty $requestObject "costScope") +if ($costScope -notin @("local_compute", "subscription_cli", "free_or_internal_quota", "metered_api")) { throw "unsupported costScope '$costScope'" } +$responseFormat = if ($requestObject.PSObject.Properties["responseFormat"]) { [string]$requestObject.responseFormat } else { "json" } +if ($responseFormat -notin @("json", "jsonl")) { throw "responseFormat must be json or jsonl" } +if ($isHttpAdapter -and $responseFormat -ne "json") { throw "HTTP model adapters require responseFormat=json" } +if ($isV2) { + $handlePath = if ($null -eq $requestObject.executableHandle) { "" } else { [string]$requestObject.executableHandle } + if ($isHttpAdapter -and -not [string]::IsNullOrWhiteSpace($handlePath)) { throw "HTTP model adapters require executableHandle=null" } + if (-not $isHttpAdapter -and [string]::IsNullOrWhiteSpace($handlePath)) { throw "CLI model adapters require executableHandle" } + if (-not $isHttpAdapter) { $executable = Resolve-VerifiedExecutableHandle -HandlePath $handlePath -ExpectedAdapter $adapter } +} + +$artifactDir = [IO.Path]::GetFullPath($ArtifactRoot) +New-Item -ItemType Directory -Force -Path $artifactDir | Out-Null +$resultPath = Join-Path $artifactDir "model-channel-result.json" +$responsePath = Join-Path $artifactDir "model-channel-response.jsonl" + +$consent = $requestObject.consent +$denials = [Collections.Generic.List[string]]::new() +$paidDenied = $false +$externalDenied = $false +if (-not (Test-ConsentGranted $consent "consumption")) { $denials.Add("consumption consent is not granted") } +if ($costScope -eq "local_compute" -and -not (Test-ConsentGranted $consent "localCompute")) { $denials.Add("local compute consent is not granted") } +if ($costScope -eq "metered_api" -and -not (Test-ConsentGranted $consent "paidSpend")) { $paidDenied = $true; $denials.Add("paid spend consent is not granted") } +$externalData = [bool]$requestObject.externalData +if ($isHttpAdapter) { + $endpointUri = $null + if (-not [Uri]::TryCreate($endpoint, [UriKind]::Absolute, [ref]$endpointUri) -or $endpointUri.Scheme -notin @("http", "https")) { throw "endpoint must be an absolute HTTP(S) URI" } + if (-not $endpointUri.IsLoopback) { $externalData = $true } +} +elseif ($adapter -in @("claude_cli", "opencode_cli", "codex_cli")) { + # A CLI cost label does not prove that repository data stays local. Even an + # OSS/local-compute route can have telemetry or provider resolution outside + # this adapter's control, so require the external-data grant fail-closed. + $externalData = $true +} +if ($externalData -and -not (Test-ConsentGranted $consent "externalData")) { $externalDenied = $true; $denials.Add("external data consent is not granted") } + +if ($denials.Count -gt 0) { + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1" + status = "consent_required" + adapter = $adapter + category = if ($externalDenied) { "external_data_forbidden" } elseif ($paidDenied) { "paid_usage_forbidden" } else { "consent_required" } + responseArtifact = $null + reasons = @($denials) + attempt = [ordered]@{ invoked = $false; timedOut = $false; exitCode = $null } + }) $resultPath + exit 2 +} + +if (-not $isHttpAdapter -and -not (Test-Path -LiteralPath $executable -PathType Leaf) -and $null -eq (Get-Command $executable -ErrorAction SilentlyContinue)) { + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1"; status = "unavailable"; adapter = $adapter + category = "provider_unavailable"; responseArtifact = $null; reasons = @("configured executable is unavailable") + attempt = [ordered]@{ invoked = $false; timedOut = $false; exitCode = $null } + }) $resultPath + exit 69 +} + +if ($isHttpAdapter) { + $credential = "" + if (-not [string]::IsNullOrWhiteSpace($credentialEnvName)) { + $credential = [Environment]::GetEnvironmentVariable($credentialEnvName, "Process") + if ([string]::IsNullOrWhiteSpace($credential)) { $credential = [Environment]::GetEnvironmentVariable($credentialEnvName, "User") } + if ([string]::IsNullOrWhiteSpace($credential)) { + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1"; status = "failed"; adapter = $adapter + category = "config_error"; responseArtifact = $null; reasons = @("configured credential environment variable is absent") + attempt = [ordered]@{ invoked = $false; timedOut = $false; exitCode = $null } + }) $resultPath + exit 69 + } + } + $prompt = [IO.File]::ReadAllText($promptPath) + $body = switch ($protocol) { + "ollama" { [ordered]@{ model = $model; prompt = $prompt; stream = $false } } + "openai" { [ordered]@{ model = $model; messages = @([ordered]@{ role = "user"; content = $prompt }); stream = $false } } + "anthropic" { [ordered]@{ model = $model; max_tokens = 4096; messages = @([ordered]@{ role = "user"; content = $prompt }) } } + } + $handler = [Net.Http.HttpClientHandler]::new() + $client = [Net.Http.HttpClient]::new($handler) + try { + $client.Timeout = [TimeSpan]::FromSeconds($timeoutSeconds) + $message = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::Post, $endpointUri) + $message.Content = [Net.Http.StringContent]::new(($body | ConvertTo-Json -Depth 8 -Compress), [Text.Encoding]::UTF8, "application/json") + if (-not [string]::IsNullOrWhiteSpace($credential)) { + if ($protocol -eq "anthropic") { [void]$message.Headers.TryAddWithoutValidation("x-api-key", $credential); [void]$message.Headers.TryAddWithoutValidation("anthropic-version", "2023-06-01") } + else { $message.Headers.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new("Bearer", $credential) } + } + try { $response = $client.Send($message) } + catch [Threading.Tasks.TaskCanceledException] { + Write-Result ([ordered]@{ schema="code-intel-model-adapter-result.v1";status="failed";adapter=$adapter;category="provider_unavailable";responseArtifact=$null;reasons=@("delegate timed out");attempt=[ordered]@{invoked=$true;timedOut=$true;exitCode=$null} }) $resultPath + exit 75 + } + catch [Net.Http.HttpRequestException] { + Write-Result ([ordered]@{ schema="code-intel-model-adapter-result.v1";status="failed";adapter=$adapter;category="provider_unavailable";responseArtifact=$null;reasons=@("model endpoint connection failed");attempt=[ordered]@{invoked=$true;timedOut=$false;exitCode=$null} }) $resultPath + exit 75 + } + $responseText = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() + if (-not $response.IsSuccessStatusCode) { + $failureCategory = Get-FailureCategory ("HTTP {0} {1}" -f [int]$response.StatusCode, $responseText) + Write-Result ([ordered]@{ schema="code-intel-model-adapter-result.v1";status="failed";adapter=$adapter;category=$failureCategory;responseArtifact=$null;reasons=@("model endpoint returned a non-success status");attempt=[ordered]@{invoked=$true;timedOut=$false;exitCode=[int]$response.StatusCode} }) $resultPath + exit $(if ($failureCategory -in @("provider_quota", "provider_unavailable")) { 75 } else { 69 }) + } + if (-not (Test-StructuredOutput -Text $responseText -Format "json")) { + Write-Result ([ordered]@{ schema="code-intel-model-adapter-result.v1";status="failed";adapter=$adapter;category="adapter_protocol_error";responseArtifact=$null;reasons=@("model endpoint returned non-JSON output");attempt=[ordered]@{invoked=$true;timedOut=$false;exitCode=0} }) $resultPath + exit 65 + } + [IO.File]::WriteAllText($responsePath, $responseText, [Text.UTF8Encoding]::new($false)) + Write-Result ([ordered]@{ schema="code-intel-model-adapter-result.v1";status="completed";adapter=$adapter;category=$null;responseArtifact=$responsePath;reasons=@();attempt=[ordered]@{invoked=$true;timedOut=$false;exitCode=0} }) $resultPath + exit 0 + } + finally { $client.Dispose(); $handler.Dispose() } +} + +$arguments = [Collections.Generic.List[string]]::new() +switch ($adapter) { + "claude_cli" { + $arguments.Add("-p"); $arguments.Add("--output-format"); $arguments.Add($(if ($responseFormat -eq "jsonl") { "stream-json" } else { "json" })) + $arguments.Add("--tools"); $arguments.Add("") + if ($model) { $arguments.Add("--model"); $arguments.Add($model) } + } + "opencode_cli" { + $arguments.Add("run"); $arguments.Add("--format"); $arguments.Add("json") + $arguments.Add("--pure"); $arguments.Add("--agent"); $arguments.Add("plan") + if ($model) { $arguments.Add("-m"); $arguments.Add($model) } + } + "codex_cli" { + $arguments.Add("exec"); $arguments.Add("--json"); $arguments.Add("--skip-git-repo-check") + $arguments.Add("--sandbox"); $arguments.Add("read-only") + if ($costScope -eq "local_compute") { $arguments.Add("--oss") } + if ($model) { $arguments.Add("-m"); $arguments.Add($model) } + $arguments.Add("-") + } +} + +$stdout = Join-Path $artifactDir ("delegate-stdout-{0}.tmp" -f [guid]::NewGuid().ToString("N")) +$stderr = Join-Path $artifactDir ("delegate-stderr-{0}.tmp" -f [guid]::NewGuid().ToString("N")) +$process = $null +try { + $start = [Diagnostics.ProcessStartInfo]::new() + $start.FileName = $executable + $start.UseShellExecute = $false + $start.RedirectStandardInput = $true + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + $start.CreateNoWindow = $true + $start.WorkingDirectory = $artifactDir + foreach ($argument in $arguments) { [void]$start.ArgumentList.Add($argument) } + $process = [Diagnostics.Process]::new() + $process.StartInfo = $start + if (-not $process.Start()) { throw "delegate process did not start" } + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + $prompt = [IO.File]::ReadAllText($promptPath) + $process.StandardInput.Write($prompt) + $process.StandardInput.Close() + $timedOut = -not $process.WaitForExit($timeoutSeconds * 1000) + if ($timedOut) { $process.Kill($true); $process.WaitForExit() } + $outText = $stdoutTask.GetAwaiter().GetResult() + $errText = $stderrTask.GetAwaiter().GetResult() + [IO.File]::WriteAllText($stdout, $outText, [Text.UTF8Encoding]::new($false)) + [IO.File]::WriteAllText($stderr, $errText, [Text.UTF8Encoding]::new($false)) + if ($timedOut) { + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1"; status = "failed"; adapter = $adapter + category = "provider_unavailable"; responseArtifact = $null; reasons = @("delegate timed out") + attempt = [ordered]@{ invoked = $true; timedOut = $true; exitCode = $null } + }) $resultPath + exit 75 + } + if ($process.ExitCode -ne 0) { + $failureCategory = Get-FailureCategory $errText + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1"; status = "failed"; adapter = $adapter + category = $failureCategory; responseArtifact = $null; reasons = @("delegate process returned a non-zero exit code") + attempt = [ordered]@{ invoked = $true; timedOut = $false; exitCode = [int]$process.ExitCode } + }) $resultPath + exit $(if ($failureCategory -in @("provider_quota", "provider_unavailable")) { 75 } else { 69 }) + } + if (-not (Test-StructuredOutput -Text $outText -Format $responseFormat)) { + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1"; status = "failed"; adapter = $adapter + category = "adapter_protocol_error"; responseArtifact = $null; reasons = @("delegate output did not match the requested structured response format") + attempt = [ordered]@{ invoked = $true; timedOut = $false; exitCode = 0 } + }) $resultPath + exit 65 + } + Move-Item -LiteralPath $stdout -Destination $responsePath -Force + Write-Result ([ordered]@{ + schema = "code-intel-model-adapter-result.v1"; status = "completed"; adapter = $adapter + category = $null; responseArtifact = $responsePath; reasons = @() + attempt = [ordered]@{ invoked = $true; timedOut = $false; exitCode = 0 } + }) $resultPath + exit 0 +} +finally { + if ($null -ne $process) { $process.Dispose() } + Remove-Item -LiteralPath $stdout -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $stderr -Force -ErrorAction SilentlyContinue +} diff --git a/Invoke-MultiAgentMergeQueue.ps1 b/Invoke-MultiAgentMergeQueue.ps1 new file mode 100644 index 0000000..8fcc762 --- /dev/null +++ b/Invoke-MultiAgentMergeQueue.ps1 @@ -0,0 +1,238 @@ +#requires -Version 7.2 + +param( + [ValidateSet("status", "validate", "land", "reconcile", "history")] + [string]$Action = "status", + + [string]$RepoPath = ".", + + [string]$QueueCommand = "", + + [string]$Policy = (Join-Path $PSScriptRoot "orchestration\multi-agent-merge-queue-policy.v1.json"), + + [switch]$AllowRepositoryMutation, + + [switch]$AllowNetworkPush, + + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$gates = [System.Collections.Generic.List[object]]::new() + +function Add-Gate { + param([string]$Id, [bool]$Passed, [string]$Detail) + $gates.Add([pscustomobject]@{ id = $Id; passed = $Passed; detail = $Detail }) +} + +function Get-JsScalar { + param([string]$Text, [string]$Name) + $pattern = "(?m)^\s*" + [regex]::Escape($Name) + "\s*:\s*(?[^,\r\n]+)" + $match = [regex]::Match($Text, $pattern) + if (-not $match.Success) { return $null } + return $match.Groups["value"].Value.Trim() +} + +function ConvertFrom-JsStringLiteral { + param([AllowNull()][string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return $null } + $match = [regex]::Match($Value.Trim(), '^["''](?.*)["'']$') + if (-not $match.Success) { return $null } + return $match.Groups["value"].Value +} + +function Resolve-QueueCommand { + param([string]$Repo, [string]$Explicit) + if (-not [string]::IsNullOrWhiteSpace($Explicit)) { + if (Test-Path -LiteralPath $Explicit -PathType Leaf) { + return [System.IO.Path]::GetFullPath($Explicit) + } + $explicitCommand = Get-Command $Explicit -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($null -ne $explicitCommand) { return [string]$explicitCommand.Source } + return $null + } + + $candidates = if ($IsWindows) { + @("node_modules\.bin\claude-code-merge-queue.cmd", "node_modules\.bin\claude-code-merge-queue.exe") + } else { + @("node_modules/.bin/claude-code-merge-queue") + } + foreach ($candidate in $candidates) { + $path = Join-Path $Repo $candidate + if (Test-Path -LiteralPath $path -PathType Leaf) { + return [System.IO.Path]::GetFullPath($path) + } + } + return $null +} + +function Invoke-QueueCapture { + param([string]$Command, [string[]]$Arguments, [string]$WorkingDirectory) + Push-Location $WorkingDirectory + try { + $output = if ([System.IO.Path]::GetExtension($Command) -ieq ".ps1") { + @(& pwsh -NoProfile -File $Command @Arguments 2>&1 | ForEach-Object { $_.ToString() }) + } else { + @(& $Command @Arguments 2>&1 | ForEach-Object { $_.ToString() }) + } + return [pscustomobject]@{ exitCode = $LASTEXITCODE; output = $output } + } finally { + Pop-Location + } +} + +function Invoke-QueueStreaming { + param([string]$Command, [string[]]$Arguments, [string]$WorkingDirectory) + Push-Location $WorkingDirectory + try { + if ([System.IO.Path]::GetExtension($Command) -ieq ".ps1") { + & pwsh -NoProfile -File $Command @Arguments + } else { + & $Command @Arguments + } + return $LASTEXITCODE + } finally { + Pop-Location + } +} + +try { + $policyDocument = Get-Content -Raw -LiteralPath $Policy | ConvertFrom-Json -Depth 20 +} catch { + Write-Error "Invalid merge queue policy: $($_.Exception.Message)" + exit 2 +} + +$expectedGates = @( + "git-repository", "provider-local-install", "provider-version", "provider-config", + "acceptance-check-configured", "checks-required", "direct-push-protection", + "human-promotion-boundary" +) +$policyShapePass = [string]$policyDocument.schema -eq "code-intel-multi-agent-merge-queue-policy.v1" -and + [string]$policyDocument.source.revision -match '^[0-9a-f]{40}$' -and + @($policyDocument.requiredLandingGates).Count -eq $expectedGates.Count -and + @(Compare-Object @($policyDocument.requiredLandingGates | Sort-Object) @($expectedGates | Sort-Object)).Count -eq 0 -and + @($policyDocument.forbiddenActions) -contains "promote" -and + -not [bool]$policyDocument.authority.agentsMayPromoteProduction -and + [bool]$policyDocument.authority.landRequiresExplicitRepositoryMutation -and + [bool]$policyDocument.authority.landRequiresExplicitNetworkPush +if (-not $policyShapePass) { + Write-Error "Merge queue policy shape or authority boundary is invalid." + exit 2 +} + +$repo = [System.IO.Path]::GetFullPath($RepoPath) +$isGitRepo = $false +if (Test-Path -LiteralPath $repo -PathType Container) { + & git -C $repo rev-parse --is-inside-work-tree *> $null + $isGitRepo = $LASTEXITCODE -eq 0 +} +Add-Gate "git-repository" $isGitRepo "repo=$repo" + +$resolvedCommand = Resolve-QueueCommand -Repo $repo -Explicit $QueueCommand +Add-Gate "provider-local-install" ($null -ne $resolvedCommand) $(if ($null -ne $resolvedCommand) { "command=$resolvedCommand" } else { "repository-local claude-code-merge-queue command not found" }) + +$version = $null +$versionPass = $false +if ($null -ne $resolvedCommand) { + $versionResult = Invoke-QueueCapture -Command $resolvedCommand -Arguments @("--version") -WorkingDirectory $repo + if ($versionResult.exitCode -eq 0 -and @($versionResult.output).Count -gt 0) { + $version = [string]@($versionResult.output)[-1] + try { + $versionPass = [version]$version -ge [version]([string]$policyDocument.provider.minimumVersion) + } catch { + $versionPass = $false + } + } +} +Add-Gate "provider-version" $versionPass $(if ($versionPass) { "version=$version" } else { "minimum=$($policyDocument.provider.minimumVersion); observed=$version" }) + +$configPath = @( + (Join-Path $repo "claude-code-merge-queue.config.mjs"), + (Join-Path $repo "claude-code-merge-queue.config.js") +) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 +$configPass = $null -ne $configPath +Add-Gate "provider-config" $configPass $(if ($configPass) { "config=$configPath" } else { "queue config is missing" }) + +$configText = if ($configPass) { Get-Content -Raw -LiteralPath $configPath } else { "" } +$checkCommandValue = Get-JsScalar -Text $configText -Name "checkCommand" +$acceptanceCommand = ConvertFrom-JsStringLiteral $checkCommandValue +$acceptancePass = -not [string]::IsNullOrWhiteSpace($acceptanceCommand) +Add-Gate "acceptance-check-configured" $acceptancePass $(if ($acceptancePass) { "checkCommand is explicit" } else { "checkCommand must be a static non-empty string" }) + +$checksRequiredValue = Get-JsScalar -Text $configText -Name "checksRequired" +$checksRequiredPass = $configPass -and ([string]::IsNullOrWhiteSpace($checksRequiredValue) -or $checksRequiredValue -notmatch '(?i)^false$') +Add-Gate "checks-required" $checksRequiredPass $(if ($checksRequiredPass) { "checksRequired defaults to or is true" } else { "checksRequired:false is not admitted" }) + +$hookPath = $null +if ($isGitRepo) { + $gitHookOutput = @(& git -C $repo rev-parse --git-path hooks 2>$null) + if ($LASTEXITCODE -eq 0 -and $gitHookOutput.Count -gt 0) { + $hooksRoot = [string]$gitHookOutput[-1] + if (-not [System.IO.Path]::IsPathRooted($hooksRoot)) { $hooksRoot = Join-Path $repo $hooksRoot } + $hookPath = Join-Path ([System.IO.Path]::GetFullPath($hooksRoot)) "pre-push" + } +} +$hookPass = $null -ne $hookPath -and (Test-Path -LiteralPath $hookPath -PathType Leaf) -and + (Get-Content -Raw -LiteralPath $hookPath) -match 'claude-code-merge-queue\s+check-push' +Add-Gate "direct-push-protection" $hookPass $(if ($hookPass) { "active hook=$hookPath" } else { "active pre-push hook does not enforce queue check-push" }) + +$integrationBranch = ConvertFrom-JsStringLiteral (Get-JsScalar -Text $configText -Name "integrationBranch") +$productionBranch = ConvertFrom-JsStringLiteral (Get-JsScalar -Text $configText -Name "productionBranch") +$promotionPass = -not [string]::IsNullOrWhiteSpace($integrationBranch) -and + -not [string]::IsNullOrWhiteSpace($productionBranch) -and + $integrationBranch -ne $productionBranch +Add-Gate "human-promotion-boundary" $promotionPass $(if ($promotionPass) { "integration=$integrationBranch; production=$productionBranch" } else { "distinct static integrationBranch and productionBranch are required" }) + +$failed = @($gates | Where-Object { -not $_.passed }) +$status = [ordered]@{ + schema = "code-intel-multi-agent-merge-queue-status.v1" + provider = "claude-code-merge-queue" + sourceRevision = [string]$policyDocument.source.revision + repo = $repo + ready = $failed.Count -eq 0 + command = $resolvedCommand + config = $configPath + version = $version + gates = @($gates) + failedGateIds = @($failed | ForEach-Object id) +} + +if ($Action -in @("status", "validate")) { + if ($Json) { + $status | ConvertTo-Json -Depth 10 + } else { + Write-Host "Multi-Agent Merge Queue: $(if ($status.ready) { 'ready' } else { 'not-ready' })" + foreach ($gate in $gates) { + Write-Host "$(if ($gate.passed) { 'PASS' } else { 'FAIL' }) $($gate.id): $($gate.detail)" + } + } + if ($Action -eq "validate" -and -not $status.ready) { exit 1 } + exit 0 +} + +if ($Action -eq "land" -and (-not $AllowRepositoryMutation -or -not $AllowNetworkPush)) { + Write-Error "land requires both -AllowRepositoryMutation and -AllowNetworkPush; readiness alone is not mutation authority." + exit 1 +} + +$requiredForAction = if ($Action -eq "land") { + $expectedGates +} else { + @("git-repository", "provider-local-install", "provider-version", "provider-config") +} +$actionFailures = @($gates | Where-Object { $_.id -in $requiredForAction -and -not $_.passed }) +if ($actionFailures.Count -gt 0) { + Write-Error "$Action rejected by merge queue readiness: $($actionFailures.id -join ', ')" + exit 1 +} + +$queueArguments = switch ($Action) { + "land" { @("land") } + "reconcile" { @("reconcile") } + "history" { @("land-history") } +} +Write-Host "Multi-Agent Merge Queue: delegating '$Action' to the repository-local provider." +$queueExit = Invoke-QueueStreaming -Command $resolvedCommand -Arguments $queueArguments -WorkingDirectory $repo +exit $queueExit diff --git a/Invoke-MultiAgentWorkspacePreflight.ps1 b/Invoke-MultiAgentWorkspacePreflight.ps1 new file mode 100644 index 0000000..03e09ff --- /dev/null +++ b/Invoke-MultiAgentWorkspacePreflight.ps1 @@ -0,0 +1,342 @@ +#requires -Version 7.2 + +param( + [string]$RepoPath = ".", + + [ValidateSet("mutation", "observation")] + [string]$Intent = "mutation", + + [string]$Policy = (Join-Path $PSScriptRoot "orchestration\multi-agent-workspace-policy.v1.json"), + + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Invoke-ReadOnlyGit { + param( + [string]$WorkingDirectory, + [string[]]$Arguments + ) + + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = "git" + $startInfo.UseShellExecute = $false + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.StandardOutputEncoding = [System.Text.UTF8Encoding]::new($false) + $startInfo.StandardErrorEncoding = [System.Text.UTF8Encoding]::new($false) + $startInfo.Environment["GIT_OPTIONAL_LOCKS"] = "0" + [void]$startInfo.ArgumentList.Add("--no-optional-locks") + [void]$startInfo.ArgumentList.Add("-C") + [void]$startInfo.ArgumentList.Add($WorkingDirectory) + foreach ($argument in $Arguments) { + [void]$startInfo.ArgumentList.Add($argument) + } + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + try { + [void]$process.Start() + $stdout = $process.StandardOutput.ReadToEnd() + $stderr = $process.StandardError.ReadToEnd() + $process.WaitForExit() + return [pscustomobject]@{ + exitCode = $process.ExitCode + stdout = $stdout + stderr = $stderr.Trim() + } + } finally { + $process.Dispose() + } +} + +function Get-NormalizedPath { + param([string]$Path) + return $Path.Replace("\", "/") +} + +function Get-ChangeGroup { + param([string]$Status) + + if ($Status -eq "??") { return "untracked" } + if ($Status -match "U" -or $Status -in @("AA", "DD")) { return "unmerged" } + if ($Status -match "R") { return "renamed" } + if ($Status -match "C") { return "copied" } + if ($Status -match "D") { return "deleted" } + if ($Status -match "A") { return "added" } + if ($Status -match "T") { return "typeChanged" } + if ($Status -match "M") { return "modified" } + return "other" +} + +function Get-Sha256 { + param([string]$Text) + $bytes = [System.Text.UTF8Encoding]::new($false).GetBytes($Text) + $digest = [System.Security.Cryptography.SHA256]::HashData($bytes) + return [Convert]::ToHexString($digest).ToLowerInvariant() +} + +function Write-ResultAndExit { + param( + [System.Collections.IDictionary]$Result, + [int]$ExitCode, + [switch]$AsJson + ) + + if ($AsJson) { + $Result | ConvertTo-Json -Depth 20 + } else { + $state = if ([bool]$Result.allowed) { "allowed" } else { "denied" } + Write-Host "Multi-Agent Workspace Preflight: $state ($($Result.decision))" + Write-Host "Intent: $($Result.intent)" + Write-Host "Repository: $($Result.repo)" + Write-Host "Changes: tracked=$($Result.counts.tracked) untracked=$($Result.counts.untracked) total=$($Result.counts.total)" + Write-Host "Inventory SHA-256: $($Result.inventoryHash)" + Write-Host "Reason: $($Result.reason)" + } + exit $ExitCode +} + +try { + $policyDocument = Get-Content -Raw -LiteralPath $Policy | ConvertFrom-Json -Depth 20 +} catch { + [Console]::Error.WriteLine("Invalid multi-agent workspace policy: $($_.Exception.Message)") + exit 2 +} + +$policyValid = $false +try { + $policyValid = [string]$policyDocument.schema -eq "code-intel-multi-agent-workspace-policy.v1" -and + [string]$policyDocument.defaultIntent -eq "mutation" -and + [string]$policyDocument.inspectionAuthority -eq "observation_only" -and + [bool]$policyDocument.requirements.repositoryRootRequired -and + [bool]$policyDocument.requirements.dirtyRootBlocksMutation -and + [bool]$policyDocument.requirements.observationMustBeExplicit -and + -not [bool]$policyDocument.requirements.observationMayModifyRepository -and + [string]$policyDocument.requirements.inventoryFormat -eq "git-status-porcelain-v1-z" -and + [string]$policyDocument.requirements.inventoryHash -eq "sha256-canonical-json-v1" +} catch { + $policyValid = $false +} +if (-not $policyValid) { + [Console]::Error.WriteLine("Multi-agent workspace policy shape or fail-closed boundary is invalid.") + exit 2 +} + +$repo = [System.IO.Path]::GetFullPath($RepoPath) +$emptyCounts = [ordered]@{ + tracked = 0 + untracked = 0 + total = 0 + staged = 0 + worktree = 0 +} +$emptyGroups = [ordered]@{ + added = 0 + modified = 0 + deleted = 0 + renamed = 0 + copied = 0 + typeChanged = 0 + unmerged = 0 + untracked = 0 + other = 0 +} + +if (-not (Test-Path -LiteralPath $repo -PathType Container)) { + $result = [ordered]@{ + schema = "code-intel-multi-agent-workspace-preflight.v1" + authority = "observation_only" + repo = $repo + gitRoot = $null + isRepositoryRoot = $false + intent = $Intent + dirty = $null + allowed = $false + decision = "deny_inspection_failed" + reason = "repository path does not exist or is not a directory" + counts = $emptyCounts + groups = $emptyGroups + inventoryHashAlgorithm = "sha256-canonical-json-v1" + inventoryHash = $null + entries = @() + } + Write-ResultAndExit -Result $result -ExitCode 22 -AsJson:$Json +} + +$rootResult = Invoke-ReadOnlyGit -WorkingDirectory $repo -Arguments @("rev-parse", "--show-toplevel") +if ($rootResult.exitCode -ne 0 -or [string]::IsNullOrWhiteSpace($rootResult.stdout)) { + $result = [ordered]@{ + schema = "code-intel-multi-agent-workspace-preflight.v1" + authority = "observation_only" + repo = $repo + gitRoot = $null + isRepositoryRoot = $false + intent = $Intent + dirty = $null + allowed = $false + decision = "deny_inspection_failed" + reason = $(if ([string]::IsNullOrWhiteSpace($rootResult.stderr)) { "path is not a Git worktree" } else { $rootResult.stderr }) + counts = $emptyCounts + groups = $emptyGroups + inventoryHashAlgorithm = "sha256-canonical-json-v1" + inventoryHash = $null + entries = @() + } + Write-ResultAndExit -Result $result -ExitCode 22 -AsJson:$Json +} + +$gitRoot = [System.IO.Path]::GetFullPath($rootResult.stdout.Trim()) +$comparison = if ($IsWindows) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal } +$trimChars = @([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) +$isRepositoryRoot = $repo.TrimEnd($trimChars).Equals($gitRoot.TrimEnd($trimChars), $comparison) + +$statusResult = Invoke-ReadOnlyGit -WorkingDirectory $gitRoot -Arguments @("status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored=no") +if ($statusResult.exitCode -ne 0) { + $result = [ordered]@{ + schema = "code-intel-multi-agent-workspace-preflight.v1" + authority = "observation_only" + repo = $repo + gitRoot = $gitRoot + isRepositoryRoot = $isRepositoryRoot + intent = $Intent + dirty = $null + allowed = $false + decision = "deny_inspection_failed" + reason = $(if ([string]::IsNullOrWhiteSpace($statusResult.stderr)) { "git status inspection failed" } else { $statusResult.stderr }) + counts = $emptyCounts + groups = $emptyGroups + inventoryHashAlgorithm = "sha256-canonical-json-v1" + inventoryHash = $null + entries = @() + } + Write-ResultAndExit -Result $result -ExitCode 22 -AsJson:$Json +} + +$entries = [System.Collections.Generic.List[object]]::new() +$tokens = @($statusResult.stdout.Split([char]0, [System.StringSplitOptions]::RemoveEmptyEntries)) +for ($index = 0; $index -lt $tokens.Count; $index++) { + $token = [string]$tokens[$index] + if ($token.Length -lt 4 -or $token[2] -ne " ") { + $result = [ordered]@{ + schema = "code-intel-multi-agent-workspace-preflight.v1" + authority = "observation_only" + repo = $repo + gitRoot = $gitRoot + isRepositoryRoot = $isRepositoryRoot + intent = $Intent + dirty = $null + allowed = $false + decision = "deny_inspection_failed" + reason = "unrecognized git status porcelain entry" + counts = $emptyCounts + groups = $emptyGroups + inventoryHashAlgorithm = "sha256-canonical-json-v1" + inventoryHash = $null + entries = @() + } + Write-ResultAndExit -Result $result -ExitCode 22 -AsJson:$Json + } + + $status = $token.Substring(0, 2) + $path = Get-NormalizedPath $token.Substring(3) + $originalPath = $null + if ($status -match "[RC]") { + $index++ + if ($index -ge $tokens.Count) { + $result = [ordered]@{ + schema = "code-intel-multi-agent-workspace-preflight.v1" + authority = "observation_only" + repo = $repo + gitRoot = $gitRoot + isRepositoryRoot = $isRepositoryRoot + intent = $Intent + dirty = $null + allowed = $false + decision = "deny_inspection_failed" + reason = "rename or copy entry is missing its original path" + counts = $emptyCounts + groups = $emptyGroups + inventoryHashAlgorithm = "sha256-canonical-json-v1" + inventoryHash = $null + entries = @() + } + Write-ResultAndExit -Result $result -ExitCode 22 -AsJson:$Json + } + $originalPath = Get-NormalizedPath ([string]$tokens[$index]) + } + + $entries.Add([pscustomobject][ordered]@{ + status = $status + group = Get-ChangeGroup $status + path = $path + originalPath = $originalPath + }) +} + +$orderedEntries = @($entries | Sort-Object path, originalPath, status) +$trackedCount = @($orderedEntries | Where-Object status -ne "??").Count +$untrackedCount = @($orderedEntries | Where-Object status -eq "??").Count +$stagedCount = @($orderedEntries | Where-Object { $_.status -ne "??" -and $_.status[0] -ne " " }).Count +$worktreeCount = @($orderedEntries | Where-Object { $_.status -ne "??" -and $_.status[1] -ne " " }).Count +$counts = [ordered]@{ + tracked = $trackedCount + untracked = $untrackedCount + total = $orderedEntries.Count + staged = $stagedCount + worktree = $worktreeCount +} +$groups = [ordered]@{} +foreach ($name in @("added", "modified", "deleted", "renamed", "copied", "typeChanged", "unmerged", "untracked", "other")) { + $groups[$name] = @($orderedEntries | Where-Object group -eq $name).Count +} +$canonicalInventory = [ordered]@{ + schema = "code-intel-multi-agent-workspace-inventory.v1" + entries = $orderedEntries +} +$canonicalJson = $canonicalInventory | ConvertTo-Json -Depth 10 -Compress +$inventoryHash = Get-Sha256 $canonicalJson +$dirty = $orderedEntries.Count -gt 0 + +if (-not $isRepositoryRoot) { + $allowed = $false + $decision = "deny_repository_root_required" + $reason = "preflight must be run against the Git worktree root" + $exitCode = 21 +} elseif ($dirty -and $Intent -eq "mutation") { + $allowed = $false + $decision = "deny_dirty_root" + $reason = "dirty repository root cannot authorize mutation-oriented agent work" + $exitCode = 20 +} elseif ($Intent -eq "observation") { + $allowed = $true + $decision = "allow_observation" + $reason = "explicit observation-only intent does not grant repository mutation authority" + $exitCode = 0 +} else { + $allowed = $true + $decision = "allow_clean_mutation" + $reason = "repository root is clean" + $exitCode = 0 +} + +$result = [ordered]@{ + schema = "code-intel-multi-agent-workspace-preflight.v1" + authority = "observation_only" + repo = $repo + gitRoot = $gitRoot + isRepositoryRoot = $isRepositoryRoot + intent = $Intent + dirty = $dirty + allowed = $allowed + decision = $decision + reason = $reason + counts = $counts + groups = $groups + inventoryHashAlgorithm = "sha256-canonical-json-v1" + inventoryHash = $inventoryHash + entries = $orderedEntries +} +Write-ResultAndExit -Result $result -ExitCode $exitCode -AsJson:$Json diff --git a/Invoke-ProviderRuntimeInventory.ps1 b/Invoke-ProviderRuntimeInventory.ps1 new file mode 100644 index 0000000..4bf594c --- /dev/null +++ b/Invoke-ProviderRuntimeInventory.ps1 @@ -0,0 +1,451 @@ +#requires -Version 7.2 + +[CmdletBinding()] +param( + [ValidateRange(100, 30000)] + [int]$TimeoutMs = 3000, + [string[]]$ClaudeCandidates = @(), + [string[]]$CodexCandidates = @(), + [string[]]$OpenCodeCandidates = @(), + [string[]]$OllamaCandidates = @(), + [string[]]$CcSwitchCandidates = @(), + [ValidateSet("", "openai", "anthropic")] + [string]$UserProvider = "", + [string]$UserModel = "", + [string]$UserBaseUrl = "", + [string]$UserCredentialEnvName = "", + [ValidateSet("local_compute", "subscription_cli", "free_or_internal_quota", "metered_api")] + [string]$UserCostScope = "local_compute", + [switch]$ProbeUserEndpoint, + [string]$UserModelCatalogFixture = "", + [switch]$UseOnlyProvidedCandidates, + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Protect-InventoryText { + param([AllowEmptyString()][string]$Text) + + $safe = [string]$Text + $safe = $safe -replace '(?i)Bearer\s+[A-Za-z0-9._~+/-]+', 'Bearer [REDACTED]' + $safe = $safe -replace '(?i)\b(sk|key|token|secret)-[A-Za-z0-9._~+/-]{4,}\b', '[REDACTED]' + $safe = $safe -replace '(?i)(api[_-]?key|auth[_-]?token|access[_-]?token|secret)\s*[:=]\s*[^\s,;]+', '$1=[REDACTED]' + $safe = $safe -replace '[\r\n]+', ' ' + if ($safe.Length -gt 160) { $safe = $safe.Substring(0, 160) } + return $safe.Trim() +} + +function Get-UniqueCandidatePaths { + param( + [string]$CommandName, + [string[]]$Provided, + [string[]]$KnownPaths = @() + ) + + $items = [System.Collections.Generic.List[string]]::new() + foreach ($item in @($Provided)) { + if (-not [string]::IsNullOrWhiteSpace($item)) { $items.Add($item) } + } + if (-not $UseOnlyProvidedCandidates) { + foreach ($command in @(Get-Command $CommandName -All -ErrorAction SilentlyContinue)) { + $path = "" + if ($command.PSObject.Properties.Name -contains "Path" -and -not [string]::IsNullOrWhiteSpace([string]$command.Path)) { + $path = [string]$command.Path + } + elseif ($command.PSObject.Properties.Name -contains "Source") { + $path = [string]$command.Source + } + if (-not [string]::IsNullOrWhiteSpace($path)) { $items.Add($path) } + } + foreach ($path in $KnownPaths) { + if (-not [string]::IsNullOrWhiteSpace($path)) { $items.Add($path) } + } + } + + return @($items | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -Unique) +} + +function Invoke-BoundedExecutable { + param( + [Parameter(Mandatory)][string]$Path, + [Parameter(Mandatory)][string[]]$Arguments + ) + + $extension = [IO.Path]::GetExtension($Path).ToLowerInvariant() + $start = [System.Diagnostics.ProcessStartInfo]::new() + $start.UseShellExecute = $false + $start.CreateNoWindow = $true + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + + if ($extension -eq ".ps1") { + $pwsh = (Get-Process -Id $PID).Path + $start.FileName = $pwsh + foreach ($prefix in @("-NoLogo", "-NoProfile", "-NonInteractive", "-File", $Path)) { + [void]$start.ArgumentList.Add($prefix) + } + } + elseif ($extension -in @(".cmd", ".bat")) { + if ($Path.IndexOfAny([char[]]'"&|<>^') -ge 0) { + return [pscustomobject][ordered]@{ ok = $false; timed_out = $false; exit_code = -1; stdout = ""; category = "local_tool_error" } + } + $start.FileName = $env:ComSpec + foreach ($prefix in @("/d", "/s", "/c", ('"{0}"' -f $Path))) { + [void]$start.ArgumentList.Add($prefix) + } + } + else { + $start.FileName = $Path + } + foreach ($argument in $Arguments) { [void]$start.ArgumentList.Add($argument) } + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $start + try { + [void]$process.Start() + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + if (-not $process.WaitForExit($TimeoutMs)) { + try { $process.Kill($true) } catch { } + [void]$process.WaitForExit(1000) + return [pscustomobject][ordered]@{ ok = $false; timed_out = $true; exit_code = -1; stdout = ""; category = "local_tool_error" } + } + $stdout = $stdoutTask.GetAwaiter().GetResult() + [void]$stderrTask.GetAwaiter().GetResult() + return [pscustomobject][ordered]@{ + ok = ($process.ExitCode -eq 0) + timed_out = $false + exit_code = $process.ExitCode + stdout = [string]$stdout + category = if ($process.ExitCode -eq 0) { "" } else { "local_tool_error" } + } + } + catch { + return [pscustomobject][ordered]@{ ok = $false; timed_out = $false; exit_code = -1; stdout = ""; category = "local_tool_error" } + } + finally { + $process.Dispose() + } +} + +function Get-VersionCandidates { + param( + [string[]]$Paths, + [string[]]$Arguments = @("--version") + ) + + $results = [System.Collections.Generic.List[object]]::new() + foreach ($path in $Paths) { + $probe = Invoke-BoundedExecutable -Path $path -Arguments $Arguments + $version = "" + if ($probe.ok) { + $firstLine = @(([string]$probe.stdout -split '\r?\n') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1) + if ($firstLine.Count -gt 0) { $version = Protect-InventoryText ([string]$firstLine[0]) } + } + $results.Add([pscustomobject][ordered]@{ + path = [IO.Path]::GetFullPath($path) + executable_verified = [bool]$probe.ok + timed_out = [bool]$probe.timed_out + version = $version + category = [string]$probe.category + }) + } + return @($results) +} + +function Get-FirstVerifiedCandidate { + param([object[]]$Candidates) + return @($Candidates | Where-Object { $_.executable_verified } | Select-Object -First 1) +} + +function Get-Presence { + param([string[]]$Names) + foreach ($name in $Names) { + if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name, "Process")) -or + -not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name, "User"))) { + return $true + } + } + return $false +} + +function Test-LocalEndpointUri { + param([string]$Value) + $uri = $null + if (-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$uri)) { return $false } + if ($uri.Scheme -notin @("http", "https")) { return $false } + return $uri.IsLoopback -or $uri.Host -ieq "localhost" +} + +function Get-UserEndpointProbe { + param( + [string]$Provider, + [string]$BaseUrl, + [string]$Model, + [string]$CredentialEnvName, + [string]$CatalogFixture + ) + + $authState = "not_applicable" + $credential = "" + if (-not [string]::IsNullOrWhiteSpace($CredentialEnvName)) { + if ($CredentialEnvName -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { + return [pscustomobject]@{ verified = $false; auth = "unknown"; model = "unknown"; diagnostic = "endpoint_probe_failed" } + } + $credential = [Environment]::GetEnvironmentVariable($CredentialEnvName, "Process") + if ([string]::IsNullOrWhiteSpace($credential)) { $credential = [Environment]::GetEnvironmentVariable($CredentialEnvName, "User") } + $authState = if ([string]::IsNullOrWhiteSpace($credential)) { "absent" } else { "present" } + } + + try { + $catalog = $null + if (-not [string]::IsNullOrWhiteSpace($CatalogFixture)) { + if (-not $UseOnlyProvidedCandidates) { throw "catalog fixtures are test-mode only" } + $catalog = Get-Content -LiteralPath $CatalogFixture -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop + } + else { + if (-not (Test-LocalEndpointUri $BaseUrl)) { + return [pscustomobject]@{ verified = $false; auth = $authState; model = "unknown"; diagnostic = "endpoint_probe_failed" } + } + $headers = @{} + if (-not [string]::IsNullOrWhiteSpace($credential)) { + if ($Provider -eq "anthropic") { + $headers["x-api-key"] = $credential + $headers["anthropic-version"] = "2023-06-01" + } + else { + $headers["Authorization"] = "Bearer $credential" + } + } + $catalogUri = ([Uri]::new(([Uri]$BaseUrl), "models")).AbsoluteUri + $catalog = Invoke-RestMethod -Method Get -Uri $catalogUri -Headers $headers -TimeoutSec ([Math]::Max(1, [Math]::Ceiling($TimeoutMs / 1000))) -ErrorAction Stop + } + + $ids = @($catalog.data | ForEach-Object { [string]$_.id } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + $modelState = if ([string]::IsNullOrWhiteSpace($Model)) { "unknown" } elseif ($ids -contains $Model) { "available" } else { "unavailable" } + return [pscustomobject]@{ + verified = $true + auth = $authState + model = $modelState + diagnostic = if ($modelState -eq "unavailable") { "model_not_in_catalog" } else { "endpoint_probe_passed" } + } + } + catch { + return [pscustomobject]@{ verified = $false; auth = $authState; model = "unknown"; diagnostic = "endpoint_probe_failed" } + } + finally { + $credential = "" + } +} + +function New-CandidateObservation { + param( + [string]$Id, + [string]$ChannelKind, + [AllowNull()][object]$Provider, + [AllowNull()][object]$Model, + [string]$CostScope, + [bool]$EndpointConfigured, + [bool]$Discovered, + [bool]$ExecutableVerified, + [string]$AuthPresent, + [string]$ModelAvailable, + [bool]$ExternalEgress, + [string]$Source, + [string[]]$Diagnostics + ) + return [pscustomobject][ordered]@{ + id = $Id + channelKind = $ChannelKind + provider = $Provider + model = $Model + costScope = $CostScope + endpointConfigured = $EndpointConfigured + discovered = $Discovered + executableVerified = $ExecutableVerified + authPresent = $AuthPresent + modelAvailable = $ModelAvailable + externalEgress = $ExternalEgress + source = $Source + diagnostics = @($Diagnostics | Select-Object -Unique) + } +} + +function Get-CandidateDiagnostics { + param([object[]]$Candidates) + $diagnostics = [System.Collections.Generic.List[string]]::new() + $sawFailure = $false + for ($index = 0; $index -lt $Candidates.Count; $index++) { + $candidate = $Candidates[$index] + if ($candidate.executable_verified) { + $diagnostics.Add("candidate_verified") + $diagnostics.Add("version_probe_passed") + if ($sawFailure) { $diagnostics.Add("fallback_candidate_selected") } + } + elseif ($candidate.timed_out) { + $diagnostics.Add("candidate_timed_out") + $sawFailure = $true + } + else { + $diagnostics.Add("candidate_verification_failed") + $sawFailure = $true + } + } + return @($diagnostics | Select-Object -Unique) +} + +$localBin = if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { Join-Path $env:USERPROFILE ".local\bin" } else { "" } +$localAppData = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData) +$appData = [Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData) + +$claudePaths = @(Get-UniqueCandidatePaths -CommandName "claude" -Provided $ClaudeCandidates -KnownPaths @( + $(if ($localBin) { Join-Path $localBin "claude.cmd" }) +)) +$codexPaths = @(Get-UniqueCandidatePaths -CommandName "codex" -Provided $CodexCandidates) +$openCodePaths = @(Get-UniqueCandidatePaths -CommandName "opencode" -Provided $OpenCodeCandidates -KnownPaths @( + $(if ($localAppData) { Join-Path $localAppData "OpenCode\opencode-cli.exe" }) +)) +$ollamaPaths = @(Get-UniqueCandidatePaths -CommandName "ollama" -Provided $OllamaCandidates) +$ccSwitchPaths = @(Get-UniqueCandidatePaths -CommandName "cc-switch" -Provided $CcSwitchCandidates -KnownPaths @( + $(if ($localAppData) { Join-Path $localAppData "Programs\CC Switch\cc-switch.exe" }) +)) + +$claudeCandidateResults = @(Get-VersionCandidates -Paths $claudePaths) +$codexCandidateResults = @(Get-VersionCandidates -Paths $codexPaths) +$openCodeCandidateResults = @(Get-VersionCandidates -Paths $openCodePaths) +$ollamaCandidateResults = @(Get-VersionCandidates -Paths $ollamaPaths) + +$claudeSelected = @(Get-FirstVerifiedCandidate $claudeCandidateResults) +$codexSelected = @(Get-FirstVerifiedCandidate $codexCandidateResults) +$openCodeSelected = @(Get-FirstVerifiedCandidate $openCodeCandidateResults) +$ollamaSelected = @(Get-FirstVerifiedCandidate $ollamaCandidateResults) + +$claudeAuth = $false +$claudeAuthMethod = "unknown" +if ($claudeSelected.Count -eq 1) { + $auth = Invoke-BoundedExecutable -Path $claudeSelected[0].path -Arguments @("auth", "status", "--json") + if ($auth.ok) { + try { + $authJson = ([string]$auth.stdout | ConvertFrom-Json -ErrorAction Stop) + $claudeAuth = [bool]$authJson.authenticated + if ($claudeAuth -and $authJson.PSObject.Properties.Name -contains "authMethod") { + $allowedMethods = @("api_key", "oauth", "subscription", "unknown") + $candidateMethod = ([string]$authJson.authMethod).ToLowerInvariant() + if ($candidateMethod -in $allowedMethods) { $claudeAuthMethod = $candidateMethod } + } + } + catch { $claudeAuth = $false } + } +} + +$codexAuth = $false +if ($codexSelected.Count -eq 1) { + $login = Invoke-BoundedExecutable -Path $codexSelected[0].path -Arguments @("login", "status") + $codexAuth = [bool]$login.ok +} + +$ollamaModels = @() +if ($ollamaSelected.Count -eq 1) { + $list = Invoke-BoundedExecutable -Path $ollamaSelected[0].path -Arguments @("list") + if ($list.ok) { + $ollamaModels = @(([string]$list.stdout -split '\r?\n') | + Select-Object -Skip 1 | + ForEach-Object { @($_ -split '\s+')[0] } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + ForEach-Object { Protect-InventoryText $_ } | + Select-Object -Unique) + } +} + +$ccConfigPresent = $false +if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { + $ccRoot = Join-Path $env:USERPROFILE ".cc-switch" + $ccConfigPresent = (Test-Path -LiteralPath (Join-Path $ccRoot "settings.json") -PathType Leaf) -or + (Test-Path -LiteralPath (Join-Path $ccRoot "cc-switch.db") -PathType Leaf) +} + +$claudeConfigPresent = (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) -and + (Test-Path -LiteralPath (Join-Path $env:USERPROFILE ".claude\settings.json") -PathType Leaf) +$codexConfigPresent = (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) -and + (Test-Path -LiteralPath (Join-Path $env:USERPROFILE ".codex\config.toml") -PathType Leaf) +$openCodeConfigPresent = (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) -and + (Test-Path -LiteralPath (Join-Path $env:USERPROFILE ".config\opencode\opencode.json") -PathType Leaf) + +$openAiEndpoint = Get-Presence @("OPENAI_BASE_URL", "CODE_INTEL_BASE_URL") +$anthropicEndpoint = Get-Presence @("ANTHROPIC_BASE_URL", "CODE_INTEL_ANTHROPIC_BASE_URL") + +$candidates = [System.Collections.Generic.List[object]]::new() + +if (-not [string]::IsNullOrWhiteSpace($UserProvider) -or -not [string]::IsNullOrWhiteSpace($UserModel) -or -not [string]::IsNullOrWhiteSpace($UserBaseUrl)) { + $providerValue = if ([string]::IsNullOrWhiteSpace($UserProvider)) { $null } else { $UserProvider } + $modelValue = if ([string]::IsNullOrWhiteSpace($UserModel)) { $null } else { Protect-InventoryText $UserModel } + $userUri = $null + $userUriValid = [Uri]::TryCreate($UserBaseUrl, [UriKind]::Absolute, [ref]$userUri) + $userExternalEgress = $userUriValid -and -not ($userUri.IsLoopback -or $userUri.Host -ieq "localhost") + $userProbe = [pscustomobject]@{ verified = $false; auth = "unknown"; model = "unknown"; diagnostic = "endpoint_not_probed" } + if ($ProbeUserEndpoint) { + $userProbe = Get-UserEndpointProbe -Provider $UserProvider -BaseUrl $UserBaseUrl -Model $UserModel -CredentialEnvName $UserCredentialEnvName -CatalogFixture $UserModelCatalogFixture + } + $candidates.Add((New-CandidateObservation -Id "user_local_compatible" -ChannelKind "local_compatible" -Provider $providerValue -Model $modelValue -CostScope $UserCostScope -EndpointConfigured (-not [string]::IsNullOrWhiteSpace($UserBaseUrl)) -Discovered $true -ExecutableVerified ([bool]$userProbe.verified) -AuthPresent ([string]$userProbe.auth) -ModelAvailable ([string]$userProbe.model) -ExternalEgress $userExternalEgress -Source "user_input" -Diagnostics @( + $(if (-not [string]::IsNullOrWhiteSpace($UserBaseUrl)) { "endpoint_configured" } else { "endpoint_not_configured" }), + $(if ($null -ne $modelValue) { "model_declared_by_user" } else { "model_not_declared" }), + [string]$userProbe.diagnostic + ))) +} +else { + foreach ($endpoint in @( + @{ id = "local_openai_compatible"; provider = "openai"; present = $openAiEndpoint }, + @{ id = "local_anthropic_compatible"; provider = "anthropic"; present = $anthropicEndpoint } + )) { + if ($endpoint.present) { + # The endpoint value is intentionally not collected. Since locality therefore + # cannot be proven, classify the channel conservatively as external egress. + $candidates.Add((New-CandidateObservation -Id $endpoint.id -ChannelKind "local_compatible" -Provider $endpoint.provider -Model $null -CostScope "local_compute" -EndpointConfigured $true -Discovered $true -ExecutableVerified $false -AuthPresent "unknown" -ModelAvailable "unknown" -ExternalEgress $true -Source "local_discovery" -Diagnostics @("endpoint_configured", "endpoint_value_not_collected", "external_egress_assumed"))) + } + } +} + +if ($ollamaModels.Count -gt 0) { + foreach ($ollamaModel in $ollamaModels) { + $idSuffix = ([string]$ollamaModel -replace '[^A-Za-z0-9._-]', '_').ToLowerInvariant() + $ollamaDiagnostics = @((Get-CandidateDiagnostics $ollamaCandidateResults)) + @("model_catalog_observed") + $candidates.Add((New-CandidateObservation -Id "ollama_$idSuffix" -ChannelKind "ollama" -Provider "ollama" -Model ([string]$ollamaModel) -CostScope "local_compute" -EndpointConfigured $true -Discovered $true -ExecutableVerified $true -AuthPresent "not_applicable" -ModelAvailable "available" -ExternalEgress $false -Source "local_discovery" -Diagnostics $ollamaDiagnostics)) + } +} +else { + $candidates.Add((New-CandidateObservation -Id "ollama_runtime" -ChannelKind "ollama" -Provider "ollama" -Model $null -CostScope "local_compute" -EndpointConfigured ($ollamaSelected.Count -eq 1) -Discovered ($ollamaPaths.Count -gt 0) -ExecutableVerified ($ollamaSelected.Count -eq 1) -AuthPresent "not_applicable" -ModelAvailable "unavailable" -ExternalEgress $false -Source "local_discovery" -Diagnostics (Get-CandidateDiagnostics $ollamaCandidateResults))) +} + +$claudeDiagnostics = @((Get-CandidateDiagnostics $claudeCandidateResults)) + @("auth_method_$claudeAuthMethod") +$candidates.Add((New-CandidateObservation -Id "claude_cli" -ChannelKind "claude_cli" -Provider "anthropic" -Model $null -CostScope "subscription_cli" -EndpointConfigured $claudeConfigPresent -Discovered ($claudePaths.Count -gt 0) -ExecutableVerified ($claudeSelected.Count -eq 1) -AuthPresent $(if ($claudeAuth) { "present" } elseif ($claudeSelected.Count -eq 1) { "absent" } else { "unknown" }) -ModelAvailable "unknown" -ExternalEgress $true -Source "cli_config" -Diagnostics $claudeDiagnostics)) +$candidates.Add((New-CandidateObservation -Id "opencode_cli" -ChannelKind "opencode_cli" -Provider $null -Model $null -CostScope "subscription_cli" -EndpointConfigured $openCodeConfigPresent -Discovered ($openCodePaths.Count -gt 0) -ExecutableVerified ($openCodeSelected.Count -eq 1) -AuthPresent "unknown" -ModelAvailable "unknown" -ExternalEgress $true -Source "cli_config" -Diagnostics (Get-CandidateDiagnostics $openCodeCandidateResults))) +$candidates.Add((New-CandidateObservation -Id "codex_cli" -ChannelKind "codex_cli" -Provider "openai" -Model $null -CostScope "subscription_cli" -EndpointConfigured $codexConfigPresent -Discovered ($codexPaths.Count -gt 0) -ExecutableVerified ($codexSelected.Count -eq 1) -AuthPresent $(if ($codexAuth) { "present" } elseif ($codexSelected.Count -eq 1) { "absent" } else { "unknown" }) -ModelAvailable "unknown" -ExternalEgress $true -Source "cli_config" -Diagnostics (Get-CandidateDiagnostics $codexCandidateResults))) + +$result = [pscustomobject][ordered]@{ + schema = "code-intel-model-channel-inventory-result.v1" + candidates = @($candidates) + configurationBrokers = @( + [pscustomobject][ordered]@{ + id = "cc_switch" + kind = "cc_switch" + discovered = (($ccSwitchPaths.Count -gt 0) -or $ccConfigPresent) + configPresent = $ccConfigPresent + diagnostics = @( + $(if ($ccSwitchPaths.Count -gt 0) { "installation_present" } else { "installation_not_found" }), + $(if ($ccConfigPresent) { "config_present_content_not_read" } else { "config_not_found" }) + ) + }, + [pscustomobject][ordered]@{ + id = "manual_config" + kind = "manual_config" + discovered = ($openAiEndpoint -or $anthropicEndpoint -or -not [string]::IsNullOrWhiteSpace($UserBaseUrl)) + configPresent = ($openAiEndpoint -or $anthropicEndpoint -or -not [string]::IsNullOrWhiteSpace($UserBaseUrl)) + diagnostics = @("presence_only", "credential_values_not_collected", "endpoint_values_not_collected") + } + ) +} + +if ($Json) { $result | ConvertTo-Json -Depth 8 } +else { $result } diff --git a/Invoke-RepowiseProviderProbe.ps1 b/Invoke-RepowiseProviderProbe.ps1 new file mode 100644 index 0000000..328f728 --- /dev/null +++ b/Invoke-RepowiseProviderProbe.ps1 @@ -0,0 +1,222 @@ +#requires -Version 7.2 + +param( + [string]$Provider = "", + [string]$Model = "", + [ValidateSet("", "model_not_found", "authentication_error", "rate_limit", "local_import_error")] + [string]$FailureFixture = "", + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Get-CodeIntelEnvValue { + param([string]$Name) + $value = [Environment]::GetEnvironmentVariable($Name, "Process") + if ([string]::IsNullOrWhiteSpace($value)) { + $value = [Environment]::GetEnvironmentVariable($Name, "User") + } + return $value +} + +function Import-ProcessEnv { + param([string]$Name) + if (-not [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($Name, "Process"))) { return } + $value = [Environment]::GetEnvironmentVariable($Name, "User") + if (-not [string]::IsNullOrWhiteSpace($value)) { + [Environment]::SetEnvironmentVariable($Name, $value, "Process") + } +} + +function Get-RepowisePython { + if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) { + $candidate = Join-Path $env:APPDATA "uv\tools\repowise\Scripts\python.exe" + if (Test-Path -LiteralPath $candidate -PathType Leaf) { return $candidate } + } + return "python" +} + +function Protect-ProviderMessage { + param([string]$Message) + $safe = [string]$Message + foreach ($name in @( + "CODE_INTEL_API_KEY", "CODE_INTEL_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", "ANTHROPIC_AUTH_TOKEN" + )) { + $value = Get-CodeIntelEnvValue $name + if (-not [string]::IsNullOrWhiteSpace($value)) { + $safe = $safe.Replace($value, "[REDACTED]") + } + } + $safe = $safe -replace '(?i)Bearer\s+[A-Za-z0-9._~+/-]+', 'Bearer [REDACTED]' + $safe = $safe -replace '(?i)\b(sk-[A-Za-z0-9_-]{6,})\b', '[REDACTED]' + if ($safe.Length -gt 1000) { $safe = $safe.Substring(0, 1000) } + return $safe +} + +foreach ($name in @( + "ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "OPENAI_API_KEY", "OPENAI_BASE_URL", + "CODE_INTEL_PROVIDER", "CODE_INTEL_MODEL", "CODE_INTEL_API_KEY", "CODE_INTEL_BASE_URL", + "REPOWISE_PROVIDER", "REPOWISE_MODEL", "REPOWISE_REASONING" +)) { + Import-ProcessEnv $name +} + +$dedicatedKey = Get-CodeIntelEnvValue "CODE_INTEL_ANTHROPIC_API_KEY" +$dedicatedUrl = Get-CodeIntelEnvValue "CODE_INTEL_ANTHROPIC_BASE_URL" +if (-not [string]::IsNullOrWhiteSpace($dedicatedKey)) { $env:ANTHROPIC_API_KEY = $dedicatedKey } +if (-not [string]::IsNullOrWhiteSpace($dedicatedUrl)) { $env:ANTHROPIC_BASE_URL = $dedicatedUrl } + +if ([string]::IsNullOrWhiteSpace($Provider)) { $Provider = Get-CodeIntelEnvValue "CODE_INTEL_PROVIDER" } +if ([string]::IsNullOrWhiteSpace($Provider)) { $Provider = "anthropic" } +if ($Provider -ieq "ccw") { $Provider = "codex_cli" } +if ([string]::IsNullOrWhiteSpace($Model)) { $Model = Get-CodeIntelEnvValue "CODE_INTEL_MODEL" } +if ($null -eq $Model) { $Model = "" } + +$result = $null +switch -Regex ($Provider) { + "^mock$" { + $result = [ordered]@{ ok = $true; provider = $Provider; model = $Model; category = ""; message = "mock provider" } + break + } + "^codex_cli$" { + $available = $null -ne (Get-Command codex -ErrorAction SilentlyContinue) + $result = [ordered]@{ + ok = $available; provider = $Provider; model = $Model + category = if ($available) { "" } else { "local_tool_error" } + message = if ($available) { "codex CLI available" } else { "codex CLI not found for Repowise provider" } + } + break + } + "^opencode$" { + $available = $null -ne (Get-Command opencode -ErrorAction SilentlyContinue) + $result = [ordered]@{ + ok = $available; provider = $Provider; model = $Model + category = if ($available) { "" } else { "local_tool_error" } + message = if ($available) { "opencode CLI available" } else { "opencode CLI not found for Repowise provider" } + } + break + } +} + +if ($null -eq $result) { + $python = @' +import json +import os +import sys + +def env(name): + return (os.environ.get(name) or "").strip() + +def classify_failure(exc): + text = str(exc) + lower = text.lower() + type_name = type(exc).__name__.lower() + status = getattr(exc, "status_code", None) + + if isinstance(exc, (ImportError, ModuleNotFoundError, FileNotFoundError)): + return "local_tool_error" + if status == 429 or any(term in lower for term in ("429", "rate_limit", "quota", "usage limit", "status_code': 2056", '"status_code":2056')): + return "provider_quota" + if status == 404 or any(term in lower for term in ("model_not_found", "not_found_error", "error code: 404", "status code: 404")): + return "provider_unavailable" + if status in (401, 403) or any(term in lower for term in ( + "authentication_error", "invalid api key", "not authorized", "token not match", + "no api key configured", "requires key and model", "unsupported repowise provider", + "invalid params", "invalid_request_error", "error code: 400", "status code: 400", + "status_code': 1004", '"status_code":1004', "status_code': 2049", '"status_code":2049' + )): + return "config_error" + if status is not None and status >= 500: + return "provider_unavailable" + if any(term in lower or term in type_name for term in ( + "connection", "connecterror", "timeout", "timed out", "service unavailable", + "internal server error", "bad gateway", "gateway timeout" + )): + return "provider_unavailable" + return "provider_error" + +provider = env("CODE_INTEL_PROVIDER").lower() or "anthropic" +model = env("CODE_INTEL_MODEL") +api_key = env("CODE_INTEL_API_KEY") +base_url = env("CODE_INTEL_BASE_URL") +model = model or {"anthropic": "MiniMax-M2.7"}.get(provider, "") +result = {"ok": False, "provider": provider, "model": model, "category": "", "message": ""} + +try: + fixture = env("CODE_INTEL_PROVIDER_PROBE_FAILURE_FIXTURE") + if fixture == "model_not_found": + raise RuntimeError("Error code: 404 - {'type':'error','error':{'type':'not_found_error','code':'model_not_found','message':'model not found'}}") + if fixture == "authentication_error": + raise RuntimeError("Error code: 401 - {'type':'authentication_error','message':'invalid API key'}") + if fixture == "rate_limit": + raise RuntimeError("Error code: 429 - {'type':'rate_limit_error','message':'usage limit exceeded'}") + if fixture == "local_import_error": + raise ModuleNotFoundError("No module named 'anthropic'") + if provider == "anthropic": + key = api_key or env("ANTHROPIC_API_KEY") + url = base_url or env("ANTHROPIC_BASE_URL") or None + if not key: + raise RuntimeError("No API key configured for the Repowise Anthropic provider") + from anthropic import Anthropic + Anthropic(api_key=key, base_url=url, max_retries=1).messages.create( + model=model, max_tokens=16, messages=[{"role": "user", "content": "reply ok"}] + ) + elif provider == "openai": + key = api_key or env("OPENAI_API_KEY") + url = base_url or env("OPENAI_BASE_URL") or None + if not key or not model: + raise RuntimeError("OpenAI-compatible Repowise provider requires key and model") + from openai import OpenAI + OpenAI(api_key=key, base_url=url, timeout=30, max_retries=1).chat.completions.create( + model=model, max_tokens=16, messages=[{"role": "user", "content": "reply ok"}] + ) + elif provider == "ollama": + import httpx + url = (base_url or env("OLLAMA_BASE_URL") or "http://localhost:11434").rstrip("/") + response = httpx.get(url + "/api/tags", timeout=10) + response.raise_for_status() + names = [item.get("name", "") for item in response.json().get("models", [])] + if model and model not in names and f"{model}:latest" not in names: + raise RuntimeError("configured Ollama model is unavailable") + else: + raise RuntimeError("unsupported Repowise provider health probe") + result["ok"] = True + result["message"] = "provider health probe passed" +except Exception as exc: + text = str(exc) + result["category"] = classify_failure(exc) + result["message"] = text + +print(json.dumps(result, ensure_ascii=False)) +sys.exit(0 if result["ok"] else 1) +'@ + $env:CODE_INTEL_PROVIDER = $Provider + $env:CODE_INTEL_MODEL = $Model + $env:CODE_INTEL_PROVIDER_PROBE_FAILURE_FIXTURE = $FailureFixture + $raw = & (Get-RepowisePython) -c $python + $probeExit = $LASTEXITCODE + $result = $raw | ConvertFrom-Json + $result.message = Protect-ProviderMessage ([string]$result.message) +} +else { + $probeExit = if ($result.ok) { 0 } else { 1 } + $result.message = Protect-ProviderMessage ([string]$result.message) +} + +$output = [ordered]@{ + schema = "code-intel-repowise-provider-health.v1" + kind = "health" + evidence = $false + ok = [bool]$result.ok + provider = [string]$result.provider + model = [string]$result.model + category = [string]$result.category + message = [string]$result.message +} +if ($Json) { $output | ConvertTo-Json -Depth 4 } +elseif ($output.ok) { Write-Host "Repowise provider health: OK $($output.provider)/$($output.model)" } +else { Write-Host "Repowise provider health: FAILED $($output.category) $($output.message)" } + +exit $probeExit diff --git a/Invoke-ScopedRepowise.ps1 b/Invoke-ScopedRepowise.ps1 index 2a7f88c..2d25885 100644 --- a/Invoke-ScopedRepowise.ps1 +++ b/Invoke-ScopedRepowise.ps1 @@ -14,6 +14,15 @@ param( [string]$Provider = "mock", [string]$Model = "", [string]$Reasoning = "auto", + [ValidateSet("unanswered", "granted", "denied")] + [string]$ConsumptionConsent = "unanswered", + [ValidateSet("unanswered", "granted", "denied")] + [string]$ExternalDataConsent = "unanswered", + [ValidateSet("unanswered", "granted", "denied")] + [string]$PaidSpendConsent = "unanswered", + [ValidateSet("local_compute", "subscription_cli", "free_or_internal_quota", "metered_api")] + [string]$CostScope = "metered_api", + [switch]$AllowShadowWorktreeMutation, [switch]$IncludeWorkingTree, [switch]$Docs ) @@ -21,6 +30,17 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +if (-not $AllowShadowWorktreeMutation) { + throw "Scoped Repowise requires explicit -AllowShadowWorktreeMutation authority because it creates and resets an adapter-owned Git worktree." +} +if ($Docs) { + if ($ConsumptionConsent -ne "granted") { throw "Provider-backed docs require explicit consumption consent." } + if ($CostScope -ne "local_compute" -and $ExternalDataConsent -ne "granted") { throw "Non-local provider-backed docs require explicit external-data consent." } + if ($CostScope -eq "metered_api" -and $PaidSpendConsent -ne "granted") { throw "Metered provider-backed docs require explicit paid-spend consent." } + if ([string]::IsNullOrWhiteSpace($Provider) -or $Provider -ieq "mock") { throw "Provider-backed docs require an explicitly pinned non-mock provider route." } + if ([string]::IsNullOrWhiteSpace($Model)) { throw "Provider-backed docs require an explicitly pinned model." } +} + $platformModule = Join-Path (Join-Path $PSScriptRoot "tools") "code-intel-platform.psm1" Import-Module $platformModule -Force $effectivePlatform = Get-CodeIntelPlatform -Platform $Platform @@ -216,18 +236,6 @@ function Get-EffectiveEgressProvider { ) $effective = $RequestedProvider - if ($DocsEnabled) { - $configured = Get-CodeIntelEnvValue "CODE_INTEL_PROVIDER" - if (-not [string]::IsNullOrWhiteSpace($configured)) { - $effective = $configured - } - elseif ($effective -ieq "mock" -or [string]::IsNullOrWhiteSpace($effective)) { - $effective = Get-CodeIntelEnvValue "REPOWISE_PROVIDER" - if ([string]::IsNullOrWhiteSpace($effective)) { - $effective = "anthropic" - } - } - } if ($effective -ieq "ccw") { return "codex_cli" } return $effective.ToLowerInvariant() } @@ -318,10 +326,9 @@ function Write-ScopedConfig { [string]$Reasoning ) - $provider = Get-CodeIntelEnvValue "CODE_INTEL_PROVIDER" - if ([string]::IsNullOrWhiteSpace($provider)) { $provider = "anthropic" } - $model = Get-CodeIntelEnvValue "CODE_INTEL_MODEL" - if ([string]::IsNullOrWhiteSpace($model) -and $provider -eq "anthropic") { $model = "MiniMax-M2.7" } + $provider = $Provider + if ([string]::IsNullOrWhiteSpace($provider)) { $provider = "mock" } + $model = $Model $configDir = Join-Path $ShadowPath ".repowise" New-Item -ItemType Directory -Force -Path $configDir | Out-Null @@ -332,7 +339,7 @@ function Write-ScopedConfig { $lines.Add("model: $model") } $lines.Add("embedder: mock") - $lines.Add("reasoning: auto") + $lines.Add("reasoning: $Reasoning") $lines.Add("commit_limit: $CommitLimit") $lines.Add("editor_files:") $lines.Add(" claude_md: false") @@ -616,10 +623,8 @@ $Provider = if ($Provider -ieq "ccw") { "codex_cli" } else { $Provider } $providerArgs = @("--provider", $Provider) if (-not [string]::IsNullOrWhiteSpace($Model)) { $providerArgs += @("--model", $Model) } if (-not [string]::IsNullOrWhiteSpace($Reasoning)) { $providerArgs += @("--reasoning", $Reasoning) } -# Docs must never silently run the "mock" default (index-only legs use mock on -# purpose; mock docs would emit junk pages). When -Provider was not explicitly -# set past the default, omit --provider so Run-ScopedRepowiseDocs.py resolves -# the real provider from CODE_INTEL_PROVIDER (default anthropic). +# Docs use only the explicitly pinned provider/model route. Environment values +# may supply credentials for that route but may not select a different route. $docsProviderArgs = @() if (-not [string]::IsNullOrWhiteSpace($Provider) -and $Provider -ine "mock") { $docsProviderArgs += @("--provider", $Provider) } if (-not [string]::IsNullOrWhiteSpace($Model)) { $docsProviderArgs += @("--model", $Model) } diff --git a/Invoke-WorkflowRecommendation.ps1 b/Invoke-WorkflowRecommendation.ps1 new file mode 100644 index 0000000..130bdfe --- /dev/null +++ b/Invoke-WorkflowRecommendation.ps1 @@ -0,0 +1,26 @@ +param( + [Parameter(Mandatory = $true)] + [string]$RepoPath, + [switch]$Auto, + [switch]$Quiet, + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$atom = Join-Path $PSScriptRoot "OpenSpec-Detector.ps1" +if (-not (Test-Path -LiteralPath $atom -PathType Leaf)) { + throw "Workflow recommendation atom not found: $atom" +} + +if ($Quiet) { + $result = & $atom -RepoPath $RepoPath -Auto:$Auto -Quiet 6>$null +} +else { + $result = & $atom -RepoPath $RepoPath -Auto:$Auto +} +if ($Json) { + return $result | ConvertTo-Json -Depth 30 -Compress +} +return $result diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b2ba9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Code Intel Pipeline contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/New-ModelAdapterRequest.ps1 b/New-ModelAdapterRequest.ps1 new file mode 100644 index 0000000..e9f8b86 --- /dev/null +++ b/New-ModelAdapterRequest.ps1 @@ -0,0 +1,100 @@ +#requires -Version 7.2 + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$Inventory, + [Parameter(Mandatory = $true)][string]$Routing, + [Parameter(Mandatory = $true)][string]$PromptFile, + [Parameter(Mandatory = $true)][string]$OutputPath, + [string]$ExecutableHandle, + [string]$Endpoint, + [ValidateSet("openai", "anthropic", "ollama")][string]$Protocol, + [string]$CredentialEnvName, + [ValidateRange(1, 3600)][int]$TimeoutSeconds = 300, + [ValidateSet("json", "jsonl")][string]$ResponseFormat = "json" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Read-ClosedJson([string]$Path, [string]$Label) { + try { $raw = Get-Content -LiteralPath (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path -Raw } + catch { throw "cannot read $Label" } + try { $doc = [System.Text.Json.JsonDocument]::Parse($raw) } catch { throw "$Label is not valid JSON" } + try { + function Assert-Unique([System.Text.Json.JsonElement]$Element, [string]$At = '$') { + if ($Element.ValueKind -eq [System.Text.Json.JsonValueKind]::Object) { + $names = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + foreach ($p in $Element.EnumerateObject()) { + if (-not $names.Add($p.Name)) { throw "duplicate JSON key at $At.$($p.Name)" } + Assert-Unique $p.Value "$At.$($p.Name)" + } + } elseif ($Element.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) { + $i = 0; foreach ($v in $Element.EnumerateArray()) { Assert-Unique $v "$At[$i]"; $i++ } + } + } + Assert-Unique $doc.RootElement + } finally { $doc.Dispose() } + return ($raw | ConvertFrom-Json -ErrorAction Stop) +} + +function Assert-ExactFields([object]$Object, [string[]]$Expected, [string]$Label) { + $actual = @($Object.PSObject.Properties.Name | Sort-Object) + if (($actual -join "`n") -ne (($Expected | Sort-Object) -join "`n")) { throw "$Label has an unsupported shape" } +} + +$inventoryObject = Read-ClosedJson $Inventory "model inventory" +$routingObject = Read-ClosedJson $Routing "model routing" +Assert-ExactFields $inventoryObject @("schema", "candidates", "configurationBrokers") "model inventory" +Assert-ExactFields $routingObject @("schema", "status", "selected", "authorization", "attempts", "manualAction") "model routing" +if ([string]$inventoryObject.schema -ne "code-intel-model-channel-inventory-result.v1") { throw "model inventory schema is unsupported" } +if ([string]$routingObject.schema -ne "code-intel-model-routing-result.v1" -or [string]$routingObject.status -ne "ready" -or $null -eq $routingObject.selected) { throw "model routing is not ready" } +$selected = $routingObject.selected +$matches = @($inventoryObject.candidates | Where-Object { [string]$_.id -eq [string]$selected.candidateId }) +if ($matches.Count -ne 1) { throw "selected candidate does not resolve uniquely in inventory" } +$candidate = $matches[0] +foreach ($field in @("channelKind", "provider", "model", "costScope")) { + if ([string]$candidate.$field -ne [string]$selected.$field) { throw "selected candidate does not match inventory field '$field'" } +} +if (-not [bool]$candidate.discovered -or -not [bool]$candidate.executableVerified -or [string]$candidate.modelAvailable -ne "available") { throw "selected candidate is not execution-ready in inventory" } +$adapter = [string]$candidate.channelKind +$isHttp = $adapter -in @("ollama", "local_compatible") +$model = [string]$selected.model +if ([string]::IsNullOrWhiteSpace($model)) { throw "selected route has no concrete model" } +if (-not $isHttp -and [string]::IsNullOrWhiteSpace($ExecutableHandle)) { throw "CLI request synthesis requires an executable handle" } +if ($isHttp -and [string]::IsNullOrWhiteSpace($Endpoint)) { throw "HTTP request synthesis requires an explicit endpoint" } +if ($adapter -eq "ollama" -and $Protocol -ne "ollama") { throw "ollama synthesis requires protocol=ollama" } +if ($adapter -eq "local_compatible" -and $Protocol -notin @("openai", "anthropic")) { throw "local compatible synthesis requires protocol=openai or anthropic" } +if (-not $isHttp -and (-not [string]::IsNullOrWhiteSpace($Endpoint) -or -not [string]::IsNullOrWhiteSpace($Protocol) -or -not [string]::IsNullOrWhiteSpace($CredentialEnvName))) { throw "CLI synthesis does not accept HTTP configuration" } + +$consumption = [string]$routingObject.authorization.consumptionAuthorization.status +$scopes = @($routingObject.authorization.consumptionAuthorization.scopes) +$external = [string]$routingObject.authorization.externalData.status +$paid = [string]$routingObject.authorization.paidSpend.status +$local = if ($consumption -eq "granted" -and $scopes -contains "local_compute") { "granted" } else { "unanswered" } +$derivedExternalData = [bool]$candidate.externalEgress +if (-not $isHttp) { $derivedExternalData = $true } +if ($isHttp) { + $uri = $null + if (-not [Uri]::TryCreate($Endpoint, [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -notin @("http", "https")) { throw "endpoint must be an absolute HTTP(S) URI" } + if (-not $uri.IsLoopback) { $derivedExternalData = $true } +} +$request = [ordered]@{ + schema = "code-intel-model-adapter-request.v2" + adapter = $adapter + executableHandle = if ($isHttp) { $null } else { (Resolve-Path -LiteralPath $ExecutableHandle -ErrorAction Stop).Path } + endpoint = if ($isHttp) { $Endpoint } else { $null } + protocol = if ($isHttp) { $Protocol } else { $null } + credentialEnvName = if ($isHttp -and -not [string]::IsNullOrWhiteSpace($CredentialEnvName)) { $CredentialEnvName } else { $null } + model = $model + promptFile = (Resolve-Path -LiteralPath $PromptFile -ErrorAction Stop).Path + timeoutSeconds = $TimeoutSeconds + consent = [ordered]@{ consumption = $consumption; localCompute = $local; paidSpend = $paid; externalData = $external } + costScope = [string]$candidate.costScope + externalData = $derivedExternalData + responseFormat = $ResponseFormat +} +$parent = Split-Path -Parent ([IO.Path]::GetFullPath($OutputPath)) +if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } +[IO.File]::WriteAllText([IO.Path]::GetFullPath($OutputPath), ($request | ConvertTo-Json -Depth 8), [Text.UTF8Encoding]::new($false)) +$request | ConvertTo-Json -Depth 8 -Compress diff --git a/New-ModelExecutableHandle.ps1 b/New-ModelExecutableHandle.ps1 new file mode 100644 index 0000000..4107967 --- /dev/null +++ b/New-ModelExecutableHandle.ps1 @@ -0,0 +1,48 @@ +#requires -Version 7.2 + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][ValidateSet("claude_cli", "opencode_cli", "codex_cli")][string]$Adapter, + [Parameter(Mandatory = $true)][string]$Executable, + [Parameter(Mandatory = $true)][string]$OutputPath, + [ValidateRange(1, 3600)][int]$LifetimeSeconds = 300 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$path = (Resolve-Path -LiteralPath $Executable -ErrorAction Stop).Path +$item = Get-Item -LiteralPath $path -ErrorAction Stop +if (-not $item.PSIsContainer -and $item.Length -ge 0) { + $observedAt = [DateTimeOffset]::UtcNow + $sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() + $identityMaterial = @( + "code-intel-model-executable-handle.v1", + $Adapter, + $path, + $sha256, + [string]$item.Length, + [string]$item.LastWriteTimeUtc.Ticks, + $observedAt.ToString("O"), + $observedAt.AddSeconds($LifetimeSeconds).ToString("O") + ) -join "`n" + $identityBytes = [Text.Encoding]::UTF8.GetBytes($identityMaterial) + $identity = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($identityBytes)).ToLowerInvariant() + $handle = [ordered]@{ + schema = "code-intel-model-executable-handle.v1" + adapter = $Adapter + executablePath = $path + sha256 = $sha256 + length = [long]$item.Length + lastWriteTimeUtcTicks = [long]$item.LastWriteTimeUtc.Ticks + observedAt = $observedAt.ToString("O") + expiresAt = $observedAt.AddSeconds($LifetimeSeconds).ToString("O") + identity = $identity + } + $parent = Split-Path -Parent ([IO.Path]::GetFullPath($OutputPath)) + if ($parent) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } + [IO.File]::WriteAllText([IO.Path]::GetFullPath($OutputPath), ($handle | ConvertTo-Json -Depth 4), [Text.UTF8Encoding]::new($false)) + $handle | ConvertTo-Json -Depth 4 -Compress + exit 0 +} +throw "executable must be a file" diff --git a/OpenSpec-Detector.ps1 b/OpenSpec-Detector.ps1 index 6b90de0..a56def0 100644 --- a/OpenSpec-Detector.ps1 +++ b/OpenSpec-Detector.ps1 @@ -1,15 +1,16 @@ -# 三栈工作流推荐器 (Workflow Stack Recommender) - standalone script -# 按项目特征分层推荐: matt-flow (idea->ship) / gstack (delivery/quality) / spec-driven (openspec-opsx | spec-kit) -# NOTE: this logic is duplicated in run-code-intel.ps1 (inline, ~line 86+). Keep both copies in sync when editing. +# Advisory workflow recommendation atom. +# Candidates are recommendations only; this atom never adopts, initializes, or executes them. param( [string]$RepoPath, [switch]$Auto, # 自动模式,不询问用户(本检测器不做交互提示,保留兼容旧参数) + [switch]$Quiet, [switch]$Verbose ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +if ($Quiet) { $InformationPreference = "SilentlyContinue" } # ============ 特征检测 ============ @@ -507,20 +508,42 @@ foreach ($wf in $workflows) { } Write-Host "" -# 返回结果用于管道 (向后兼容: 顶层字段沿用旧 openSpec 结果形状 = spec-driven 层) -$result = [ordered]@{ - workflows = $workflows - specDriven = $specDriven - mattFlow = $mattFlow - gstack = $gstack - # legacy top-level aliases (spec-driven layer) - stack = $specDriven.stack - tool = $specDriven.tool - verdict = $specDriven.verdict - score = $specDriven.score - reasons = $specDriven.reasons -entrySkills = $specDriven.entrySkills -recommendationBrief = $specDriven.recommendationBrief -} +$confidence = [string]$specDriven.recommendationBrief.confidence +$evidence = @( + [ordered]@{ kind = "repository-metrics"; value = "files=$($metrics.files);lines=$($metrics.lines);repoAgeDays=$($collaboration.repoAgeDays)" }, + [ordered]@{ kind = "governance"; value = "openSpec=$($governance.hasOpenSpec);specKit=$($governance.hasSpecKit);tests=$hasTests;cicdScore=$cicdScore" } +) +$alternatives = @($workflows | ForEach-Object { + [ordered]@{ + candidate = if ($_.ContainsKey("tool") -and $_.tool) { [string]$_.tool } else { [string]$_.stack } + stack = [string]$_.stack + verdict = [string]$_.verdict + score = if ($_.ContainsKey("score")) { [int]$_.score } else { 0 } + reasons = @($_.reasons) + entrySkills = @($_.entrySkills) + } +}) -return $result +return [ordered]@{ + schema = "code-intel-advisory-workflow-recommendation.v1" + kind = "proposal" + recommendation = [ordered]@{ + candidate = [string]$specDriven.tool + stack = [string]$specDriven.stack + verdict = [string]$specDriven.verdict + score = [int]$specDriven.score + reasons = @($specDriven.reasons) + entrySkills = @($specDriven.entrySkills) + brief = $specDriven.recommendationBrief + } + evidence = $evidence + confidence = $confidence + alternatives = $alternatives + provenance = [ordered]@{ + capabilityId = "advisory.workflow-recommend" + implementation = "OpenSpec-Detector.ps1" + repository = $RepoPath + compatibilityOptions = [ordered]@{ auto = [bool]$Auto } + } + effects = @() +} diff --git a/README.md b/README.md index bf9ac80..b5c9df3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Code Intel Pipeline +> **语言方向:** 停止新增 PowerShell。现有 `.ps1` 只作为兼容入口维护,不再承载新产品逻辑;生产能力默认使用 Rust。MoonBit 可用于隔离实验,只有通过 artifact 契约一致性、跨平台构建和测试后才进入正式路径。迁移不做一次性重写,按入口逐个替换和退休。Agent 规则见 [AGENTS.md](AGENTS.md)。 + +Workflow-stack guidance is emitted by the read-only `advisory.workflow-recommend` atom. See [docs/advisory-workflow-recommendation.md](docs/advisory-workflow-recommendation.md); recommendations are proposals with zero effects and never authorize tool initialization or adoption. + +Follow-up automation can proactively propose `/investigate` for actionable scan failures and can ask whether to enter the exact draft-PR flow. The one-command orchestrator composes proposal → user decision → C07 record/replay → fail-closed executor. It defaults to suggestion-on and PR-consent-required; neither path silently executes a skill or creates a PR. See [docs/follow-up-automation.md](docs/follow-up-automation.md). +

GPT娘正在给代码仓库画结构地图

@@ -18,12 +24,12 @@ ## 仓库入口 -这个仓库的根目录暂时保留 PowerShell 入口,是为了兼容已发布包和团队脚本。真正的治理边界见 [Repository Layout](docs/repository-layout.md)。 +这个仓库的根目录暂时保留 PowerShell 入口,只为兼容已发布包和团队脚本;它们处于退休路径,不接受新产品逻辑。新实现优先进入 Rust core,MoonBit 仅用于隔离验证。真正的治理边界见 [Repository Layout](docs/repository-layout.md)。 公共入口: - `invoke-code-intel.ps1`: 推荐人工入口,先 doctor 再运行 pipeline。 -- `run-code-intel.ps1`: 当前 PowerShell orchestrator,负责生成 artifacts。 +- `run-code-intel.ps1`: 兼容 facade;默认 normal 路径调用 Rust DAG、原子提交和 committed-only 索引,旧扫描器分支必须显式启用。 - `check-code-intel-tools.ps1`: 环境 doctor。 - `install-code-intel-pipeline.ps1`: 安装和修复入口。 - `Find-CodeIntelProjects.ps1`: 项目发现入口。 @@ -32,6 +38,26 @@ 内部脚本、benchmark、实验入口后续分批迁到 `scripts/` 或 incubator 目录;每次迁移必须保留兼容 shim 或同步更新 CI/release。 +## Public beta 范围 + +当前已发布兼容面仍是 **Windows PowerShell public beta**,但后续功能开发已经转向 Rust/MoonBit 路线,不再扩大 PowerShell surface。测试版承诺的是:稳定入口可运行、核心报告可事务化落盘、结构退化不会被伪装成成功、发布 ZIP 可在没有源码树和 Rust toolchain 的干净目录中启动。 + +0.3.0 核心路径: + +- `invoke-code-intel.ps1` / `run-code-intel.ps1` +- `code-intel.exe` 的 A01-A09 capability/DAG/policy/artifact core +- `rg` inventory、native code evidence、内部 graph provider、真实 Sentrux `gate`/`check` 命令证据和 Hospital diagnosis +- snapshot-bound staging、A07 原子提交、A08 completed-only 索引、query/impact/freshness + +默认包含但不阻塞测试版的增强能力: + +- Repowise 语义索引与文档生成 +- Understand Anything 图谱 +- CodeNexus context、Repomix、模型辅助通道 +- runtime/CI 证据和 file-boundary provider + +缺少这些增强工具时,流水线必须明确记录 skipped、manual-required 或 provider failure,不能把它们冒充成功,也不能因此阻断只依赖核心路径的 public beta。详细支持矩阵和已知边界见 [Public beta guide](docs/public-beta.md)。 + ## 这是什么 `Code Intel Pipeline` 是一套本地仓库理解工具链。 @@ -137,6 +163,18 @@ Project management support contract 测试: .\test-project-management-support.ps1 -RepoPath C:\path\to\your\repo ``` +从 GitHub Release ZIP 运行时,解压后直接使用稳定入口;不需要 Cargo,也不依赖仓库里的 `target/`: + +```powershell +.\invoke-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal +``` + +如果不需要语义索引,或本机没有 Repowise: + +```powershell +.\invoke-code-intel.ps1 -RepoPath C:\path\to\your\repo -Mode normal -SkipRepowise +``` + Greenfield 行为规格适配器测试: ```powershell @@ -183,7 +221,7 @@ install -> doctor -> smoke test | --- | --- | --- | | Integration orchestration | 融合注册、能力编排、扩展边界 | `target/debug/code-intel.exe orchestrate` | | `code-intel` Rust CLI | orchestration、artifact resume、failure classify、artifact doctor | `target/debug/code-intel.exe` | -| `code-nexus-lite` Rust worker | CodeNexus scan/lite/doctor worker | `target/debug/code-nexus-lite.exe` | +| CodeNexus compatibility adapter | 可选热点定位、引用搜索、下一步上下文;失败不阻塞 beta core | `codenexus-context.json` | | `rg` | 快速文件清单、文本搜索 | `files.txt` | | `Repowise` | 语义索引、长期记忆、项目上下文 | `.repowise/` 或 scoped shadow | | `Repomix` | 把本地或远程仓库打包成 AI 友好的单文件上下文 | `repomix-output.md`、`repomix-summary.json` | @@ -216,6 +254,7 @@ cargo build -p code-intel 核心报告: ```text +run-complete.json summary.md report.json understanding.md @@ -225,6 +264,9 @@ surgery-plan.md surgery-plan.json ``` +`run-complete.json` 是最后写入的事务提交标记;索引只接受标记存在且 +`reportSha256` 与已发布 `report.json` 一致的运行目录。 + Artifact ownership and stable routing fields are defined in [`docs/artifact-data-contract.md`](docs/artifact-data-contract.md). For vague or long-running Agent work, define the task contract first with @@ -272,6 +314,16 @@ greenfield-plan.md 5. 做治理判断看 `hospital.md`。 6. 要开工修结构看 `surgery-plan.md`。 +## Portable Snapshot Identity + +仓库输入身份可以独立计算,不依赖时间戳目录或机器绝对路径: + +```powershell +target/debug/code-intel.exe snapshot identity --repo --working-tree-policy explicit_overlay --scope . +``` + +它绑定 Git lineage、HEAD、工作树策略、规范化 scope 与实际输入字节,并逐类报告 dirty overlay。shallow、unborn、无 Git、ignored、symlink、submodule、LFS 与并发变化规则见 `docs/repository-snapshot-identity.md`。 + ## Governance Mode Governance Mode 是这套工具的产品层。它把工具输出变成一个状态机: @@ -753,6 +805,8 @@ cargo build -p code-intel .\target\debug\code-intel.exe classify --report C:\path\to\artifact\report.json ``` -The Rust CLI owns integration orchestration and cross-session artifact reads. -PowerShell scripts remain Windows compatibility wrappers for scanner steps that -have not yet been absorbed into Rust. +The Rust CLI owns the default normal production spine, integration orchestration, +snapshot-bound evidence, atomic run publication, committed-only indexing, and +cross-session query/impact reads. PowerShell scripts remain thin Windows +compatibility and installation facades; legacy report generation is not an +alternative authority path. diff --git a/Run-ScopedRepowiseDocs.py b/Run-ScopedRepowiseDocs.py index b33bd56..1e8eaca 100644 --- a/Run-ScopedRepowiseDocs.py +++ b/Run-ScopedRepowiseDocs.py @@ -30,8 +30,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--repo", required=True) parser.add_argument("--coverage-pct", type=float, default=0.02) parser.add_argument("--concurrency", type=int, default=1) - parser.add_argument("--provider", default=os.environ.get("REPOWISE_PROVIDER", "anthropic")) - parser.add_argument("--model", default=os.environ.get("REPOWISE_MODEL", "")) + parser.add_argument("--provider", default="") + parser.add_argument("--model", default="") parser.add_argument("--reasoning", default=os.environ.get("REPOWISE_REASONING", "auto")) parser.add_argument("--egress-manifest", required=True) return parser.parse_args() @@ -40,11 +40,6 @@ def parse_args() -> argparse.Namespace: # Providers whose __init__ does not accept an api_key kwarg. _KEYLESS_PROVIDERS = {"ollama", "codex_cli", "opencode", "mock"} -_DEFAULT_MODELS = { - "anthropic": "MiniMax-M2.7", -} - - def _env(name: str) -> str: return (os.environ.get(name) or "").strip() @@ -53,8 +48,7 @@ def resolve_provider_settings(default_provider: str = "", default_model: str = " """Resolve provider name + kwargs from CODE_INTEL_* env vars. Env contract: - CODE_INTEL_PROVIDER provider name from repowise registry (default: anthropic) - CODE_INTEL_MODEL model name (anthropic default: MiniMax-M2.7) + CODE_INTEL_PROVIDER provider name from repowise registry CODE_INTEL_API_KEY generic credential (skipped for keyless providers e.g. ollama) CODE_INTEL_BASE_URL generic endpoint override @@ -62,15 +56,14 @@ def resolve_provider_settings(default_provider: str = "", default_model: str = " the process-scoped ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL that the calling PowerShell wrapper injects from user-scoped CODE_INTEL_ANTHROPIC_*. - default_provider/default_model come from --provider/--model CLI args - (REPOWISE_PROVIDER/REPOWISE_MODEL env), used only when the CODE_INTEL_* - vars are unset -- CODE_INTEL_* takes priority since it is what the - PowerShell wrapper actively manages for this pipeline. + default_provider/default_model come from the explicitly routed + --provider/--model CLI args and are pinned. Model selection never falls + back to the environment or a provider default. """ - name = _env("CODE_INTEL_PROVIDER").lower() or (default_provider or "").lower() or "anthropic" + name = (default_provider or "").lower() or _env("CODE_INTEL_PROVIDER").lower() or "mock" if name == "ccw": name = "codex_cli" - model = _env("CODE_INTEL_MODEL") or default_model or _DEFAULT_MODELS.get(name, "") + model = default_model api_key = _env("CODE_INTEL_API_KEY") base_url = _env("CODE_INTEL_BASE_URL") diff --git a/Test-CodeIntelProjectConformance.ps1 b/Test-CodeIntelProjectConformance.ps1 new file mode 100644 index 0000000..7b35647 --- /dev/null +++ b/Test-CodeIntelProjectConformance.ps1 @@ -0,0 +1,189 @@ +#requires -Version 7.2 + +param( + [ValidateSet("fast", "full")] + [string]$Profile = "fast", + + [string]$Policy = (Join-Path $PSScriptRoot "orchestration\code-intel-project-conformance-policy.v1.json"), + + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$root = [System.IO.Path]::GetFullPath($PSScriptRoot) +$gates = [System.Collections.Generic.List[object]]::new() +$suiteResults = [System.Collections.Generic.List[object]]::new() + +function Add-Gate { + param([string]$Id, [bool]$Passed, [string]$Detail) + $gates.Add([pscustomobject]@{ id = $Id; passed = $Passed; detail = $Detail }) +} + +function Test-ExactSet { + param([object[]]$Actual, [object[]]$Expected) + $actualItems = @($Actual | ForEach-Object { [string]$_ } | Sort-Object) + $expectedItems = @($Expected | ForEach-Object { [string]$_ } | Sort-Object) + return $actualItems.Count -eq $expectedItems.Count -and @(Compare-Object $actualItems $expectedItems).Count -eq 0 +} + +function Resolve-RepoPath { + param([string]$RelativePath, [switch]$Leaf) + if ([string]::IsNullOrWhiteSpace($RelativePath) -or [System.IO.Path]::IsPathRooted($RelativePath)) { + throw "path must be repository-relative: $RelativePath" + } + $prefix = [System.IO.Path]::TrimEndingDirectorySeparator($root) + [System.IO.Path]::DirectorySeparatorChar + $resolved = [System.IO.Path]::GetFullPath((Join-Path $root $RelativePath)) + if (-not $resolved.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "path escapes repository: $RelativePath" + } + if (-not (Test-Path -LiteralPath $resolved)) { + throw "required path is missing: $RelativePath" + } + if ($Leaf -and -not (Test-Path -LiteralPath $resolved -PathType Leaf)) { + throw "required file is missing: $RelativePath" + } + return $resolved +} + +function Complete-Result { + param([int]$MalformedExit = 0) + $failed = @($gates | Where-Object { -not $_.passed }) + $result = [ordered]@{ + schema = "code-intel-project-conformance-result.v1" + profile = $Profile + verdict = if ($failed.Count -eq 0 -and $MalformedExit -eq 0) { "pass" } else { "fail" } + gates = @($gates) + failedGateIds = @($failed | ForEach-Object id) + suites = @($suiteResults) + } + if ($Json) { + $result | ConvertTo-Json -Depth 12 + } else { + Write-Host "Code Intel project conformance: $($result.verdict) profile=$Profile" + foreach ($gate in $gates) { + Write-Host "$(if ($gate.passed) { 'PASS' } else { 'FAIL' }) $($gate.id): $($gate.detail)" + } + foreach ($suite in $suiteResults) { + Write-Host "$(if ($suite.passed) { 'PASS' } else { 'FAIL' }) suite/$($suite.id): exit=$($suite.exitCode)" + } + } + if ($MalformedExit -ne 0) { exit $MalformedExit } + if ($failed.Count -gt 0) { exit 1 } + exit 0 +} + +try { + $policyDocument = Get-Content -Raw -LiteralPath $Policy | ConvertFrom-Json -Depth 30 +} catch { + Add-Gate "input-shape" $false $_.Exception.Message + Complete-Result -MalformedExit 2 +} + +$profileProperty = $policyDocument.profiles.PSObject.Properties[$Profile] +$shapePass = [string]$policyDocument.schema -eq "code-intel-project-conformance-policy.v1" -and + $null -ne $profileProperty -and + -not [string]::IsNullOrWhiteSpace([string]$policyDocument.sourceMethod.uri) -and + [string]$policyDocument.sourceMethod.revision -match '^[0-9a-f]{40}$' +Add-Gate "input-shape" $shapePass "policy schema, profile, source URI, and pinned revision are required" +if (-not $shapePass) { Complete-Result -MalformedExit 2 } + +$requiredMapping = @( + "reference-oracle", + "conformance-corpus", + "normalized-output-parity", + "monotonic-floor", + "expected-divergence-ledger", + "mutation-robustness", + "deterministic-stress", + "performance-ratchet" +) +$mechanisms = @($policyDocument.mechanisms) +$mechanismIds = @($mechanisms | ForEach-Object { [string]$_.id }) +$mappingPass = @($mechanismIds | Sort-Object -Unique).Count -eq $mechanismIds.Count -and + @($requiredMapping | Where-Object { $_ -notin $mechanismIds }).Count -eq 0 +foreach ($mechanism in $mechanisms) { + if ([string]$mechanism.status -notin @("implemented", "partial", "designed", "deferred")) { $mappingPass = $false } + if ([string]::IsNullOrWhiteSpace([string]$mechanism.ponForm) -or [string]::IsNullOrWhiteSpace([string]$mechanism.codeIntelForm)) { $mappingPass = $false } + foreach ($evidencePath in @($mechanism.evidence)) { + try { $null = Resolve-RepoPath ([string]$evidencePath) } catch { $mappingPass = $false } + } +} +Add-Gate "policy-mapping" $mappingPass "all eight Pon mechanisms have explicit Code Intel mappings and valid statuses" + +$profilePolicy = $profileProperty.Value +$acceptedStatuses = @($profilePolicy.acceptedMechanismStatuses | ForEach-Object { [string]$_ }) +$expectedProfileMechanisms = if ($Profile -eq "fast") { + @("reference-oracle", "conformance-corpus", "normalized-output-parity", "monotonic-floor", "mutation-robustness", "deterministic-stress") +} else { + $requiredMapping +} +$expectedProfileSuites = if ($Profile -eq "fast") { + @("parity-floor", "adapter-contract", "multilanguage-corpus", "python314-development", "merge-queue-contract") +} else { + @("parity-floor", "adapter-contract", "multilanguage-corpus", "python314-development", "merge-queue-contract", "pipeline-smoke") +} +$expectedStatuses = if ($Profile -eq "fast") { @("implemented", "partial") } else { @("implemented") } +$declaredProfileMechanisms = @($profilePolicy.requiredMechanisms | ForEach-Object { [string]$_ }) +$declaredProfileSuites = @($profilePolicy.requiredSuites | ForEach-Object { [string]$_ }) +$profileContractPass = (Test-ExactSet $declaredProfileMechanisms $expectedProfileMechanisms) -and + (Test-ExactSet $declaredProfileSuites $expectedProfileSuites) -and + (Test-ExactSet $acceptedStatuses $expectedStatuses) +Add-Gate "profile-contract" $profileContractPass "required mechanisms, suites, and accepted statuses cannot be weakened" + +$unready = [System.Collections.Generic.List[string]]::new() +foreach ($requiredId in @($profilePolicy.requiredMechanisms)) { + $mechanism = $mechanisms | Where-Object id -eq $requiredId | Select-Object -First 1 + if ($null -eq $mechanism -or [string]$mechanism.status -notin $acceptedStatuses) { + $status = if ($null -eq $mechanism) { "missing" } else { [string]$mechanism.status } + $unready.Add("$requiredId=$status") + } +} +$readinessPass = $unready.Count -eq 0 +Add-Gate "mechanism-readiness" $readinessPass "unready=$($unready -join ', '); accepted=$($acceptedStatuses -join ',')" + +$suiteIds = @($policyDocument.suites | ForEach-Object { [string]$_.id }) +$requiredSuiteIds = @($profilePolicy.requiredSuites | ForEach-Object { [string]$_ }) +$suiteContractPass = @($suiteIds | Sort-Object -Unique).Count -eq $suiteIds.Count -and + @($requiredSuiteIds | Where-Object { $_ -notin $suiteIds }).Count -eq 0 +Add-Gate "suite-contract" $suiteContractPass "required suites exist and suite ids are unique" + +if ($mappingPass -and $profileContractPass -and $readinessPass -and $suiteContractPass) { + foreach ($suiteId in $requiredSuiteIds) { + $suite = $policyDocument.suites | Where-Object id -eq $suiteId | Select-Object -First 1 + $command = $suite.command + $output = @() + $exitCode = 1 + try { + if ([string]$command.kind -eq "pwsh") { + $scriptPath = Resolve-RepoPath ([string]$command.file) -Leaf + $output = @(& pwsh -NoProfile -File $scriptPath @($command.args) 2>&1 | ForEach-Object { $_.ToString() }) + $exitCode = $LASTEXITCODE + } elseif ([string]$command.kind -eq "process") { + $output = @(& ([string]$command.executable) @($command.args) 2>&1 | ForEach-Object { $_.ToString() }) + $exitCode = $LASTEXITCODE + } else { + throw "unsupported suite command kind: $($command.kind)" + } + } catch { + $output = @($_.Exception.Message) + $exitCode = 1 + } + $text = ($output -join "`n") + if ($text.Length -gt 4000) { $text = $text.Substring($text.Length - 4000) } + $suiteResults.Add([pscustomobject]@{ + id = $suiteId + passed = $exitCode -eq 0 + exitCode = $exitCode + mechanisms = @($suite.mechanisms) + outputTail = $text + }) + } +} + +$suitePass = $readinessPass -and $suiteContractPass -and + $suiteResults.Count -eq $requiredSuiteIds.Count -and + @($suiteResults | Where-Object { -not $_.passed }).Count -eq 0 +Add-Gate "executable-suites" $suitePass "passed=$(@($suiteResults | Where-Object passed).Count)/$($requiredSuiteIds.Count)" + +Complete-Result diff --git a/Test-LanguageAdapterAcceptance.ps1 b/Test-LanguageAdapterAcceptance.ps1 new file mode 100644 index 0000000..0c82bba --- /dev/null +++ b/Test-LanguageAdapterAcceptance.ps1 @@ -0,0 +1,202 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Report, + + [string]$Policy = (Join-Path $PSScriptRoot "orchestration\language-adapter-acceptance-policy.v1.json"), + + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$gates = [System.Collections.Generic.List[object]]::new() + +function Add-Gate { + param([string]$Id, [bool]$Passed, [string]$Detail) + $gates.Add([pscustomobject]@{ id = $Id; passed = $Passed; detail = $Detail }) +} + +function Test-ContainsAll { + param([object[]]$Actual, [object[]]$Required) + foreach ($item in $Required) { + if ($Actual -notcontains $item) { return $false } + } + return $true +} + +function Resolve-RepoBoundFile { + param([string]$RelativePath) + if ([string]::IsNullOrWhiteSpace($RelativePath) -or [System.IO.Path]::IsPathRooted($RelativePath)) { + throw "provenance path must be repository-relative: $RelativePath" + } + $root = [System.IO.Path]::GetFullPath($PSScriptRoot) + $prefix = [System.IO.Path]::TrimEndingDirectorySeparator($root) + [System.IO.Path]::DirectorySeparatorChar + $resolved = [System.IO.Path]::GetFullPath((Join-Path $root $RelativePath)) + if (-not $resolved.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "provenance path escapes repository: $RelativePath" + } + if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { + throw "provenance file is missing: $RelativePath" + } + return $resolved +} + +function Write-ResultAndExit { + param([string]$AdapterId, [string]$Stage, [int]$MalformedExit = 0) + $failed = @($gates | Where-Object { -not $_.passed }) + $result = [ordered]@{ + schema = "code-intel-language-adapter-acceptance-result.v1" + adapterId = $AdapterId + requestedStage = $Stage + verdict = if ($failed.Count -eq 0 -and $MalformedExit -eq 0) { "pass" } else { "fail" } + gates = @($gates) + failedGateIds = @($failed | ForEach-Object { $_.id }) + } + if ($Json) { + $result | ConvertTo-Json -Depth 8 + } else { + Write-Host "Language adapter acceptance: $($result.verdict) adapter=$AdapterId stage=$Stage" + foreach ($gate in $gates) { + Write-Host "$(if ($gate.passed) { 'PASS' } else { 'FAIL' }) $($gate.id): $($gate.detail)" + } + } + if ($MalformedExit -ne 0) { exit $MalformedExit } + if ($failed.Count -gt 0) { exit 1 } + exit 0 +} + +try { + $reportDocument = Get-Content -Raw -LiteralPath $Report | ConvertFrom-Json + $policyDocument = Get-Content -Raw -LiteralPath $Policy | ConvertFrom-Json +} catch { + Add-Gate -Id "input-shape" -Passed $false -Detail $_.Exception.Message + Write-ResultAndExit -AdapterId "" -Stage "" -MalformedExit 2 +} + +$adapterId = [string]$reportDocument.adapter.id +$stage = [string]$reportDocument.adapter.requestedStage +$claimLevel = [string]$reportDocument.adapter.claimLevel +$stageProperty = $policyDocument.stages.PSObject.Properties[$stage] + +if ([string]$reportDocument.schema -ne "code-intel-language-adapter-acceptance.v1" -or + [string]$policyDocument.schema -ne "code-intel-language-adapter-acceptance-policy.v1" -or + $null -eq $stageProperty -or + @($policyDocument.claimLevels) -notcontains $claimLevel) { + Add-Gate -Id "input-shape" -Passed $false -Detail "unknown schema, stage, or claim level" + Write-ResultAndExit -AdapterId $adapterId -Stage $stage -MalformedExit 2 +} +Add-Gate -Id "input-shape" -Passed $true -Detail "schemas, stage, and claim level recognized" + +$expectedClaimLevels = @("inventory", "structural", "semantic", "behavioral") +$policyMonotonic = @($policyDocument.claimLevels).Count -eq $expectedClaimLevels.Count +for ($index = 0; $index -lt $expectedClaimLevels.Count -and $policyMonotonic; $index++) { + if ([string]$policyDocument.claimLevels[$index] -ne $expectedClaimLevels[$index]) { $policyMonotonic = $false } +} +$minimumFields = @("languages", "labeledSamples", "precision", "recall", "declaredCoverage", "deterministicReplays", "parityArtifacts", "semanticOracleCases", "behavioralOracleCases") +$requirementFields = @("knownLicense", "rollbackTested", "independentVerification") +$stageOrder = @("research", "candidate", "production") +for ($index = 1; $index -lt $stageOrder.Count -and $policyMonotonic; $index++) { + $previous = $policyDocument.stages.PSObject.Properties[$stageOrder[$index - 1]].Value + $current = $policyDocument.stages.PSObject.Properties[$stageOrder[$index]].Value + foreach ($field in $minimumFields) { + if ([double]$current.minimums.$field -lt [double]$previous.minimums.$field) { $policyMonotonic = $false } + } + foreach ($field in $requirementFields) { + if ([bool]$previous.requirements.$field -and -not [bool]$current.requirements.$field) { $policyMonotonic = $false } + } +} +Add-Gate -Id "policy-monotonicity" -Passed $policyMonotonic -Detail "claim order fixed; research <= candidate <= production for all thresholds and requirements" + +$stagePolicy = $stageProperty.Value +$minimums = $stagePolicy.minimums +$requirements = $stagePolicy.requirements +$languages = @($reportDocument.adapter.languages) +$uniqueLanguages = @($languages | Sort-Object -Unique) +$languagePass = $languages.Count -ge [int]$minimums.languages -and $uniqueLanguages.Count -eq $languages.Count +Add-Gate -Id "language-set" -Passed $languagePass -Detail "languages=$($languages.Count), required=$($minimums.languages), unique=$($uniqueLanguages.Count)" + +$artifactSchemas = @($reportDocument.contract.artifactSchemas) +$contractPass = [bool]$reportDocument.contract.schemaValidated -and + [bool]$reportDocument.contract.backwardCompatible -and + (Test-ContainsAll -Actual $artifactSchemas -Required @($policyDocument.requiredArtifactSchemas)) +Add-Gate -Id "contract" -Passed $contractPass -Detail "schemaValidated=$($reportDocument.contract.schemaValidated), backwardCompatible=$($reportDocument.contract.backwardCompatible)" + +$claimOrder = @($policyDocument.claimLevels) +$claimIndex = [array]::IndexOf($claimOrder, $claimLevel) +$claimPass = $true +for ($index = 0; $index -lt $claimOrder.Count; $index++) { + $name = [string]$claimOrder[$index] + $actual = [bool]$reportDocument.claims.$name + $expected = $index -le $claimIndex + if ($actual -ne $expected) { $claimPass = $false } +} +Add-Gate -Id "claim-boundary" -Passed $claimPass -Detail "declared=$claimLevel with lower levels required and higher levels forbidden" + +$qualityPass = [int]$reportDocument.corpus.labeledSamples -ge [int]$minimums.labeledSamples +if ($claimIndex -ge 1) { + $qualityPass = $qualityPass -and + [double]$reportDocument.corpus.precision -ge [double]$minimums.precision -and + [double]$reportDocument.corpus.recall -ge [double]$minimums.recall -and + [double]$reportDocument.corpus.declaredCoverage -ge [double]$minimums.declaredCoverage +} +Add-Gate -Id "measured-quality" -Passed $qualityPass -Detail "samples=$($reportDocument.corpus.labeledSamples), precision=$($reportDocument.corpus.precision), recall=$($reportDocument.corpus.recall), coverage=$($reportDocument.corpus.declaredCoverage)" + +$unsupportedPass = [bool]$reportDocument.corpus.unsupportedExplicit -and [int]$reportDocument.corpus.fabricatedFactsForUnsupported -eq 0 +Add-Gate -Id "unsupported-behavior" -Passed $unsupportedPass -Detail "explicit=$($reportDocument.corpus.unsupportedExplicit), fabricated=$($reportDocument.corpus.fabricatedFactsForUnsupported)" + +$determinismPass = [bool]$reportDocument.determinism.stable -and [int]$reportDocument.determinism.replays -ge [int]$minimums.deterministicReplays +Add-Gate -Id "determinism" -Passed $determinismPass -Detail "stable=$($reportDocument.determinism.stable), replays=$($reportDocument.determinism.replays), required=$($minimums.deterministicReplays)" + +$compatibilityPass = [bool]$reportDocument.compatibility.passed -and [int]$reportDocument.compatibility.parityArtifacts -ge [int]$minimums.parityArtifacts +Add-Gate -Id "compatibility" -Passed $compatibilityPass -Detail "passed=$($reportDocument.compatibility.passed), artifacts=$($reportDocument.compatibility.parityArtifacts), required=$($minimums.parityArtifacts)" + +$declaredEffects = @($reportDocument.effects.declared) +$observedEffects = @($reportDocument.effects.observed) +$effectPass = (Test-ContainsAll -Actual $declaredEffects -Required $observedEffects) -and + (Test-ContainsAll -Actual @($policyDocument.allowedEffects) -Required $declaredEffects) -and + -not [bool]$reportDocument.effects.networkUsed -and + -not [bool]$reportDocument.effects.repoMutationUsed +Add-Gate -Id "effect-boundary" -Passed $effectPass -Detail "declared=$($declaredEffects -join ','), observed=$($observedEffects -join ','), network=$($reportDocument.effects.networkUsed), mutation=$($reportDocument.effects.repoMutationUsed)" + +$license = [string]$reportDocument.provenance.implementationLicense +$knownLicense = -not [string]::IsNullOrWhiteSpace($license) -and $license -notmatch '^UNKNOWN' +$digestPattern = '^[0-9a-f]{64}$' +$digestBound = $false +$digestDetail = "not checked" +try { + $sourcePath = Resolve-RepoBoundFile -RelativePath ([string]$reportDocument.provenance.sourcePath) + $conformancePath = Resolve-RepoBoundFile -RelativePath ([string]$reportDocument.provenance.conformancePath) + $actualSourceDigest = (Get-FileHash -Algorithm SHA256 -LiteralPath $sourcePath).Hash.ToLowerInvariant() + $actualConformanceDigest = (Get-FileHash -Algorithm SHA256 -LiteralPath $conformancePath).Hash.ToLowerInvariant() + $digestBound = $actualSourceDigest -eq [string]$reportDocument.provenance.sourceDigest -and + $actualConformanceDigest -eq [string]$reportDocument.provenance.conformanceDigest + $digestDetail = "sourceBound=$($actualSourceDigest -eq [string]$reportDocument.provenance.sourceDigest), conformanceBound=$($actualConformanceDigest -eq [string]$reportDocument.provenance.conformanceDigest)" +} catch { + $digestDetail = $_.Exception.Message +} +$provenancePass = [bool]$reportDocument.provenance.sourceRevisionPinned -and + [string]$reportDocument.provenance.sourceDigest -match $digestPattern -and + [string]$reportDocument.provenance.conformanceDigest -match $digestPattern -and + $digestBound -and + (-not [bool]$requirements.knownLicense -or $knownLicense) +Add-Gate -Id "provenance" -Passed $provenancePass -Detail "revisionPinned=$($reportDocument.provenance.sourceRevisionPinned), license=$license, knownRequired=$($requirements.knownLicense), $digestDetail" + +$rollbackPass = [bool]$reportDocument.rollback.documented -and (-not [bool]$requirements.rollbackTested -or [bool]$reportDocument.rollback.tested) +Add-Gate -Id "rollback" -Passed $rollbackPass -Detail "documented=$($reportDocument.rollback.documented), tested=$($reportDocument.rollback.tested), testedRequired=$($requirements.rollbackTested)" + +$verificationPass = -not [bool]$requirements.independentVerification -or [bool]$reportDocument.verification.independent +Add-Gate -Id "independent-verification" -Passed $verificationPass -Detail "independent=$($reportDocument.verification.independent), required=$($requirements.independentVerification)" + +$oraclePass = $true +if ($claimIndex -ge 2) { + $oraclePass = [int]$reportDocument.oracles.semanticCases -ge [int]$minimums.semanticOracleCases +} +if ($claimIndex -ge 3) { + $oraclePass = $oraclePass -and [int]$reportDocument.oracles.behavioralCases -ge [int]$minimums.behavioralOracleCases +} +Add-Gate -Id "oracle-depth" -Passed $oraclePass -Detail "semantic=$($reportDocument.oracles.semanticCases), behavioral=$($reportDocument.oracles.behavioralCases)" + +$evidencePass = @($reportDocument.evidence).Count -gt 0 +Add-Gate -Id "evidence" -Passed $evidencePass -Detail "evidenceRefs=$(@($reportDocument.evidence).Count)" + +Write-ResultAndExit -AdapterId $adapterId -Stage $stage diff --git a/Test-Python314PonCompatibility.ps1 b/Test-Python314PonCompatibility.ps1 new file mode 100644 index 0000000..933b8f3 --- /dev/null +++ b/Test-Python314PonCompatibility.ps1 @@ -0,0 +1,216 @@ +#requires -Version 7.2 + +param( + [ValidateSet("development", "pon-candidate")] + [string]$Profile = "development", + + [string]$Policy = (Join-Path $PSScriptRoot "orchestration\python314-pon-development-policy.v1.json"), + + [string]$CPythonCommand = "", + + [string[]]$CPythonPrefixArgs = @(), + + [string]$PonCommand = "", + + [string[]]$PonPrefixArgs = @(), + + [switch]$Json +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$root = [System.IO.Path]::GetFullPath($PSScriptRoot) +$gates = [System.Collections.Generic.List[object]]::new() +$caseResults = [System.Collections.Generic.List[object]]::new() + +function Add-Gate { + param([string]$Id, [bool]$Passed, [string]$Detail) + $gates.Add([pscustomobject]@{ id = $Id; passed = $Passed; detail = $Detail }) +} + +function Resolve-RepoFile { + param([string]$RelativePath) + if ([string]::IsNullOrWhiteSpace($RelativePath) -or [System.IO.Path]::IsPathRooted($RelativePath)) { + throw "path must be repository-relative: $RelativePath" + } + $prefix = [System.IO.Path]::TrimEndingDirectorySeparator($root) + [System.IO.Path]::DirectorySeparatorChar + $resolved = [System.IO.Path]::GetFullPath((Join-Path $root $RelativePath)) + if (-not $resolved.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "path escapes repository: $RelativePath" + } + if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { + throw "required file is missing: $RelativePath" + } + return $resolved +} + +function Resolve-Executable { + param([string]$Command) + if ([string]::IsNullOrWhiteSpace($Command)) { return $null } + if ([System.IO.Path]::IsPathRooted($Command)) { + if (Test-Path -LiteralPath $Command -PathType Leaf) { return [System.IO.Path]::GetFullPath($Command) } + return $null + } + $resolved = Get-Command $Command -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($null -eq $resolved) { return $null } + return [string]$resolved.Source +} + +function Invoke-CapturedProcess { + param([string]$Executable, [string[]]$Arguments) + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $Executable + $startInfo.UseShellExecute = $false + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.WorkingDirectory = $root + foreach ($argument in @($Arguments)) { $startInfo.ArgumentList.Add([string]$argument) } + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + try { + if (-not $process.Start()) { throw "process did not start: $Executable" } + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + if (-not $process.WaitForExit(30000)) { + $process.Kill($true) + throw "process timed out after 30 seconds: $Executable" + } + return [pscustomobject]@{ + exitCode = $process.ExitCode + stdout = $stdoutTask.GetAwaiter().GetResult() + stderr = $stderrTask.GetAwaiter().GetResult() + } + } finally { + $process.Dispose() + } +} + +function Normalize-Newlines { + param([string]$Text) + return $Text.Replace("`r`n", "`n").Replace("`r", "`n") +} + +function Find-CPython314 { + $candidates = [System.Collections.Generic.List[object]]::new() + if (-not [string]::IsNullOrWhiteSpace($CPythonCommand)) { + $candidates.Add([pscustomobject]@{ command = $CPythonCommand; prefix = @($CPythonPrefixArgs) }) + } else { + $candidates.Add([pscustomobject]@{ command = "py"; prefix = @("-3.14") }) + $candidates.Add([pscustomobject]@{ command = "python3.14"; prefix = @() }) + $candidates.Add([pscustomobject]@{ command = "python"; prefix = @() }) + } + foreach ($candidate in $candidates) { + $executable = Resolve-Executable ([string]$candidate.command) + if ($null -eq $executable) { continue } + try { + $probe = Invoke-CapturedProcess $executable (@($candidate.prefix) + @("-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")) + if ($probe.exitCode -eq 0 -and $probe.stdout.Trim() -eq "3.14") { + return [pscustomobject]@{ executable = $executable; prefix = @($candidate.prefix); version = $probe.stdout.Trim() } + } + } catch { continue } + } + return $null +} + +function Complete-Result { + param([int]$MalformedExit = 0, [string]$CPython = "", [string]$PonStatus = "not_checked") + $failed = @($gates | Where-Object { -not $_.passed }) + $result = [ordered]@{ + schema = "code-intel-python314-pon-compatibility-result.v1" + profile = $Profile + verdict = if ($failed.Count -eq 0 -and $MalformedExit -eq 0) { "pass" } else { "fail" } + cpython = $CPython + ponStatus = $PonStatus + gates = @($gates) + failedGateIds = @($failed | ForEach-Object id) + cases = @($caseResults) + } + if ($Json) { $result | ConvertTo-Json -Depth 12 } + else { + Write-Host "Python 3.14 / Pon compatibility: $($result.verdict) profile=$Profile pon=$PonStatus" + foreach ($gate in $gates) { Write-Host "$(if ($gate.passed) { 'PASS' } else { 'FAIL' }) $($gate.id): $($gate.detail)" } + } + if ($MalformedExit -ne 0) { exit $MalformedExit } + if ($failed.Count -gt 0) { exit 1 } + exit 0 +} + +try { + $policyDocument = Get-Content -Raw -LiteralPath $Policy | ConvertFrom-Json -Depth 30 + $profileProperty = $policyDocument.profiles.PSObject.Properties[$Profile] + $shapePass = [string]$policyDocument.schema -eq "code-intel-python314-pon-development-policy.v1" -and + $null -ne $profileProperty -and + [int]$policyDocument.authority.requiredMajor -eq 3 -and + [int]$policyDocument.authority.requiredMinor -eq 14 -and + [string]$policyDocument.sourceMethod.revision -match '^[0-9a-f]{40}$' + Add-Gate "input-shape" $shapePass "pinned policy requires CPython 3.14 and a known profile" + if (-not $shapePass) { Complete-Result -MalformedExit 2 } + $manifestPath = Resolve-RepoFile ([string]$policyDocument.corpus) + $manifest = Get-Content -Raw -LiteralPath $manifestPath | ConvertFrom-Json -Depth 20 + if ([string]$manifest.schema -ne "code-intel-python314-compat-corpus.v1" -or @($manifest.cases).Count -eq 0) { + throw "invalid or empty Python 3.14 corpus" + } +} catch { + if (@($gates | Where-Object id -eq "input-shape").Count -eq 0) { Add-Gate "input-shape" $false $_.Exception.Message } + Complete-Result -MalformedExit 2 +} + +$cpython = Find-CPython314 +$cpythonPass = $null -ne $cpython +Add-Gate "cpython314-availability" $cpythonPass "resolved=$(if ($cpythonPass) { $cpython.executable } else { 'none' })" +if (-not $cpythonPass) { Complete-Result -CPython "" -PonStatus "not_checked" } +$cpythonIdentity = "$($cpython.executable) $($cpython.prefix -join ' ')".Trim() + +$compileFiles = @($policyDocument.projectPythonFiles | ForEach-Object { Resolve-RepoFile ([string]$_) }) +$compileProgram = "import pathlib, sys; [compile(pathlib.Path(path).read_text(encoding='utf-8'), path, 'exec') for path in sys.argv[1:]]" +$compile = Invoke-CapturedProcess $cpython.executable (@($cpython.prefix) + @("-c", $compileProgram) + $compileFiles) +$compilePass = -not [bool]$profileProperty.Value.requireProjectCompile -or $compile.exitCode -eq 0 +Add-Gate "project-python-compile" $compilePass "files=$($compileFiles.Count), exit=$($compile.exitCode)" + +$corpusPass = $true +foreach ($case in @($manifest.cases)) { + $casePath = Resolve-RepoFile ([string]$case.path) + $actual = Invoke-CapturedProcess $cpython.executable (@($cpython.prefix) + @($casePath)) + $passed = $actual.exitCode -eq [int]$case.expectedExitCode -and + (Normalize-Newlines $actual.stdout) -ceq [string]$case.expectedStdout -and + (Normalize-Newlines $actual.stderr) -ceq [string]$case.expectedStderr + if (-not $passed) { $corpusPass = $false } + $caseResults.Add([pscustomobject]@{ + id = [string]$case.id + ponRequired = [bool]$case.ponRequired + cpythonPassed = $passed + ponPassed = $null + }) +} +Add-Gate "cpython314-corpus" $corpusPass "cases=$(@($manifest.cases).Count), passed=$(@($caseResults | Where-Object cpythonPassed).Count)" + +$ponName = if ([string]::IsNullOrWhiteSpace($PonCommand)) { [string]$policyDocument.pon.defaultCommand } else { $PonCommand } +$ponExecutable = Resolve-Executable $ponName +$requirePon = [bool]$profileProperty.Value.requirePon +$ponAvailable = $null -ne $ponExecutable +$availabilityPass = -not $requirePon -or $ponAvailable +Add-Gate "pon-availability" $availabilityPass "required=$requirePon, resolved=$(if ($ponAvailable) { $ponExecutable } else { 'none' })" + +$ponStatus = if ($ponAvailable) { "available" } else { "unavailable" } +$ponParityPass = $true +if ($ponAvailable -and [bool]$profileProperty.Value.runPonWhenAvailable) { + foreach ($case in @($manifest.cases | Where-Object ponRequired)) { + $casePath = Resolve-RepoFile ([string]$case.path) + $cpythonResult = Invoke-CapturedProcess $cpython.executable (@($cpython.prefix) + @($casePath)) + $ponArgs = @($PonPrefixArgs) + @($policyDocument.pon.scriptArgsBeforePath) + @($casePath) + $ponResult = Invoke-CapturedProcess $ponExecutable $ponArgs + $passed = $ponResult.exitCode -eq $cpythonResult.exitCode -and + $ponResult.stdout -ceq $cpythonResult.stdout -and + $ponResult.stderr -ceq $cpythonResult.stderr + if (-not $passed) { $ponParityPass = $false } + $existing = $caseResults | Where-Object id -eq ([string]$case.id) | Select-Object -First 1 + $existing.ponPassed = $passed + } + $ponStatus = if ($ponParityPass) { "pass" } else { "diverged" } +} +if (-not $ponAvailable -and -not $requirePon) { $ponParityPass = $true } +if (-not $ponAvailable -and $requirePon) { $ponParityPass = $false } +Add-Gate "pon-parity" $ponParityPass "status=$ponStatus, requiredCases=$(@($manifest.cases | Where-Object ponRequired).Count)" + +Complete-Result -CPython $cpythonIdentity -PonStatus $ponStatus diff --git a/bootstrap-new-machine.ps1 b/bootstrap-new-machine.ps1 index 9219f7f..6a103b4 100644 --- a/bootstrap-new-machine.ps1 +++ b/bootstrap-new-machine.ps1 @@ -71,6 +71,7 @@ $installParams = @{ RepoPath = $repo Platform = $effectivePlatform Json = $true + RequireRepowise = [bool]$RequireRepowise } if (-not $NoInstallMissing) { $installParams.InstallMissing = $true @@ -78,15 +79,14 @@ if (-not $NoInstallMissing) { } if (-not $NoRepairSkillLinks) { $installParams.RepairSkillLinks = $true } if ($CheckProvider) { $installParams.CheckProvider = $true } -if ($RequireRepowise) { $installParams.RequireRepowise = $true } if ($RequireUnderstand) { $installParams.RequireUnderstand = $true } $doctorParams = @{ RepoPath = $repo Platform = $effectivePlatform Json = $true + RequireRepowise = [bool]$RequireRepowise } -if ($RequireRepowise) { $doctorParams.RequireRepowise = $true } if ($RequireUnderstand) { $doctorParams.RequireUnderstand = $true } $installResult = Invoke-JsonScript (Join-Path $root "install-code-intel-pipeline.ps1") $installParams diff --git a/check-code-intel-tools.ps1 b/check-code-intel-tools.ps1 index 40a999c..1fbcdc2 100644 --- a/check-code-intel-tools.ps1 +++ b/check-code-intel-tools.ps1 @@ -301,6 +301,8 @@ if ($RequireUnderstand -and -not $checks.graphProvider.cargoFound) { $missing.Ad if ($repoState -and -not $repoState.exists) { $missing.Add("repo path") } $result = [ordered]@{ + schema = "code-intel-doctor-bootstrap-observation.v1" + authority = "observation_only" ok = $missing.Count -eq 0 missing = $missing platform = [ordered]@{ diff --git a/claude-code-merge-queue.config.mjs b/claude-code-merge-queue.config.mjs new file mode 100644 index 0000000..2fa7d79 --- /dev/null +++ b/claude-code-merge-queue.config.mjs @@ -0,0 +1,11 @@ +export default { + integrationBranch: "codex/code-intel-atomic-model", + productionBranch: "main", + protectedBranches: [], + regenerableFiles: [], + disposableUntracked: [], + symlinks: ["node_modules"], + buildOutputDirs: ["target", "artifacts", "dist"], + checkCommand: "pwsh -NoProfile -File ./Invoke-CodeIntelAcceptance.ps1 -Stage land", + checksRequired: true, +}; diff --git a/crates/code-intel-cli/Cargo.toml b/crates/code-intel-cli/Cargo.toml index f4964de..c5c8a7e 100644 --- a/crates/code-intel-cli/Cargo.toml +++ b/crates/code-intel-cli/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "code-intel" -version = "0.3.0-beta.1" +version = "0.3.0" edition = "2021" description = "Local Code Intel Pipeline CLI for artifact resume and contract checks" -license = "Apache-2.0" +license = "MIT" repository = "https://github.com/2233admin/code-intel-pipeline" [dependencies] diff --git a/crates/code-intel-cli/src/admissibility.rs b/crates/code-intel-cli/src/admissibility.rs new file mode 100644 index 0000000..5b50b7a --- /dev/null +++ b/crates/code-intel-cli/src/admissibility.rs @@ -0,0 +1,415 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +use crate::artifact_ref::{self, ArtifactContract, ArtifactError}; +use crate::capability::{reject_duplicate_json_keys, sha256_hex}; + +const MAX_REQUEST_BYTES: u64 = 8 * 1024 * 1024; +const MAX_PAYLOAD_BYTES: u64 = 64 * 1024 * 1024; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let cli = match parse_cli(raw) { + Ok(cli) => cli, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let request = match read_request(&cli.request) { + Ok(value) => value, + Err((code, message)) => { + println!("{}", serde_json::to_string(&rejected(&message)).unwrap()); + eprintln!("{message}"); + return code; + } + }; + match validate(&request, &cli.artifact_root) { + Ok(result) => { + println!("{}", serde_json::to_string(&result).unwrap()); + 0 + } + Err(error) => { + let (code, message) = match error { + ValidationError::Contract(message) => (65, message), + ValidationError::Io(message) => (74, message), + }; + println!("{}", serde_json::to_string(&rejected(&message)).unwrap()); + eprintln!("{message}"); + code + } + } +} + +struct Cli { + request: PathBuf, + artifact_root: PathBuf, +} + +fn parse_cli(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("validate") { + return Err( + "usage: evidence validate --request --artifact-root " + .to_string(), + ); + } + let mut request = None; + let mut artifact_root = None; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!(flag, "--request" | "--artifact-root") { + return Err(format!("unknown evidence argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|v| !v.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + let slot = if flag == "--request" { + &mut request + } else { + &mut artifact_root + }; + if slot.replace(PathBuf::from(value)).is_some() { + return Err(format!("duplicate evidence argument: {flag}")); + } + index += 2; + } + Ok(Cli { + request: request.ok_or("evidence validate requires --request")?, + artifact_root: artifact_root.ok_or("evidence validate requires --artifact-root")?, + }) +} + +fn read_request(path: &Path) -> Result { + let metadata = + fs::metadata(path).map_err(|e| (74, format!("read evidence request metadata: {e}")))?; + if !metadata.is_file() { + return Err((65, "evidence request must be a regular file".to_string())); + } + if metadata.len() > MAX_REQUEST_BYTES { + return Err((65, "evidence request exceeds size limit".to_string())); + } + let bytes = fs::read(path).map_err(|e| (74, format!("read evidence request: {e}")))?; + let text = std::str::from_utf8(&bytes) + .map_err(|e| (65, format!("evidence request is not UTF-8: {e}")))?; + reject_duplicate_json_keys(text).map_err(|e| (65, e))?; + serde_json::from_str(text).map_err(|e| (65, format!("invalid evidence request JSON: {e}"))) +} + +enum ValidationError { + Contract(String), + Io(String), +} + +pub(crate) struct ValidatedAdmission { + result: Value, + payload: Value, +} + +impl ValidatedAdmission { + pub(crate) fn result(&self) -> &Value { + &self.result + } + + pub(crate) fn payload(&self) -> &Value { + &self.payload + } +} + +pub(crate) fn validate_for_consumer( + request: &Value, + root: &Path, +) -> Result { + validate_sealed(request, root).map_err(|error| match error { + ValidationError::Contract(message) | ValidationError::Io(message) => message, + }) +} +impl From for ValidationError { + fn from(value: ArtifactError) -> Self { + match value { + ArtifactError::Contract(m) => Self::Contract(m), + ArtifactError::Io(m) => Self::Io(m), + } + } +} + +fn validate(request: &Value, root: &Path) -> Result { + validate_sealed(request, root).map(|validated| validated.result) +} + +fn validate_sealed(request: &Value, root: &Path) -> Result { + validate_request_shape(request).map_err(ValidationError::Contract)?; + let observation = &request["observation"]; + let expected_snapshot = request["expectedSnapshotIdentity"].as_str().unwrap(); + if observation["consumedSnapshotIdentity"] != request["expectedSnapshotIdentity"] { + return Err(ValidationError::Contract( + "observed evidence consumed snapshot mismatch".to_string(), + )); + } + let evaluated_at = request["policy"]["evaluatedAt"].as_u64().unwrap(); + let observed_at = observation["observedAt"].as_u64().unwrap(); + let max_age = request["policy"]["maxAgeSeconds"].as_u64().unwrap(); + if observed_at > evaluated_at || evaluated_at - observed_at > max_age { + return Err(ValidationError::Contract( + "observed evidence is stale for freshness policy".to_string(), + )); + } + let artifact = artifact_ref::verify_artifact_ref( + root, + expected_snapshot, + ArtifactContract { + artifact_schema: "code-intel-evidence-payload.v1", + artifact_type: "observed.evidence.payload", + max_bytes: MAX_PAYLOAD_BYTES, + validate_payload, + }, + &observation["payload"], + )?; + let verdict = if observation["completeness"] == "complete" { + "observed" + } else { + "unknown" + }; + let admission_identity = sha256_hex( + &serde_json::to_vec(observation).expect("validated observation always serializes"), + ); + let payload: Value = serde_json::from_slice(artifact.bytes()) + .expect("the A04 payload validator accepted JSON bytes"); + let result = json!({ + "schema":"code-intel-evidence-admissibility-result.v1", + "status":"admitted", + "domainVerdict":verdict, + "admissionIdentity":admission_identity, + "evidence":observation, + "verifiedPayload":{"sha256":artifact.sha256(),"artifactSchema":artifact.artifact_schema(),"type":artifact.artifact_type(),"consumedSnapshotIdentity":artifact.consumed_snapshot_identity(),"data":payload["data"]}, + "engineeringFacts":[] + }); + Ok(ValidatedAdmission { result, payload }) +} + +fn validate_request_shape(request: &Value) -> Result<(), String> { + exact_object( + request, + &[ + "schema", + "expectedSnapshotIdentity", + "policy", + "observation", + ], + "request", + )?; + if request["schema"] != "code-intel-evidence-admissibility-request.v1" + || !digest(&request["expectedSnapshotIdentity"]) + { + return Err("evidence request schema/snapshot identity is invalid".to_string()); + } + let policy = &request["policy"]; + exact_object( + policy, + &["evaluatedAt", "maxAgeSeconds"], + "freshness policy", + )?; + if policy["evaluatedAt"].as_u64().is_none() + || !policy["maxAgeSeconds"].as_u64().is_some_and(|n| n > 0) + { + return Err("freshness policy is invalid".to_string()); + } + let o = &request["observation"]; + exact_object( + o, + &[ + "schema", + "provider", + "source", + "consumedSnapshotIdentity", + "observedAt", + "completeness", + "claimedComplete", + "payload", + "provenance", + "failure", + ], + "observation", + )?; + if o["schema"] != "code-intel-observed-evidence.v1" + || !digest(&o["consumedSnapshotIdentity"]) + || o["observedAt"].as_u64().is_none() + { + return Err("observation identity/time is invalid".to_string()); + } + validate_provider(&o["provider"])?; + validate_source(&o["source"])?; + crate::capability::validate_artifact_ref_shape(&o["payload"])?; + if o["payload"]["artifactSchema"] != "code-intel-evidence-payload.v1" + || o["payload"]["type"] != "observed.evidence.payload" + { + return Err("evidence payload contract is invalid".to_string()); + } + validate_provenance(&o["provenance"])?; + if o["observedAt"] != o["provenance"]["completedAt"] { + return Err("observation time must equal provenance completion time".to_string()); + } + let completeness = o["completeness"].as_str().unwrap_or(""); + let claimed = o["claimedComplete"] + .as_bool() + .ok_or("claimedComplete must be boolean")?; + if !matches!(completeness, "complete" | "partial") || claimed != (completeness == "complete") { + return Err("evidence completeness claim is inconsistent".to_string()); + } + let failure = &o["failure"]; + let fields = failure.as_object().ok_or("failure must be an object")?; + if !fields + .keys() + .all(|k| matches!(k.as_str(), "kind" | "message")) + || !fields.contains_key("kind") + { + return Err("failure fields are invalid".to_string()); + } + let kind = failure["kind"].as_str().unwrap_or(""); + if !matches!( + kind, + "none" | "provider_unavailable" | "domain_unknown" | "process_failure" + ) { + return Err("failure kind is invalid".to_string()); + } + if completeness == "complete" && kind != "none" { + return Err("complete evidence cannot report a failure".to_string()); + } + if kind == "process_failure" { + return Err("process failure output is not admissible evidence".to_string()); + } + if kind != "none" && !failure["message"].as_str().is_some_and(|s| !s.is_empty()) { + return Err("non-none failure requires a message".to_string()); + } + if kind == "none" && fields.len() != 1 { + return Err("none failure cannot carry a message".to_string()); + } + Ok(()) +} + +fn validate_provider(v: &Value) -> Result<(), String> { + exact_object(v, &["id", "implementation"], "provider")?; + nonempty(&v["id"], "provider id")?; + let implementation = &v["implementation"]; + exact_object( + implementation, + &["id", "version", "digest"], + "provider implementation", + )?; + nonempty(&implementation["id"], "implementation id")?; + nonempty(&implementation["version"], "implementation version")?; + if !digest(&implementation["digest"]) { + return Err("implementation digest is invalid".to_string()); + } + Ok(()) +} + +fn validate_source(v: &Value) -> Result<(), String> { + let o = v.as_object().ok_or("source must be an object")?; + if o.is_empty() + || !o + .keys() + .all(|k| matches!(k.as_str(), "revision" | "endpointIdentity")) + { + return Err("source fields are invalid".to_string()); + } + let identities = ["revision", "endpointIdentity"] + .iter() + .filter(|k| v[**k].as_str().is_some_and(|s| !s.is_empty())) + .count(); + if identities != 1 { + return Err("source requires exactly one of revision or endpoint identity".to_string()); + } + if o.values() + .any(|x| !x.as_str().is_some_and(|s| !s.is_empty())) + { + return Err("source identity is invalid".to_string()); + } + Ok(()) +} + +fn validate_provenance(v: &Value) -> Result<(), String> { + exact_object( + v, + &["collectionId", "command", "startedAt", "completedAt"], + "provenance", + )?; + nonempty(&v["collectionId"], "collectionId")?; + nonempty(&v["command"], "command")?; + let start = v["startedAt"] + .as_u64() + .ok_or("provenance startedAt is invalid")?; + let end = v["completedAt"] + .as_u64() + .ok_or("provenance completedAt is invalid")?; + if end < start { + return Err("provenance time range is invalid".to_string()); + } + Ok(()) +} + +fn validate_payload(bytes: &[u8]) -> Result<(), String> { + let text = + std::str::from_utf8(bytes).map_err(|e| format!("evidence payload is not UTF-8: {e}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|e| format!("evidence payload is invalid JSON: {e}"))?; + exact_object(&value, &["schema", "data"], "evidence payload")?; + if value["schema"] != "code-intel-evidence-payload.v1" || !value["data"].is_object() { + return Err("evidence payload schema/data is invalid".to_string()); + } + Ok(()) +} + +fn exact_object(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual != expected { + return Err(format!("{label} fields are invalid")); + } + Ok(()) +} +fn nonempty(v: &Value, label: &str) -> Result<(), String> { + if v.as_str().is_some_and(|s| !s.is_empty()) { + Ok(()) + } else { + Err(format!("{label} is invalid")) + } +} +fn digest(v: &Value) -> bool { + v.as_str().is_some_and(|s| { + s.len() == 64 + && s.bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + }) +} +fn rejected(message: &str) -> Value { + json!({"schema":"code-intel-evidence-admissibility-result.v1","status":"rejected","domainVerdict":"unknown","admissionIdentity":null,"evidence":null,"verifiedPayload":null,"engineeringFacts":[],"diagnostics":[message]}) +} + +#[cfg(test)] +mod tests { + #[test] + fn core_source_contains_no_provider_specific_branch_names() { + let source = include_str!("admissibility.rs").to_ascii_lowercase(); + for forbidden in [ + "repo".to_string() + "wise", + "sen".to_string() + "trux", + "code".to_string() + "nexus", + "gra".to_string() + "ph", + ] { + assert!( + !source.contains(&forbidden), + "provider-specific name leaked: {forbidden}" + ); + } + } +} diff --git a/crates/code-intel-cli/src/artifact_index.rs b/crates/code-intel-cli/src/artifact_index.rs new file mode 100644 index 0000000..b061016 --- /dev/null +++ b/crates/code-intel-cli/src/artifact_index.rs @@ -0,0 +1,496 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +use crate::capability::{reject_duplicate_json_keys, validate_artifact_ref_shape}; +use crate::run_commit; + +const INDEX_SCHEMA: &str = "code-intel-artifact-index.v1"; +const MAX_INDEX_BYTES: u64 = 16 * 1024 * 1024; + +#[derive(Debug)] +pub(crate) enum IndexError { + Contract(String), + HostIo(String), +} + +impl fmt::Display for IndexError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Contract(message) | Self::HostIo(message) => formatter.write_str(message), + } + } +} + +impl std::error::Error for IndexError {} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match parse_cli(raw).and_then(execute_cli) { + Ok(index) => { + println!("{}", serde_json::to_string(&index).unwrap()); + 0 + } + Err(IndexError::Contract(message)) => { + eprintln!("{message}"); + 65 + } + Err(IndexError::HostIo(message)) => { + eprintln!("{message}"); + 74 + } + } +} + +struct Cli { + artifact_root: PathBuf, + output: Option, + operation: Operation, + existing: Option, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Operation { + Rebuild, + Incremental, +} + +fn parse_cli(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("index") { + return Err(IndexError::Contract("usage: artifact index --artifact-root [--output ] [--operation rebuild|incremental] [--existing ]".into())); + } + let mut artifact_root = None; + let mut output = None; + let mut operation = Operation::Rebuild; + let mut operation_seen = false; + let mut existing = None; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--artifact-root" | "--output" | "--operation" | "--existing" + ) { + return Err(IndexError::Contract(format!( + "unknown artifact index argument: {flag}" + ))); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| IndexError::Contract(format!("{flag} requires one value")))?; + match flag { + "--artifact-root" => set_once(&mut artifact_root, PathBuf::from(value), flag)?, + "--output" => set_once(&mut output, PathBuf::from(value), flag)?, + "--existing" => set_once(&mut existing, PathBuf::from(value), flag)?, + "--operation" => { + if operation_seen { + return Err(IndexError::Contract("duplicate --operation".into())); + } + operation_seen = true; + operation = match value.as_str() { + "rebuild" => Operation::Rebuild, + "incremental" => Operation::Incremental, + _ => { + return Err(IndexError::Contract( + "--operation must be rebuild or incremental".into(), + )) + } + }; + } + _ => unreachable!(), + } + index += 2; + } + let artifact_root = + artifact_root.ok_or_else(|| IndexError::Contract("--artifact-root is required".into()))?; + if !artifact_root.is_dir() { + return Err(IndexError::Contract( + "artifact root must be an existing directory".into(), + )); + } + if operation == Operation::Incremental && existing.is_none() { + return Err(IndexError::Contract( + "incremental operation requires --existing".into(), + )); + } + if operation == Operation::Rebuild && existing.is_some() { + return Err(IndexError::Contract( + "--existing is only valid for incremental operation".into(), + )); + } + Ok(Cli { + artifact_root, + output, + operation, + existing, + }) +} + +fn set_once(slot: &mut Option, value: T, flag: &str) -> Result<(), IndexError> { + if slot.replace(value).is_some() { + Err(IndexError::Contract(format!("duplicate {flag}"))) + } else { + Ok(()) + } +} + +fn execute_cli(cli: Cli) -> Result { + let index = match cli.operation { + Operation::Rebuild => rebuild(&cli.artifact_root)?, + Operation::Incremental => { + let path = cli.existing.as_ref().unwrap(); + let existing = read_index(path)?; + incremental(&cli.artifact_root, &existing)? + } + }; + if let Some(path) = cli.output { + write_index(&path, &index)?; + } + Ok(index) +} + +pub(crate) fn rebuild(artifact_root: &Path) -> Result { + scan(artifact_root) +} + +pub(crate) fn incremental(artifact_root: &Path, existing: &Value) -> Result { + validate_index(existing)?; + // Existing rows are hints only. Every refresh revalidates the A07 authority + // boundary so a stale or tampered committed run cannot survive incrementally. + scan(artifact_root) +} + +fn scan(artifact_root: &Path) -> Result { + let mut admitted: BTreeMap = BTreeMap::new(); + let mut diagnostics = Vec::new(); + for repo in child_directories(artifact_root)? { + let repo_name = file_name(&repo)?; + for run in child_directories(&repo)? { + let run_name = file_name(&run)?; + if is_staging_name(&run_name) { + diagnostics.push(diagnostic( + &repo_name, + &run_name, + "staging", + "run directory is staging and has no publication authority", + )); + continue; + } + match run_commit::validate_committed_run(&run) { + Ok((marker, manifest)) => { + let outcome = manifest["outcome"] + .as_str() + .expect("validated run manifest outcome"); + if outcome != "completed" { + diagnostics.push(diagnostic( + &repo_name, + &run_name, + "non_completed", + &format!( + "committed audit run outcome is {outcome}; only completed runs are query authority" + ), + )); + continue; + } + let entry = entry(&repo_name, &run_name, &marker, &manifest); + let replace = admitted + .get(&repo_name) + .and_then(|value| value["run"].as_str()) + .is_none_or(|prior| run_name.as_str() > prior); + if replace { + admitted.insert(repo_name.clone(), entry); + } + } + Err(error) => { + let marker_exists = run.join("run-complete.json").is_file(); + let (classification, reason) = if marker_exists { + ( + "forged", + "completion marker or its manifest/Artifact Refs failed validation", + ) + } else if run.join("objects").is_dir() { + ("incomplete", "A07 completion marker is absent") + } else { + ("legacy", "legacy run has no A07 completion marker") + }; + let _ = error; + diagnostics.push(diagnostic(&repo_name, &run_name, classification, reason)); + } + } + } + } + diagnostics.sort_by(|left, right| { + (left["repo"].as_str(), left["run"].as_str()) + .cmp(&(right["repo"].as_str(), right["run"].as_str())) + }); + Ok(json!({ + "schema": INDEX_SCHEMA, + "entries": admitted.into_values().collect::>(), + "diagnostics": diagnostics, + })) +} + +fn entry(repo: &str, run: &str, marker: &Value, manifest: &Value) -> Value { + let mut refs = Vec::new(); + for node in manifest["nodes"].as_object().unwrap().values() { + if let Some(artifacts) = node["artifacts"].as_array() { + refs.extend(artifacts.iter().cloned()); + } + } + refs.sort_by(|left, right| left["path"].as_str().cmp(&right["path"].as_str())); + json!({ + "repo": repo, + "run": run, + "runIdentity": marker["runIdentity"], + "snapshotIdentity": marker["snapshotIdentity"], + "outcome": manifest["outcome"], + "manifest": marker["manifest"], + "artifactRefs": refs, + }) +} + +fn diagnostic(repo: &str, run: &str, classification: &str, reason: &str) -> Value { + json!({"repo":repo,"run":run,"classification":classification,"reason":reason}) +} + +fn child_directories(root: &Path) -> Result, IndexError> { + let mut paths = Vec::new(); + let entries = fs::read_dir(root) + .map_err(|error| IndexError::HostIo(format!("read artifact index directory: {error}")))?; + for entry in entries { + let entry = entry + .map_err(|error| IndexError::HostIo(format!("read artifact index entry: {error}")))?; + let file_type = entry.file_type().map_err(|error| { + IndexError::HostIo(format!("inspect artifact index entry: {error}")) + })?; + if file_type.is_dir() && !file_type.is_symlink() { + paths.push(entry.path()); + } + } + paths.sort_by(|left, right| left.file_name().cmp(&right.file_name())); + Ok(paths) +} + +fn file_name(path: &Path) -> Result { + path.file_name() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .map(str::to_string) + .ok_or_else(|| IndexError::Contract("artifact index path is not portable UTF-8".into())) +} + +fn is_staging_name(name: &str) -> bool { + name.starts_with('.') || name.starts_with("stage-") || name.contains("staging") +} + +fn read_index(path: &Path) -> Result { + let metadata = fs::metadata(path) + .map_err(|error| IndexError::HostIo(format!("inspect existing artifact index: {error}")))?; + if !metadata.is_file() || metadata.len() > MAX_INDEX_BYTES { + return Err(IndexError::Contract( + "existing artifact index must be a bounded regular file".into(), + )); + } + let bytes = fs::read(path) + .map_err(|error| IndexError::HostIo(format!("read existing artifact index: {error}")))?; + let text = std::str::from_utf8(&bytes) + .map_err(|_| IndexError::Contract("existing artifact index must be UTF-8 JSON".into()))?; + reject_duplicate_json_keys(text).map_err(IndexError::Contract)?; + let value = serde_json::from_str(text) + .map_err(|_| IndexError::Contract("existing artifact index is invalid JSON".into()))?; + validate_index(&value)?; + Ok(value) +} + +fn validate_index(value: &Value) -> Result<(), IndexError> { + let object = value + .as_object() + .ok_or_else(|| IndexError::Contract("existing artifact index must be an object".into()))?; + if object.len() != 3 + || value["schema"] != INDEX_SCHEMA + || !value["entries"].is_array() + || !value["diagnostics"].is_array() + { + return Err(IndexError::Contract( + "existing artifact index contract is invalid".into(), + )); + } + let mut repos = BTreeSet::new(); + for entry in value["entries"].as_array().unwrap() { + validate_index_entry(entry)?; + let repo = entry["repo"].as_str().unwrap(); + if !repos.insert(repo) { + return Err(IndexError::Contract( + "existing artifact index contains duplicate repositories".into(), + )); + } + } + for diagnostic in value["diagnostics"].as_array().unwrap() { + validate_index_diagnostic(diagnostic)?; + } + Ok(()) +} + +fn validate_index_entry(value: &Value) -> Result<(), IndexError> { + exact_keys( + value, + &[ + "repo", + "run", + "runIdentity", + "snapshotIdentity", + "outcome", + "manifest", + "artifactRefs", + ], + "artifact index entry", + )?; + let repo = portable_name(&value["repo"], "entry repo")?; + let run = portable_name(&value["run"], "entry run")?; + let _ = (repo, run); + let run_identity = value["runIdentity"] + .as_str() + .filter(|identity| { + identity.strip_prefix("dag-v1:").is_some_and(|digest| { + digest.len() >= 2 && digest.len() % 2 == 0 && is_lower_hex(digest) + }) + }) + .ok_or_else(|| IndexError::Contract("entry runIdentity is invalid".into()))?; + let _ = run_identity; + let snapshot = digest(&value["snapshotIdentity"], "entry snapshotIdentity")?; + if value["outcome"] != "completed" { + return Err(IndexError::Contract("entry outcome is invalid".into())); + } + exact_keys(&value["manifest"], &["path", "sha256"], "entry manifest")?; + let manifest_digest = digest(&value["manifest"]["sha256"], "entry manifest sha256")?; + if value["manifest"]["path"].as_str() != Some(&format!("objects/sha256/{manifest_digest}")) { + return Err(IndexError::Contract( + "entry manifest path is not content-addressed".into(), + )); + } + let refs = value["artifactRefs"] + .as_array() + .ok_or_else(|| IndexError::Contract("entry artifactRefs must be an array".into()))?; + let mut paths = BTreeSet::new(); + for artifact in refs { + validate_artifact_ref_shape(artifact).map_err(IndexError::Contract)?; + let path = artifact["path"].as_str().unwrap(); + if !portable_relative_path(path) || !paths.insert(path) { + return Err(IndexError::Contract( + "entry Artifact Ref path is invalid or duplicated".into(), + )); + } + if artifact["consumedSnapshotIdentity"].as_str() != Some(snapshot) { + return Err(IndexError::Contract( + "entry Artifact Ref snapshot binding differs from the entry".into(), + )); + } + } + Ok(()) +} + +fn validate_index_diagnostic(value: &Value) -> Result<(), IndexError> { + exact_keys( + value, + &["repo", "run", "classification", "reason"], + "artifact index diagnostic", + )?; + portable_name(&value["repo"], "diagnostic repo")?; + portable_name(&value["run"], "diagnostic run")?; + if !matches!( + value["classification"].as_str(), + Some("staging" | "incomplete" | "forged" | "legacy" | "non_completed") + ) { + return Err(IndexError::Contract( + "diagnostic classification is invalid".into(), + )); + } + if !value["reason"] + .as_str() + .is_some_and(|reason| !reason.trim().is_empty()) + { + return Err(IndexError::Contract("diagnostic reason is invalid".into())); + } + Ok(()) +} + +fn exact_keys(value: &Value, keys: &[&str], context: &str) -> Result<(), IndexError> { + let object = value + .as_object() + .ok_or_else(|| IndexError::Contract(format!("{context} must be an object")))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = keys.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(IndexError::Contract(format!( + "{context} fields are invalid" + ))) + } +} + +fn portable_name<'a>(value: &'a Value, context: &str) -> Result<&'a str, IndexError> { + value + .as_str() + .filter(|name| { + !name.is_empty() + && *name != "." + && *name != ".." + && !name.ends_with(['.', ' ']) + && !name.chars().any(|character| { + character.is_control() + || matches!( + character, + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' + ) + }) + }) + .ok_or_else(|| IndexError::Contract(format!("{context} is not a portable directory name"))) +} + +fn portable_relative_path(path: &str) -> bool { + !path.is_empty() + && !path.starts_with('/') + && !path.contains('\\') + && !path.contains(':') + && path.split('/').all(|component| { + !component.is_empty() + && component != "." + && component != ".." + && !component.ends_with(['.', ' ']) + && !component.chars().any(char::is_control) + }) +} + +fn digest<'a>(value: &'a Value, context: &str) -> Result<&'a str, IndexError> { + value + .as_str() + .filter(|digest| digest.len() == 64 && is_lower_hex(digest)) + .ok_or_else(|| IndexError::Contract(format!("{context} is invalid"))) +} + +fn is_lower_hex(value: &str) -> bool { + value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn write_index(path: &Path, value: &Value) -> Result<(), IndexError> { + let parent = path + .parent() + .filter(|parent| parent.is_dir()) + .ok_or_else(|| IndexError::Contract("index output parent must exist".into()))?; + let bytes = serde_json::to_vec_pretty(value).unwrap(); + let temp = parent.join(format!(".artifact-index.tmp.{}", std::process::id())); + fs::write(&temp, &bytes) + .map_err(|error| IndexError::HostIo(format!("write artifact index temp: {error}")))?; + if path.exists() { + fs::remove_file(path) + .map_err(|error| IndexError::HostIo(format!("replace artifact index: {error}")))?; + } + fs::rename(&temp, path) + .map_err(|error| IndexError::HostIo(format!("publish artifact index: {error}"))) +} diff --git a/crates/code-intel-cli/src/artifact_ref.rs b/crates/code-intel-cli/src/artifact_ref.rs new file mode 100644 index 0000000..5a4a295 --- /dev/null +++ b/crates/code-intel-cli/src/artifact_ref.rs @@ -0,0 +1,3205 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Component, Path}; + +use serde_json::{json, Value}; + +use crate::capability::{reject_duplicate_json_keys, sha256_hex, validate_artifact_ref_shape}; +use crate::stable_artifact::{self, FileId, StableReadError}; + +const MAX_ARTIFACT_BYTES: u64 = 64 * 1024 * 1024; + +#[derive(Clone, Copy)] +pub(crate) struct ArtifactContract { + pub(crate) artifact_schema: &'static str, + pub(crate) artifact_type: &'static str, + pub(crate) max_bytes: u64, + pub(crate) validate_payload: fn(&[u8]) -> Result<(), String>, +} + +pub(crate) struct VerifiedArtifact { + bytes: Vec, + artifact_schema: String, + artifact_type: String, + sha256: String, + consumed_snapshot_identity: String, + stable_file_id: FileId, +} + +impl VerifiedArtifact { + pub(crate) fn bytes(&self) -> &[u8] { + &self.bytes + } + + pub(crate) fn artifact_schema(&self) -> &str { + &self.artifact_schema + } + + pub(crate) fn artifact_type(&self) -> &str { + &self.artifact_type + } + + pub(crate) fn sha256(&self) -> &str { + &self.sha256 + } + + pub(crate) fn consumed_snapshot_identity(&self) -> &str { + &self.consumed_snapshot_identity + } +} + +#[derive(Debug)] +pub(crate) enum ArtifactError { + Contract(String), + Io(String), +} + +impl ArtifactError { + pub(crate) fn message(&self) -> &str { + match self { + Self::Contract(message) | Self::Io(message) => message, + } + } +} + +pub(crate) fn verify_inputs( + inputs: &Value, + artifact_root: Option<&Path>, + expected_snapshot_identity: &str, +) -> Result, ArtifactError> { + let inputs = inputs + .as_array() + .ok_or_else(|| ArtifactError::Contract("request inputs must be an array".to_string()))?; + if inputs.is_empty() { + return Ok(Vec::new()); + } + let root = artifact_root.ok_or_else(|| { + ArtifactError::Contract( + "request with Artifact Ref inputs requires an explicit --artifact-root".to_string(), + ) + })?; + let mut paths = BTreeSet::new(); + let mut identities = BTreeSet::new(); + let mut preflight = Vec::with_capacity(inputs.len()); + for artifact in inputs { + validate_artifact_ref_shape(artifact).map_err(ArtifactError::Contract)?; + let path = artifact.get("path").and_then(Value::as_str).unwrap_or(""); + let canonical_path = portable_relative_path(path)?; + if !paths.insert(canonical_path.to_lowercase()) { + return Err(ArtifactError::Contract( + "Artifact Ref inputs contain duplicate or case-colliding paths".to_string(), + )); + } + let digest = artifact.get("sha256").and_then(Value::as_str).unwrap_or(""); + if !identities.insert((digest.to_string(), canonical_path.clone())) { + return Err(ArtifactError::Contract( + "Artifact Ref inputs contain duplicate identities".to_string(), + )); + } + let contract = registered_contract(artifact)?; + validate_preflight_contract(artifact, expected_snapshot_identity, contract)?; + preflight.push((artifact, contract)); + } + + let mut stable_files = BTreeSet::new(); + let mut verified = Vec::with_capacity(preflight.len()); + for (artifact, contract) in preflight { + let item = verify_artifact_ref(root, expected_snapshot_identity, contract, artifact)?; + if !stable_files.insert(item.stable_file_id) { + return Err(ArtifactError::Contract( + "Artifact Ref inputs alias the same stable file identity".to_string(), + )); + } + verified.push(item); + } + Ok(verified) +} + +fn validate_preflight_contract( + artifact: &Value, + expected_snapshot_identity: &str, + expected_contract: ArtifactContract, +) -> Result<(), ArtifactError> { + if artifact["artifactSchema"] != expected_contract.artifact_schema + || artifact["type"] != expected_contract.artifact_type + { + return Err(ArtifactError::Contract( + "Artifact Ref schema/type differs from the expected input contract".to_string(), + )); + } + let consumed = artifact["consumedSnapshotIdentity"] + .as_str() + .ok_or_else(|| { + ArtifactError::Contract( + "capability input Artifact Ref requires consumedSnapshotIdentity".to_string(), + ) + })?; + if consumed != expected_snapshot_identity { + return Err(ArtifactError::Contract( + "Artifact Ref consumed snapshot identity mismatch".to_string(), + )); + } + Ok(()) +} + +pub(crate) fn verify_artifact_ref( + root_authority: &Path, + expected_snapshot_identity: &str, + expected_contract: ArtifactContract, + artifact: &Value, +) -> Result { + validate_artifact_ref_shape(artifact).map_err(ArtifactError::Contract)?; + validate_preflight_contract(artifact, expected_snapshot_identity, expected_contract)?; + let consumed = artifact["consumedSnapshotIdentity"] + .as_str() + .expect("preflight validated snapshot identity"); + let relative = portable_relative_path(artifact["path"].as_str().expect("validated path"))?; + let components = relative.split('/').collect::>(); + let stable = + stable_artifact::read_beneath(root_authority, &components, expected_contract.max_bytes) + .map_err(|error| match error { + StableReadError::HostIo(message) => ArtifactError::Io(message), + StableReadError::TooLarge(message) + | StableReadError::Boundary(message) + | StableReadError::Identity(message) => ArtifactError::Contract(message), + })?; + let bytes = stable.bytes; + let actual = sha256_hex(&bytes); + let expected = artifact["sha256"].as_str().expect("validated digest"); + if actual != expected { + return Err(ArtifactError::Contract( + "Artifact Ref payload SHA-256 mismatch".to_string(), + )); + } + (expected_contract.validate_payload)(&bytes).map_err(ArtifactError::Contract)?; + Ok(VerifiedArtifact { + bytes, + artifact_schema: expected_contract.artifact_schema.to_string(), + artifact_type: expected_contract.artifact_type.to_string(), + sha256: actual, + consumed_snapshot_identity: consumed.to_string(), + stable_file_id: stable.id, + }) +} + +pub(crate) fn registered_contract(artifact: &Value) -> Result { + match ( + artifact.get("artifactSchema").and_then(Value::as_str), + artifact.get("type").and_then(Value::as_str), + ) { + (Some("code-intel-file-inventory.v1"), Some("inventory.files")) => Ok(ArtifactContract { + artifact_schema: "code-intel-file-inventory.v1", + artifact_type: "inventory.files", + max_bytes: MAX_ARTIFACT_BYTES, + validate_payload: validate_inventory, + }), + (Some("code-intel-repository-snapshot.v1"), Some("repository.snapshot")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-repository-snapshot.v1", + artifact_type: "repository.snapshot", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_repository_snapshot, + }) + } + (Some("code-intel-doctor-observation.v1"), Some("doctor.observation")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-doctor-observation.v1", + artifact_type: "doctor.observation", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_doctor_observation, + }) + } + ( + Some("code-intel-repository-survival-scan-result.v1"), + Some("repository.survival-scan"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-repository-survival-scan-result.v1", + artifact_type: "repository.survival-scan", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_survival_scan, + }), + (Some("code-intel-evidence-admissibility-result.v1"), Some("evidence.admission")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-evidence-admissibility-result.v1", + artifact_type: "evidence.admission", + max_bytes: 16 * 1024 * 1024, + validate_payload: validate_evidence_admission, + }) + } + (Some("code-intel-evidence-payload.v1"), Some("observed.evidence.payload")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-evidence-payload.v1", + artifact_type: "observed.evidence.payload", + max_bytes: 64 * 1024 * 1024, + validate_payload: validate_evidence_payload, + }) + } + ( + Some("code-intel-sentrux-command-observation.v1"), + Some("provider.sentrux.command-observation"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-sentrux-command-observation.v1", + artifact_type: "provider.sentrux.command-observation", + max_bytes: 2 * 1024 * 1024, + validate_payload: validate_sentrux_command_observation, + }), + (Some("code-intel-hospital.v1"), Some("diagnosis.hospital")) => Ok(ArtifactContract { + artifact_schema: "code-intel-hospital.v1", + artifact_type: "diagnosis.hospital", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_hospital_report, + }), + (Some("code-intel-hospital-markdown.v1"), Some("diagnosis.hospital-view")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-hospital-markdown.v1", + artifact_type: "diagnosis.hospital-view", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_hospital_markdown, + }) + } + (Some("code-intel-surgery-plan.v1"), Some("diagnosis.surgery-plan")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-surgery-plan.v1", + artifact_type: "diagnosis.surgery-plan", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_surgery_plan, + }) + } + (Some("code-intel-surgery-plan-markdown.v1"), Some("diagnosis.surgery-plan-view")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-surgery-plan-markdown.v1", + artifact_type: "diagnosis.surgery-plan-view", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_surgery_markdown, + }) + } + (Some("code-intel-project-orientation.v1"), Some("project.orientation")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-project-orientation.v1", + artifact_type: "project.orientation", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_project_orientation, + }) + } + (Some("code-intel-understanding-quadrant.v1"), Some("understanding.quadrant")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-understanding-quadrant.v1", + artifact_type: "understanding.quadrant", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_understanding_quadrant, + }) + } + ( + Some("code-intel-compatibility-retirement-manifest.v1"), + Some("compatibility.retirement-manifest"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-compatibility-retirement-manifest.v1", + artifact_type: "compatibility.retirement-manifest", + max_bytes: 4 * 1024 * 1024, + validate_payload: validate_retirement_manifest, + }), + ( + Some("code-intel-compatibility-retirement-evidence.v1"), + Some("compatibility.retirement-evidence"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-compatibility-retirement-evidence.v1", + artifact_type: "compatibility.retirement-evidence", + max_bytes: 4 * 1024 * 1024, + validate_payload: validate_retirement_evidence, + }), + ( + Some("code-intel-compatibility-retirement-decision.v1"), + Some("compatibility.retirement-decision"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-compatibility-retirement-decision.v1", + artifact_type: "compatibility.retirement-decision", + max_bytes: 4 * 1024 * 1024, + validate_payload: validate_retirement_decision, + }), + ( + Some("code-intel-compatibility-retirement-ticket-template.v1"), + Some("compatibility.retirement-ticket-template"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-compatibility-retirement-ticket-template.v1", + artifact_type: "compatibility.retirement-ticket-template", + max_bytes: 4 * 1024 * 1024, + validate_payload: validate_retirement_ticket_template, + }), + ( + Some("code-intel-compatibility-retirement-deletion-diff.v1"), + Some("compatibility.retirement-deletion-diff"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-compatibility-retirement-deletion-diff.v1", + artifact_type: "compatibility.retirement-deletion-diff", + max_bytes: 4 * 1024 * 1024, + validate_payload: validate_retirement_deletion_diff, + }), + ( + Some("code-intel-project-orientation-benchmark-observations.v1"), + Some("benchmark.orientation-observations"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-project-orientation-benchmark-observations.v1", + artifact_type: "benchmark.orientation-observations", + max_bytes: 64 * 1024 * 1024, + validate_payload: validate_orientation_benchmark_observations, + }), + ( + Some("code-intel-project-orientation-benchmark.v1"), + Some("benchmark.orientation-report"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-project-orientation-benchmark.v1", + artifact_type: "benchmark.orientation-report", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_orientation_benchmark_report, + }), + ( + Some("code-intel-project-orientation-benchmark-markdown.v1"), + Some("benchmark.orientation-report-view"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-project-orientation-benchmark-markdown.v1", + artifact_type: "benchmark.orientation-report-view", + max_bytes: 1024 * 1024, + validate_payload: validate_orientation_benchmark_markdown, + }), + (Some("code-intel-run-timing-events.v1"), Some("delivery.run-timing-events")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-run-timing-events.v1", + artifact_type: "delivery.run-timing-events", + max_bytes: 64 * 1024 * 1024, + validate_payload: validate_run_timing_events, + }) + } + (Some("code-intel-run-commit.v1"), Some("run.commit")) => Ok(ArtifactContract { + artifact_schema: "code-intel-run-commit.v1", + artifact_type: "run.commit", + max_bytes: 64 * 1024, + validate_payload: validate_run_commit, + }), + (Some("code-intel-run-manifest.v1"), Some("run.manifest")) => Ok(ArtifactContract { + artifact_schema: "code-intel-run-manifest.v1", + artifact_type: "run.manifest", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_run_manifest, + }), + (Some("code-intel-session-evidence.v1"), Some("verification.session-evidence")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-session-evidence.v1", + artifact_type: "verification.session-evidence", + max_bytes: 128 * 1024 * 1024, + validate_payload: validate_session_evidence, + }) + } + (Some("code-intel-method-catalog.v1"), Some("method.catalog")) => Ok(ArtifactContract { + artifact_schema: "code-intel-method-catalog.v1", + artifact_type: "method.catalog", + max_bytes: 256 * 1024, + validate_payload: validate_method_catalog, + }), + (Some("code-intel-method-card.v1"), Some("method.card")) => Ok(ArtifactContract { + artifact_schema: "code-intel-method-card.v1", + artifact_type: "method.card", + max_bytes: 256 * 1024, + validate_payload: validate_method_card, + }), + (Some("code-intel-delivery-light-speed.v1"), Some("delivery.light-speed-report")) => { + Ok(ArtifactContract { + artifact_schema: "code-intel-delivery-light-speed.v1", + artifact_type: "delivery.light-speed-report", + max_bytes: 8 * 1024 * 1024, + validate_payload: validate_light_speed_report, + }) + } + ( + Some("code-intel-delivery-light-speed-markdown.v1"), + Some("delivery.light-speed-report-view"), + ) => Ok(ArtifactContract { + artifact_schema: "code-intel-delivery-light-speed-markdown.v1", + artifact_type: "delivery.light-speed-report-view", + max_bytes: 1024 * 1024, + validate_payload: validate_light_speed_markdown, + }), + (Some("code-intel-decision-record.v1"), Some("decision.record")) => Ok(ArtifactContract { + artifact_schema: "code-intel-decision-record.v1", + artifact_type: "decision.record", + max_bytes: 1024 * 1024, + validate_payload: validate_decision_record_schema, + }), + (Some(schema), Some(artifact_type)) + if native_code_contract(schema, artifact_type).is_some() => + { + let (artifact_schema, artifact_type, validate_payload) = + native_code_contract(schema, artifact_type).expect("guard matched native contract"); + Ok(ArtifactContract { + artifact_schema, + artifact_type, + max_bytes: MAX_ARTIFACT_BYTES, + validate_payload, + }) + } + _ => Err(ArtifactError::Contract( + "Artifact Ref schema/type is not registered for capability input consumption" + .to_string(), + )), + } +} + +fn validate_evidence_payload(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "observed evidence payload")?; + exact_object_keys(&value, &["schema", "data"], "observed evidence payload")?; + if value["schema"] != "code-intel-evidence-payload.v1" + || value["data"].as_object().is_none_or(|data| data.is_empty()) + { + return Err("observed evidence payload contract is invalid".into()); + } + Ok(()) +} + +fn validate_sentrux_command_observation(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "Sentrux command observation")?; + exact_object_keys( + &value, + &["schema", "snapshotIdentity", "commands"], + "Sentrux command observation", + )?; + if value["schema"] != "code-intel-sentrux-command-observation.v1" + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + { + return Err("Sentrux command observation header is invalid".into()); + } + let commands = value["commands"] + .as_array() + .filter(|commands| commands.len() == 2) + .ok_or("Sentrux command observation must contain gate and check")?; + let mut seen = BTreeSet::new(); + for command in commands { + exact_object_keys( + command, + &["id", "argv", "exitCode", "success", "stdout", "stderr"], + "Sentrux command result", + )?; + let id = command["id"] + .as_str() + .filter(|id| matches!(*id, "gate" | "check")) + .ok_or("Sentrux command id is invalid")?; + if !seen.insert(id) { + return Err("Sentrux command ids must be unique".into()); + } + if command["argv"] != json!(["sentrux", id, "."]) + || (!command["exitCode"].is_null() && command["exitCode"].as_i64().is_none()) + || !command["success"].is_boolean() + || !command["stdout"].is_string() + || !command["stderr"].is_string() + { + return Err("Sentrux command result is invalid".into()); + } + } + Ok(()) +} + +fn validate_retirement_manifest(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "retirement manifest")?; + exact_object_keys( + &value, + &[ + "schema", + "snapshotIdentity", + "retirementId", + "approvalSubject", + "independentApproval", + ], + "retirement manifest", + )?; + if value["schema"] != "code-intel-compatibility-retirement-manifest.v1" + || !value["snapshotIdentity"] + .as_str() + .is_some_and(|v| !v.is_empty()) + || !value["retirementId"] + .as_str() + .is_some_and(|v| !v.is_empty()) + || !value["approvalSubject"].is_object() + || !value["independentApproval"].is_object() + { + return Err("retirement manifest contract is invalid".into()); + } + let subject = &value["approvalSubject"]; + exact_object_keys( + subject, + &[ + "legacyBranch", + "replacement", + "parity", + "registryReconciliation", + "compatibilityWindow", + "rollback", + "usageObservation", + "necessityEvidence", + "dependencyStates", + "lineReductionEvidence", + ], + "retirement approvalSubject", + )?; + exact_object_keys( + &subject["legacyBranch"], + &[ + "capabilityId", + "branchId", + "callPath", + "affectedFiles", + "owner", + "registryParticipantId", + ], + "retirement legacyBranch", + )?; + let branch_id = subject["legacyBranch"]["branchId"] + .as_str() + .ok_or("retirement legacyBranch branchId is invalid")?; + normalized_retirement_call_path(&subject["legacyBranch"]["callPath"], branch_id)?; + retirement_portable_paths( + &subject["legacyBranch"]["affectedFiles"], + "retirement legacyBranch.affectedFiles", + )?; + exact_object_keys( + &subject["replacement"], + &[ + "capabilityId", + "implementationId", + "dependencies", + "atomEvidence", + ], + "retirement replacement", + )?; + exact_object_keys( + &subject["parity"], + &["golden", "contract", "effects"], + "retirement parity", + )?; + exact_object_keys( + &subject["rollback"], + &["command", "executionEvidence"], + "retirement rollback", + )?; + if subject["lineReductionEvidence"] != false + || !subject["replacement"]["dependencies"].is_array() + || !subject["dependencyStates"] + .as_array() + .is_some_and(|v| !v.is_empty()) + || !subject["rollback"]["command"] + .as_str() + .is_some_and(|v| !v.is_empty()) + { + return Err("retirement approval subject is invalid".into()); + } + for reference in [ + &subject["replacement"]["atomEvidence"], + &subject["parity"]["golden"], + &subject["parity"]["contract"], + &subject["parity"]["effects"], + &subject["registryReconciliation"], + &subject["compatibilityWindow"], + &subject["rollback"]["executionEvidence"], + &subject["usageObservation"], + &subject["necessityEvidence"], + &value["independentApproval"], + ] { + validate_retirement_evidence_ref(reference)?; + } + for reference in subject["dependencyStates"].as_array().unwrap() { + validate_retirement_evidence_ref(reference)?; + } + Ok(()) +} + +fn validate_retirement_evidence_ref(value: &Value) -> Result<(), String> { + exact_object_keys( + value, + &[ + "schema", + "artifactSchema", + "type", + "path", + "sha256", + "consumedSnapshotIdentity", + ], + "retirement evidence ref", + )?; + if value["schema"] != "code-intel-artifact-ref.v1" + || value["artifactSchema"] != "code-intel-compatibility-retirement-evidence.v1" + || value["type"] != "compatibility.retirement-evidence" + || !value["path"].as_str().is_some_and(|v| !v.is_empty()) + || !value["consumedSnapshotIdentity"] + .as_str() + .is_some_and(|v| !v.is_empty()) + || !value["sha256"] + .as_str() + .is_some_and(|v| v.len() == 64 && v.bytes().all(|b| b.is_ascii_hexdigit())) + { + return Err("retirement evidence ref is invalid".into()); + } + Ok(()) +} + +fn validate_retirement_evidence(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "retirement evidence")?; + exact_object_keys( + &value, + &[ + "schema", + "snapshotIdentity", + "id", + "evidenceClass", + "retirementId", + "legacyBranchId", + "replacementCapabilityId", + "details", + ], + "retirement evidence", + )?; + const CLASSES: [&str; 11] = [ + "replacement_atom", + "golden_parity", + "contract_parity", + "effect_parity", + "registry_reconciliation", + "compatibility_window", + "rollback_execution", + "usage_observation", + "independent_approval", + "c00_necessity", + "dependency_approval", + ]; + if value["schema"] != "code-intel-compatibility-retirement-evidence.v1" + || !CLASSES.contains(&value["evidenceClass"].as_str().unwrap_or("")) + || !value["details"].is_object() + || [ + "snapshotIdentity", + "id", + "retirementId", + "legacyBranchId", + "replacementCapabilityId", + ] + .iter() + .any(|field| !value[field].as_str().is_some_and(|v| !v.is_empty())) + { + return Err("retirement evidence contract is invalid".into()); + } + Ok(()) +} + +fn validate_retirement_decision(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "retirement decision")?; + exact_object_keys( + &value, + &[ + "schema", + "snapshotIdentity", + "retirementId", + "legacyBranch", + "replacement", + "approvalSubjectSha256", + "decision", + "blockers", + "authorityBoundary", + "gainLedgerProjection", + ], + "retirement decision", + )?; + if value["schema"] != "code-intel-compatibility-retirement-decision.v1" + || !matches!(value["decision"].as_str(), Some("approved" | "blocked")) + || value["authorityBoundary"] != "approval_only_no_deletion_authority" + || !value["approvalSubjectSha256"] + .as_str() + .is_some_and(|v| v.len() == 64 && v.bytes().all(|b| b.is_ascii_hexdigit())) + || !value["blockers"].is_array() + || !value["gainLedgerProjection"].is_object() + { + return Err("retirement decision contract is invalid".into()); + } + Ok(()) +} + +fn validate_retirement_ticket_template(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "retirement ticket template")?; + exact_object_keys( + &value, + &[ + "schema", + "snapshotIdentity", + "ticketId", + "retirementId", + "legacyBranch", + "replacement", + "affectedFiles", + "evidence", + "source", + "owner", + "verifier", + "observationExpiry", + "status", + "authorityBoundary", + ], + "retirement ticket template", + )?; + exact_object_keys( + &value["legacyBranch"], + &["capabilityId", "branchId", "callPath"], + "ticket legacyBranch", + )?; + exact_object_keys( + &value["replacement"], + &["capabilityId", "dependencies"], + "ticket replacement", + )?; + exact_object_keys( + &value["evidence"], + &[ + "golden", + "contract", + "effects", + "usage", + "rollbackRehearsal", + "deletionDiff", + ], + "ticket evidence", + )?; + exact_object_keys( + &value["source"], + &["retirementDecision", "retirementManifest"], + "ticket source", + )?; + if value["schema"] != "code-intel-compatibility-retirement-ticket-template.v1" + || value["status"] != "draft" + || value["authorityBoundary"] != "template_only_no_approval_or_deletion_authority" + || [ + "snapshotIdentity", + "ticketId", + "retirementId", + "owner", + "verifier", + ] + .iter() + .any(|key| !value[key].as_str().is_some_and(|v| !v.is_empty())) + || value["owner"] == value["verifier"] + || value["observationExpiry"].as_u64().is_none() + || !value["affectedFiles"] + .as_array() + .is_some_and(|v| !v.is_empty()) + { + return Err("retirement ticket template contract is invalid".into()); + } + for key in ["capabilityId", "branchId", "callPath"] { + if !value["legacyBranch"][key] + .as_str() + .is_some_and(|v| !v.is_empty()) + { + return Err("retirement ticket legacy branch is invalid".into()); + } + } + if !value["replacement"]["capabilityId"] + .as_str() + .is_some_and(|v| !v.is_empty()) + || !closed_unique_strings(&value["replacement"]["dependencies"], false) + || !closed_unique_strings(&value["affectedFiles"], true) + { + return Err("retirement ticket replacement/files are invalid".into()); + } + for key in [ + "golden", + "contract", + "effects", + "usage", + "rollbackRehearsal", + ] { + validate_retirement_evidence_ref(&value["evidence"][key])?; + } + validate_ticket_ref( + &value["evidence"]["deletionDiff"], + "code-intel-compatibility-retirement-deletion-diff.v1", + "compatibility.retirement-deletion-diff", + )?; + validate_ticket_ref( + &value["source"]["retirementDecision"], + "code-intel-compatibility-retirement-decision.v1", + "compatibility.retirement-decision", + )?; + validate_ticket_ref( + &value["source"]["retirementManifest"], + "code-intel-compatibility-retirement-manifest.v1", + "compatibility.retirement-manifest", + )?; + Ok(()) +} + +fn validate_ticket_ref(value: &Value, schema: &str, kind: &str) -> Result<(), String> { + exact_object_keys( + value, + &[ + "schema", + "artifactSchema", + "type", + "path", + "sha256", + "consumedSnapshotIdentity", + ], + "ticket Artifact Ref", + )?; + if value["schema"] != "code-intel-artifact-ref.v1" + || value["artifactSchema"] != schema + || value["type"] != kind + || !value["path"].as_str().is_some_and(|v| !v.is_empty()) + || !value["consumedSnapshotIdentity"] + .as_str() + .is_some_and(|v| !v.is_empty()) + || !value["sha256"] + .as_str() + .is_some_and(|v| v.len() == 64 && v.bytes().all(|b| b.is_ascii_hexdigit())) + { + return Err("ticket Artifact Ref is invalid".into()); + } + Ok(()) +} + +fn closed_unique_strings(value: &Value, portable_paths: bool) -> bool { + let Some(values) = value.as_array().filter(|v| !v.is_empty()) else { + return false; + }; + let mut seen = BTreeSet::new(); + values.iter().all(|value| { + value.as_str().is_some_and(|text| { + !text.is_empty() + && (!portable_paths + || (!text.contains('\\') + && !text.starts_with('/') + && !text.split('/').any(|part| part == ".."))) + && seen.insert(text) + }) + }) +} + +fn validate_retirement_deletion_diff(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "retirement deletion diff")?; + validate_retirement_deletion_diff_value(&value) +} + +pub(crate) fn validate_retirement_deletion_diff_value(value: &Value) -> Result<(), String> { + exact_object_keys( + value, + &[ + "schema", + "snapshotIdentity", + "retirementId", + "legacyBranchId", + "affectedFiles", + "deletionsOnly", + "summary", + "patch", + ], + "retirement deletion diff", + )?; + if value["schema"] != "code-intel-compatibility-retirement-deletion-diff.v1" + || [ + "snapshotIdentity", + "retirementId", + "legacyBranchId", + "summary", + ] + .iter() + .any(|key| !value[key].as_str().is_some_and(|v| !v.is_empty())) + || value["deletionsOnly"] != true + { + return Err("retirement deletion diff contract is invalid".into()); + } + let affected = retirement_portable_paths(&value["affectedFiles"], "affectedFiles")?; + let patch = &value["patch"]; + exact_object_keys( + patch, + &["algorithm", "sha256", "files"], + "retirement deletion patch", + )?; + if patch["algorithm"] != "replayable-delete-only-v1" || !is_lower_sha(&patch["sha256"]) { + return Err("retirement deletion patch contract is invalid".into()); + } + let files = patch["files"] + .as_array() + .filter(|files| !files.is_empty()) + .ok_or("retirement deletion patch files must not be empty")?; + let patch_sha = sha256_hex( + &serde_json::to_vec(files).map_err(|error| format!("serialize deletion patch: {error}"))?, + ); + if patch["sha256"] != patch_sha { + return Err("retirement deletion patch SHA-256 mismatch".into()); + } + let mut touched = Vec::with_capacity(files.len()); + for file in files { + exact_object_keys( + file, + &[ + "path", + "baseBlobSha256", + "resultBlobSha256", + "baseText", + "resultText", + "hunks", + ], + "retirement deletion patch file", + )?; + let path = file["path"] + .as_str() + .ok_or("retirement deletion patch path is invalid")?; + validate_portable_path(path, "retirement deletion patch path")?; + touched.push(path.to_string()); + let base = file["baseText"] + .as_str() + .filter(|text| !text.contains('\r')) + .ok_or("retirement deletion baseText must use normalized LF text")?; + let result = file["resultText"] + .as_str() + .filter(|text| !text.contains('\r')) + .ok_or("retirement deletion resultText must use normalized LF text")?; + if !is_lower_sha(&file["baseBlobSha256"]) + || !is_lower_sha(&file["resultBlobSha256"]) + || file["baseBlobSha256"] != sha256_hex(base.as_bytes()) + || file["resultBlobSha256"] != sha256_hex(result.as_bytes()) + { + return Err("retirement deletion blob SHA-256 mismatch".into()); + } + replay_delete_only(base, result, &file["hunks"])?; + } + if touched != affected { + return Err("retirement deletion touched paths differ from affectedFiles".into()); + } + Ok(()) +} + +fn replay_delete_only(base: &str, result: &str, hunks: &Value) -> Result<(), String> { + let hunks = hunks + .as_array() + .filter(|hunks| !hunks.is_empty()) + .ok_or("retirement deletion patch requires at least one hunk")?; + let base_lines = base.split('\n').collect::>(); + let mut rebuilt = Vec::<&str>::new(); + let mut cursor = 0usize; + let mut deleted_before = 0usize; + for hunk in hunks { + exact_object_keys( + hunk, + &[ + "oldStart", + "oldLines", + "newStart", + "newLines", + "deletedLines", + "addedLines", + ], + "retirement deletion hunk", + )?; + let old_start = hunk["oldStart"] + .as_u64() + .and_then(|value| usize::try_from(value).ok()) + .filter(|value| *value > 0) + .ok_or("retirement deletion hunk oldStart is invalid")?; + let old_lines = hunk["oldLines"] + .as_u64() + .and_then(|value| usize::try_from(value).ok()) + .filter(|value| *value > 0) + .ok_or("retirement deletion hunk oldLines is invalid")?; + let new_start = hunk["newStart"] + .as_u64() + .and_then(|value| usize::try_from(value).ok()) + .filter(|value| *value > 0) + .ok_or("retirement deletion hunk newStart is invalid")?; + if hunk["newLines"] != 0 + || !hunk["addedLines"] + .as_array() + .is_some_and(|lines| lines.is_empty()) + { + return Err("retirement deletion patch contains added or replacement lines".into()); + } + let deleted = hunk["deletedLines"] + .as_array() + .filter(|lines| lines.len() == old_lines) + .ok_or("retirement deletion hunk line count mismatch")?; + let start = old_start - 1; + if start < cursor + || start + old_lines > base_lines.len() + || new_start != old_start.saturating_sub(deleted_before) + { + return Err("retirement deletion hunks overlap or use invalid coordinates".into()); + } + rebuilt.extend_from_slice(&base_lines[cursor..start]); + for (actual, expected) in base_lines[start..start + old_lines].iter().zip(deleted) { + if expected.as_str() != Some(*actual) { + return Err("retirement deletion hunk does not match base text".into()); + } + } + cursor = start + old_lines; + deleted_before += old_lines; + } + rebuilt.extend_from_slice(&base_lines[cursor..]); + if rebuilt.join("\n") != result { + return Err("retirement deletion patch does not reproduce result text".into()); + } + Ok(()) +} + +pub(crate) fn normalized_retirement_call_path( + value: &Value, + branch_id: &str, +) -> Result { + let text = value + .as_str() + .filter(|text| !text.trim().is_empty()) + .ok_or("retirement callPath is missing")?; + let (path, branch) = text + .split_once("::") + .filter(|(_, branch)| !branch.contains("::")) + .ok_or("retirement callPath must use ::")?; + validate_portable_path(path, "retirement callPath")?; + if branch != branch_id || text != format!("{path}::{branch_id}") { + return Err("retirement callPath is not canonical for the approved branch".into()); + } + Ok(text.to_string()) +} + +pub(crate) fn retirement_portable_paths(value: &Value, label: &str) -> Result, String> { + let values = value + .as_array() + .filter(|values| !values.is_empty()) + .ok_or_else(|| format!("{label} must be a non-empty array"))?; + let mut paths = Vec::with_capacity(values.len()); + for value in values { + let path = value + .as_str() + .ok_or_else(|| format!("{label} contains an invalid path"))?; + validate_portable_path(path, label)?; + paths.push(path.to_string()); + } + if paths.windows(2).any(|pair| pair[0] >= pair[1]) { + return Err(format!("{label} must be sorted and unique")); + } + Ok(paths) +} + +fn validate_portable_path(path: &str, label: &str) -> Result<(), String> { + if path.is_empty() + || path.contains('\\') + || path.starts_with('/') + || path.ends_with('/') + || path + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + || path.contains(':') + { + Err(format!("{label} contains a non-portable path")) + } else { + Ok(()) + } +} + +fn is_lower_sha(value: &Value) -> bool { + value.as_str().is_some_and(|value| { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + }) +} + +fn parse_contract_json(bytes: &[u8], label: &str) -> Result { + let text = std::str::from_utf8(bytes).map_err(|e| format!("{label} is not UTF-8: {e}"))?; + reject_duplicate_json_keys(text)?; + serde_json::from_str(text).map_err(|e| format!("{label} is not JSON: {e}")) +} + +fn validate_hospital_report(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("hospital report is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("hospital report is not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "domainVerdict", + "generatedAt", + "repo", + "mode", + "artifacts", + "triage", + "state_machine", + "modalities", + "policies", + "report_quality", + "diagnosis", + "treatment", + "protocols", + "tools", + "surgery_plan", + ], + "hospital report", + )?; + if value["schema"] != "code-intel-hospital.v1" + || !matches!( + value["domainVerdict"].as_str(), + Some("pass" | "fail" | "unknown") + ) + || !matches!( + value.pointer("/triage/status").and_then(Value::as_str), + Some("green" | "amber" | "red" | "unknown") + ) + || !matches!( + value.pointer("/triage/disposition").and_then(Value::as_str), + Some("admit" | "observe") + ) + || !matches!( + value + .pointer("/triage/next_protocol") + .and_then(Value::as_str), + Some("triage" | "diagnose" | "govern" | "surgery_plan" | "post_op") + ) + { + return Err("hospital report verdict/triage contract is invalid".into()); + } + validate_surgery_plan_value(&value["surgery_plan"]) +} + +fn validate_surgery_plan(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("surgery plan is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|error| format!("surgery plan is not JSON: {error}"))?; + validate_surgery_plan_value(&value) +} + +fn validate_surgery_plan_value(value: &Value) -> Result<(), String> { + exact_object_keys( + value, + &[ + "schema", + "status", + "admission", + "primary_target", + "operating_plan", + "verification", + "discharge_criteria", + ], + "surgery plan", + )?; + if value["schema"] != "code-intel-surgery-plan.v1" + || !matches!(value["status"].as_str(), Some("planned" | "not_required")) + || !value["admission"].is_object() + || !value["primary_target"].is_object() + || !value["operating_plan"].is_array() + || !value["verification"].is_array() + || !value["discharge_criteria"].is_array() + { + return Err("surgery plan contract is invalid".into()); + } + Ok(()) +} + +fn validate_hospital_markdown(bytes: &[u8]) -> Result<(), String> { + validate_markdown_view(bytes, "# Code Intel Hospital Report") +} + +fn validate_surgery_markdown(bytes: &[u8]) -> Result<(), String> { + validate_markdown_view(bytes, "# Code Intel Surgery Plan") +} + +fn validate_project_orientation(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("project orientation is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("project orientation is not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "snapshotIdentity", + "identity", + "purpose", + "languages", + "boundaries", + "entryPoints", + "commands", + "activeChange", + "evidenceAvailability", + "risks", + "unknowns", + "confidence", + ], + "project orientation", + )?; + if value["schema"] != "code-intel-project-orientation.v1" + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !value["identity"].is_object() + || !value["purpose"].is_object() + || !value["languages"].is_array() + || !value["boundaries"].is_array() + || !value["entryPoints"].is_array() + || !value["commands"].is_array() + || !value["activeChange"].is_object() + || !value["evidenceAvailability"].is_array() + || !value["risks"].is_array() + || !value["unknowns"] + .as_array() + .is_some_and(|unknowns| !unknowns.is_empty()) + || !matches!( + value.pointer("/confidence/level").and_then(Value::as_str), + Some("low" | "medium" | "high") + ) + { + return Err("project orientation contract is invalid".into()); + } + for (label, claim) in [ + ("identity", &value["identity"]), + ("purpose", &value["purpose"]), + ("activeChange", &value["activeChange"]), + ("confidence", &value["confidence"]), + ] { + validate_claim_provenance(&claim["provenance"], label)?; + } + for field in [ + "languages", + "boundaries", + "entryPoints", + "commands", + "evidenceAvailability", + "risks", + "unknowns", + ] { + for (index, claim) in value[field].as_array().unwrap().iter().enumerate() { + validate_claim_provenance(&claim["provenance"], &format!("{field}[{index}]"))?; + } + } + Ok(()) +} + +fn validate_claim_provenance(value: &Value, label: &str) -> Result<(), String> { + let entries = value + .as_array() + .filter(|entries| !entries.is_empty()) + .ok_or_else(|| format!("{label} provenance must be a nonempty array"))?; + let mut identities = BTreeSet::new(); + for entry in entries { + exact_object_keys( + entry, + &["artifactType", "artifactSha256", "jsonPointer"], + &format!("{label} provenance entry"), + )?; + if !entry["artifactType"] + .as_str() + .is_some_and(|value| !value.is_empty()) + || !entry["artifactSha256"].as_str().is_some_and(valid_digest) + || !entry["jsonPointer"] + .as_str() + .is_some_and(|value| value.starts_with('/')) + { + return Err(format!("{label} provenance entry is invalid")); + } + let identity = serde_json::to_string(entry) + .map_err(|error| format!("serialize {label} provenance entry: {error}"))?; + if !identities.insert(identity) { + return Err(format!("{label} provenance entries must be unique")); + } + } + Ok(()) +} + +fn validate_understanding_quadrant(bytes: &[u8]) -> Result<(), String> { + let value = parse_understanding_quadrant(bytes)?; + validate_understanding_quadrant_shape(&value)?; + validate_understanding_quadrant_identity(&value)?; + let (expected_unknowns, expected_counts) = validate_understanding_quadrant_items(&value)?; + validate_understanding_quadrant_summary(&value, expected_unknowns, &expected_counts) +} + +fn parse_understanding_quadrant(bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("understanding quadrant is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + serde_json::from_str(text) + .map_err(|error| format!("understanding quadrant is not JSON: {error}")) +} + +fn validate_understanding_quadrant_shape(value: &Value) -> Result<(), String> { + exact_object_keys( + value, + &[ + "schema", + "snapshotIdentity", + "sourceOrientation", + "classificationPolicy", + "items", + "visibleUnknowns", + "counts", + ], + "understanding quadrant", + )?; + exact_object_keys( + &value["sourceOrientation"], + &["artifactSchema", "artifactType", "sha256"], + "understanding quadrant source", + )?; + exact_object_keys( + &value["classificationPolicy"], + &[ + "schema", + "scoreRange", + "systemCriticalityThreshold", + "evidenceConfidenceThreshold", + "thresholdRule", + "unknownCriticalityRule", + "methodConsumerPolicy", + ], + "understanding quadrant policy", + )?; + exact_object_keys( + &value["classificationPolicy"]["scoreRange"], + &["minimum", "maximum"], + "understanding quadrant score range", + )?; + exact_object_keys( + &value["counts"], + &[ + "Known Core", + "Critical Unknown", + "Supporting Context", + "Deferred Unknown", + ], + "understanding quadrant counts", + ) +} + +fn validate_understanding_quadrant_identity(value: &Value) -> Result<(), String> { + if value["schema"] != "code-intel-understanding-quadrant.v1" + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || value.pointer("/sourceOrientation/artifactSchema") + != Some(&json!("code-intel-project-orientation.v1")) + || value.pointer("/sourceOrientation/artifactType") != Some(&json!("project.orientation")) + || !value + .pointer("/sourceOrientation/sha256") + .and_then(Value::as_str) + .is_some_and(valid_digest) + || value.pointer("/classificationPolicy/schema") + != Some(&json!("code-intel-understanding-quadrant-policy.v1")) + || value.pointer("/classificationPolicy/scoreRange/minimum") != Some(&json!(0)) + || value.pointer("/classificationPolicy/scoreRange/maximum") != Some(&json!(100)) + || value.pointer("/classificationPolicy/systemCriticalityThreshold") != Some(&json!(50)) + || value.pointer("/classificationPolicy/evidenceConfidenceThreshold") != Some(&json!(50)) + || value.pointer("/classificationPolicy/thresholdRule") + != Some(&json!("greater_than_or_equal_is_upper_band")) + || value.pointer("/classificationPolicy/unknownCriticalityRule") + != Some(&json!( + "critical_by_default_except_declared_supporting_context" + )) + || value.pointer("/classificationPolicy/methodConsumerPolicy") + != Some(&json!( + "C01_cards_and_C02_selection_may_consume_but_cannot_rewrite" + )) + { + return Err("understanding quadrant identity/policy contract is invalid".into()); + } + Ok(()) +} + +fn validate_understanding_quadrant_items( + value: &Value, +) -> Result<(Vec, BTreeMap<&'static str, u64>), String> { + let items = value["items"] + .as_array() + .filter(|items| !items.is_empty()) + .ok_or("understanding quadrant items must be nonempty")?; + let mut prior = None::; + let mut expected_unknowns = Vec::new(); + let mut expected_counts = BTreeMap::<&'static str, u64>::new(); + for item in items { + let (id, quadrant, is_unknown) = validate_understanding_quadrant_item(item, &prior)?; + prior = Some(id.clone()); + *expected_counts.entry(quadrant).or_default() += 1; + if is_unknown { + expected_unknowns.push(Value::String(id)); + } + } + Ok((expected_unknowns, expected_counts)) +} + +fn validate_understanding_quadrant_item( + item: &Value, + prior: &Option, +) -> Result<(String, &'static str, bool), String> { + exact_object_keys( + item, + &[ + "id", + "subject", + "sourceState", + "systemCriticality", + "evidenceConfidence", + "quadrant", + "statement", + "provenance", + ], + "understanding quadrant item", + )?; + exact_object_keys( + &item["systemCriticality"], + &["score", "band"], + "system criticality", + )?; + exact_object_keys( + &item["evidenceConfidence"], + &["score", "band"], + "evidence confidence", + )?; + let id = item["id"] + .as_str() + .filter(|id| !id.is_empty()) + .ok_or("understanding quadrant item id is missing")?; + if prior.as_deref().is_some_and(|prior| prior >= id) { + return Err("understanding quadrant items are not uniquely sorted by id".into()); + } + let criticality = item + .pointer("/systemCriticality/score") + .and_then(Value::as_u64); + let confidence = item + .pointer("/evidenceConfidence/score") + .and_then(Value::as_u64); + let (criticality, confidence) = match (criticality, confidence) { + (Some(criticality @ 0..=100), Some(confidence @ 0..=100)) => (criticality, confidence), + _ => return Err("understanding quadrant score is outside 0..=100".into()), + }; + let expected = expected_understanding_quadrant(criticality, confidence); + validate_claim_provenance(&item["provenance"], id)?; + let source_state = item["sourceState"].as_str(); + if item + .pointer("/systemCriticality/band") + .and_then(Value::as_str) + != Some(expected.0) + || item + .pointer("/evidenceConfidence/band") + .and_then(Value::as_str) + != Some(expected.1) + || item["quadrant"] != expected.2 + || !matches!(source_state, Some("known" | "unknown")) + { + return Err("understanding quadrant item classification is incoherent".into()); + } + Ok((id.to_string(), expected.2, source_state == Some("unknown"))) +} + +fn expected_understanding_quadrant( + criticality: u64, + confidence: u64, +) -> (&'static str, &'static str, &'static str) { + match (criticality >= 50, confidence >= 50) { + (true, true) => ("critical", "high", "Known Core"), + (true, false) => ("critical", "low", "Critical Unknown"), + (false, true) => ("supporting", "high", "Supporting Context"), + (false, false) => ("supporting", "low", "Deferred Unknown"), + } +} + +fn validate_understanding_quadrant_summary( + value: &Value, + expected_unknowns: Vec, + expected_counts: &BTreeMap<&'static str, u64>, +) -> Result<(), String> { + if value["visibleUnknowns"] != Value::Array(expected_unknowns) { + return Err("understanding quadrant hides or invents unknowns".into()); + } + for quadrant in [ + "Known Core", + "Critical Unknown", + "Supporting Context", + "Deferred Unknown", + ] { + if value["counts"][quadrant].as_u64() + != Some(expected_counts.get(quadrant).copied().unwrap_or(0)) + { + return Err("understanding quadrant counts do not match items".into()); + } + } + Ok(()) +} + +fn validate_orientation_benchmark_observations(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("orientation benchmark observations are not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("orientation benchmark observations are not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "snapshotIdentity", + "method", + "environment", + "fixtures", + ], + "orientation benchmark observations", + )?; + if value["schema"] != "code-intel-project-orientation-benchmark-observations.v1" + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || value.pointer("/method/clock") != Some(&Value::String("std::time::Instant".into())) + || value.pointer("/method/execution") + != Some(&Value::String("sequential_child_process".into())) + || value.pointer("/method/concurrency").and_then(Value::as_u64) != Some(1) + || value.pointer("/method/llm") != Some(&Value::String("disabled".into())) + || value + .pointer("/environment/cleanMachine") + .and_then(Value::as_bool) + != Some(false) + || value["fixtures"] + .as_array() + .map_or(true, |items| items.len() != 9) + { + return Err("orientation benchmark observation contract is invalid".into()); + } + for fixture in value["fixtures"].as_array().unwrap() { + exact_object_keys( + &fixture["expected"], + &[ + "activeChange", + "fileCount", + "providerStatus", + "unknownFields", + "unsupportedFiles", + ], + "orientation benchmark expected fields", + )?; + if !fixture["expected"]["fileCount"].is_u64() + || !matches!( + fixture["expected"]["providerStatus"].as_str(), + Some("available" | "unavailable") + ) + || !fixture["expected"]["unknownFields"] + .as_array() + .is_some_and(|items| items.iter().all(Value::is_string)) + || !fixture["expected"]["unsupportedFiles"] + .as_array() + .is_some_and(|items| items.iter().all(Value::is_string)) + { + return Err("orientation benchmark expected fields are invalid".into()); + } + for temperature in ["cold", "warm"] { + let samples = fixture["samples"][temperature] + .as_array() + .ok_or_else(|| "orientation benchmark samples are invalid".to_string())?; + for sample in samples { + if sample + .pointer("/artifact/bytes") + .and_then(Value::as_u64) + .is_none() + || !sample + .pointer("/artifact/sha256") + .and_then(Value::as_str) + .is_some_and(valid_digest) + || !sample + .pointer("/coverage/unsupportedFiles") + .and_then(Value::as_array) + .is_some_and(|items| items.iter().all(Value::is_string)) + { + return Err("orientation benchmark sample measurement is invalid".into()); + } + } + } + } + Ok(()) +} + +fn validate_orientation_benchmark_report(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("orientation benchmark report is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("orientation benchmark report is not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "verdict", + "target", + "corpus", + "method", + "environment", + "latency", + "artifactSize", + "quality", + "costCenters", + "limitations", + ], + "orientation benchmark report", + )?; + if value["schema"] != "code-intel-project-orientation-benchmark.v1" + || !matches!(value["verdict"].as_str(), Some("pass" | "fail")) + || value.pointer("/target/llm") != Some(&Value::String("disabled".into())) + || value + .pointer("/latency/typical/p50WallTimeMs") + .and_then(Value::as_u64) + .is_none() + || value + .pointer("/latency/typical/p95WallTimeMs") + .and_then(Value::as_u64) + .is_none() + || value + .pointer("/artifactSize/typical/p95Bytes") + .and_then(Value::as_u64) + .is_none() + || [ + "fieldCorrectness", + "unresolvedCoverage", + "unsupportedCoverage", + "deterministicReplayRate", + "provenanceCompleteness", + ] + .into_iter() + .any(|field| { + value["quality"][field] + .as_f64() + .is_none_or(|metric| !(0.0..=1.0).contains(&metric)) + }) + || value["costCenters"].as_array().is_none_or(Vec::is_empty) + { + return Err("orientation benchmark report contract is invalid".into()); + } + Ok(()) +} + +fn validate_orientation_benchmark_markdown(bytes: &[u8]) -> Result<(), String> { + validate_markdown_view(bytes, "# Project Orientation Benchmark") +} + +fn validate_run_commit(bytes: &[u8]) -> Result<(), String> { + let text = + std::str::from_utf8(bytes).map_err(|error| format!("run commit is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|error| format!("run commit is not JSON: {error}"))?; + exact_object_keys( + &value, + &["schema", "runIdentity", "snapshotIdentity", "manifest"], + "run commit", + )?; + exact_object_keys( + &value["manifest"], + &["path", "sha256"], + "run commit manifest", + )?; + let manifest_sha = value["manifest"]["sha256"].as_str(); + if value["schema"] != "code-intel-run-commit.v1" + || !value["runIdentity"] + .as_str() + .is_some_and(valid_run_identity) + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !manifest_sha.is_some_and(valid_digest) + || value["manifest"]["path"].as_str() + != manifest_sha + .map(|sha| format!("objects/sha256/{sha}")) + .as_deref() + { + return Err("run commit contract is invalid".into()); + } + Ok(()) +} + +fn validate_run_manifest(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("run manifest is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|error| format!("run manifest is not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "runIdentity", + "snapshotIdentity", + "outcome", + "nodes", + ], + "run manifest", + )?; + if value["schema"] != "code-intel-run-manifest.v1" + || !value["runIdentity"] + .as_str() + .is_some_and(valid_run_identity) + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !matches!( + value["outcome"].as_str(), + Some("completed" | "domain_failed" | "domain_unknown" | "process_failed") + ) + { + return Err("run manifest identity/outcome is invalid".into()); + } + let nodes = value["nodes"] + .as_object() + .filter(|nodes| !nodes.is_empty()) + .ok_or("run manifest nodes must be a non-empty object")?; + for node in nodes.values() { + match node["status"].as_str() { + Some("succeeded") => { + exact_object_keys( + node, + &["status", "verdict", "artifacts"], + "succeeded run node", + )?; + if !matches!( + node["verdict"].as_str(), + Some("pass" | "unknown" | "not_applicable") + ) || !node["artifacts"].is_array() + { + return Err("succeeded run node is invalid".into()); + } + for reference in node["artifacts"].as_array().unwrap() { + validate_artifact_ref_shape(reference)?; + } + } + Some("domain_failed") => { + exact_object_keys( + node, + &["status", "verdict", "diagnostic", "artifacts"], + "domain-failed run node", + )?; + if node["verdict"] != "fail" + || node["diagnostic"].as_str().is_none_or(str::is_empty) + || !node["artifacts"].is_array() + { + return Err("domain-failed run node is invalid".into()); + } + for reference in node["artifacts"].as_array().unwrap() { + validate_artifact_ref_shape(reference)?; + } + } + Some("process_failed") => { + exact_object_keys( + node, + &["status", "failure", "diagnostic"], + "process-failed run node", + )?; + if !matches!( + node["failure"].as_str(), + Some("contract" | "unavailable" | "internal" | "io") + ) || node["diagnostic"].as_str().is_none_or(str::is_empty) + { + return Err("process-failed run node is invalid".into()); + } + } + Some("dependency_blocked") => { + exact_object_keys(node, &["status", "blockedBy"], "blocked run node")?; + if node["blockedBy"].as_array().is_none_or(Vec::is_empty) { + return Err("blocked run node is invalid".into()); + } + } + _ => return Err("run manifest contains a non-terminal node".into()), + } + } + Ok(()) +} + +fn validate_method_catalog(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("method catalog is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("method catalog is not JSON: {error}"))?; + exact_object_keys( + &value, + &["schema", "catalogVersion", "selectionPolicy", "cards"], + "method catalog", + )?; + let cards = value["cards"] + .as_array() + .filter(|cards| !cards.is_empty()) + .ok_or("method catalog cards must be non-empty")?; + if value["schema"] != "code-intel-method-catalog.v1" + || value["selectionPolicy"] != "none_catalog_only" + || value["catalogVersion"].as_str().is_none_or(str::is_empty) + { + return Err("method catalog contract is invalid".into()); + } + let mut ids = BTreeSet::new(); + for card in cards { + exact_object_keys(card, &["id", "path"], "method catalog entry")?; + let id = card["id"] + .as_str() + .filter(|id| !id.is_empty()) + .ok_or("method catalog id is invalid")?; + if !ids.insert(id) || card["path"] != format!("cards/{id}.v1.json") { + return Err("method catalog entry is invalid or duplicated".into()); + } + } + Ok(()) +} + +fn validate_method_card(bytes: &[u8]) -> Result<(), String> { + let text = + std::str::from_utf8(bytes).map_err(|error| format!("method card is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|error| format!("method card is not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "id", + "version", + "name", + "problemSignals", + "requiredEvidence", + "assumptions", + "deterministicSteps", + "outputs", + "confidenceRules", + "cost", + "contraindications", + "implementationPorts", + "source", + "applicabilityBoundary", + "relatedMethodIds", + "executionPolicy", + ], + "method card", + )?; + if value["schema"] != "code-intel-method-card.v1" + || value["id"].as_str().is_none_or(str::is_empty) + || value["version"].as_str().is_none_or(str::is_empty) + || value["executionPolicy"] != "catalog_only_no_selection_or_execution" + || value["deterministicSteps"] + .as_array() + .is_none_or(Vec::is_empty) + { + return Err("method card contract is invalid".into()); + } + Ok(()) +} + +fn validate_run_timing_events(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("run timing events are not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("run timing events are not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "measurementSnapshotIdentity", + "telemetry", + "baseline", + "current", + ], + "run timing events", + )?; + exact_object_keys( + &value["telemetry"], + &["mode", "clock", "externalPlatform"], + "run timing telemetry", + )?; + if value["schema"] != "code-intel-run-timing-events.v1" + || !value["measurementSnapshotIdentity"] + .as_str() + .is_some_and(valid_digest) + || value.pointer("/telemetry/mode") != Some(&Value::String("local_opt_in".into())) + || value.pointer("/telemetry/clock") != Some(&Value::String("monotonic_elapsed_ms".into())) + || value + .pointer("/telemetry/externalPlatform") + .and_then(Value::as_bool) + != Some(false) + { + return Err("run timing telemetry policy is invalid".into()); + } + for label in ["baseline", "current"] { + let trace = &value[label]; + exact_object_keys(trace, &["commitRef", "events"], "run timing trace")?; + let commit_ref = &trace["commitRef"]; + validate_artifact_ref_shape(commit_ref)?; + if commit_ref["artifactSchema"] != "code-intel-run-commit.v1" + || commit_ref["type"] != "run.commit" + || commit_ref["consumedSnapshotIdentity"] != value["measurementSnapshotIdentity"] + { + return Err("run timing trace is not bound to an A07 commit Artifact Ref".into()); + } + let events = trace["events"] + .as_array() + .filter(|events| !events.is_empty()) + .ok_or("run timing trace events must be non-empty")?; + for event in events { + exact_object_keys( + event, + &[ + "id", + "kind", + "subject", + "startedAtMs", + "completedAtMs", + "mandatory", + "coordinationNeed", + "predecessors", + ], + "run timing event", + )?; + let start = event["startedAtMs"].as_u64(); + let end = event["completedAtMs"].as_u64(); + if event["id"].as_str().is_none_or(str::is_empty) + || event["subject"].as_str().is_none_or(str::is_empty) + || !matches!( + event["kind"].as_str(), + Some( + "technical_work" + | "test" + | "verification" + | "queue" + | "handoff" + | "understanding" + | "rework" + | "coordination" + ) + ) + || start.is_none() + || end.zip(start).is_none_or(|(end, start)| end <= start) + || event["mandatory"].as_bool().is_none() + || !event["predecessors"].is_array() + { + return Err("run timing event contract is invalid".into()); + } + } + } + Ok(()) +} + +fn validate_light_speed_report(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("light-speed report is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("light-speed report is not JSON: {error}"))?; + exact_object_keys( + &value, + &[ + "schema", + "measurementSnapshotIdentity", + "authority", + "method", + "rules", + "baseline", + "current", + "delta", + "limitations", + ], + "light-speed report", + )?; + if value["schema"] != "code-intel-delivery-light-speed.v1" + || !value["measurementSnapshotIdentity"] + .as_str() + .is_some_and(valid_digest) + || value["authority"] != "derived_measurement_no_schedule_commitment" + || !value["rules"] + .as_array() + .is_some_and(|rules| rules.len() == 7) + || !value["baseline"].is_object() + || !value["current"].is_object() + || !value["delta"].is_object() + || value["limitations"].as_array().is_none_or(Vec::is_empty) + { + return Err("light-speed report contract is invalid".into()); + } + Ok(()) +} + +fn validate_light_speed_markdown(bytes: &[u8]) -> Result<(), String> { + validate_markdown_view(bytes, "# Delivery Light-Speed Measurement") +} + +fn validate_session_evidence(bytes: &[u8]) -> Result<(), String> { + let value = parse_contract_json(bytes, "session evidence")?; + validate_session_evidence_value(&value) +} + +#[cfg(not(test))] +fn validate_session_evidence_value(value: &Value) -> Result<(), String> { + crate::session_evidence::validate_artifact_value(value) +} + +// Many integration tests compile artifact_ref.rs as a stand-alone path module. They do not +// consume session evidence; keep that test-only compilation surface independent of the binary +// crate root. End-to-end session tests exercise the non-test binary and the full validator above. +#[cfg(test)] +fn validate_session_evidence_value(value: &Value) -> Result<(), String> { + exact_object_keys( + value, + &[ + "schema", + "status", + "reviewAuthority", + "snapshot", + "source", + "implementation", + "privacy", + "observability", + "summary", + "events", + "signals", + ], + "session evidence", + )?; + if value["schema"] != "code-intel-session-evidence.v1" + || !matches!(value["status"].as_str(), Some("complete" | "partial")) + || value["reviewAuthority"] != "advisory_only" + || !value["snapshot"].is_object() + || !value["source"].is_object() + || !value["implementation"].is_object() + || !value["privacy"].is_object() + || !value["observability"].is_object() + || !value["summary"].is_object() + || value["events"].as_array().is_none_or(Vec::is_empty) + || !value["signals"].is_array() + { + return Err("session evidence contract is invalid".into()); + } + Ok(()) +} + +fn validate_markdown_view(bytes: &[u8], heading: &str) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("diagnosis view is not UTF-8: {error}"))?; + if !text.starts_with(heading) || text.trim().is_empty() { + return Err("diagnosis Markdown view contract is invalid".into()); + } + Ok(()) +} + +fn validate_evidence_admission(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("evidence admission is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("evidence admission is not JSON: {error}"))?; + let keys = value + .as_object() + .ok_or("evidence admission must be an object")? + .keys() + .map(String::as_str) + .collect::>(); + let expected = [ + "schema", + "status", + "domainVerdict", + "admissionIdentity", + "evidence", + "verifiedPayload", + "engineeringFacts", + ] + .into_iter() + .collect::>(); + if keys != expected { + return Err("evidence admission fields are not exact".into()); + } + if value["schema"] != "code-intel-evidence-admissibility-result.v1" + || value["status"] != "admitted" + || !matches!( + value["domainVerdict"].as_str(), + Some("observed" | "unknown") + ) + || !value["admissionIdentity"] + .as_str() + .is_some_and(valid_digest) + || !value["engineeringFacts"] + .as_array() + .is_some_and(Vec::is_empty) + { + return Err("evidence admission identity/status/verdict is invalid".into()); + } + let evidence = value["evidence"] + .as_object() + .ok_or("evidence admission lacks observed evidence")?; + let verified = value["verifiedPayload"] + .as_object() + .ok_or("evidence admission lacks verified payload")?; + if evidence + .get("consumedSnapshotIdentity") + .and_then(Value::as_str) + != verified + .get("consumedSnapshotIdentity") + .and_then(Value::as_str) + || verified.get("artifactSchema").and_then(Value::as_str) + != Some("code-intel-evidence-payload.v1") + || verified.get("type").and_then(Value::as_str) != Some("observed.evidence.payload") + || !verified + .get("sha256") + .and_then(Value::as_str) + .is_some_and(valid_digest) + || !verified.get("data").is_some_and(Value::is_object) + { + return Err("evidence admission verified payload is invalid or incoherent".into()); + } + Ok(()) +} + +fn native_code_contract( + schema: &str, + artifact_type: &str, +) -> Option<(&'static str, &'static str, fn(&[u8]) -> Result<(), String>)> { + match (schema, artifact_type) { + ("code-evidence-files.v1", "code_evidence.files") => Some(( + "code-evidence-files.v1", + "code_evidence.files", + validate_native_files, + )), + ("code-evidence-symbols.v1", "code_evidence.symbols") => Some(( + "code-evidence-symbols.v1", + "code_evidence.symbols", + validate_native_symbols, + )), + ("code-evidence-chunks.v1", "code_evidence.chunks") => Some(( + "code-evidence-chunks.v1", + "code_evidence.chunks", + validate_native_chunks, + )), + ("code-evidence-symbol-chunks.v1", "code_evidence.symbol_chunks") => Some(( + "code-evidence-symbol-chunks.v1", + "code_evidence.symbol_chunks", + validate_native_symbol_chunks, + )), + ("code-evidence-imports.v1", "code_evidence.imports") => Some(( + "code-evidence-imports.v1", + "code_evidence.imports", + validate_native_imports, + )), + ("code-evidence-scorecard.v1", "code_evidence.scorecard") => Some(( + "code-evidence-scorecard.v1", + "code_evidence.scorecard", + validate_native_scorecard, + )), + ("code-evidence-coverage.v1", "code_evidence.coverage") => Some(( + "code-evidence-coverage.v1", + "code_evidence.coverage", + validate_native_coverage, + )), + ("agent-code-slice-ranking.v1", "code_evidence.agent_slice") => Some(( + "agent-code-slice-ranking.v1", + "code_evidence.agent_slice", + validate_native_ranking, + )), + _ => None, + } +} + +fn validate_native_files(bytes: &[u8]) -> Result<(), String> { + validate_native_array_artifact(bytes, "code-evidence-files.v1", "files", 2) +} + +fn validate_native_symbols(bytes: &[u8]) -> Result<(), String> { + validate_native_array_artifact(bytes, "code-evidence-symbols.v1", "symbols", 2) +} + +fn validate_native_chunks(bytes: &[u8]) -> Result<(), String> { + validate_native_array_artifact(bytes, "code-evidence-chunks.v1", "chunks", 2) +} + +fn validate_native_symbol_chunks(bytes: &[u8]) -> Result<(), String> { + validate_native_array_artifact(bytes, "code-evidence-symbol-chunks.v1", "mappings", 2) +} + +fn validate_native_imports(bytes: &[u8]) -> Result<(), String> { + validate_native_array_artifact(bytes, "code-evidence-imports.v1", "imports", 2) +} + +fn validate_native_ranking(bytes: &[u8]) -> Result<(), String> { + validate_native_array_artifact(bytes, "agent-code-slice-ranking.v1", "files", 3) +} + +fn parse_native_object(bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("native code evidence artifact is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("native code evidence artifact is invalid JSON: {error}"))?; + value + .as_object() + .ok_or_else(|| "native code evidence artifact must be an object".to_string())?; + Ok(value) +} + +fn validate_native_array_artifact( + bytes: &[u8], + expected_schema: &str, + payload: &str, + expected_fields: usize, +) -> Result<(), String> { + let value = parse_native_object(bytes)?; + let object = value.as_object().expect("parse validated object"); + if value["schema"] != expected_schema + || object.len() != expected_fields + || !value[payload].is_array() + { + return Err(format!("{expected_schema} artifact shape is invalid")); + } + Ok(()) +} + +fn validate_native_scorecard(bytes: &[u8]) -> Result<(), String> { + let value = parse_native_object(bytes)?; + if value["schema"] != "code-evidence-scorecard.v1" + || !value + .as_object() + .is_some_and(|object| object.contains_key("metrics")) + || value["status"] != "ok" + { + return Err("native code evidence scorecard is invalid".into()); + } + Ok(()) +} + +fn validate_native_coverage(bytes: &[u8]) -> Result<(), String> { + let value = parse_native_object(bytes)?; + if value["schema"] != "code-evidence-coverage.v1" + || value["parserKind"] != "line-heuristic" + || value["relationshipPrecision"] != "unknown" + || value["callGraph"] != "unknown" + { + return Err("native code evidence coverage overclaims precision".into()); + } + Ok(()) +} + +pub(crate) fn validate_decision_record_schema(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("decision record artifact is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("decision record artifact is invalid JSON: {error}"))?; + let object = value + .as_object() + .ok_or_else(|| "decision record artifact must be an object".to_string())?; + let expected = [ + "schema", + "id", + "bindingDigest", + "gap", + "request", + "response", + "evidenceBinding", + "snapshotIdentity", + "acceptedChoice", + "authorityEvent", + "consequences", + "affectedBranches", + "recordedAt", + "freshness", + "reopenRule", + ] + .into_iter() + .collect::>(); + if object.keys().map(String::as_str).collect::>() != expected + || value["schema"] != "code-intel-decision-record.v1" + || !value["id"].as_str().is_some_and(|id| { + id.strip_prefix("decision-record-v1:") + .is_some_and(valid_digest) + }) + || !value["bindingDigest"].as_str().is_some_and(valid_digest) + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !value["recordedAt"].is_u64() + { + return Err("decision record identity/schema fields are invalid".to_string()); + } + let evidence = value["evidenceBinding"] + .as_object() + .ok_or_else(|| "decision record evidenceBinding must be an object".to_string())?; + if evidence.keys().map(String::as_str).collect::>() + != ["refs", "digest"].into_iter().collect() + || !value["evidenceBinding"]["digest"] + .as_str() + .is_some_and(valid_digest) + || !value["evidenceBinding"]["refs"] + .as_array() + .is_some_and(|refs| !refs.is_empty()) + { + return Err("decision record evidenceBinding fields are invalid".to_string()); + } + let branches = value["affectedBranches"] + .as_array() + .ok_or_else(|| "decision record affectedBranches must be an array".to_string())?; + let mut seen = BTreeSet::new(); + if branches.is_empty() + || !branches.iter().all(|branch| { + branch + .as_str() + .is_some_and(|branch| !branch.is_empty() && seen.insert(branch)) + }) + || !value["consequences"].as_array().is_some_and(|items| { + items + .iter() + .all(|item| item.as_str().is_some_and(|item| !item.is_empty())) + }) + { + return Err("decision record branch/consequence fields are invalid".to_string()); + } + let freshness = value["freshness"] + .as_object() + .ok_or_else(|| "decision record freshness must be an object".to_string())?; + if freshness + .keys() + .map(String::as_str) + .collect::>() + != ["evidenceExpiresAt", "state"].into_iter().collect() + || !value["freshness"]["evidenceExpiresAt"].is_u64() + || value["freshness"]["state"] != "current" + { + return Err("decision record freshness fields are invalid".to_string()); + } + let reopen = value["reopenRule"] + .as_object() + .ok_or_else(|| "decision record reopenRule must be an object".to_string())?; + if reopen.keys().map(String::as_str).collect::>() + != [ + "evidenceDigestChanged", + "snapshotChanged", + "evidenceExpired", + ] + .into_iter() + .collect() + || value["reopenRule"]["evidenceDigestChanged"] != true + || value["reopenRule"]["snapshotChanged"] != true + || value["reopenRule"]["evidenceExpired"] != true + { + return Err("decision record reopenRule fields are invalid".to_string()); + } + Ok(()) +} + +fn validate_repository_snapshot(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("repository snapshot payload is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("repository snapshot payload is invalid JSON: {error}"))?; + let object = value + .as_object() + .ok_or_else(|| "repository snapshot payload must be an object".to_string())?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = ["schema", "snapshot", "dirtyOverlay", "repository"] + .into_iter() + .collect::>(); + if actual != expected || value["schema"] != "code-intel-repository-snapshot.v1" { + return Err("repository snapshot payload fields/schema are invalid".to_string()); + } + validate_repository_snapshot_identity(&value["snapshot"])?; + let repository = value["repository"] + .as_object() + .ok_or_else(|| "repository snapshot repository is invalid".to_string())?; + if repository.len() != 1 + || !matches!( + repository.get("kind").and_then(Value::as_str), + Some("git" | "git_unborn" | "unversioned") + ) + { + return Err("repository snapshot repository kind is invalid".to_string()); + } + let overlay = value["dirtyOverlay"] + .as_object() + .ok_or_else(|| "repository snapshot dirtyOverlay is invalid".to_string())?; + let overlay_keys = overlay.keys().map(String::as_str).collect::>(); + let expected_overlay = ["present", "digest", "paths", "members", "ignoredPolicy"] + .into_iter() + .collect::>(); + if overlay_keys != expected_overlay + || !value["dirtyOverlay"]["present"].is_boolean() + || value["dirtyOverlay"]["ignoredPolicy"] != "excluded_by_git_ignore" + { + return Err("repository snapshot dirtyOverlay fields are invalid".to_string()); + } + let digest_valid = value["dirtyOverlay"]["digest"].is_null() + || value["dirtyOverlay"]["digest"] + .as_str() + .is_some_and(valid_digest); + if !digest_valid || !valid_path_array(&value["dirtyOverlay"]["paths"]) { + return Err("repository snapshot dirtyOverlay digest/paths are invalid".to_string()); + } + let members = value["dirtyOverlay"]["members"] + .as_object() + .ok_or_else(|| "repository snapshot dirtyOverlay members are invalid".to_string())?; + let expected_members = [ + "trackedModified", + "trackedDeleted", + "untracked", + "renamed", + "typeChanged", + "staged", + ] + .into_iter() + .collect::>(); + if members.keys().map(String::as_str).collect::>() != expected_members + || members.values().any(|value| !valid_path_array(value)) + { + return Err("repository snapshot dirtyOverlay members are invalid".to_string()); + } + Ok(()) +} + +fn validate_doctor_observation(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("doctor observation is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("doctor observation is invalid JSON: {error}"))?; + let object = value + .as_object() + .ok_or_else(|| "doctor observation must be an object".to_string())?; + let expected = [ + "schema", + "snapshotIdentity", + "environmentPolicy", + "bootstrap", + "repository", + "tools", + "providers", + "manifest", + "diagnostics", + "engineeringFacts", + ] + .into_iter() + .collect::>(); + if object.keys().map(String::as_str).collect::>() != expected + || value["schema"] != "code-intel-doctor-observation.v1" + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !value + .pointer("/environmentPolicy/sha256") + .and_then(Value::as_str) + .is_some_and(valid_digest) + || value.pointer("/bootstrap/authority") != Some(&Value::String("observation_only".into())) + || value["engineeringFacts"] + .as_array() + .map_or(true, |facts| !facts.is_empty()) + { + return Err("doctor observation top-level contract is invalid".into()); + } + let policy = value + .pointer("/environmentPolicy/policy") + .ok_or_else(|| "doctor observation environment policy is missing".to_string())?; + let policy_digest = sha256_hex( + &serde_json::to_vec(policy) + .map_err(|error| format!("serialize doctor environment policy: {error}"))?, + ); + if value + .pointer("/environmentPolicy/sha256") + .and_then(Value::as_str) + != Some(policy_digest.as_str()) + { + return Err("doctor observation environment policy digest mismatch".into()); + } + exact_object_keys( + &value["repository"], + &["presence", "readiness", "conformance", "admissibility"], + "doctor repository", + )?; + for tool in value["tools"] + .as_array() + .ok_or("doctor tools must be an array")? + { + exact_object_keys( + tool, + &[ + "name", + "required", + "presence", + "readiness", + "conformance", + "admissibility", + ], + "doctor tool", + )?; + } + for provider in value["providers"] + .as_array() + .ok_or("doctor providers must be an array")? + { + exact_object_keys( + provider, + &[ + "id", + "presence", + "readiness", + "conformance", + "admissibility", + ], + "doctor provider", + )?; + } + let observations = std::iter::once(&value["repository"]) + .chain(value["tools"].as_array().into_iter().flatten()) + .chain(value["providers"].as_array().into_iter().flatten()); + for observation in observations { + if !matches!( + observation["presence"].as_str(), + Some("present" | "missing") + ) || !matches!( + observation["readiness"].as_str(), + Some("ready" | "unavailable") + ) || !matches!( + observation["conformance"].as_str(), + Some("conforming" | "nonconforming" | "not_evaluated") + ) || observation["admissibility"] != "not_evaluated" + { + return Err( + "doctor observation collapses presence/readiness/conformance/admissibility".into(), + ); + } + } + Ok(()) +} + +fn exact_object_keys(value: &Value, expected: &[&str], context: &str) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{context} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + if actual != expected { + return Err(format!("{context} fields are invalid")); + } + Ok(()) +} + +fn validate_survival_scan(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("survival scan payload is not UTF-8: {error}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = serde_json::from_str(text) + .map_err(|error| format!("survival scan payload is invalid JSON: {error}"))?; + let object = value + .as_object() + .ok_or_else(|| "survival scan payload must be an object".to_string())?; + let expected = [ + "schema", + "status", + "snapshotIdentity", + "repository", + "inventory", + "providerDiagnosis", + "completeness", + "structuralVerdict", + "limitations", + "engineeringFacts", + ] + .into_iter() + .collect::>(); + if object.keys().map(String::as_str).collect::>() != expected + || value["schema"] != "code-intel-repository-survival-scan-result.v1" + || value["status"] != "completed" + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || value["completeness"] != "reduced" + || value["structuralVerdict"] != "unknown" + { + return Err("survival scan top-level contract is invalid".into()); + } + let repository = value["repository"] + .as_object() + .ok_or_else(|| "survival scan repository is invalid".to_string())?; + if repository + .keys() + .map(String::as_str) + .collect::>() + != ["kind", "identity", "revision", "dirty", "sourceSha256"] + .into_iter() + .collect() + || !matches!( + value["repository"]["kind"].as_str(), + Some("git" | "git_unborn" | "unversioned") + ) + || !value["repository"]["sourceSha256"] + .as_str() + .is_some_and(valid_digest) + || !value["repository"]["dirty"].is_boolean() + { + return Err("survival scan repository contract is invalid".into()); + } + let inventory = value["inventory"] + .as_object() + .ok_or_else(|| "survival scan inventory is invalid".to_string())?; + if inventory + .keys() + .map(String::as_str) + .collect::>() + != ["fileCount", "extensions", "sourceSha256"] + .into_iter() + .collect() + || !value["inventory"]["fileCount"].is_u64() + || !value["inventory"]["extensions"].is_object() + || !value["inventory"]["sourceSha256"] + .as_str() + .is_some_and(valid_digest) + { + return Err("survival scan inventory contract is invalid".into()); + } + if value["providerDiagnosis"]["status"] != "provider_unavailable" + || value["providerDiagnosis"]["domainVerdict"] != "unknown" + || !value["limitations"] + .as_array() + .is_some_and(|items| items.len() >= 2) + || !value["engineeringFacts"] + .as_array() + .is_some_and(|items| items.len() == 3) + { + return Err("survival scan reduced-evidence boundary is invalid".into()); + } + Ok(()) +} + +fn validate_repository_snapshot_identity(value: &Value) -> Result<(), String> { + let snapshot = value + .as_object() + .ok_or_else(|| "repository snapshot identity must be an object".to_string())?; + let expected = [ + "identity", + "repoIdentity", + "head", + "workingTreePolicy", + "scope", + "inputDigest", + ] + .into_iter() + .collect::>(); + if snapshot.keys().map(String::as_str).collect::>() != expected { + return Err("repository snapshot identity fields are invalid".to_string()); + } + let repo_identity = value["repoIdentity"].as_str().unwrap_or(""); + let repo_identity_valid = ["git-lineage-v1:", "content-v1:"] + .iter() + .any(|prefix| repo_identity.strip_prefix(prefix).is_some_and(valid_digest)); + let scope = value["scope"].as_array(); + if !value["identity"].as_str().is_some_and(valid_digest) + || !repo_identity_valid + || !value["head"].as_str().is_some_and(|head| !head.is_empty()) + || !matches!( + value["workingTreePolicy"].as_str(), + Some("head_only" | "explicit_overlay") + ) + || !scope.is_some_and(|items| { + !items.is_empty() && { + let mut seen = BTreeSet::new(); + items.iter().all(|item| { + item.as_str() + .is_some_and(|text| !text.is_empty() && seen.insert(text)) + }) + } + }) + || !value["inputDigest"].as_str().is_some_and(valid_digest) + { + return Err("repository snapshot identity values are invalid".to_string()); + } + Ok(()) +} + +fn valid_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn valid_run_identity(value: &str) -> bool { + value.strip_prefix("dag-v1:").is_some_and(|tail| { + !tail.is_empty() + && tail.len() % 2 == 0 + && tail + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + }) +} + +fn valid_path_array(value: &Value) -> bool { + let Some(values) = value.as_array() else { + return false; + }; + let mut seen = BTreeSet::new(); + values.iter().all(|value| { + value + .as_str() + .is_some_and(|value| !value.is_empty() && seen.insert(value)) + }) +} + +fn validate_inventory(bytes: &[u8]) -> Result<(), String> { + let text = std::str::from_utf8(bytes) + .map_err(|error| format!("inventory payload is not UTF-8: {error}"))?; + let records = if text.contains('\0') { + text.split('\0') + .filter(|record| !record.is_empty()) + .collect::>() + } else { + text.lines() + .filter(|record| !record.is_empty()) + .collect::>() + }; + let mut previous: Option = None; + for record in records { + let normalized = + portable_relative_path(record).map_err(|error| error.message().to_string())?; + if previous.as_ref().is_some_and(|value| value >= &normalized) { + return Err("inventory payload paths must be unique and sorted".to_string()); + } + previous = Some(normalized); + } + Ok(()) +} + +fn portable_relative_path(value: &str) -> Result { + if value.is_empty() + || value.contains('\0') + || value.contains('\\') + || value.starts_with('/') + || value.starts_with("//") + || value.contains(':') + || value.split('/').any(|component| component.is_empty()) + { + return Err(ArtifactError::Contract( + "Artifact Ref path is not portable root-relative syntax".to_string(), + )); + } + let path = Path::new(value); + let mut normalized = Vec::new(); + for component in path.components() { + let name = match component { + Component::Normal(name) => name.to_str().ok_or_else(|| { + ArtifactError::Contract("Artifact Ref path is not UTF-8".to_string()) + })?, + Component::CurDir + | Component::ParentDir + | Component::RootDir + | Component::Prefix(_) => { + return Err(ArtifactError::Contract( + "Artifact Ref path contains a non-portable component".to_string(), + )) + } + }; + if name.is_empty() + || name.ends_with('.') + || name.ends_with(' ') + || name + .chars() + .any(|character| ('\u{0300}'..='\u{036f}').contains(&character)) + || windows_reserved(name) + { + return Err(ArtifactError::Contract( + "Artifact Ref path contains a Windows-ambiguous component".to_string(), + )); + } + normalized.push(name); + } + if normalized.is_empty() { + return Err(ArtifactError::Contract( + "Artifact Ref path must name a file".to_string(), + )); + } + Ok(normalized.join("/")) +} + +fn windows_reserved(name: &str) -> bool { + let stem = name.split('.').next().unwrap_or(name).to_ascii_lowercase(); + matches!( + stem.as_str(), + "con" | "prn" | "aux" | "nul" | "conin$" | "conout$" + ) || stem + .strip_prefix("com") + .is_some_and(|n| matches!(n, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")) + || stem + .strip_prefix("lpt") + .is_some_and(|n| matches!(n, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")) + || stem + .strip_prefix("com") + .is_some_and(|n| matches!(n, "¹" | "²" | "³")) + || stem + .strip_prefix("lpt") + .is_some_and(|n| matches!(n, "¹" | "²" | "³")) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::fs; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn portable_path_rejects_cross_platform_aliases() { + for path in [ + "", + ".", + "./a", + "../a", + "/a", + "//server/a", + r"C:\\a", + "a:b", + "a\\b", + "con", + "AUX.txt", + "a.", + "a ", + "a//b", + "a/", + "CONIN$", + "conout$.txt", + "COM¹", + "LPT².log", + "e\u{301}.txt", + ] { + assert!(portable_relative_path(path).is_err(), "{path}"); + } + assert_eq!( + portable_relative_path("nested/子.bin").unwrap(), + "nested/子.bin" + ); + } + + #[test] + fn verified_artifact_owns_bytes_after_hardlink_content_changes() { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let root = std::env::temp_dir().join(format!("code-intel-a03-owned-{nonce}")); + fs::create_dir(&root).unwrap(); + let outside = root.with_extension("outside"); + fs::write(&outside, b"portable evidence\n").unwrap(); + fs::hard_link(&outside, root.join("payload.bin")).unwrap(); + let snapshot = "a".repeat(64); + let reference = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"fixture.v1", + "type":"fixture.data", + "path":"payload.bin", + "sha256":"924278019c18519b69088648b6d5b4f58fc96afa66204bab1274a5a4ee2bd2c2", + "consumedSnapshotIdentity":snapshot + }); + let verified = verify_artifact_ref( + &root, + &snapshot, + ArtifactContract { + artifact_schema: "fixture.v1", + artifact_type: "fixture.data", + max_bytes: 1024, + validate_payload: |_| Ok(()), + }, + &reference, + ) + .unwrap(); + fs::write(&outside, b"changed evidence!\n").unwrap(); + assert_eq!(verified.bytes, b"portable evidence\n"); + let _ = fs::remove_file(outside); + let _ = fs::remove_dir_all(root); + } + + fn valid_snapshot_payload() -> Value { + json!({ + "schema":"code-intel-repository-snapshot.v1", + "snapshot":{ + "identity":"a".repeat(64), + "repoIdentity":format!("content-v1:{}", "b".repeat(64)), + "head":"unversioned", + "workingTreePolicy":"explicit_overlay", + "scope":["."], + "inputDigest":"c".repeat(64) + }, + "dirtyOverlay":{ + "present":false, + "digest":null, + "paths":[], + "members":{"trackedModified":[],"trackedDeleted":[],"untracked":[],"renamed":[],"typeChanged":[],"staged":[]}, + "ignoredPolicy":"excluded_by_git_ignore" + }, + "repository":{"kind":"unversioned"} + }) + } + + fn valid_understanding_quadrant_payload() -> Value { + json!({ + "schema":"code-intel-understanding-quadrant.v1", + "snapshotIdentity":"a".repeat(64), + "sourceOrientation":{ + "artifactSchema":"code-intel-project-orientation.v1", + "artifactType":"project.orientation", + "sha256":"b".repeat(64) + }, + "classificationPolicy":{ + "schema":"code-intel-understanding-quadrant-policy.v1", + "scoreRange":{"minimum":0,"maximum":100}, + "systemCriticalityThreshold":50, + "evidenceConfidenceThreshold":50, + "thresholdRule":"greater_than_or_equal_is_upper_band", + "unknownCriticalityRule":"critical_by_default_except_declared_supporting_context", + "methodConsumerPolicy":"C01_cards_and_C02_selection_may_consume_but_cannot_rewrite" + }, + "items":[{ + "id":"unknown:dependencies.runtime", + "subject":"dependencies.runtime", + "sourceState":"unknown", + "systemCriticality":{"score":90,"band":"critical"}, + "evidenceConfidence":{"score":0,"band":"low"}, + "quadrant":"Critical Unknown", + "statement":"Runtime dependency authority is absent.", + "provenance":[{"artifactType":"repository.survival-scan","artifactSha256":"c".repeat(64),"jsonPointer":"/unknowns/0"}] + }], + "visibleUnknowns":["unknown:dependencies.runtime"], + "counts":{"Known Core":0,"Critical Unknown":1,"Supporting Context":0,"Deferred Unknown":0} + }) + } + + #[test] + fn understanding_quadrant_rejects_null_provenance_and_policy_constant_tampering() { + let valid = valid_understanding_quadrant_payload(); + validate_understanding_quadrant(&serde_json::to_vec(&valid).unwrap()).unwrap(); + + let mut null_provenance = valid.clone(); + null_provenance["items"][0]["provenance"] = json!([null]); + assert!( + validate_understanding_quadrant(&serde_json::to_vec(&null_provenance).unwrap()) + .is_err() + ); + + for (pointer, tampered) in [ + ( + "/classificationPolicy/schema", + json!("code-intel-understanding-quadrant-policy.v2"), + ), + ("/classificationPolicy/scoreRange/maximum", json!(999)), + ( + "/classificationPolicy/systemCriticalityThreshold", + json!(51), + ), + ( + "/classificationPolicy/evidenceConfidenceThreshold", + json!(49), + ), + ( + "/classificationPolicy/unknownCriticalityRule", + json!("optimistic"), + ), + ] { + let mut document = valid.clone(); + *document.pointer_mut(pointer).unwrap() = tampered; + assert!( + validate_understanding_quadrant(&serde_json::to_vec(&document).unwrap()).is_err(), + "accepted policy tamper at {pointer}" + ); + } + } + + #[test] + fn understanding_quadrant_rejects_duplicate_items_hidden_unknowns_and_wrong_counts() { + let valid = valid_understanding_quadrant_payload(); + + let mut duplicate_item = valid.clone(); + duplicate_item["items"] = json!([valid["items"][0].clone(), valid["items"][0].clone()]); + assert!( + validate_understanding_quadrant(&serde_json::to_vec(&duplicate_item).unwrap()) + .unwrap_err() + .contains("uniquely sorted") + ); + + let mut hidden_unknown = valid.clone(); + hidden_unknown["visibleUnknowns"] = json!([]); + assert!( + validate_understanding_quadrant(&serde_json::to_vec(&hidden_unknown).unwrap()) + .unwrap_err() + .contains("hides or invents unknowns") + ); + + let mut wrong_counts = valid; + wrong_counts["counts"]["Critical Unknown"] = json!(0); + assert!( + validate_understanding_quadrant(&serde_json::to_vec(&wrong_counts).unwrap()) + .unwrap_err() + .contains("counts do not match") + ); + } + + #[test] + fn registered_repository_snapshot_json_rejects_duplicate_extra_wrong_and_unknown_schema() { + let valid = serde_json::to_vec(&valid_snapshot_payload()).unwrap(); + validate_repository_snapshot(&valid).unwrap(); + + let duplicate = br#"{"schema":"code-intel-repository-snapshot.v1","schema":"code-intel-repository-snapshot.v1"}"#; + assert!(validate_repository_snapshot(duplicate) + .unwrap_err() + .contains("duplicate")); + + let mut extra = valid_snapshot_payload(); + extra["extra"] = json!(true); + assert!(validate_repository_snapshot(&serde_json::to_vec(&extra).unwrap()).is_err()); + + let mut wrong = valid_snapshot_payload(); + wrong["schema"] = json!("code-intel-repository-snapshot.v2"); + assert!(validate_repository_snapshot(&serde_json::to_vec(&wrong).unwrap()).is_err()); + + let unknown_ref = json!({"artifactSchema":"unknown-json.v1","type":"repository.snapshot"}); + assert!(registered_contract(&unknown_ref).is_err()); + } + + #[test] + fn registered_repository_snapshot_enforces_every_nested_schema_constraint() { + let mut invalid_repo = valid_snapshot_payload(); + invalid_repo["snapshot"]["repoIdentity"] = json!("INVALID"); + assert!(validate_repository_snapshot(&serde_json::to_vec(&invalid_repo).unwrap()).is_err()); + + let mut empty_scope = valid_snapshot_payload(); + empty_scope["snapshot"]["scope"] = json!([]); + assert!(validate_repository_snapshot(&serde_json::to_vec(&empty_scope).unwrap()).is_err()); + + let mut nested_extra = valid_snapshot_payload(); + nested_extra["snapshot"]["extra"] = json!(true); + assert!(validate_repository_snapshot(&serde_json::to_vec(&nested_extra).unwrap()).is_err()); + + let mut overlay_extra = valid_snapshot_payload(); + overlay_extra["dirtyOverlay"]["members"]["extra"] = json!([]); + assert!( + validate_repository_snapshot(&serde_json::to_vec(&overlay_extra).unwrap()).is_err() + ); + + let mut overlay_duplicate = valid_snapshot_payload(); + overlay_duplicate["dirtyOverlay"]["paths"] = json!(["a", "a"]); + assert!( + validate_repository_snapshot(&serde_json::to_vec(&overlay_duplicate).unwrap()).is_err() + ); + + let mut invalid_member = valid_snapshot_payload(); + invalid_member["dirtyOverlay"]["members"]["untracked"] = json!([""]); + assert!( + validate_repository_snapshot(&serde_json::to_vec(&invalid_member).unwrap()).is_err() + ); + } + + #[test] + fn native_code_contracts_bind_each_ref_pair_to_its_payload_schema() { + let cases = [ + ( + "code-evidence-files.v1", + "code_evidence.files", + json!({"schema":"code-evidence-files.v1","files":[]}), + ), + ( + "code-evidence-symbols.v1", + "code_evidence.symbols", + json!({"schema":"code-evidence-symbols.v1","symbols":[]}), + ), + ( + "code-evidence-chunks.v1", + "code_evidence.chunks", + json!({"schema":"code-evidence-chunks.v1","chunks":[]}), + ), + ( + "code-evidence-symbol-chunks.v1", + "code_evidence.symbol_chunks", + json!({"schema":"code-evidence-symbol-chunks.v1","mappings":[]}), + ), + ( + "code-evidence-imports.v1", + "code_evidence.imports", + json!({"schema":"code-evidence-imports.v1","imports":[]}), + ), + ( + "code-evidence-scorecard.v1", + "code_evidence.scorecard", + json!({"schema":"code-evidence-scorecard.v1","status":"ok","metrics":{}}), + ), + ( + "code-evidence-coverage.v1", + "code_evidence.coverage", + json!({"schema":"code-evidence-coverage.v1","parserKind":"line-heuristic","relationshipPrecision":"unknown","callGraph":"unknown"}), + ), + ( + "agent-code-slice-ranking.v1", + "code_evidence.agent_slice", + json!({"schema":"agent-code-slice-ranking.v1","strategy":"native-evidence-default","files":[]}), + ), + ]; + + for (index, (schema, artifact_type, payload)) in cases.iter().enumerate() { + let reference = json!({"artifactSchema":schema,"type":artifact_type}); + let contract = registered_contract(&reference).expect("all eight pairs are registered"); + (contract.validate_payload)(&serde_json::to_vec(payload).unwrap()) + .expect("matching payload must pass"); + + let wrong_payload = &cases[(index + 1) % cases.len()].2; + assert!( + (contract.validate_payload)(&serde_json::to_vec(wrong_payload).unwrap()).is_err(), + "{schema}/{artifact_type} accepted payload {}", + wrong_payload["schema"] + ); + } + + let files_ref = json!({ + "artifactSchema":"code-evidence-files.v1", + "type":"code_evidence.files" + }); + let files_contract = registered_contract(&files_ref).unwrap(); + let symbols_payload = br#"{"schema":"code-evidence-symbols.v1","symbols":[]}"#; + assert!((files_contract.validate_payload)(symbols_payload).is_err()); + } + + fn deletion_file(path: &str, base: &str, result: &str, added: Vec<&str>) -> Value { + json!({ + "path":path, + "baseBlobSha256":sha256_hex(base.as_bytes()), + "resultBlobSha256":sha256_hex(result.as_bytes()), + "baseText":base, + "resultText":result, + "hunks":[{ + "oldStart":1,"oldLines":1,"newStart":1,"newLines":added.len(), + "deletedLines":["legacy"],"addedLines":added + }] + }) + } + + fn deletion_diff(files: Vec, affected: Vec<&str>) -> Value { + let patch_sha = sha256_hex(&serde_json::to_vec(&files).unwrap()); + json!({ + "schema":"code-intel-compatibility-retirement-deletion-diff.v1", + "snapshotIdentity":"a".repeat(64),"retirementId":"ret-1","legacyBranchId":"legacy.branch", + "affectedFiles":affected,"deletionsOnly":true,"summary":"delete only; summary has no authority", + "patch":{"algorithm":"replayable-delete-only-v1","sha256":patch_sha,"files":files} + }) + } + + #[test] + fn retirement_deletion_patch_replays_pure_deletion_and_rejects_forged_addition() { + let valid = deletion_diff( + vec![deletion_file( + "run-code-intel.ps1", + "legacy\nkeep\n", + "keep\n", + vec![], + )], + vec!["run-code-intel.ps1"], + ); + validate_retirement_deletion_diff_value(&valid).unwrap(); + + let forged = deletion_diff( + vec![deletion_file( + "run-code-intel.ps1", + "legacy\nkeep\n", + "new-executable-code\nkeep\n", + vec!["new-executable-code"], + )], + vec!["run-code-intel.ps1"], + ); + let error = validate_retirement_deletion_diff_value(&forged).unwrap_err(); + assert!(error.contains("added or replacement")); + } + + #[test] + fn retirement_deletion_patch_rejects_hidden_touched_path_even_with_valid_hashes() { + let hidden = deletion_diff( + vec![ + deletion_file("run-code-intel.ps1", "legacy\nkeep\n", "keep\n", vec![]), + deletion_file("second-branch.ps1", "legacy\nkeep\n", "keep\n", vec![]), + ], + vec!["run-code-intel.ps1"], + ); + let error = validate_retirement_deletion_diff_value(&hidden).unwrap_err(); + assert!(error.contains("touched paths differ")); + } +} diff --git a/crates/code-intel-cli/src/assistance_discovery.rs b/crates/code-intel-cli/src/assistance_discovery.rs new file mode 100644 index 0000000..37e7ecd --- /dev/null +++ b/crates/code-intel-cli/src/assistance_discovery.rs @@ -0,0 +1,282 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use serde_json::{json, Value}; + +const KINDS: [&str; 4] = [ + "internal_atom", + "established_method", + "external_tool", + "documentation", +]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DiscoveryError(String); + +impl fmt::Display for DiscoveryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for DiscoveryError {} + +pub(crate) fn discover(request: &Value) -> Result { + exact_object(request, "request", &["schema", "gap", "candidates"])?; + require_exact( + request, + "schema", + "code-intel-assistance-discovery-request.v1", + "request", + )?; + let gap = parse_gap(&request["gap"])?; + let candidates = request["candidates"] + .as_array() + .ok_or_else(|| DiscoveryError("request.candidates must be an array".into()))?; + if candidates.is_empty() { + return Err(DiscoveryError( + "assistance discovery requires at least one candidate".into(), + )); + } + + let mut ids = BTreeSet::new(); + let mut dossiers = BTreeMap::new(); + for (index, candidate) in candidates.iter().enumerate() { + let dossier = parse_candidate(candidate, index)?; + let id = dossier["id"].as_str().unwrap().to_string(); + if !ids.insert(id.clone()) { + return Err(DiscoveryError(format!("duplicate candidate id {id}"))); + } + let kind = dossier["kind"].as_str().unwrap(); + let order = KINDS.iter().position(|value| value == &kind).unwrap(); + dossiers.insert((order, id), dossier); + } + + Ok(json!({ + "schema": "code-intel-assistance-discovery-result.v1", + "status": "completed", + "gapId": gap.id, + "capability": gap.capability, + "gapEvidenceRefs": gap.evidence_refs, + "dossiers": dossiers.into_values().collect::>(), + "proposalOnly": true, + "selectionPolicy": "evidence_and_constraints_not_popularity", + "effects": [], + "authorityEvents": [], + "adoptionDecisions": [], + "committedEngineeringPlans": [], + })) +} + +struct Gap { + id: String, + capability: String, + evidence_refs: Vec, +} + +fn parse_gap(value: &Value) -> Result { + exact_object( + value, + "gap", + &[ + "schema", + "id", + "capability", + "description", + "constraints", + "evidenceRefs", + ], + )?; + require_exact( + value, + "schema", + "code-intel-engineering-capability-gap.v1", + "gap", + )?; + let id = nonempty(value, "id", "gap")?.to_string(); + let capability = nonempty(value, "capability", "gap")?.to_string(); + nonempty(value, "description", "gap")?; + string_list(&value["constraints"], "gap.constraints", true)?; + let evidence_refs = string_list(&value["evidenceRefs"], "gap.evidenceRefs", true)?; + Ok(Gap { + id, + capability, + evidence_refs, + }) +} + +fn parse_candidate(value: &Value, index: usize) -> Result { + let context = format!("candidates[{index}]"); + exact_object( + value, + &context, + &[ + "id", + "kind", + "name", + "fit", + "license", + "security", + "integration", + "reversibility", + "evidenceRefs", + ], + )?; + let id = nonempty(value, "id", &context)?; + let kind = nonempty(value, "kind", &context)?; + if !KINDS.contains(&kind) { + return Err(DiscoveryError(format!("{context}.kind is invalid"))); + } + let name = nonempty(value, "name", &context)?; + let fit = assessment( + &value["fit"], + &format!("{context}.fit"), + "status", + &["strong", "partial", "weak", "unknown"], + )?; + let license = assessment( + &value["license"], + &format!("{context}.license"), + "status", + &["acceptable", "review_required", "not_applicable", "unknown"], + )?; + let security = assessment( + &value["security"], + &format!("{context}.security"), + "status", + &["acceptable", "review_required", "unacceptable", "unknown"], + )?; + let integration = assessment( + &value["integration"], + &format!("{context}.integration"), + "effort", + &["low", "medium", "high", "unknown"], + )?; + let reversibility = assessment( + &value["reversibility"], + &format!("{context}.reversibility"), + "status", + &["high", "medium", "low", "unknown"], + )?; + let evidence_refs = string_list( + &value["evidenceRefs"], + &format!("{context}.evidenceRefs"), + true, + )?; + reject_popularity_only(&fit, &evidence_refs, &context)?; + + Ok(json!({ + "schema": "code-intel-assistance-dossier.v1", + "id": id, + "kind": kind, + "name": name, + "fit": fit, + "license": license, + "security": security, + "integration": integration, + "reversibility": reversibility, + "evidenceRefs": evidence_refs, + "disposition": "proposal", + "authorityState": "unresolved", + })) +} + +fn assessment( + value: &Value, + context: &str, + rating_key: &str, + allowed: &[&str], +) -> Result { + exact_object(value, context, &[rating_key, "basis"])?; + let rating = nonempty(value, rating_key, context)?; + if !allowed.contains(&rating) { + return Err(DiscoveryError(format!("{context}.{rating_key} is invalid"))); + } + nonempty(value, "basis", context)?; + Ok(value.clone()) +} + +fn reject_popularity_only( + fit: &Value, + evidence_refs: &[String], + context: &str, +) -> Result<(), DiscoveryError> { + let basis = fit["basis"].as_str().unwrap().to_ascii_lowercase(); + let popularity_basis = ["popular", "github star", "star count", "downloads"] + .iter() + .any(|term| basis.contains(term)); + let only_popularity_refs = evidence_refs.iter().all(|reference| { + let reference = reference.to_ascii_lowercase(); + reference.contains("star") + || reference.contains("download") + || reference.contains("popular") + }); + if popularity_basis && only_popularity_refs { + return Err(DiscoveryError(format!( + "{context} cannot rely on popularity alone" + ))); + } + Ok(()) +} + +fn exact_object(value: &Value, context: &str, allowed: &[&str]) -> Result<(), DiscoveryError> { + let object = value + .as_object() + .ok_or_else(|| DiscoveryError(format!("{context} must be an object")))?; + let allowed = allowed.iter().copied().collect::>(); + if let Some(extra) = object.keys().find(|key| !allowed.contains(key.as_str())) { + return Err(DiscoveryError(format!( + "{context} contains unknown field {extra}" + ))); + } + if let Some(missing) = allowed.iter().find(|key| !object.contains_key(**key)) { + return Err(DiscoveryError(format!( + "{context} is missing field {missing}" + ))); + } + Ok(()) +} + +fn require_exact( + value: &Value, + key: &str, + expected: &str, + context: &str, +) -> Result<(), DiscoveryError> { + if value.get(key).and_then(Value::as_str) != Some(expected) { + return Err(DiscoveryError(format!("{context}.{key} is invalid"))); + } + Ok(()) +} + +fn nonempty<'a>(value: &'a Value, key: &str, context: &str) -> Result<&'a str, DiscoveryError> { + value + .get(key) + .and_then(Value::as_str) + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| DiscoveryError(format!("{context}.{key} must be a non-empty string"))) +} + +fn string_list( + value: &Value, + context: &str, + nonempty_required: bool, +) -> Result, DiscoveryError> { + let values = value + .as_array() + .ok_or_else(|| DiscoveryError(format!("{context} must be an array")))?; + if nonempty_required && values.is_empty() { + return Err(DiscoveryError(format!("{context} must not be empty"))); + } + let mut unique = BTreeSet::new(); + for item in values { + let item = item + .as_str() + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| DiscoveryError(format!("{context} contains an invalid value")))?; + if !unique.insert(item.to_string()) { + return Err(DiscoveryError(format!("{context} contains a duplicate"))); + } + } + Ok(unique.into_iter().collect()) +} diff --git a/crates/code-intel-cli/src/authority.rs b/crates/code-intel-cli/src/authority.rs new file mode 100644 index 0000000..a1f10ac --- /dev/null +++ b/crates/code-intel-cli/src/authority.rs @@ -0,0 +1,530 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde_json::{json, Value}; + +const KINDS: [&str; 6] = [ + "observed_evidence", + "engineering_fact", + "derived_engineering_model", + "proposal", + "adoption_decision", + "committed_engineering_plan", +]; +const ACTORS: [&str; 5] = [ + "deterministic_pipeline", + "human", + "llm", + "provider", + "recommender", +]; +const TRUSTED_APPROVERS: [(&str, &str); 1] = [("code-intel-maintainers", "repository_governance")]; +const ATTESTATION_SCHEME: &str = "repository-governed-sha256-v1"; +const EDGES: [(&str, &str, bool); 7] = [ + ("observed_evidence", "engineering_fact", false), + ("observed_evidence", "proposal", false), + ("engineering_fact", "derived_engineering_model", false), + ("derived_engineering_model", "proposal", false), + ("proposal", "adoption_decision", true), + ("adoption_decision", "committed_engineering_plan", true), + ("proposal", "committed_engineering_plan", true), +]; + +pub(crate) fn policy_document() -> Value { + json!({ + "schema":"code-intel-authority-transition-policy.v1", + "artifactKinds":KINDS, + "actorKinds":ACTORS, + "edges":EDGES.iter().map(|(from,to,event)| json!({"from":from,"to":to,"authorityEventRequired":event})).collect::>(), + "restrictedActors":{"llm":["observed_evidence","proposal"],"provider":["observed_evidence","proposal"],"recommender":["observed_evidence","proposal"]}, + "trustedApprovers":TRUSTED_APPROVERS.iter().map(|(id,role)| json!({"id":id,"role":role})).collect::>(), + "attestation":{"scheme":ATTESTATION_SCHEME,"meaning":"content-bound repository sign-off, not cryptographic identity authentication"}, + "rule":"protected transitions are owned by an explicit approved authority event; source output alone has no commitment authority" + }) +} + +pub(crate) fn evaluate_batch(request: &Value) -> Result { + validate_batch(request)?; + let evaluated_at = request["evaluatedAt"].as_u64().unwrap(); + let known = string_set(&request["knownEvidenceIds"], "knownEvidenceIds")?; + let consumed = string_set( + &request["consumedAuthorityEventIds"], + "consumedAuthorityEventIds", + )?; + let branches = request["branches"].as_array().unwrap(); + let duplicate_branches = duplicates(branches.iter().filter_map(|b| b["branchId"].as_str())); + let duplicate_outputs = duplicates( + branches + .iter() + .filter_map(|b| b.pointer("/transition/outputId").and_then(Value::as_str)), + ); + let duplicate_events = duplicates(branches.iter().filter_map(|b| { + b.pointer("/transition/authorityEvent/id") + .and_then(Value::as_str) + })); + + let results = branches + .iter() + .enumerate() + .map(|(index, branch)| { + evaluate_branch( + branch, + index, + evaluated_at, + &known, + &consumed, + &duplicate_branches, + &duplicate_outputs, + &duplicate_events, + ) + }) + .collect::>(); + let accepted = results.iter().filter(|r| r["status"] == "accepted").count(); + let rejected = results.len() - accepted; + let mut consumed_events = consumed.clone(); + consumed_events.extend( + results + .iter() + .filter(|result| result["status"] == "accepted") + .filter_map(|result| result["authorityEventId"].as_str()) + .map(str::to_string), + ); + Ok(json!({ + "schema":"code-intel-authority-transition-result.v1", + "status":"completed", + "summary":{"accepted":accepted,"rejected":rejected}, + "consumedAuthorityEventIds":consumed_events, + "branches":results + })) +} + +#[allow(clippy::too_many_arguments)] +fn evaluate_branch( + branch: &Value, + index: usize, + evaluated_at: u64, + known: &BTreeSet, + consumed: &BTreeSet, + duplicate_branches: &BTreeSet, + duplicate_outputs: &BTreeSet, + duplicate_events: &BTreeSet, +) -> Value { + let branch_id = branch["branchId"] + .as_str() + .map(str::to_string) + .unwrap_or_else(|| format!("invalid-branch-{index}")); + let from = branch + .pointer("/current/kind") + .and_then(Value::as_str) + .unwrap_or(""); + let to = branch + .pointer("/transition/to") + .and_then(Value::as_str) + .unwrap_or(""); + let output_id = branch + .pointer("/transition/outputId") + .and_then(Value::as_str) + .unwrap_or(""); + let event_id = branch + .pointer("/transition/authorityEvent/id") + .and_then(Value::as_str); + let authority_event = branch.pointer("/transition/authorityEvent"); + let decision = validate_branch(branch, evaluated_at, known, consumed).and_then(|_| { + if duplicate_branches.contains(&branch_id) { + Err("duplicate branchId".to_string()) + } else if duplicate_outputs.contains(output_id) { + Err("duplicate transition outputId".to_string()) + } else if event_id.is_some_and(|id| duplicate_events.contains(id)) { + Err("duplicate authority event use".to_string()) + } else { + Ok(()) + } + }); + match decision { + Ok(()) => json!({ + "branchId":branch_id,"status":"accepted","from":from,"to":to, + "outputId":output_id,"authorityEventId":event_id, + "authorityEvent":authority_event, + "effectiveAuthority":if event_id.is_some() { "authority_event" } else { "deterministic_policy" }, + "diagnostics":[] + }), + Err(message) => json!({ + "branchId":branch_id,"status":"rejected","from":from,"to":to, + "outputId":null,"authorityEventId":null,"authorityEvent":null,"effectiveAuthority":null,"diagnostics":[message] + }), + } +} + +fn validate_batch(request: &Value) -> Result<(), String> { + exact( + request, + &[ + "schema", + "evaluatedAt", + "knownEvidenceIds", + "consumedAuthorityEventIds", + "branches", + ], + "authority batch", + )?; + if request["schema"] != "code-intel-authority-transition-batch.v1" { + return Err("authority batch schema is invalid".to_string()); + } + request["evaluatedAt"] + .as_u64() + .ok_or("evaluatedAt must be a non-negative integer")?; + string_set(&request["knownEvidenceIds"], "knownEvidenceIds")?; + string_set( + &request["consumedAuthorityEventIds"], + "consumedAuthorityEventIds", + )?; + let branches = request["branches"] + .as_array() + .ok_or("branches must be an array")?; + if branches.is_empty() { + return Err("branches must not be empty".to_string()); + } + Ok(()) +} + +fn validate_branch( + branch: &Value, + evaluated_at: u64, + known: &BTreeSet, + consumed: &BTreeSet, +) -> Result<(), String> { + exact( + branch, + &["branchId", "source", "current", "transition"], + "transition branch", + )?; + nonempty(&branch["branchId"], "branchId")?; + let source = &branch["source"]; + exact(source, &["kind", "id"], "source")?; + let actor = source["kind"].as_str().ok_or("source kind is invalid")?; + if !ACTORS.contains(&actor) { + return Err("source kind is unknown".to_string()); + } + nonempty(&source["id"], "source id")?; + let current = &branch["current"]; + exact(current, &["kind", "id"], "current artifact")?; + let from = current["kind"].as_str().ok_or("current kind is invalid")?; + if !KINDS.contains(&from) { + return Err("current kind is unknown".to_string()); + } + nonempty(¤t["id"], "current id")?; + let transition = &branch["transition"]; + let fields = transition + .as_object() + .ok_or("transition must be an object")?; + if !fields.keys().all(|key| { + matches!( + key.as_str(), + "to" | "outputId" | "evidenceIds" | "authorityEvent" + ) + }) || !["to", "outputId", "evidenceIds"] + .iter() + .all(|key| fields.contains_key(*key)) + { + return Err("transition fields are invalid".to_string()); + } + let to = transition["to"] + .as_str() + .ok_or("transition target is invalid")?; + if !KINDS.contains(&to) { + return Err("transition target is unknown".to_string()); + } + nonempty(&transition["outputId"], "transition outputId")?; + let evidence = string_set(&transition["evidenceIds"], "transition evidenceIds")?; + if evidence.is_empty() { + return Err("transition requires evidence".to_string()); + } + if !evidence.is_subset(known) { + return Err("transition references unknown evidence".to_string()); + } + let (_, _, event_required) = EDGES + .iter() + .find(|(a, b, _)| *a == from && *b == to) + .ok_or("transition edge is not allowed")?; + if matches!(actor, "llm" | "provider" | "recommender") + && !matches!( + to, + "observed_evidence" | "proposal" | "adoption_decision" | "committed_engineering_plan" + ) + { + return Err("source actor cannot create facts or derived models".to_string()); + } + let event = transition.get("authorityEvent"); + if *event_required { + validate_event( + event.ok_or("protected transition requires authority event")?, + evaluated_at, + known, + &evidence, + consumed, + )?; + } else if event.is_some() { + return Err("unprotected transition must not consume an authority event".to_string()); + } + Ok(()) +} + +fn validate_event( + event: &Value, + evaluated_at: u64, + known: &BTreeSet, + transition_evidence: &BTreeSet, + consumed: &BTreeSet, +) -> Result<(), String> { + validate_authority_event(event, evaluated_at, known, transition_evidence, consumed).map(|_| ()) +} + +pub(crate) fn validate_authority_event( + event: &Value, + evaluated_at: u64, + known: &BTreeSet, + required_evidence: &BTreeSet, + consumed: &BTreeSet, +) -> Result { + let mut event_fields = vec![ + "schema", + "id", + "decision", + "approver", + "evidenceIds", + "issuedAt", + "expiresAt", + ]; + if event.get("attestation").is_some() { + event_fields.push("attestation"); + } + exact(event, &event_fields, "authority event")?; + if event["schema"] != "code-intel-authority-event.v1" || event["decision"] != "approved" { + return Err("authority event must be explicitly approved".to_string()); + } + let id = event["id"] + .as_str() + .filter(|id| !id.is_empty()) + .ok_or("authority event id is invalid")?; + if consumed.contains(id) { + return Err("authority event replay is rejected".to_string()); + } + let approver = &event["approver"]; + exact(approver, &["id", "role"], "authority approver")?; + nonempty(&approver["id"], "approver id")?; + nonempty(&approver["role"], "approver role")?; + let event_evidence = string_set(&event["evidenceIds"], "authority event evidenceIds")?; + if event_evidence.is_empty() + || !event_evidence.is_subset(known) + || !required_evidence.is_subset(&event_evidence) + { + return Err("authority event evidence is unknown or incomplete".to_string()); + } + let issued = event["issuedAt"] + .as_u64() + .ok_or("authority event issuedAt is invalid")?; + let expires = event["expiresAt"] + .as_u64() + .ok_or("authority event expiresAt is invalid")?; + if issued > evaluated_at || expires < evaluated_at || expires < issued { + return Err("authority event is future-dated or expired".to_string()); + } + if event.get("attestation").is_some() { + validate_repository_attestation(event)?; + } + Ok(id.to_string()) +} + +/// Requires the backward-compatible v1 event extension used for repository-owned sign-off. +/// The digest detects content changes; trust comes only from the checked-in id/role allow-list. +pub(crate) fn validate_signed_authority_event( + event: &Value, + evaluated_at: u64, + known: &BTreeSet, + required_evidence: &BTreeSet, + consumed: &BTreeSet, +) -> Result { + let id = validate_authority_event(event, evaluated_at, known, required_evidence, consumed)?; + if event.get("attestation").is_none() { + return Err("repository sign-off attestation is required".to_string()); + } + Ok(id) +} + +pub(crate) fn authority_event_digest(event: &Value) -> Result { + let mut evidence = event["evidenceIds"] + .as_array() + .ok_or("authority event evidenceIds must be an array")? + .iter() + .map(|value| { + value + .as_str() + .map(str::to_string) + .ok_or_else(|| "authority event evidenceIds contains an invalid id".to_string()) + }) + .collect::, _>>()?; + evidence.sort(); + let payload = json!({ + "schema":event["schema"], + "id":event["id"], + "decision":event["decision"], + "approver":event["approver"], + "evidenceIds":evidence, + "issuedAt":event["issuedAt"], + "expiresAt":event["expiresAt"] + }); + Ok(sha256_hex(&serde_json::to_vec(&payload).unwrap())) +} + +fn validate_repository_attestation(event: &Value) -> Result<(), String> { + let approver_id = event["approver"]["id"] + .as_str() + .ok_or("approver id is invalid")?; + let approver_role = event["approver"]["role"] + .as_str() + .ok_or("approver role is invalid")?; + if !TRUSTED_APPROVERS.contains(&(approver_id, approver_role)) { + return Err("authority event approver is not trusted by repository policy".to_string()); + } + let attestation = &event["attestation"]; + exact( + attestation, + &["scheme", "digest"], + "authority event attestation", + )?; + if attestation["scheme"] != ATTESTATION_SCHEME { + return Err("authority event attestation scheme is invalid".to_string()); + } + let digest = attestation["digest"] + .as_str() + .filter(|value| { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + }) + .ok_or("authority event attestation digest is invalid")?; + if digest != authority_event_digest(event)? { + return Err("authority event attestation content digest mismatch".to_string()); + } + Ok(()) +} + +fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0); + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (i, word) in chunk.chunks_exact(4).enumerate() { + w[i] = u32::from_be_bytes(word.try_into().unwrap()); + } + for i in 16..64 { + let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); + let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for i in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ (!e & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value); + } + } + h.iter().map(|value| format!("{value:08x}")).collect() +} + +fn exact(value: &Value, expected: &[&str], label: &str) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn string_set(value: &Value, label: &str) -> Result, String> { + let values = value + .as_array() + .ok_or_else(|| format!("{label} must be an array"))?; + let mut result = BTreeSet::new(); + for value in values { + let item = value + .as_str() + .filter(|s| !s.is_empty()) + .ok_or_else(|| format!("{label} contains an invalid id"))?; + if !result.insert(item.to_string()) { + return Err(format!("{label} contains duplicate ids")); + } + } + Ok(result) +} + +fn duplicates<'a>(values: impl Iterator) -> BTreeSet { + let mut counts = BTreeMap::new(); + for value in values { + *counts.entry(value.to_string()).or_insert(0usize) += 1; + } + counts + .into_iter() + .filter_map(|(value, count)| (count > 1).then_some(value)) + .collect() +} + +fn nonempty(value: &Value, label: &str) -> Result<(), String> { + if value.as_str().is_some_and(|value| !value.is_empty()) { + Ok(()) + } else { + Err(format!("{label} is invalid")) + } +} diff --git a/crates/code-intel-cli/src/builtin_provider_evidence.rs b/crates/code-intel-cli/src/builtin_provider_evidence.rs new file mode 100644 index 0000000..9b3cabe --- /dev/null +++ b/crates/code-intel-cli/src/builtin_provider_evidence.rs @@ -0,0 +1,443 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +use super::{AdapterArtifact, AdapterError, AdapterOutput}; +use crate::artifact_ref::VerifiedArtifact; +use crate::capability::sha256_hex; +use crate::snapshot; + +#[path = "admissibility.rs"] +mod admissibility; +#[path = "graph.rs"] +mod graph; +#[path = "graph_adapter.rs"] +mod graph_adapter; +#[path = "sentrux_adapter.rs"] +mod sentrux_adapter; +const MAX_AGE_SECONDS: u64 = 300; +const MAX_COMMAND_EVIDENCE_BYTES: usize = 1024 * 1024; + +pub(super) fn graph_admission( + request: &Value, + inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + let repo = provider_repo(request, inputs, "provider.graph-adapt")?; + let lease = + snapshot::begin_consumption(repo, &request["snapshot"]).map_err(AdapterError::Contract)?; + let collected_at = now()?; + let document = graph::generate(repo, "zh", false, false) + .map_err(|error| AdapterError::Internal(format!("generate built-in graph: {error}")))?; + lease.verify_after(repo).map_err(AdapterError::Contract)?; + let observed_at = now()?.max(collected_at); + let identity = snapshot_identity(request)?; + let payload = json!({ + "schema":"code-intel-evidence-payload.v1", + "data":{"architectureGraph":{ + "schema":"code-intel-architecture-graph-evidence.v1", + "snapshotIdentity":identity, + "provider":{ + "mode":"internal", + "implementationId":"architecture-graph.internal-rust", + "fallbackIdentity":Value::Null + }, + "provenance":{ + "sourceRevision":source_revision(request), + "observedAt":observed_at + }, + "completeness":"complete", + "graph":document + }} + }); + fs::create_dir(out) + .map_err(|error| AdapterError::Io(format!("create graph provider output: {error}")))?; + let payload_bytes = serde_json::to_vec(&payload) + .map_err(|error| AdapterError::Internal(format!("serialize graph payload: {error}")))?; + fs::write(out.join("graph-payload.json"), &payload_bytes) + .map_err(|error| AdapterError::Io(format!("write graph payload: {error}")))?; + let native = json!({ + "schema":"code-intel-graph-provider-native.v1", + "providerMode":"internal", + "status":"current", + "implementation":{ + "id":"architecture-graph.internal-rust", + "version":"1.0.0", + "digest":sha256_hex(include_bytes!("graph.rs")) + }, + "sourceRevision":source_revision(request), + "expectedSnapshotIdentity":identity, + "sourceSnapshotIdentity":identity, + "collectedAt":collected_at, + "observedAt":observed_at, + "payload":payload_ref("graph-payload.json", &payload_bytes, identity), + "fallback":Value::Null + }); + let adapter = graph_adapter::translate(&native, observed_at, MAX_AGE_SECONDS) + .map_err(AdapterError::Contract)?; + let admission = admissibility::validate_for_consumer(&adapter["evidence"]["request"], out) + .map_err(AdapterError::Contract)?; + graph_adapter::validate_admitted_payload(admission.payload(), &adapter) + .map_err(AdapterError::Contract)?; + if admission.result()["domainVerdict"] != "observed" { + return Err(AdapterError::Contract( + "built-in current graph was not admitted as observed evidence".into(), + )); + } + let mut output = publish_admission( + out, + "graph-admission.json", + admission.result().clone(), + &["repo_read", "local_write"], + )?; + output.artifacts.push(AdapterArtifact { + artifact_schema: "code-intel-evidence-payload.v1".into(), + artifact_type: "observed.evidence.payload".into(), + relative_path: "graph-payload.json".into(), + bytes: payload_bytes, + }); + Ok(output) +} + +pub(super) fn sentrux_admission( + request: &Value, + inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + let (repo, tool_path_prefix) = sentrux_provider_options(request, inputs)?; + let lease = + snapshot::begin_consumption(repo, &request["snapshot"]).map_err(AdapterError::Contract)?; + let collected_at = now()?; + let gate = run_sentrux(repo, tool_path_prefix, "gate")?; + let check = run_sentrux(repo, tool_path_prefix, "check")?; + lease.verify_after(repo).map_err(AdapterError::Contract)?; + let observed_at = now()?.max(collected_at); + let identity = snapshot_identity(request)?; + let command_observation = json!({ + "schema":"code-intel-sentrux-command-observation.v1", + "snapshotIdentity":identity, + "commands":[command_evidence("gate", &gate), command_evidence("check", &check)] + }); + let command_observation_bytes = serde_json::to_vec(&command_observation).map_err(|error| { + AdapterError::Internal(format!("serialize Sentrux command observation: {error}")) + })?; + let rules = json!([ + command_rule("sentrux_gate", gate.status.success()), + command_rule("sentrux_check", check.status.success()) + ]); + let native = json!({ + "schema":"code-intel-sentrux-provider-native.v1", + "status":"complete", + "implementation":{ + "id":"sentrux.command-adapter", + "version":"1.0.0", + "digest":sha256_hex(include_bytes!("builtin_provider_evidence.rs")) + }, + "rollbackIdentity":"sentrux gate/check", + "sourceRevision":source_revision(request), + "expectedSnapshotIdentity":identity, + "sourceSnapshotIdentity":identity, + "collectedAt":collected_at, + "observedAt":observed_at, + "declaredEffects":["local_write","process_spawn","repo_read"], + "observedEffects":["local_write","process_spawn","repo_read"], + "authoritativeRules":rules, + "nativeFailure":{"kind":"none"}, + "payload":{ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-evidence-payload.v1", + "type":"observed.evidence.payload", + "path":"sentrux-payload.json", + "sha256":"0".repeat(64), + "consumedSnapshotIdentity":identity + } + }); + let first = sentrux_adapter::translate(&native, observed_at, MAX_AGE_SECONDS) + .map_err(AdapterError::Contract)?; + let payload = json!({ + "schema":"code-intel-evidence-payload.v1", + "data":{"structuralEvidence":{ + "schema":"code-intel-structural-evidence-payload.v1", + "snapshotIdentity":identity, + "provider":first["port"]["provider"], + "provenance":first["port"]["provenance"], + "effects":first["port"]["effects"], + "completeness":first["port"]["completeness"], + "rules":first["port"]["rules"] + }} + }); + fs::create_dir(out) + .map_err(|error| AdapterError::Io(format!("create Sentrux provider output: {error}")))?; + fs::write( + out.join("sentrux-command-observation.json"), + &command_observation_bytes, + ) + .map_err(|error| AdapterError::Io(format!("write Sentrux command observation: {error}")))?; + let payload_bytes = serde_json::to_vec(&payload) + .map_err(|error| AdapterError::Internal(format!("serialize Sentrux payload: {error}")))?; + fs::write(out.join("sentrux-payload.json"), &payload_bytes) + .map_err(|error| AdapterError::Io(format!("write Sentrux payload: {error}")))?; + let mut native = native; + native["payload"] = payload_ref("sentrux-payload.json", &payload_bytes, identity); + let adapter = sentrux_adapter::translate(&native, observed_at, MAX_AGE_SECONDS) + .map_err(AdapterError::Contract)?; + let admission = admissibility::validate_for_consumer(&adapter["evidence"]["request"], out) + .map_err(AdapterError::Contract)?; + sentrux_adapter::validate_admitted_payload(admission.payload(), &adapter) + .map_err(AdapterError::Contract)?; + let mut output = publish_admission( + out, + "sentrux-admission.json", + admission.result().clone(), + &["repo_read", "local_write", "process_spawn"], + )?; + output.artifacts.extend([ + AdapterArtifact { + artifact_schema: "code-intel-evidence-payload.v1".into(), + artifact_type: "observed.evidence.payload".into(), + relative_path: "sentrux-payload.json".into(), + bytes: payload_bytes, + }, + AdapterArtifact { + artifact_schema: "code-intel-sentrux-command-observation.v1".into(), + artifact_type: "provider.sentrux.command-observation".into(), + relative_path: "sentrux-command-observation.json".into(), + bytes: command_observation_bytes, + }, + ]); + Ok(output) +} + +fn sentrux_provider_options<'a>( + request: &'a Value, + inputs: &[VerifiedArtifact], +) -> Result<(&'a Path, Option<&'a Path>), AdapterError> { + let [snapshot_input] = inputs else { + return Err(AdapterError::Contract( + "provider.sentrux-adapt requires exactly one repository.snapshot input".into(), + )); + }; + if snapshot_input.artifact_schema() != "code-intel-repository-snapshot.v1" + || snapshot_input.artifact_type() != "repository.snapshot" + { + return Err(AdapterError::Contract( + "provider.sentrux-adapt consumes only repository.snapshot".into(), + )); + } + let options = request + .get("options") + .and_then(Value::as_object) + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if options + .keys() + .any(|key| !matches!(key.as_str(), "repoPath" | "toolPathPrefix")) + { + return Err(AdapterError::InvalidOptions( + "provider.sentrux-adapt accepts only options.repoPath/toolPathPrefix".into(), + )); + } + let repo = options + .get("repoPath") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(Path::new) + .filter(|path| path.is_dir()) + .ok_or_else(|| { + AdapterError::InvalidOptions("options.repoPath must be a directory".into()) + })?; + let tool_path_prefix = options + .get("toolPathPrefix") + .map(|value| { + value + .as_str() + .filter(|value| !value.is_empty()) + .map(Path::new) + .filter(|path| path.is_dir()) + .ok_or_else(|| { + AdapterError::InvalidOptions( + "options.toolPathPrefix must be a directory".into(), + ) + }) + }) + .transpose()?; + Ok((repo, tool_path_prefix)) +} + +fn run_sentrux( + repo: &Path, + tool_path_prefix: Option<&Path>, + subcommand: &str, +) -> Result { + let explicit = tool_path_prefix.map(resolve_sentrux).transpose()?; + let mut command = match explicit.as_deref() { + #[cfg(windows)] + Some(path) + if path + .extension() + .and_then(|value| value.to_str()) + .is_some_and(|extension| { + matches!(extension.to_ascii_lowercase().as_str(), "cmd" | "bat") + }) => + { + let mut command = Command::new("cmd.exe"); + command.args(["/d", "/c"]).arg(path); + command + } + Some(path) => Command::new(path), + None => Command::new("sentrux"), + }; + let output = command + .arg(subcommand) + .arg(".") + .current_dir(repo) + .output() + .map_err(|error| { + AdapterError::Unavailable(format!("start Sentrux {subcommand}: {error}")) + })?; + if output.stdout.len() > MAX_COMMAND_EVIDENCE_BYTES + || output.stderr.len() > MAX_COMMAND_EVIDENCE_BYTES + { + return Err(AdapterError::Contract(format!( + "Sentrux {subcommand} output exceeds the bounded evidence limit" + ))); + } + Ok(output) +} + +fn resolve_sentrux(prefix: &Path) -> Result { + let names: &[&str] = if cfg!(windows) { + &["sentrux.exe", "sentrux.cmd", "sentrux.bat", "sentrux"] + } else { + &["sentrux"] + }; + names + .iter() + .map(|name| prefix.join(name)) + .find(|path| path.is_file()) + .ok_or_else(|| { + AdapterError::Unavailable(format!( + "Sentrux executable is absent from options.toolPathPrefix: {}", + prefix.display() + )) + }) +} + +fn command_evidence(subcommand: &str, output: &Output) -> Value { + json!({ + "id":subcommand, + "argv":["sentrux",subcommand,"."], + "exitCode":output.status.code(), + "success":output.status.success(), + "stdout":String::from_utf8_lossy(&output.stdout), + "stderr":String::from_utf8_lossy(&output.stderr) + }) +} + +fn command_rule(kind: &str, pass: bool) -> Value { + json!({ + "kind":kind, + "status":"evaluated", + "verdict":if pass { "pass" } else { "fail" }, + "failure":{"kind":"none"} + }) +} + +fn provider_repo<'a>( + request: &'a Value, + inputs: &[VerifiedArtifact], + capability: &str, +) -> Result<&'a Path, AdapterError> { + let [snapshot_input] = inputs else { + return Err(AdapterError::Contract(format!( + "{capability} requires exactly one repository.snapshot input" + ))); + }; + if snapshot_input.artifact_schema() != "code-intel-repository-snapshot.v1" + || snapshot_input.artifact_type() != "repository.snapshot" + { + return Err(AdapterError::Contract(format!( + "{capability} consumes only repository.snapshot" + ))); + } + let options = request + .get("options") + .and_then(Value::as_object) + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if options.len() != 1 || !options.contains_key("repoPath") { + return Err(AdapterError::InvalidOptions(format!( + "{capability} accepts only options.repoPath" + ))); + } + options["repoPath"] + .as_str() + .filter(|value| !value.is_empty()) + .map(Path::new) + .filter(|path| path.is_dir()) + .ok_or_else(|| AdapterError::InvalidOptions("options.repoPath must be a directory".into())) +} + +fn snapshot_identity(request: &Value) -> Result<&str, AdapterError> { + request["snapshot"]["identity"] + .as_str() + .ok_or_else(|| AdapterError::Contract("request snapshot identity is missing".into())) +} + +fn source_revision(request: &Value) -> &str { + request["snapshot"]["head"].as_str().unwrap_or("unknown") +} + +fn now() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .map_err(|error| AdapterError::Internal(format!("read provider clock: {error}"))) +} + +fn payload_ref(path: &str, bytes: &[u8], identity: &str) -> Value { + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-evidence-payload.v1", + "type":"observed.evidence.payload", + "path":path, + "sha256":sha256_hex(bytes), + "consumedSnapshotIdentity":identity + }) +} + +fn publish_admission( + out: &Path, + file_name: &str, + admission: Value, + effects: &[&str], +) -> Result { + let domain_verdict = match admission["domainVerdict"].as_str() { + Some("observed") => crate::capability_inventory::AdapterDomainVerdict::Pass, + Some("unknown") => crate::capability_inventory::AdapterDomainVerdict::Unknown, + Some("not_applicable") => crate::capability_inventory::AdapterDomainVerdict::NotApplicable, + Some("fail") => crate::capability_inventory::AdapterDomainVerdict::Fail, + other => { + return Err(AdapterError::Contract(format!( + "evidence admission has unsupported domain verdict: {other:?}" + ))) + } + }; + let bytes = serde_json::to_vec(&admission).map_err(|error| { + AdapterError::Internal(format!("serialize evidence admission: {error}")) + })?; + fs::write(out.join(file_name), &bytes) + .map_err(|error| AdapterError::Io(format!("write evidence admission: {error}")))?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-evidence-admissibility-result.v1".into(), + artifact_type: "evidence.admission".into(), + relative_path: file_name.into(), + bytes, + }], + observed_effects: effects.iter().map(|effect| (*effect).to_string()).collect(), + domain_verdict, + domain_failure: None, + }) +} diff --git a/crates/code-intel-cli/src/capability.rs b/crates/code-intel-cli/src/capability.rs new file mode 100644 index 0000000..2ab9eb3 --- /dev/null +++ b/crates/code-intel-cli/src/capability.rs @@ -0,0 +1,1257 @@ +use std::collections::BTreeSet; +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Map, Value}; + +use crate::artifact_ref; +use crate::capability_inventory::{self, AdapterError}; + +const ZERO_DIGEST: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const MAX_JSON_BYTES: usize = 8 * 1024 * 1024; +const MAX_JSON_DEPTH: usize = 128; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let parsed = match parse_cli(raw) { + Ok(parsed) => parsed, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let outcome = execute_cli( + &parsed.capability, + &parsed.request, + &parsed.out, + parsed.artifact_root.as_deref(), + parsed.manifest.as_deref(), + ); + if let Some(result) = outcome.result { + if let Err(message) = validate_result_envelope(&result) { + eprintln!("executor refused to emit an invalid result envelope: {message}"); + return 70; + } + println!( + "{}", + serde_json::to_string(&result).expect("result envelope serializes") + ); + } + if let Some(diagnostic) = outcome.stderr { + eprintln!("{diagnostic}"); + } + outcome.exit_code +} + +struct ExecCli { + capability: String, + request: PathBuf, + out: PathBuf, + manifest: Option, + artifact_root: Option, +} + +fn parse_cli(raw: &[String]) -> Result { + if raw.len() < 2 || raw[0] != "exec" || raw[1].starts_with('-') { + return Err( + "usage: capability exec --request --out [--artifact-root ]" + .to_string(), + ); + } + let capability = raw[1].clone(); + let mut request = None; + let mut out = None; + let mut manifest = None; + let mut artifact_root = None; + let mut index = 2; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--request" | "--out" | "--manifest" | "--artifact-root" + ) { + return Err(format!( + "unknown or conflicting capability argument: {flag}" + )); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + let slot = match flag { + "--request" => &mut request, + "--out" => &mut out, + "--manifest" => &mut manifest, + "--artifact-root" => &mut artifact_root, + _ => unreachable!(), + }; + if slot.replace(PathBuf::from(value)).is_some() { + return Err(format!("duplicate capability argument: {flag}")); + } + index += 2; + } + Ok(ExecCli { + capability, + request: request.ok_or("capability exec requires exactly one --request")?, + out: out.ok_or("capability exec requires exactly one --out")?, + manifest, + artifact_root, + }) +} + +struct CliOutcome { + result: Option, + stderr: Option, + exit_code: i32, +} + +fn execute_cli( + cli_capability: &str, + request_file: &Path, + out_dir: &Path, + artifact_root: Option<&Path>, + manifest: Option<&Path>, +) -> CliOutcome { + let request = match read_one_request(request_file) { + Ok(request) => request, + Err((code, message)) => return pre_envelope(code, &message), + }; + if let Err(message) = validate_request(&request) { + return failure_from(&request, 64, &message); + } + let registry = match load_registry(manifest) { + Ok(registry) => registry, + Err(RegistryError::Unavailable(message)) => return failure_from(&request, 69, &message), + Err(RegistryError::Invalid(message)) => return failure_from(&request, 65, &message), + }; + let (declaration, adapter) = match find_declaration(®istry, cli_capability) { + Ok(Some(value)) => value, + Ok(None) => { + return failure_from(&request, 64, "CLI capability has no registered declaration") + } + Err(message) => return failure_from(&request, 65, &message), + }; + if request["capability"].as_str() != Some(cli_capability) { + return failure_from_declaration( + &request, + &declaration, + 64, + "CLI capability differs from request capability", + ); + } + if let Err(message) = validate_declaration(&declaration) { + return failure_from_declaration(&request, &declaration, 65, &message); + } + if let Err(message) = cohere(&declaration, &request) { + return failure_from_declaration(&request, &declaration, 64, &message); + } + let verified_inputs = match artifact_ref::verify_inputs( + &request["inputs"], + artifact_root, + request["snapshot"]["identity"] + .as_str() + .expect("validated snapshot identity"), + ) { + Ok(verified) => verified, + Err(error) => { + let exit = if matches!(error, artifact_ref::ArtifactError::Io(_)) { + 74 + } else { + 65 + }; + return failure_from_declaration(&request, &declaration, exit, error.message()); + } + }; + match capability_inventory::execute(&adapter, &request, &verified_inputs, out_dir) { + Ok(output) => { + let domain_verdict = output.domain_verdict.as_str(); + let domain_failure = output.domain_failure.clone(); + let artifacts = output + .artifacts + .into_iter() + .map(|artifact| { + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":artifact.artifact_schema, + "type":artifact.artifact_type, + "path":artifact.relative_path, + "sha256":sha256_hex(&artifact.bytes), + "consumedSnapshotIdentity":request["snapshot"]["identity"] + }) + }) + .collect(); + let (exit_code, verdict, diagnostics) = match (domain_verdict, domain_failure) { + ("fail", Some(message)) => (10, "fail", vec![message]), + ("fail", None) => ( + 10, + "fail", + vec!["adapter returned a domain fail verdict without a diagnostic".into()], + ), + ("pass" | "unknown" | "not_applicable", None) => (0, "pass", vec![]), + (_, Some(message)) => { + return failure_from_declaration( + &request, + &declaration, + 70, + &format!( + "adapter returned a domain failure diagnostic for {domain_verdict}: {message}" + ), + ) + } + _ => unreachable!("AdapterDomainVerdict is closed"), + }; + let result = base_result( + &request, + &declaration, + exit_code, + "completed", + verdict, + domain_verdict, + artifacts, + output.observed_effects, + diagnostics, + ); + if let Err(message) = validate_result(&result, &request, &declaration) { + return failure_from_declaration( + &request, + &declaration, + 70, + &format!("executor produced invalid result: {message}"), + ); + } + CliOutcome { + result: Some(result), + stderr: None, + exit_code, + } + } + Err(AdapterError::InvalidOptions(message)) => { + failure_from_declaration(&request, &declaration, 64, &message) + } + Err(AdapterError::Contract(message)) => { + failure_from_declaration(&request, &declaration, 65, &message) + } + Err(AdapterError::Unavailable(message)) => { + failure_from_declaration(&request, &declaration, 69, &message) + } + Err(AdapterError::Internal(message)) => { + failure_from_declaration(&request, &declaration, 70, &message) + } + Err(AdapterError::Io(message)) => { + failure_from_declaration(&request, &declaration, 74, &message) + } + } +} + +fn pre_envelope(exit_code: i32, message: &str) -> CliOutcome { + CliOutcome { + result: None, + stderr: Some(message.to_string()), + exit_code, + } +} + +fn read_one_request(path: &Path) -> Result { + let bytes = if path == Path::new("-") { + read_limited(io::stdin()) + .map_err(|err| (74, format!("cannot read stdin request: {err}")))? + } else { + let file = fs::File::open(path).map_err(|err| { + ( + 74, + format!("cannot read request file {}: {err}", path.display()), + ) + })?; + read_limited(file).map_err(|err| { + ( + 74, + format!("cannot read request file {}: {err}", path.display()), + ) + })? + }; + if bytes.len() > MAX_JSON_BYTES { + return Err((64, format!("JSON input exceeds {MAX_JSON_BYTES} bytes"))); + } + let text = String::from_utf8(bytes) + .map_err(|err| (64, format!("request is not valid UTF-8: {err}")))?; + let text = text.strip_prefix('\u{feff}').unwrap_or(&text); + reject_duplicate_json_keys(text).map_err(|message| (64, message))?; + let mut stream = serde_json::Deserializer::from_str(text).into_iter::(); + let first = stream + .next() + .ok_or_else(|| (64, "expected exactly one request JSON document".to_string()))? + .map_err(|err| (64, format!("invalid request JSON: {err}")))?; + if stream.next().is_some() { + return Err(( + 64, + "expected exactly one request JSON document; found additional input".to_string(), + )); + } + Ok(first) +} + +enum RegistryError { + Unavailable(String), + Invalid(String), +} + +fn load_registry(explicit: Option<&Path>) -> Result { + let path = discover_manifest(explicit).ok_or_else(|| { + RegistryError::Unavailable("cannot locate orchestration/integrations.json".to_string()) + })?; + let file = fs::File::open(&path).map_err(|err| { + RegistryError::Unavailable(format!("cannot read registry {}: {err}", path.display())) + })?; + let bytes = read_limited(file).map_err(|err| { + RegistryError::Unavailable(format!("cannot read registry {}: {err}", path.display())) + })?; + if bytes.len() > MAX_JSON_BYTES { + return Err(RegistryError::Invalid(format!( + "JSON input exceeds {MAX_JSON_BYTES} bytes" + ))); + } + let text = String::from_utf8(bytes).map_err(|err| { + RegistryError::Invalid(format!( + "registry {} is not valid UTF-8: {err}", + path.display() + )) + })?; + let text = text.strip_prefix('\u{feff}').unwrap_or(&text); + reject_duplicate_json_keys(text).map_err(RegistryError::Invalid)?; + serde_json::from_str(text).map_err(|err| { + RegistryError::Invalid(format!("invalid registry {}: {err}", path.display())) + }) +} + +fn read_limited(reader: impl Read) -> io::Result> { + let mut bytes = Vec::new(); + reader + .take((MAX_JSON_BYTES as u64) + 1) + .read_to_end(&mut bytes)?; + Ok(bytes) +} + +fn discover_manifest(explicit: Option<&Path>) -> Option { + if let Some(path) = explicit { + return path.is_file().then(|| path.to_path_buf()); + } + if let Some(path) = env::var_os("CODE_INTEL_INTEGRATIONS_MANIFEST") { + let path = PathBuf::from(path); + return path.is_file().then_some(path); + } + if let Some(home) = env::var_os("CODE_INTEL_HOME") { + let path = PathBuf::from(home) + .join("orchestration") + .join("integrations.json"); + return path.is_file().then_some(path); + } + let mut candidates = vec![]; + if let Ok(exe) = env::current_exe() { + if let Some(parent) = exe.parent() { + candidates.push(parent.join("orchestration").join("integrations.json")); + } + } + candidates.push( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json"), + ); + candidates.into_iter().find(|path| path.is_file()) +} + +fn find_declaration(registry: &Value, capability: &str) -> Result, String> { + let integrations = registry + .get("integrations") + .and_then(Value::as_array) + .ok_or("registry integrations must be an array")?; + let mut all_ids = BTreeSet::new(); + let mut found = None; + for entry in integrations { + let Some(declaration) = entry.get("capabilityDeclaration") else { + continue; + }; + let id = declaration + .get("id") + .and_then(Value::as_str) + .ok_or("registered capability declaration lacks id")?; + if !all_ids.insert(id) { + return Err(format!("duplicate registered capability declaration: {id}")); + } + if id == capability { + found = Some(( + declaration.clone(), + entry + .get("runtimeAdapter") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(), + )); + } + } + Ok(found) +} + +pub(crate) fn reject_duplicate_json_keys(text: &str) -> Result<(), String> { + if text.len() > MAX_JSON_BYTES { + return Err(format!("JSON input exceeds {MAX_JSON_BYTES} bytes")); + } + JsonKeyScanner { + bytes: text.as_bytes(), + pos: 0, + } + .scan_document() +} + +struct JsonKeyScanner<'a> { + bytes: &'a [u8], + pos: usize, +} + +impl JsonKeyScanner<'_> { + fn scan_document(&mut self) -> Result<(), String> { + self.ws(); + self.value(0)?; + self.ws(); + if self.pos == self.bytes.len() { + Ok(()) + } else { + Err("invalid trailing JSON input".to_string()) + } + } + fn value(&mut self, depth: usize) -> Result<(), String> { + if depth > MAX_JSON_DEPTH { + return Err(format!("JSON nesting exceeds {MAX_JSON_DEPTH}")); + } + self.ws(); + match self.bytes.get(self.pos).copied() { + Some(b'{') => self.object(depth + 1), + Some(b'[') => self.array(depth + 1), + Some(b'"') => self.string().map(|_| ()), + Some(_) => { + while self.pos < self.bytes.len() + && !matches!( + self.bytes[self.pos], + b',' | b']' | b'}' | b' ' | b'\t' | b'\r' | b'\n' + ) + { + self.pos += 1; + } + Ok(()) + } + None => Err("unexpected end of JSON".to_string()), + } + } + fn object(&mut self, depth: usize) -> Result<(), String> { + self.pos += 1; + self.ws(); + let mut keys = BTreeSet::new(); + if self.take(b'}') { + return Ok(()); + } + loop { + self.ws(); + let key = self.string()?; + if !keys.insert(key.clone()) { + return Err(format!("duplicate JSON object key: {key}")); + } + self.ws(); + if !self.take(b':') { + return Err("invalid JSON object separator".to_string()); + } + self.value(depth)?; + self.ws(); + if self.take(b'}') { + return Ok(()); + } + if !self.take(b',') { + return Err("invalid JSON object delimiter".to_string()); + } + } + } + fn array(&mut self, depth: usize) -> Result<(), String> { + self.pos += 1; + self.ws(); + if self.take(b']') { + return Ok(()); + } + loop { + self.value(depth)?; + self.ws(); + if self.take(b']') { + return Ok(()); + } + if !self.take(b',') { + return Err("invalid JSON array delimiter".to_string()); + } + } + } + fn string(&mut self) -> Result { + let start = self.pos; + if !self.take(b'"') { + return Err("expected JSON string".to_string()); + } + while self.pos < self.bytes.len() { + match self.bytes[self.pos] { + b'\\' => { + self.pos += 1; + if self.pos >= self.bytes.len() { + return Err("unterminated JSON escape".to_string()); + } + self.pos += 1; + } + b'"' => { + self.pos += 1; + return serde_json::from_slice(&self.bytes[start..self.pos]) + .map_err(|e| format!("invalid JSON string: {e}")); + } + _ => self.pos += 1, + } + } + Err("unterminated JSON string".to_string()) + } + fn ws(&mut self) { + while self + .bytes + .get(self.pos) + .is_some_and(|b| b.is_ascii_whitespace()) + { + self.pos += 1; + } + } + fn take(&mut self, byte: u8) -> bool { + if self.bytes.get(self.pos) == Some(&byte) { + self.pos += 1; + true + } else { + false + } + } +} + +fn validate_request(request: &Value) -> Result<(), String> { + let object = request.as_object().ok_or("request must be a JSON object")?; + require_exact_keys( + object, + &[ + "schema", + "capability", + "contractVersion", + "implementation", + "snapshot", + "options", + "inputs", + "effectPolicy", + ], + "request", + )?; + if request["schema"] != "code-intel-capability-request.v1" || request["contractVersion"] != 1 { + return Err("request must use the v1 schema and contract".to_string()); + } + require_id(&request["capability"], "request.capability")?; + validate_implementation(&request["implementation"], "request.implementation")?; + validate_snapshot(&request["snapshot"])?; + if !request["options"].is_object() || !request["inputs"].is_array() { + return Err("request options/inputs have invalid types".to_string()); + } + for artifact in request["inputs"].as_array().expect("checked array") { + validate_artifact_ref_shape(artifact)?; + } + let policy = request["effectPolicy"] + .as_object() + .ok_or("request.effectPolicy must be an object")?; + require_exact_keys(policy, &["allowedEffects"], "request.effectPolicy")?; + validate_effects( + &request["effectPolicy"]["allowedEffects"], + "request.effectPolicy.allowedEffects", + ) +} + +fn validate_declaration(declaration: &Value) -> Result<(), String> { + let object = declaration + .as_object() + .ok_or("declaration must be an object")?; + require_exact_keys( + object, + &[ + "schema", + "id", + "contractVersion", + "implementation", + "determinism", + "allowedEffects", + "dependencies", + ], + "declaration", + )?; + if declaration["schema"] != "code-intel-capability-declaration.v1" + || declaration["contractVersion"] != 1 + { + return Err("registered declaration is not v1".to_string()); + } + require_id(&declaration["id"], "declaration.id")?; + validate_implementation(&declaration["implementation"], "declaration.implementation")?; + if !matches!( + declaration["determinism"].as_str(), + Some("deterministic" | "external_nondeterministic") + ) { + return Err("declaration determinism is invalid".to_string()); + } + validate_effects(&declaration["allowedEffects"], "declaration.allowedEffects")?; + if !declaration["dependencies"].as_array().is_some_and(|v| { + let mut seen = BTreeSet::new(); + v.iter().all(|id| { + id.as_str() + .is_some_and(|id| valid_id(id) && seen.insert(id)) + }) + }) { + return Err("declaration dependencies are invalid".to_string()); + } + Ok(()) +} + +fn cohere(declaration: &Value, request: &Value) -> Result<(), String> { + if request["capability"] != declaration["id"] { + return Err("request capability differs from declaration id".to_string()); + } + if request["implementation"] != declaration["implementation"] { + return Err("request implementation differs from declaration".to_string()); + } + if !string_set(&request["effectPolicy"]["allowedEffects"]) + .is_subset(&string_set(&declaration["allowedEffects"])) + { + return Err("request effects exceed declaration".to_string()); + } + Ok(()) +} + +fn validate_result(result: &Value, request: &Value, declaration: &Value) -> Result<(), String> { + validate_result_envelope(result)?; + for artifact in result["artifacts"] + .as_array() + .expect("validated result artifacts") + { + if artifact["consumedSnapshotIdentity"] != result["snapshotIdentity"] { + return Err("result artifact snapshot coherence failure".to_string()); + } + } + if result["capability"] != request["capability"] + || result["implementation"] != request["implementation"] + || result["snapshotIdentity"] != request["snapshot"]["identity"] + || result["determinism"] != declaration["determinism"] + || result["declaredEffects"] != request["effectPolicy"]["allowedEffects"] + { + return Err("result coherence failure".to_string()); + } + if !string_set(&result["observedEffects"]).is_subset(&string_set(&result["declaredEffects"])) { + return Err("observed undeclared effect".to_string()); + } + Ok(()) +} + +fn validate_result_envelope(result: &Value) -> Result<(), String> { + let object = result.as_object().ok_or("result must be an object")?; + require_exact_keys( + object, + &[ + "schema", + "capability", + "implementation", + "snapshotIdentity", + "status", + "verdict", + "domainVerdict", + "exitCode", + "determinism", + "declaredEffects", + "observedEffects", + "cache", + "artifacts", + "diagnostics", + "provenance", + ], + "result", + )?; + if result["schema"] != "code-intel-capability-result.v1" { + return Err("result schema is invalid".to_string()); + } + require_id(&result["capability"], "result.capability")?; + validate_implementation(&result["implementation"], "result.implementation")?; + if !result["snapshotIdentity"].as_str().is_some_and(is_digest) { + return Err("result snapshotIdentity is invalid".to_string()); + } + if !matches!( + result["determinism"].as_str(), + Some("deterministic" | "external_nondeterministic") + ) { + return Err("result determinism is invalid".to_string()); + } + validate_effects(&result["declaredEffects"], "result.declaredEffects")?; + validate_effects(&result["observedEffects"], "result.observedEffects")?; + let cache = result["cache"] + .as_object() + .ok_or("result cache must be an object")?; + require_exact_keys(cache, &["key", "hit"], "result.cache")?; + if !result["cache"]["key"].is_null() && !result["cache"]["key"].as_str().is_some_and(is_digest) + { + return Err("result cache key is invalid".to_string()); + } + if !result["cache"]["hit"].is_boolean() { + return Err("result cache hit is invalid".to_string()); + } + let artifacts = result["artifacts"] + .as_array() + .ok_or("result artifacts must be an array")?; + for artifact in artifacts { + validate_artifact_ref_shape(artifact)?; + let path = artifact["path"].as_str().unwrap_or(""); + if Path::new(path).is_absolute() + || Path::new(path) + .components() + .any(|part| matches!(part, std::path::Component::ParentDir)) + { + return Err("result artifact path escapes output boundary".to_string()); + } + } + if !result["diagnostics"] + .as_array() + .is_some_and(|items| items.iter().all(Value::is_string)) + { + return Err("result diagnostics are invalid".to_string()); + } + let provenance = result["provenance"] + .as_object() + .ok_or("result provenance must be an object")?; + let allowed: BTreeSet<&str> = [ + "attemptId", + "generatedAt", + "provider", + "model", + "configurationDigest", + ] + .into_iter() + .collect(); + if provenance.keys().any(|key| !allowed.contains(key.as_str())) { + return Err("result provenance contains an unknown field".to_string()); + } + if !provenance + .get("attemptId") + .and_then(Value::as_str) + .is_some_and(|v| !v.is_empty()) + || !provenance + .get("generatedAt") + .and_then(Value::as_str) + .is_some_and(is_rfc3339_utc) + { + return Err("result provenance required fields are invalid".to_string()); + } + for key in ["provider", "model"] { + if provenance + .get(key) + .is_some_and(|v| !v.as_str().is_some_and(|v| !v.is_empty())) + { + return Err(format!("result provenance {key} is invalid")); + } + } + if provenance + .get("configurationDigest") + .is_some_and(|v| !v.as_str().is_some_and(is_digest)) + { + return Err("result provenance configurationDigest is invalid".to_string()); + } + let exit = result["exitCode"] + .as_i64() + .ok_or("result exitCode missing")?; + if !matches!( + result["domainVerdict"].as_str(), + Some("pass" | "fail" | "unknown" | "not_applicable") + ) { + return Err("result domainVerdict is invalid".to_string()); + } + if !matches!( + ( + result["status"].as_str(), + result["verdict"].as_str(), + result["domainVerdict"].as_str(), + exit + ), + ( + Some("completed"), + Some("pass" | "not_applicable"), + Some("pass" | "unknown" | "not_applicable"), + 0 + ) | (Some("completed"), Some("fail"), Some("fail"), 10) + | (Some("blocked"), Some("unknown"), Some("unknown"), 20) + | ( + Some("failed"), + Some("unknown"), + Some("unknown"), + 64 | 65 | 69 | 70 | 74 + ) + ) { + return Err("illegal status/verdict/domainVerdict/exitCode".to_string()); + } + Ok(()) +} + +fn is_rfc3339_utc(value: &str) -> bool { + if !(value.len() == 20 + && value.ends_with('Z') + && value.as_bytes().get(4) == Some(&b'-') + && value.as_bytes().get(7) == Some(&b'-') + && value.as_bytes().get(10) == Some(&b'T') + && value.as_bytes().get(13) == Some(&b':') + && value.as_bytes().get(16) == Some(&b':')) + { + return false; + } + let parse = + |range: std::ops::Range| value.get(range).and_then(|part| part.parse::().ok()); + let (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second)) = ( + parse(0..4), + parse(5..7), + parse(8..10), + parse(11..13), + parse(14..16), + parse(17..19), + ) else { + return false; + }; + if year == 0 || !(1..=12).contains(&month) || hour > 23 || minute > 59 || second > 59 { + return false; + } + let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + let max_day = match month { + 2 if leap => 29, + 2 => 28, + 4 | 6 | 9 | 11 => 30, + _ => 31, + }; + (1..=max_day).contains(&day) +} + +fn base_result( + request: &Value, + declaration: &Value, + exit: i32, + status: &str, + verdict: &str, + domain_verdict: &str, + artifacts: Vec, + observed: Vec, + diagnostics: Vec, +) -> Value { + json!({"schema":"code-intel-capability-result.v1","capability":request["capability"],"implementation":request["implementation"],"snapshotIdentity":request["snapshot"]["identity"],"status":status,"verdict":verdict,"domainVerdict":domain_verdict,"exitCode":exit,"determinism":declaration["determinism"],"declaredEffects":request["effectPolicy"]["allowedEffects"],"observedEffects":observed,"cache":{"key":null,"hit":false},"artifacts":artifacts,"diagnostics":diagnostics,"provenance":{"attemptId":format!("capability-{}-{}",now_seconds(),std::process::id()),"generatedAt":rfc3339_now()}}) +} + +fn failure_from(request: &Value, exit: i32, message: &str) -> CliOutcome { + failure_with_determinism(request, exit, message, "deterministic") +} + +fn failure_from_declaration( + request: &Value, + declaration: &Value, + exit: i32, + message: &str, +) -> CliOutcome { + let determinism = declaration + .get("determinism") + .and_then(Value::as_str) + .filter(|value| matches!(*value, "deterministic" | "external_nondeterministic")) + .unwrap_or("deterministic"); + failure_with_determinism(request, exit, message, determinism) +} + +fn failure_with_determinism( + request: &Value, + exit: i32, + message: &str, + determinism: &str, +) -> CliOutcome { + let capability = request + .get("capability") + .and_then(Value::as_str) + .filter(|v| valid_id(v)) + .unwrap_or("invalid.request"); + let implementation = request + .get("implementation") + .filter(|v| validate_implementation(v, "implementation").is_ok()) + .cloned() + .unwrap_or_else(|| json!({"id":"invalid.request","version":"1","toolchainDigests":[]})); + let snapshot = request + .pointer("/snapshot/identity") + .and_then(Value::as_str) + .filter(|v| is_digest(v)) + .unwrap_or(ZERO_DIGEST); + let effects = request + .pointer("/effectPolicy/allowedEffects") + .filter(|v| validate_effects(v, "effects").is_ok()) + .cloned() + .unwrap_or_else(|| json!([])); + let result = json!({"schema":"code-intel-capability-result.v1","capability":capability,"implementation":implementation,"snapshotIdentity":snapshot,"status":"failed","verdict":"unknown","domainVerdict":"unknown","exitCode":exit,"determinism":determinism,"declaredEffects":effects,"observedEffects":[],"cache":{"key":null,"hit":false},"artifacts":[],"diagnostics":[message],"provenance":{"attemptId":format!("capability-{}-{}",now_seconds(),std::process::id()),"generatedAt":rfc3339_now()}}); + CliOutcome { + result: Some(result), + stderr: Some(message.to_string()), + exit_code: exit, + } +} + +fn validate_implementation(value: &Value, name: &str) -> Result<(), String> { + let o = value + .as_object() + .ok_or_else(|| format!("{name} must be an object"))?; + require_exact_keys(o, &["id", "version", "toolchainDigests"], name)?; + if o.get("id") + .and_then(Value::as_str) + .filter(|v| !v.is_empty()) + .is_none() + || o.get("version") + .and_then(Value::as_str) + .filter(|v| !v.is_empty()) + .is_none() + { + return Err(format!("{name} id/version invalid")); + } + validate_digests( + &value["toolchainDigests"], + &format!("{name}.toolchainDigests"), + ) +} +pub(crate) fn validate_snapshot(value: &Value) -> Result<(), String> { + let o = value.as_object().ok_or("snapshot must be an object")?; + require_exact_keys( + o, + &[ + "identity", + "repoIdentity", + "head", + "workingTreePolicy", + "scope", + "inputDigest", + ], + "snapshot", + )?; + if !["identity", "inputDigest"] + .iter() + .all(|k| o.get(*k).and_then(Value::as_str).is_some_and(is_digest)) + { + return Err("snapshot digest invalid".to_string()); + } + if !["repoIdentity", "head"].iter().all(|k| { + o.get(*k) + .and_then(Value::as_str) + .is_some_and(|v| !v.is_empty()) + }) { + return Err("snapshot identity fields invalid".to_string()); + } + if !matches!( + o.get("workingTreePolicy").and_then(Value::as_str), + Some("head_only" | "explicit_overlay") + ) { + return Err("snapshot workingTreePolicy invalid".to_string()); + } + if !o.get("scope").and_then(Value::as_array).is_some_and(|v| { + let mut seen = BTreeSet::new(); + v.iter() + .all(|s| s.as_str().is_some_and(|s| !s.is_empty() && seen.insert(s))) + }) { + return Err("snapshot scope invalid".to_string()); + } + Ok(()) +} + +pub(crate) fn validate_artifact_ref_shape(value: &Value) -> Result<(), String> { + let object = value + .as_object() + .ok_or("input Artifact Ref must be an object")?; + require_exact_keys( + object, + &[ + "schema", + "artifactSchema", + "type", + "path", + "sha256", + "consumedSnapshotIdentity", + ], + "input Artifact Ref", + )?; + if value["schema"] != "code-intel-artifact-ref.v1" { + return Err("input Artifact Ref schema is invalid".to_string()); + } + for key in ["artifactSchema", "type", "path"] { + if !object + .get(key) + .and_then(Value::as_str) + .is_some_and(|v| !v.is_empty()) + { + return Err(format!("input Artifact Ref {key} is invalid")); + } + } + if !value["sha256"].as_str().is_some_and(is_digest) { + return Err("input Artifact Ref sha256 is invalid".to_string()); + } + if !value["consumedSnapshotIdentity"].is_null() + && !value["consumedSnapshotIdentity"] + .as_str() + .is_some_and(is_digest) + { + return Err("input Artifact Ref consumedSnapshotIdentity is invalid".to_string()); + } + Ok(()) +} +fn require_exact_keys(o: &Map, keys: &[&str], name: &str) -> Result<(), String> { + let a: BTreeSet<&str> = o.keys().map(String::as_str).collect(); + let e: BTreeSet<&str> = keys.iter().copied().collect(); + if a == e { + Ok(()) + } else { + Err(format!("{name} fields differ from v1 schema")) + } +} +fn validate_effects(v: &Value, name: &str) -> Result<(), String> { + let a = v + .as_array() + .ok_or_else(|| format!("{name} must be array"))?; + let mut s = BTreeSet::new(); + if a.iter().all(|v| { + v.as_str().is_some_and(|e| { + matches!( + e, + "repo_read" | "local_write" | "process_spawn" | "network" | "repo_mutation" + ) && s.insert(e) + }) + }) { + Ok(()) + } else { + Err(format!("{name} invalid")) + } +} +fn validate_digests(v: &Value, name: &str) -> Result<(), String> { + let a = v + .as_array() + .ok_or_else(|| format!("{name} must be array"))?; + let mut s = BTreeSet::new(); + if a.iter() + .all(|v| v.as_str().is_some_and(|d| is_digest(d) && s.insert(d))) + { + Ok(()) + } else { + Err(format!("{name} invalid")) + } +} +fn require_id(v: &Value, name: &str) -> Result<(), String> { + if v.as_str().is_some_and(valid_id) { + Ok(()) + } else { + Err(format!("{name} invalid")) + } +} +fn valid_id(v: &str) -> bool { + !v.is_empty() + && v.bytes().enumerate().all(|(i, b)| { + b.is_ascii_lowercase() + || b.is_ascii_digit() + || (i > 0 && matches!(b, b'.' | b'_' | b'-')) + }) +} +fn is_digest(v: &str) -> bool { + v.len() == 64 + && v.bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} +fn string_set(v: &Value) -> BTreeSet<&str> { + v.as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .collect() +} +fn now_seconds() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|v| v.as_secs()) + .unwrap_or(0) +} +fn rfc3339_now() -> String { + let s = now_seconds() as i64; + let days = s.div_euclid(86400); + let ds = s.rem_euclid(86400); + let (y, m, d) = civil_from_days(days); + format!( + "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", + ds / 3600, + (ds % 3600) / 60, + ds % 60 + ) +} +fn civil_from_days(days: i64) -> (i64, i64, i64) { + let z = days + 719468; + let era = if z >= 0 { z } else { z - 146096 } / 146097; + let doe = z - era * 146097; + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let mut y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = mp + if mp < 10 { 3 } else { -9 }; + y += if m <= 2 { 1 } else { 0 }; + (y, m, d) +} + +pub(crate) fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0) + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (i, word) in chunk.chunks_exact(4).enumerate() { + w[i] = u32::from_be_bytes(word.try_into().unwrap()) + } + for i in 16..64 { + let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); + let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1) + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for i in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ (!e & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2) + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value) + } + } + h.iter().map(|v| format!("{v:08x}")).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn sha256_vector() { + assert_eq!( + sha256_hex(b"abc"), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + } + + #[test] + fn duplicate_key_scanner_rejects_nested_duplicates() { + assert!(reject_duplicate_json_keys(r#"{"outer":{"key":1,"key":2}}"#).is_err()); + assert!( + reject_duplicate_json_keys(r#"{"outer":{"key":1},"other":[true,null,"x"]}"#).is_ok() + ); + } + + #[test] + fn json_scanner_enforces_size_and_depth_before_deserialization() { + assert!(reject_duplicate_json_keys(&" ".repeat(MAX_JSON_BYTES + 1)) + .unwrap_err() + .contains("exceeds")); + let deeply_nested = format!( + "{}0{}", + "[".repeat(MAX_JSON_DEPTH + 2), + "]".repeat(MAX_JSON_DEPTH + 2) + ); + assert!(reject_duplicate_json_keys(&deeply_nested) + .unwrap_err() + .contains("nesting")); + } + + #[test] + fn rfc3339_validator_checks_calendar_and_clock_ranges() { + assert!(is_rfc3339_utc("2024-02-29T23:59:59Z")); + assert!(!is_rfc3339_utc("2023-02-29T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-02-30T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-13-01T00:00:00Z")); + assert!(!is_rfc3339_utc("2026-01-01T24:00:00Z")); + } + + #[test] + fn result_validator_rejects_every_nested_contract_family() { + let implementation = json!({"id":"inventory.rg.compat","version":"1.0.0","toolchainDigests":["a".repeat(64)]}); + let request = json!({"schema":"code-intel-capability-request.v1","capability":"inventory.rg","contractVersion":1,"implementation":implementation,"snapshot":{"identity":"b".repeat(64),"repoIdentity":"fixture","head":"head","workingTreePolicy":"head_only","scope":["."],"inputDigest":"c".repeat(64)},"options":{},"inputs":[],"effectPolicy":{"allowedEffects":["repo_read","local_write"]}}); + let declaration = json!({"schema":"code-intel-capability-declaration.v1","id":"inventory.rg","contractVersion":1,"implementation":request["implementation"],"determinism":"deterministic","allowedEffects":["repo_read","local_write"],"dependencies":[]}); + let artifact = json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":"inventory.v1","type":"inventory.files","path":"files.txt","sha256":"d".repeat(64),"consumedSnapshotIdentity":"b".repeat(64)}); + let result = base_result( + &request, + &declaration, + 0, + "completed", + "pass", + "pass", + vec![artifact], + vec!["repo_read".into(), "local_write".into()], + vec![], + ); + assert!(validate_result(&result, &request, &declaration).is_ok()); + let mutations: Vec> = vec![ + Box::new(|v| { + v.as_object_mut() + .unwrap() + .insert("extra".into(), json!(true)); + }), + Box::new(|v| v["schema"] = json!("wrong")), + Box::new(|v| v["cache"]["hit"] = json!("false")), + Box::new(|v| v["artifacts"][0]["path"] = json!("../escape")), + Box::new(|v| v["artifacts"][0]["consumedSnapshotIdentity"] = json!("e".repeat(64))), + Box::new(|v| v["diagnostics"] = json!([1])), + Box::new(|v| v["provenance"]["generatedAt"] = json!("not-a-time")), + Box::new(|v| v["provenance"]["extra"] = json!(true)), + ]; + for mutate in mutations { + let mut candidate = result.clone(); + mutate(&mut candidate); + assert!(validate_result(&candidate, &request, &declaration).is_err()); + } + } +} diff --git a/crates/code-intel-cli/src/capability_inventory.rs b/crates/code-intel-cli/src/capability_inventory.rs new file mode 100644 index 0000000..0bcf3f1 --- /dev/null +++ b/crates/code-intel-cli/src/capability_inventory.rs @@ -0,0 +1,1267 @@ +use serde_json::Value; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::artifact_ref::VerifiedArtifact; +use crate::snapshot; + +#[path = "builtin_provider_evidence.rs"] +mod builtin_provider_evidence; +#[path = "compatibility_retirement_gate.rs"] +mod compatibility_retirement_gate; +#[path = "compatibility_retirement_ticket.rs"] +mod compatibility_retirement_ticket; +#[path = "delivery_light_speed.rs"] +mod delivery_light_speed; +#[path = "doctor_adapter.rs"] +mod doctor_adapter; +#[path = "hospital_diagnosis.rs"] +mod hospital_diagnosis; +#[path = "native_code_evidence.rs"] +mod native_code_evidence; +#[path = "project_orientation.rs"] +mod project_orientation; +#[path = "project_orientation_benchmark.rs"] +mod project_orientation_benchmark; +#[path = "understanding_quadrant.rs"] +mod understanding_quadrant; + +const EXCLUDES: [&str; 10] = [ + "!**/.git/**", + "!**/node_modules/**", + "!**/.repowise/**", + "!**/.understand-anything/**", + "!**/.sentrux/**", + "!**/target/**", + "!**/dist/**", + "!**/build/**", + "!**/.venv/**", + "!**/__pycache__/**", +]; + +static MIRROR_NONCE: AtomicU64 = AtomicU64::new(0); + +pub(crate) struct AdapterOutput { + pub(crate) artifacts: Vec, + pub(crate) observed_effects: Vec, + pub(crate) domain_verdict: AdapterDomainVerdict, + pub(crate) domain_failure: Option, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum AdapterDomainVerdict { + Pass, + Fail, + Unknown, + NotApplicable, +} + +impl AdapterDomainVerdict { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Pass => "pass", + Self::Fail => "fail", + Self::Unknown => "unknown", + Self::NotApplicable => "not_applicable", + } + } +} +pub(crate) struct AdapterArtifact { + pub(crate) artifact_schema: String, + pub(crate) artifact_type: String, + pub(crate) relative_path: String, + pub(crate) bytes: Vec, +} +#[derive(Debug)] +pub(crate) enum AdapterError { + InvalidOptions(String), + Contract(String), + Unavailable(String), + Internal(String), + Io(String), +} + +pub(crate) fn execute( + adapter: &str, + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + for input in verified_inputs { + let _owned_input_contract = ( + input.bytes(), + input.artifact_schema(), + input.artifact_type(), + input.sha256(), + input.consumed_snapshot_identity(), + ); + } + match adapter { + "repository.snapshot.compat" => repository_snapshot(request, verified_inputs, out), + "inventory.rg.compat" => inventory(request, out), + "evidence.native-code.compat" => { + native_code_evidence::execute(request, verified_inputs, out) + } + "project.orientation.compat" => project_orientation::execute(request, verified_inputs, out), + "understanding.quadrant.compat" => { + understanding_quadrant::execute(request, verified_inputs, out) + } + "compatibility.retirement-gate.compat" => { + compatibility_retirement_gate::execute(request, verified_inputs, out) + } + "compatibility.retirement-ticket-template.compat" => { + compatibility_retirement_ticket::execute(request, verified_inputs, out) + } + "project.orientation-benchmark.compat" => { + project_orientation_benchmark::execute(request, verified_inputs, out) + } + "delivery.light-speed-measure.compat" => { + delivery_light_speed::execute(request, verified_inputs, out) + } + "diagnosis.hospital.compat" => hospital_diagnosis::execute(request, verified_inputs, out), + "provider.graph-builtin.compat" => { + builtin_provider_evidence::graph_admission(request, verified_inputs, out) + } + "provider.sentrux-builtin.compat" => { + builtin_provider_evidence::sentrux_admission(request, verified_inputs, out) + } + "doctor.envelope.compat" => doctor_adapter::execute(request, verified_inputs, out), + "advisory.workflow-recommend.compat" => { + workflow_recommendation(request, verified_inputs, out) + } + other => Err(AdapterError::Unavailable(format!( + "runtime adapter is not installed: {other}" + ))), + } +} + +fn workflow_recommendation( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + if !verified_inputs.is_empty() { + return Err(AdapterError::Contract( + "advisory.workflow-recommend does not accept input artifacts".into(), + )); + } + let options = request + .get("options") + .and_then(Value::as_object) + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if options + .keys() + .any(|key| !matches!(key.as_str(), "repoPath" | "auto")) + { + return Err(AdapterError::InvalidOptions( + "advisory.workflow-recommend accepts only repoPath/auto".into(), + )); + } + let repo = options + .get("repoPath") + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(Path::new) + .ok_or_else(|| AdapterError::InvalidOptions("options.repoPath must be non-empty".into()))?; + if !repo.is_dir() { + return Err(AdapterError::InvalidOptions(format!( + "repoPath is not a directory: {}", + repo.display() + ))); + } + let auto = match options.get("auto") { + None => false, + Some(value) => value.as_bool().ok_or_else(|| { + AdapterError::InvalidOptions("options.auto must be boolean when present".into()) + })?, + }; + let script = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("Invoke-WorkflowRecommendation.ps1"); + if !script.is_file() { + return Err(AdapterError::Unavailable(format!( + "workflow recommendation facade is unavailable: {}", + script.display() + ))); + } + let mut command = Command::new("pwsh"); + command + .args(["-NoLogo", "-NoProfile", "-File"]) + .arg(&script) + .arg("-RepoPath") + .arg(repo) + .args(["-Quiet", "-Json"]); + if auto { + command.arg("-Auto"); + } + let output = command + .output() + .map_err(|error| AdapterError::Unavailable(format!("start workflow atom: {error}")))?; + if !output.status.success() { + return Err(AdapterError::Internal(format!( + "workflow atom failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + ))); + } + let proposal: Value = serde_json::from_slice(&output.stdout).map_err(|error| { + AdapterError::Contract(format!( + "workflow atom stdout is not one JSON proposal: {error}" + )) + })?; + validate_workflow_proposal(&proposal)?; + let bytes = serde_json::to_vec(&proposal) + .map_err(|error| AdapterError::Internal(format!("serialize workflow proposal: {error}")))?; + publish_named(out, "workflow-recommendation.json", &bytes, |_| Ok(()))?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-advisory-workflow-recommendation.v1".into(), + artifact_type: "advisory.workflow-recommendation".into(), + relative_path: "workflow-recommendation.json".into(), + bytes, + }], + observed_effects: vec![], + domain_verdict: AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn validate_workflow_proposal(value: &Value) -> Result<(), AdapterError> { + let object = value.as_object().ok_or_else(|| { + AdapterError::Contract("workflow recommendation must be an object".into()) + })?; + let expected = [ + "schema", + "kind", + "recommendation", + "evidence", + "confidence", + "alternatives", + "provenance", + "effects", + ]; + if object.len() != expected.len() || expected.iter().any(|key| !object.contains_key(*key)) { + return Err(AdapterError::Contract( + "workflow recommendation top-level contract is not exact".into(), + )); + } + if value["schema"] != "code-intel-advisory-workflow-recommendation.v1" + || value["kind"] != "proposal" + || !matches!( + value["confidence"].as_str(), + Some("low" | "medium" | "high") + ) + || value["evidence"].as_array().map_or(true, Vec::is_empty) + || value["alternatives"] + .as_array() + .map_or(true, |items| items.len() < 3) + || value["effects"] + .as_array() + .map_or(true, |items| !items.is_empty()) + || value + .pointer("/provenance/capabilityId") + .and_then(Value::as_str) + != Some("advisory.workflow-recommend") + { + return Err(AdapterError::Contract( + "workflow recommendation violates the advisory proposal boundary".into(), + )); + } + Ok(()) +} + +fn repository_snapshot( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + if !verified_inputs.is_empty() { + return Err(AdapterError::Contract( + "repository.snapshot does not accept input artifacts".into(), + )); + } + let options = request + .get("options") + .and_then(Value::as_object) + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if options.len() != 1 || !options.contains_key("repoPath") { + return Err(AdapterError::InvalidOptions( + "repository.snapshot accepts only options.repoPath".into(), + )); + } + let repo = options["repoPath"] + .as_str() + .filter(|value| !value.is_empty()) + .map(Path::new) + .ok_or_else(|| AdapterError::InvalidOptions("options.repoPath must be non-empty".into()))?; + let document = snapshot::build_for_capability(repo, &request["snapshot"]) + .map_err(snapshot_adapter_error)?; + let bytes = serde_json::to_vec(&document) + .map_err(|error| AdapterError::Internal(format!("serialize snapshot: {error}")))?; + publish_named(out, "snapshot.json", &bytes, |_| Ok(()))?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-repository-snapshot.v1".into(), + artifact_type: "repository.snapshot".into(), + relative_path: "snapshot.json".into(), + bytes, + }], + observed_effects: vec!["repo_read".into(), "local_write".into()], + domain_verdict: AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn inventory(request: &Value, out: &Path) -> Result { + let options = request + .get("options") + .and_then(Value::as_object) + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if options + .keys() + .any(|k| !matches!(k.as_str(), "repoPath" | "inventoryExclude")) + { + return Err(AdapterError::InvalidOptions( + "inventory.rg accepts only repoPath/inventoryExclude; --out is the only write boundary" + .into(), + )); + } + let repo = options + .get("repoPath") + .and_then(Value::as_str) + .filter(|v| !v.is_empty()) + .map(Path::new) + .ok_or_else(|| AdapterError::InvalidOptions("options.repoPath must be non-empty".into()))?; + if !repo.is_dir() { + return Err(AdapterError::InvalidOptions(format!( + "repoPath is not a directory: {}", + repo.display() + ))); + } + let lease = + snapshot::begin_consumption(repo, &request["snapshot"]).map_err(snapshot_adapter_error)?; + let rg = if cfg!(windows) { "rg.exe" } else { "rg" }; + let mut baseline_globs = EXCLUDES + .iter() + .map(|value| value.to_string()) + .collect::>(); + baseline_globs.extend( + lease + .inventory_gitlink_paths() + .iter() + .map(|path| gitlink_exclude_glob(path)), + ); + let mut glob_patterns = baseline_globs.clone(); + if let Some(extra) = options.get("inventoryExclude") { + let list = extra.as_array().ok_or_else(|| { + AdapterError::InvalidOptions( + "inventoryExclude must be unique non-empty glob strings".into(), + ) + })?; + let mut seen = std::collections::BTreeSet::new(); + for p in list { + let p = p.as_str().filter(|v| !v.is_empty()).ok_or_else(|| { + AdapterError::InvalidOptions( + "inventoryExclude must be unique non-empty glob strings".into(), + ) + })?; + if !seen.insert(p) { + return Err(AdapterError::InvalidOptions( + "duplicate inventoryExclude".into(), + )); + } + glob_patterns.push(p.to_string()); + } + } + let mut actual_baseline = run_rg_files( + rg, + repo, + lease.scopes(), + &baseline_globs, + RgIgnoreMode::SnapshotControls, + )?; + #[cfg(debug_assertions)] + if let Ok(extra) = std::env::var("CODE_INTEL_TEST_RG_EXTRA_PATH") { + actual_baseline.insert(normalize_inventory_path(&extra)); + } + let (expected_baseline, filtered) = mirror_path_sets( + rg, + lease.scopes(), + &baseline_globs, + &glob_patterns, + &lease.inventory_mirror_files(), + )?; + verify_inventory_path_sets(&actual_baseline, &expected_baseline)?; + lease.verify_after(repo).map_err(snapshot_adapter_error)?; + let records = filtered + .into_iter() + .map(String::into_bytes) + .collect::>(); + let bytes = join_records(&records, if cfg!(windows) { b'\n' } else { 0 }); + publish(out, &bytes, |_| Ok(()))?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-file-inventory.v1".into(), + artifact_type: "inventory.files".into(), + relative_path: "files.txt".into(), + bytes, + }], + observed_effects: vec!["repo_read".into(), "local_write".into()], + domain_verdict: AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn verify_inventory_path_sets( + actual: &BTreeSet, + expected: &BTreeSet, +) -> Result<(), AdapterError> { + let extra_count = actual.difference(expected).count(); + // The snapshot mirror owns frozen ignore-control semantics. The live view may + // omit snapshot-bound paths when current ignore bytes differ, but it must + // never introduce a path that the snapshot did not bind. + if extra_count == 0 { + return Ok(()); + } + const DIAGNOSTIC_SAMPLE_LIMIT: usize = 32; + let extra = actual + .difference(expected) + .take(DIAGNOSTIC_SAMPLE_LIMIT) + .cloned() + .collect::>(); + let missing_count = expected.difference(actual).count(); + let missing = expected + .difference(actual) + .take(DIAGNOSTIC_SAMPLE_LIMIT) + .cloned() + .collect::>(); + Err(AdapterError::Contract(format!( + "inventory baseline path set differs from snapshot manifest; extra_count={extra_count}; extra_samples={extra:?}; missing_count={missing_count}; missing_samples={missing:?}" + ))) +} + +fn gitlink_exclude_glob(path: &str) -> String { + let mut pattern = String::with_capacity(path.len() + 5); + pattern.push('!'); + for character in path.chars() { + if matches!(character, '\\' | '*' | '?' | '[' | ']' | '{' | '}' | '!') { + pattern.push('\\'); + } + pattern.push(character); + } + pattern.push_str("/**"); + pattern +} + +#[derive(Clone, Copy)] +enum RgIgnoreMode { + Disabled, + SnapshotControls, +} + +fn run_rg_files( + rg: &str, + cwd: &Path, + scopes: &[String], + glob_patterns: &[String], + ignore_mode: RgIgnoreMode, +) -> Result, AdapterError> { + let mut command = Command::new(rg); + command.args(["--files", "--hidden", "--null", "--no-require-git"]); + match ignore_mode { + RgIgnoreMode::Disabled => { + command.arg("--no-ignore"); + } + RgIgnoreMode::SnapshotControls => { + command.args([ + "--no-ignore-parent", + "--no-ignore-global", + "--no-ignore-exclude", + ]); + } + } + for pattern in glob_patterns { + command.args(["-g", pattern]); + } + let output = command + .env_remove("RIPGREP_CONFIG_PATH") + .current_dir(cwd) + .args(scopes) + .output() + .map_err(|error| AdapterError::Unavailable(format!("cannot launch {rg}: {error}")))?; + let empty = + output.status.code() == Some(1) && output.stdout.is_empty() && output.stderr.is_empty(); + if !output.status.success() && !empty { + return Err(AdapterError::Internal(format!( + "rg failed {:?}: {}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ))); + } + output + .stdout + .split(|byte| *byte == 0) + .filter(|path| !path.is_empty()) + .map(|value| { + String::from_utf8(value.to_vec()) + .map(|path| normalize_inventory_path(&path)) + .map_err(|error| AdapterError::Contract(format!("rg path is not UTF-8: {error}"))) + }) + .collect() +} + +fn mirror_path_sets( + rg: &str, + scopes: &[String], + baseline_globs: &[String], + glob_patterns: &[String], + manifest_paths: &BTreeMap>>, +) -> Result<(BTreeSet, BTreeSet), AdapterError> { + let mut mirror = InventoryMirror::create(manifest_paths)?; + let result = run_rg_files( + rg, + mirror.root(), + scopes, + baseline_globs, + RgIgnoreMode::Disabled, + ) + .and_then(|baseline| { + run_rg_files( + rg, + mirror.root(), + scopes, + glob_patterns, + RgIgnoreMode::SnapshotControls, + ) + .map(|filtered| (baseline, filtered)) + }); + let cleanup = mirror.cleanup(); + match (result, cleanup) { + (Ok(paths), Ok(())) => Ok(paths), + (Err(error), Ok(())) => Err(error), + (Ok(_), Err(cleanup)) => Err(AdapterError::Io(format!( + "inventory manifest mirror cleanup failed: {cleanup}" + ))), + (Err(_), Err(cleanup)) => Err(AdapterError::Io(format!( + "inventory manifest mirror failed and cleanup failed: {cleanup}" + ))), + } +} + +struct MirrorNode { + path: PathBuf, + dir: bool, + id: StableId, +} + +struct InventoryMirror { + root: PathBuf, + nodes: Vec, +} + +impl InventoryMirror { + fn create(paths: &BTreeMap>>) -> Result { + let mut mirror = Self::create_root()?; + let mut directories = BTreeSet::new(); + for path in paths.keys() { + let mut parent = Path::new(path).parent(); + while let Some(value) = parent { + if value.as_os_str().is_empty() { + break; + } + directories.insert(value.to_path_buf()); + parent = value.parent(); + } + } + let mut directories = directories.into_iter().collect::>(); + directories.sort_by(|left, right| { + left.components() + .count() + .cmp(&right.components().count()) + .then_with(|| left.cmp(right)) + }); + for relative in directories { + mirror.create_directory(&relative)?; + } + for (relative, bytes) in paths { + mirror.create_file(Path::new(relative), bytes.as_deref().unwrap_or_default())?; + } + Ok(mirror) + } + + fn create_root() -> Result { + let epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| AdapterError::Io(format!("read clock for mirror name: {error}")))? + .as_nanos(); + for _ in 0..64 { + let nonce = MIRROR_NONCE.fetch_add(1, Ordering::Relaxed); + let root = std::env::temp_dir().join(format!( + "code-intel-inventory-mirror-{}-{epoch}-{nonce}", + std::process::id() + )); + match fs::create_dir(&root) { + Ok(()) => { + let opened = open_plain(&root, true).map_err(AdapterError::Io)?; + let id = opened.id; + drop(opened); + return Ok(Self { + root: root.clone(), + nodes: vec![MirrorNode { + path: root, + dir: true, + id, + }], + }); + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => { + return Err(AdapterError::Io(format!( + "exclusive inventory mirror create failed: {error}" + ))) + } + } + } + Err(AdapterError::Io( + "exclusive inventory mirror name space exhausted".into(), + )) + } + + fn root(&self) -> &Path { + &self.root + } + + fn create_directory(&mut self, relative: &Path) -> Result<(), AdapterError> { + let path = self.root.join(relative); + fs::create_dir(&path).map_err(|error| { + AdapterError::Io(format!("create inventory mirror directory: {error}")) + })?; + let opened = open_plain(&path, true).map_err(AdapterError::Io)?; + self.nodes.push(MirrorNode { + path, + dir: true, + id: opened.id, + }); + Ok(()) + } + + fn create_file(&mut self, relative: &Path, bytes: &[u8]) -> Result<(), AdapterError> { + let path = self.root.join(relative); + let mut file = create_temp(&path) + .map_err(|error| AdapterError::Io(format!("create inventory mirror file: {error}")))?; + file.write_all(bytes).map_err(|error| { + AdapterError::Io(format!("write inventory mirror control file: {error}")) + })?; + let id = stable_id(&file).map_err(|error| { + AdapterError::Io(format!("read inventory mirror file identity: {error}")) + })?; + drop(file); + self.nodes.push(MirrorNode { + path, + dir: false, + id, + }); + Ok(()) + } + + fn cleanup(&mut self) -> Result<(), String> { + let mut failures = Vec::new(); + while let Some(node) = self.nodes.pop() { + match open_if_stable_id(&node.path, node.dir, node.id) { + Some(opened) => { + if let Err(error) = remove_owned(&node.path, &opened, node.dir) { + failures.push(error); + } + drop(opened); + } + None => failures.push(format!( + "mirror identity changed; preserved {}", + node.path.display() + )), + } + } + if failures.is_empty() { + Ok(()) + } else { + Err(failures.join("; ")) + } + } +} + +impl Drop for InventoryMirror { + fn drop(&mut self) { + let _ = self.cleanup(); + } +} + +fn normalize_inventory_path(path: &str) -> String { + let normalized = path.replace('\\', "/"); + normalized + .strip_prefix("./") + .unwrap_or(&normalized) + .to_string() +} + +fn snapshot_adapter_error(message: String) -> AdapterError { + if message.contains("cannot launch Git") || message.contains("cannot launch rg") { + AdapterError::Unavailable(message) + } else { + AdapterError::Contract(message) + } +} + +fn join_records(records: &[Vec], sep: u8) -> Vec { + let mut out = Vec::new(); + for r in records { + out.extend_from_slice(r); + out.push(sep) + } + out +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct StableId { + volume: u64, + file: u64, +} + +struct Opened { + handle: File, + id: StableId, +} + +fn open_plain(path: &Path, dir: bool) -> Result { + let before = fs::symlink_metadata(path) + .map_err(|e| format!("inspect {} before open: {e}", path.display()))?; + if (dir && !before.is_dir()) || (!dir && !before.is_file()) || reparse(&before) { + return Err(format!( + "not a plain no-follow filesystem object: {}", + path.display() + )); + } + let handle = open_handle(path, dir, true) + .map_err(|e| format!("open stable handle {}: {e}", path.display()))?; + let metadata = handle + .metadata() + .map_err(|e| format!("inspect opened handle {}: {e}", path.display()))?; + if (dir && !metadata.is_dir()) || (!dir && !metadata.is_file()) || reparse(&metadata) { + return Err(format!( + "opened object is not a plain no-follow filesystem object: {}", + path.display() + )); + } + let id = + stable_id(&handle).map_err(|e| format!("read stable identity {}: {e}", path.display()))?; + let after = fs::symlink_metadata(path) + .map_err(|e| format!("inspect {} after open: {e}", path.display()))?; + if reparse(&after) || !path_metadata_matches(&after, id) { + return Err(format!( + "path identity changed while opening: {}", + path.display() + )); + } + #[cfg(windows)] + { + let confirmation = open_handle(path, dir, false) + .and_then(|file| stable_id(&file)) + .map_err(|e| format!("confirm stable identity {}: {e}", path.display()))?; + if confirmation != id { + return Err(format!( + "path identity changed while opening: {}", + path.display() + )); + } + } + Ok(Opened { handle, id }) +} + +fn open_if_stable_id(path: &Path, dir: bool, expected: StableId) -> Option { + let handle = open_handle(path, dir, true).ok()?; + let metadata = handle.metadata().ok()?; + if (dir && !metadata.is_dir()) || (!dir && !metadata.is_file()) || reparse(&metadata) { + return None; + } + let id = stable_id(&handle).ok()?; + (id == expected).then_some(Opened { handle, id }) +} + +#[cfg(unix)] +fn open_handle(path: &Path, _dir: bool, _delete_access: bool) -> std::io::Result { + File::open(path) +} + +#[cfg(windows)] +fn open_handle(path: &Path, dir: bool, delete_access: bool) -> std::io::Result { + use std::os::windows::fs::OpenOptionsExt; + const FILE_READ_ATTRIBUTES: u32 = 0x80; + const DELETE: u32 = 0x0001_0000; + const SHARE_READ: u32 = 1; + const SHARE_WRITE: u32 = 2; + const OPEN_REPARSE_POINT: u32 = 0x0020_0000; + const BACKUP_SEMANTICS: u32 = 0x0200_0000; + let mut options = OpenOptions::new(); + options + .access_mode(FILE_READ_ATTRIBUTES | if delete_access { DELETE } else { 0 }) + .share_mode(SHARE_READ | SHARE_WRITE | 4) + .custom_flags(OPEN_REPARSE_POINT | if dir { BACKUP_SEMANTICS } else { 0 }); + options.open(path) +} + +#[cfg(not(any(unix, windows)))] +fn open_handle(path: &Path, _dir: bool, _delete_access: bool) -> std::io::Result { + File::open(path) +} + +#[cfg(unix)] +fn stable_id(file: &File) -> std::io::Result { + use std::os::unix::fs::MetadataExt; + let metadata = file.metadata()?; + Ok(StableId { + volume: metadata.dev(), + file: metadata.ino(), + }) +} + +#[cfg(unix)] +fn path_metadata_matches(metadata: &fs::Metadata, expected: StableId) -> bool { + use std::os::unix::fs::MetadataExt; + StableId { + volume: metadata.dev(), + file: metadata.ino(), + } == expected +} + +#[cfg(windows)] +fn stable_id(file: &File) -> std::io::Result { + use std::ffi::c_void; + use std::mem::MaybeUninit; + use std::os::windows::io::AsRawHandle; + + #[repr(C)] + #[allow(non_snake_case)] + struct FileTime { + dwLowDateTime: u32, + dwHighDateTime: u32, + } + #[repr(C)] + #[allow(non_snake_case)] + struct ByHandleFileInformation { + dwFileAttributes: u32, + ftCreationTime: FileTime, + ftLastAccessTime: FileTime, + ftLastWriteTime: FileTime, + dwVolumeSerialNumber: u32, + nFileSizeHigh: u32, + nFileSizeLow: u32, + nNumberOfLinks: u32, + nFileIndexHigh: u32, + nFileIndexLow: u32, + } + unsafe extern "system" { + fn GetFileInformationByHandle( + handle: *mut c_void, + information: *mut ByHandleFileInformation, + ) -> i32; + } + let mut information = MaybeUninit::::uninit(); + let ok = unsafe { + GetFileInformationByHandle(file.as_raw_handle().cast(), information.as_mut_ptr()) + }; + if ok == 0 { + return Err(std::io::Error::last_os_error()); + } + let information = unsafe { information.assume_init() }; + Ok(StableId { + volume: information.dwVolumeSerialNumber as u64, + file: ((information.nFileIndexHigh as u64) << 32) | information.nFileIndexLow as u64, + }) +} + +#[cfg(windows)] +fn path_metadata_matches(_metadata: &fs::Metadata, _expected: StableId) -> bool { + true +} + +#[cfg(not(any(unix, windows)))] +fn stable_id(file: &File) -> std::io::Result { + Ok(StableId { + volume: 0, + file: file.metadata()?.len(), + }) +} + +#[cfg(not(any(unix, windows)))] +fn path_metadata_matches(metadata: &fs::Metadata, expected: StableId) -> bool { + StableId { + volume: 0, + file: metadata.len(), + } == expected +} +fn reparse(m: &fs::Metadata) -> bool { + if m.file_type().is_symlink() { + return true; + } + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + m.file_attributes() & 0x400 != 0 + } + #[cfg(not(windows))] + { + false + } +} +fn remove_owned(path: &Path, opened: &Opened, dir: bool) -> Result<(), String> { + #[cfg(windows)] + { + let _ = dir; + use std::ffi::c_void; + use std::os::windows::io::AsRawHandle; + #[repr(C)] + struct FileDispositionInformation { + delete_file: u8, + } + unsafe extern "system" { + fn SetFileInformationByHandle( + handle: *mut c_void, + class: i32, + information: *const FileDispositionInformation, + size: u32, + ) -> i32; + } + const FILE_DISPOSITION_INFO: i32 = 4; + let information = FileDispositionInformation { delete_file: 1 }; + let ok = unsafe { + SetFileInformationByHandle( + opened.handle.as_raw_handle().cast(), + FILE_DISPOSITION_INFO, + &information, + std::mem::size_of::() as u32, + ) + }; + if ok == 0 { + return Err(format!( + "remove owned handle {}: {}", + path.display(), + std::io::Error::last_os_error() + )); + } + Ok(()) + } + #[cfg(not(windows))] + { + let current = open_plain(path, dir)?; + if current.id != opened.id { + return Err(format!("identity changed; preserved {}", path.display())); + } + if dir { + fs::remove_dir(path) + } else { + fs::remove_file(path) + } + .map_err(|e| format!("remove owned object {}: {e}", path.display())) + } +} + +fn cleanup( + out: &Path, + dir: Opened, + temp: Option<(&Path, Opened)>, + final_file: Option<(&Path, Opened)>, +) -> String { + let mut notes = Vec::new(); + if let Some((path, opened)) = final_file { + if let Err(e) = remove_owned(path, &opened, false) { + notes.push(e); + } + drop(opened); + } + if let Some((path, opened)) = temp { + if let Err(e) = remove_owned(path, &opened, false) { + notes.push(e); + } + drop(opened); + } + if let Err(e) = remove_owned(out, &dir, true) { + notes.push(e); + } + drop(dir); + if notes.is_empty() { + "; cleanup: completed".into() + } else { + format!("; cleanup: {}", notes.join("; ")) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PublishPoint { + BeforeLink, + AfterLinkValidated, +} + +fn create_temp(path: &Path) -> std::io::Result { + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + const GENERIC_WRITE: u32 = 0x4000_0000; + const FILE_READ_ATTRIBUTES: u32 = 0x80; + const DELETE: u32 = 0x0001_0000; + options + .access_mode(GENERIC_WRITE | FILE_READ_ATTRIBUTES | DELETE) + .share_mode(1 | 2 | 4); + } + options.open(path) +} + +fn publish(out: &Path, bytes: &[u8], hook: F) -> Result<(), AdapterError> +where + F: FnMut(PublishPoint) -> Result<(), String>, +{ + publish_named(out, "files.txt", bytes, hook) +} + +fn publish_named( + out: &Path, + file_name: &str, + bytes: &[u8], + mut hook: F, +) -> Result<(), AdapterError> +where + F: FnMut(PublishPoint) -> Result<(), String>, +{ + fs::create_dir(out) + .map_err(|e| AdapterError::Io(format!("exclusive output create {}: {e}", out.display())))?; + let dir = open_plain(out, true).map_err(|e| { + AdapterError::Io(format!( + "output identity unavailable: {e}; cleanup: preserved object because ownership could not be verified" + )) + })?; + let temp = out.join(format!("{file_name}.partial")); + let final_path = out.join(file_name); + let mut file = match create_temp(&temp) { + Ok(file) => file, + Err(e) => { + let detail = cleanup(out, dir, None, None); + return Err(AdapterError::Io(format!( + "exclusive temp create: {e}{detail}" + ))); + } + }; + if let Err(e) = file.write_all(bytes).and_then(|_| file.sync_all()) { + let temp_opened = stable_id(&file).ok().map(|id| Opened { handle: file, id }); + let detail = cleanup( + out, + dir, + temp_opened.map(|opened| (temp.as_path(), opened)), + None, + ); + return Err(AdapterError::Io(format!("temp write: {e}{detail}"))); + } + let temp_id = match stable_id(&file) { + Ok(identity) => identity, + Err(e) => { + drop(file); + let detail = cleanup(out, dir, None, None); + return Err(AdapterError::Io(format!( + "temp identity unavailable: {e}{detail}" + ))); + } + }; + let temp_opened = Opened { + handle: file, + id: temp_id, + }; + if let Err(e) = hook(PublishPoint::BeforeLink) { + let detail = cleanup(out, dir, Some((temp.as_path(), temp_opened)), None); + return Err(AdapterError::Io(format!("pre-link fault: {e}{detail}"))); + } + if !open_plain(out, true).is_ok_and(|actual| actual.id == dir.id) + || !open_plain(&temp, false).is_ok_and(|actual| actual.id == temp_opened.id) + { + let detail = cleanup(out, dir, Some((temp.as_path(), temp_opened)), None); + return Err(AdapterError::Io(format!( + "directory identity changed{detail}" + ))); + } + if let Err(e) = fs::hard_link(&temp, &final_path) { + let detail = cleanup(out, dir, Some((temp.as_path(), temp_opened)), None); + return Err(AdapterError::Io(format!("exclusive publish: {e}{detail}"))); + } + let final_opened = match open_plain(&final_path, false) { + Ok(opened) => opened, + Err(e) => { + let owned_final = open_if_stable_id(&final_path, false, temp_opened.id) + .map(|opened| (final_path.as_path(), opened)); + let final_note = if owned_final.is_some() { + "stable final recovered for owned rollback" + } else { + "final path is absent or no longer has the owned stable identity; preserved" + }; + let detail = cleanup(out, dir, Some((temp.as_path(), temp_opened)), owned_final); + return Err(AdapterError::Io(format!( + "published identity unavailable: {e}; {final_note}{detail}" + ))); + } + }; + if final_opened.id != temp_opened.id + || !open_plain(out, true).is_ok_and(|actual| actual.id == dir.id) + { + let detail = cleanup( + out, + dir, + Some((temp.as_path(), temp_opened)), + Some((final_path.as_path(), final_opened)), + ); + return Err(AdapterError::Io(format!( + "published identity mismatch{detail}" + ))); + } + if let Err(e) = hook(PublishPoint::AfterLinkValidated) { + let detail = cleanup( + out, + dir, + Some((temp.as_path(), temp_opened)), + Some((final_path.as_path(), final_opened)), + ); + return Err(AdapterError::Io(format!("post-link fault: {e}{detail}"))); + } + if let Err(e) = remove_owned(&temp, &temp_opened, false) { + let detail = cleanup( + out, + dir, + Some((temp.as_path(), temp_opened)), + Some((final_path.as_path(), final_opened)), + ); + return Err(AdapterError::Io(format!( + "published temp cleanup failed: {e}{detail}" + ))); + } + drop(temp_opened); + drop(final_opened); + drop(dir); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn nul_serialization_preserves_embedded_newlines() { + assert_eq!(join_records(&[b"a\nb".to_vec()], 0), b"a\nb\0"); + } + + #[test] + fn gitlink_exclusion_escapes_every_ripgrep_glob_metacharacter() { + assert_eq!( + gitlink_exclude_glob(r"vendor/sub[abc]*?{x}!\tail"), + r"!vendor/sub\[abc\]\*\?\{x\}\!\\tail/**" + ); + } + + #[test] + fn mirror_cleanup_removes_owned_tree_after_rg_failure() { + let paths = BTreeMap::from([ + ("README.md".to_string(), None), + ("nested/子/file.rs".to_string(), None), + ]); + let mut mirror = match InventoryMirror::create(&paths) { + Ok(mirror) => mirror, + Err(_) => panic!("create inventory mirror"), + }; + let root = mirror.root().to_path_buf(); + let failed = run_rg_files( + "__code_intel_missing_rg_binary__", + mirror.root(), + &[".".to_string()], + &[], + RgIgnoreMode::Disabled, + ); + assert!(matches!(failed, Err(AdapterError::Unavailable(_)))); + mirror.cleanup().unwrap(); + assert!(!root.exists(), "failed mirror rg must leave no temp tree"); + } + + #[test] + fn publish_collision_preserves_competitor_and_removes_owned_temp() { + let n = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let out = std::env::temp_dir().join(format!("code-intel-publish-{n}")); + let final_path = out.join("files.txt"); + let result = publish(&out, b"x", |point| { + if point == PublishPoint::BeforeLink { + fs::create_dir(&final_path).unwrap(); + } + Ok(()) + }); + let msg = match result { + Err(AdapterError::Io(v)) => v, + _ => panic!(), + }; + assert!(msg.contains("cleanup:")); + assert!(!out.join("files.txt.partial").exists()); + assert!(final_path.is_dir()); + fs::remove_dir_all(out).unwrap(); + } + + #[test] + fn post_link_failure_rolls_back_final_temp_and_directory() { + let n = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let out = std::env::temp_dir().join(format!("code-intel-post-link-{n}")); + let result = publish(&out, b"x", |point| { + if point == PublishPoint::AfterLinkValidated { + Err("injected".into()) + } else { + Ok(()) + } + }); + let message = match result { + Err(AdapterError::Io(message)) => message, + _ => panic!(), + }; + assert!(message.contains("cleanup: completed"), "{message}"); + assert!( + !out.exists(), + "failed publication must leave no artifact tree" + ); + } + + #[cfg(windows)] + #[test] + fn stable_file_id_rejects_same_size_and_mtime_path_replacement() { + let n = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let out = std::env::temp_dir().join(format!("code-intel-replace-{n}")); + let temp = out.join("files.txt.partial"); + let result = publish(&out, b"owned", |point| { + if point == PublishPoint::BeforeLink { + let modified = fs::metadata(&temp).unwrap().modified().unwrap(); + fs::remove_file(&temp).unwrap(); + fs::write(&temp, b"forge").unwrap(); + let replacement = OpenOptions::new().write(true).open(&temp).unwrap(); + replacement + .set_times(std::fs::FileTimes::new().set_modified(modified)) + .unwrap(); + } + Ok(()) + }); + assert!(matches!(result, Err(AdapterError::Io(_)))); + assert!(!out.join("files.txt").exists()); + assert_eq!(fs::read(&temp).unwrap(), b"forge"); + fs::remove_dir_all(out).unwrap(); + } +} diff --git a/crates/code-intel-cli/src/change_impact.rs b/crates/code-intel-cli/src/change_impact.rs new file mode 100644 index 0000000..91aa9bd --- /dev/null +++ b/crates/code-intel-cli/src/change_impact.rs @@ -0,0 +1,449 @@ +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::path::PathBuf; + +use serde_json::{json, Value}; + +use crate::committed_evidence::{self, EvidenceError}; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match Cli::parse(raw).and_then(execute) { + Ok(result) => { + println!("{}", serde_json::to_string(&result).unwrap()); + 0 + } + Err(ImpactError::Contract(message)) => { + eprintln!("{message}"); + 65 + } + Err(ImpactError::HostIo(message)) => { + eprintln!("{message}"); + 74 + } + } +} + +struct Cli { + artifact_root: PathBuf, + repo: String, + repo_path: PathBuf, + changed: Vec, +} + +impl Cli { + fn parse(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("impact") { + return Err(ImpactError::Contract("usage: change impact --artifact-root --repo --repo-path --changed [--changed ]...".into())); + } + let mut artifact_root = None; + let mut repo = None; + let mut repo_path = None; + let mut changed = Vec::new(); + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--artifact-root" | "--repo" | "--repo-path" | "--changed" + ) { + return Err(ImpactError::Contract(format!( + "unknown change impact argument: {flag}" + ))); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| ImpactError::Contract(format!("{flag} requires one value")))?; + match flag { + "--artifact-root" => { + set_once(&mut artifact_root, PathBuf::from(value), "--artifact-root")? + } + "--repo" => set_once(&mut repo, value.clone(), "--repo")?, + "--repo-path" => set_once(&mut repo_path, PathBuf::from(value), "--repo-path")?, + "--changed" => changed.push(normalize_relative(value)?), + _ => unreachable!(), + } + index += 2; + } + let artifact_root = artifact_root + .ok_or_else(|| ImpactError::Contract("--artifact-root is required".into()))?; + let repo_path = + repo_path.ok_or_else(|| ImpactError::Contract("--repo-path is required".into()))?; + if !artifact_root.is_dir() || !repo_path.is_dir() { + return Err(ImpactError::Contract( + "artifact root and repository path must be existing directories".into(), + )); + } + changed.sort(); + changed.dedup(); + if changed.is_empty() { + return Err(ImpactError::Contract( + "at least one --changed path is required".into(), + )); + } + Ok(Self { + artifact_root, + repo: repo.ok_or_else(|| ImpactError::Contract("--repo is required".into()))?, + repo_path, + changed, + }) + } +} + +fn set_once(slot: &mut Option, value: T, flag: &str) -> Result<(), ImpactError> { + if slot.replace(value).is_some() { + Err(ImpactError::Contract(format!("duplicate {flag}"))) + } else { + Ok(()) + } +} + +fn execute(cli: Cli) -> Result { + let evidence = committed_evidence::load(&cli.artifact_root, &cli.repo).map_err(map_evidence)?; + let run_outcome = evidence.entry["outcome"] + .as_str() + .expect("A08 entry outcome"); + if run_outcome != "completed" { + return Err(ImpactError::Contract(format!( + "change impact requires a completed authoritative run; latest committed run outcome is {run_outcome}" + ))); + } + let freshness = evidence + .freshness(Some(&cli.repo_path)) + .map_err(map_evidence)?; + if freshness["status"] != "current" { + return Err(ImpactError::Contract(format!( + "change impact requires the committed snapshot to be current; recorded={} current={}", + freshness["recordedIdentity"].as_str().unwrap_or("unknown"), + freshness["currentIdentity"].as_str().unwrap_or("unknown") + ))); + } + let (files_ref, files_artifact) = evidence + .artifact("code_evidence.files") + .ok_or_else(|| ImpactError::Contract("committed run lacks code_evidence.files".into()))?; + let (imports_ref, imports_artifact) = evidence + .artifact("code_evidence.imports") + .ok_or_else(|| ImpactError::Contract("committed run lacks code_evidence.imports".into()))?; + let files_json: Value = serde_json::from_slice(files_artifact.bytes()) + .map_err(|_| ImpactError::Contract("code_evidence.files is invalid JSON".into()))?; + let imports_json: Value = serde_json::from_slice(imports_artifact.bytes()) + .map_err(|_| ImpactError::Contract("code_evidence.imports is invalid JSON".into()))?; + let files = files_json["files"] + .as_array() + .expect("registered native files artifact"); + let file_paths = files + .iter() + .map(|file| file["path"].as_str().unwrap().to_string()) + .collect::>(); + let imports = imports_json["imports"] + .as_array() + .expect("registered native imports artifact"); + let (reverse, resolved_edges, unresolved_edges) = reverse_import_graph(imports, &file_paths); + let impacted = impacted_files(&cli.changed, &file_paths, &reverse); + let test_files = select_tests(&impacted, &cli.changed, &file_paths); + let commands = test_commands(&test_files); + let changed = cli + .changed + .iter() + .map(|path| json!({"path":path,"inInventory":file_paths.contains(path)})) + .collect::>(); + let impact_rows = impacted + .iter() + .map(|(path, reason)| { + json!({ + "path":path, + "distance":reason.distance, + "reason":reason.reason, + "via":reason.via, + "confidence":reason.confidence, + }) + }) + .collect::>(); + Ok(json!({ + "schema":"code-intel-change-impact.v1", + "repo":cli.repo, + "run":evidence.entry["run"], + "runIdentity":evidence.entry["runIdentity"], + "runOutcome":run_outcome, + "snapshotIdentity":evidence.snapshot_identity(), + "freshness":freshness, + "changed":changed, + "evidenceRefs":[files_ref,imports_ref], + "impact":{ + "files":impact_rows, + "resolvedImportEdges":resolved_edges, + "unresolvedImportEdges":unresolved_edges, + }, + "testSelection":{ + "status":if test_files.is_empty() { "none" } else { "candidates" }, + "files":test_files, + "commands":commands, + "advisoryOnly":true, + "rationale":"Select impacted test files reachable through the verified snapshot's reverse import graph; use same-module test co-location only as a fallback.", + }, + "limitations":[ + "Native import extraction is heuristic and does not prove runtime call paths.", + "Dynamic imports, generated code, reflection, build-system edges, and external packages may be unresolved.", + "Test commands are candidates only and are never executed by this command." + ] + })) +} + +#[derive(Clone)] +struct ReverseEdge { + importer: String, + confidence: &'static str, +} + +fn reverse_import_graph( + imports: &[Value], + files: &BTreeSet, +) -> (BTreeMap>, usize, usize) { + let mut reverse: BTreeMap> = BTreeMap::new(); + let mut resolved = 0; + let mut unresolved = 0; + for import in imports { + let importer = import["file"].as_str().unwrap(); + let target = import["target"].as_str().unwrap(); + if let Some((target, confidence)) = resolve_import(importer, target, files) { + reverse.entry(target).or_default().push(ReverseEdge { + importer: importer.to_string(), + confidence, + }); + resolved += 1; + } else { + unresolved += 1; + } + } + for edges in reverse.values_mut() { + edges.sort_by(|left, right| left.importer.cmp(&right.importer)); + edges.dedup_by(|left, right| left.importer == right.importer); + } + (reverse, resolved, unresolved) +} + +fn resolve_import( + importer: &str, + target: &str, + files: &BTreeSet, +) -> Option<(String, &'static str)> { + let target = target.replace('\\', "/"); + let mut candidates = Vec::new(); + if target.starts_with('.') { + let parent = importer.rsplit_once('/').map(|pair| pair.0).unwrap_or(""); + candidates.push(join_relative(parent, &target)?); + } else if let Some(rest) = target.strip_prefix("crate::") { + candidates.push(format!("src/{}", rest.replace("::", "/"))); + } else { + candidates.push(target.replace("::", "/").replace('.', "/")); + } + for base in &candidates { + for candidate in path_candidates(base) { + if files.contains(&candidate) { + return Some((candidate, "high")); + } + } + } + let token = candidates.last()?.trim_matches('/'); + let suffixes = files + .iter() + .filter(|path| { + let without_extension = path.rsplit_once('.').map(|pair| pair.0).unwrap_or(path); + without_extension == token || without_extension.ends_with(&format!("/{token}")) + }) + .cloned() + .collect::>(); + match suffixes.as_slice() { + [only] => Some((only.clone(), "medium")), + _ => None, + } +} + +fn join_relative(parent: &str, target: &str) -> Option { + let mut components = parent + .split('/') + .filter(|component| !component.is_empty()) + .map(str::to_string) + .collect::>(); + for component in target.split('/') { + match component { + "" | "." => {} + ".." => { + components.pop()?; + } + value => components.push(value.to_string()), + } + } + Some(components.join("/")) +} + +fn path_candidates(base: &str) -> Vec { + let mut values = vec![base.to_string()]; + if base + .rsplit('/') + .next() + .is_some_and(|name| !name.contains('.')) + { + for extension in ["rs", "py", "js", "jsx", "ts", "tsx", "go", "java"] { + values.push(format!("{base}.{extension}")); + values.push(format!("{base}/index.{extension}")); + } + values.push(format!("{base}/mod.rs")); + values.push(format!("{base}/__init__.py")); + } + values +} + +struct ImpactReason { + distance: usize, + reason: &'static str, + via: Option, + confidence: &'static str, +} + +fn impacted_files( + changed: &[String], + files: &BTreeSet, + reverse: &BTreeMap>, +) -> BTreeMap { + let mut impacted = BTreeMap::new(); + let mut queue = VecDeque::new(); + for path in changed { + if files.contains(path) { + impacted.insert( + path.clone(), + ImpactReason { + distance: 0, + reason: "changed", + via: None, + confidence: "high", + }, + ); + queue.push_back(path.clone()); + } + } + while let Some(target) = queue.pop_front() { + let distance = impacted[&target].distance + 1; + for edge in reverse.get(&target).into_iter().flatten() { + if impacted.contains_key(&edge.importer) { + continue; + } + impacted.insert( + edge.importer.clone(), + ImpactReason { + distance, + reason: "reverse_import", + via: Some(target.clone()), + confidence: edge.confidence, + }, + ); + queue.push_back(edge.importer.clone()); + } + } + impacted +} + +fn select_tests( + impacted: &BTreeMap, + changed: &[String], + files: &BTreeSet, +) -> Vec { + let mut tests = impacted + .keys() + .filter(|path| test_file(path)) + .cloned() + .collect::>(); + if tests.is_empty() { + let modules = changed + .iter() + .filter_map(|path| path.split('/').next()) + .collect::>(); + tests.extend( + files + .iter() + .filter(|path| { + test_file(path) + && path + .split('/') + .next() + .is_some_and(|module| modules.contains(module)) + }) + .cloned(), + ); + } + tests.into_iter().collect() +} + +fn test_commands(tests: &[String]) -> Vec { + let mut commands = BTreeSet::new(); + if tests.iter().any(|path| path.ends_with(".rs")) { + commands.insert("cargo test".to_string()); + } + let python = tests + .iter() + .filter(|path| path.ends_with(".py")) + .cloned() + .collect::>(); + if !python.is_empty() { + commands.insert(format!("pytest {}", python.join(" "))); + } + let javascript = tests + .iter() + .filter(|path| { + [".js", ".jsx", ".ts", ".tsx"] + .iter() + .any(|extension| path.ends_with(extension)) + }) + .cloned() + .collect::>(); + if !javascript.is_empty() { + commands.insert(format!("npm test -- {}", javascript.join(" "))); + } + if tests.iter().any(|path| path.ends_with(".go")) { + commands.insert("go test ./...".to_string()); + } + if tests.iter().any(|path| path.ends_with(".java")) { + commands.insert("mvn test".to_string()); + } + commands.into_iter().collect() +} + +fn test_file(path: &str) -> bool { + let file = path.rsplit('/').next().unwrap_or(path); + file.contains("test.") + || file.contains("_test.") + || file.contains("spec.") + || path.starts_with("test/") + || path.starts_with("tests/") + || path.starts_with("spec/") + || path.contains("/test/") + || path.contains("/tests/") + || path.contains("/spec/") +} + +fn normalize_relative(path: &str) -> Result { + let path = path.replace('\\', "/"); + if path.is_empty() + || path.starts_with('/') + || path.contains(':') + || path + .split('/') + .any(|component| component.is_empty() || matches!(component, "." | "..")) + { + return Err(ImpactError::Contract(format!( + "--changed must be a portable repository-relative path: {path}" + ))); + } + Ok(path) +} + +fn map_evidence(error: EvidenceError) -> ImpactError { + match error { + EvidenceError::Contract(message) => ImpactError::Contract(message), + EvidenceError::HostIo(message) => ImpactError::HostIo(message), + } +} + +enum ImpactError { + Contract(String), + HostIo(String), +} diff --git a/crates/code-intel-cli/src/codenexus_adapter.rs b/crates/code-intel-cli/src/codenexus_adapter.rs new file mode 100644 index 0000000..e6db265 --- /dev/null +++ b/crates/code-intel-cli/src/codenexus_adapter.rs @@ -0,0 +1,386 @@ +use std::collections::BTreeSet; + +use serde_json::{json, Value}; + +pub(crate) fn translate( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> Result { + validate_native(native)?; + if max_age_seconds == 0 { + return Err("CodeNexus freshness policy max age must be positive".to_string()); + } + + let mode = native["providerMode"].as_str().unwrap(); + let status = native["status"].as_str().unwrap(); + let observed_at = native["observedAt"].as_u64().unwrap(); + let expected = native["expectedSnapshotIdentity"].as_str().unwrap(); + let consumed = native["sourceSnapshotIdentity"].as_str().unwrap(); + let freshness = if expected != consumed { + "snapshot_mismatch" + } else if observed_at <= evaluated_at && evaluated_at - observed_at <= max_age_seconds { + "current" + } else { + "stale" + }; + let completeness = if status == "current" { + "complete" + } else { + "partial" + }; + let (failure_kind, failure) = match status { + "current" => ("none", json!({"kind":"none"})), + "partial" => ( + "domain_unknown", + json!({"kind":"domain_unknown","message":"CodeNexus evidence is partial"}), + ), + "unavailable" => ( + "provider_unavailable", + json!({"kind":"provider_unavailable","message":"CodeNexus provider is unavailable"}), + ), + _ => unreachable!("validated CodeNexus status"), + }; + let provider = json!({ + "mode":mode, + "providerId":native["providerId"], + "implementationId":native["implementation"]["id"], + "activation":native["activation"] + }); + let provenance = json!({ + "sourceRevision":native["sourceRevision"], + "observedAt":observed_at + }); + let request = json!({ + "schema":"code-intel-evidence-admissibility-request.v1", + "expectedSnapshotIdentity":native["expectedSnapshotIdentity"], + "policy":{"evaluatedAt":evaluated_at,"maxAgeSeconds":max_age_seconds}, + "observation":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{ + "id":native["providerId"], + "implementation":native["implementation"] + }, + "source":{"revision":native["sourceRevision"]}, + "consumedSnapshotIdentity":native["sourceSnapshotIdentity"], + "observedAt":observed_at, + "completeness":completeness, + "claimedComplete":completeness == "complete", + "payload":native["payload"], + "provenance":{ + "collectionId":format!("codenexus-{mode}-{}-{observed_at}", native["sourceRevision"].as_str().unwrap()), + "command":if mode == "full" { "codenexus adapter:provider-artifact" } else { "codenexus adapter:lite-compatibility-artifact" }, + "startedAt":native["collectedAt"], + "completedAt":observed_at + }, + "failure":failure + } + }); + + Ok(json!({ + "schema":"code-intel-codenexus-adapter-result.v1", + "port":{ + "schema":"code-intel-codenexus-port.v1", + "status":status, + "completeness":completeness, + "freshness":freshness, + "expectedSnapshotIdentity":expected, + "sourceSnapshotIdentity":consumed, + "provider":provider, + "provenance":provenance, + "boundary":{ + "transport":"artifact_ref", + "storageOwnership":"provider", + "impactSemanticsOwnership":"provider" + }, + "effects":native["effects"], + "payload":native["payload"], + "failureKind":failure_kind, + "perceptionUsable":false + }, + "evidence":{"request":request}, + "factPromotion":{ + "eligible":false, + "requires":"evidence.admissibility-validate", + "engineeringFacts":[] + } + })) +} + +pub(crate) fn validate_admitted_payload(payload: &Value, adapter: &Value) -> Result<(), String> { + exact(payload, &["schema", "data"], "CodeNexus evidence payload")?; + if payload["schema"] != "code-intel-evidence-payload.v1" { + return Err("CodeNexus evidence payload schema is invalid".to_string()); + } + exact(&payload["data"], &["codenexus"], "CodeNexus payload data")?; + let evidence = &payload["data"]["codenexus"]; + exact( + evidence, + &[ + "schema", + "snapshotIdentity", + "provider", + "provenance", + "completeness", + "availability", + "providerData", + ], + "CodeNexus port payload", + )?; + if evidence["schema"] != "code-intel-codenexus-evidence.v1" + || evidence["snapshotIdentity"] != adapter["port"]["sourceSnapshotIdentity"] + || evidence["completeness"] != adapter["port"]["completeness"] + { + return Err("CodeNexus payload snapshot/completeness mismatch".to_string()); + } + exact( + &evidence["provider"], + &["mode", "providerId", "implementationId", "activation"], + "CodeNexus payload provider", + )?; + if evidence["provider"] != adapter["port"]["provider"] { + return Err("CodeNexus payload provider identity mismatch".to_string()); + } + exact( + &evidence["provenance"], + &["sourceRevision", "observedAt"], + "CodeNexus payload provenance", + )?; + if evidence["provenance"] != adapter["port"]["provenance"] { + return Err("CodeNexus payload provenance mismatch".to_string()); + } + let expected_availability = if adapter["port"]["status"] == "unavailable" { + "provider_unavailable" + } else { + "available" + }; + if evidence["availability"] != expected_availability { + return Err("CodeNexus payload availability mismatch".to_string()); + } + match adapter["port"]["status"].as_str().unwrap() { + "unavailable" if !evidence["providerData"].is_null() => { + Err("unavailable CodeNexus evidence must not fabricate provider data".to_string()) + } + "current" if !evidence["providerData"].is_object() => { + Err("current CodeNexus evidence requires opaque provider data".to_string()) + } + "partial" + if !(evidence["providerData"].is_null() || evidence["providerData"].is_object()) => + { + Err("partial CodeNexus provider data is invalid".to_string()) + } + _ => Ok(()), + } +} + +pub(crate) fn validate_adapter_result(adapter: &Value) -> Result<(), String> { + exact( + adapter, + &["schema", "port", "evidence", "factPromotion"], + "CodeNexus adapter result", + )?; + if adapter["schema"] != "code-intel-codenexus-adapter-result.v1" { + return Err("CodeNexus adapter result schema is invalid".to_string()); + } + let port = &adapter["port"]; + exact( + port, + &[ + "schema", + "status", + "completeness", + "freshness", + "expectedSnapshotIdentity", + "sourceSnapshotIdentity", + "provider", + "provenance", + "boundary", + "effects", + "payload", + "failureKind", + "perceptionUsable", + ], + "CodeNexus adapter port", + )?; + exact( + &port["provider"], + &["mode", "providerId", "implementationId", "activation"], + "CodeNexus adapter provider", + )?; + exact( + &port["provenance"], + &["sourceRevision", "observedAt"], + "CodeNexus adapter provenance", + )?; + exact( + &port["boundary"], + &["transport", "storageOwnership", "impactSemanticsOwnership"], + "CodeNexus adapter boundary", + )?; + exact( + &port["payload"], + &[ + "schema", + "artifactSchema", + "type", + "path", + "sha256", + "consumedSnapshotIdentity", + ], + "CodeNexus adapter payload ref", + )?; + exact( + &adapter["evidence"], + &["request"], + "CodeNexus adapter evidence", + )?; + exact( + &adapter["factPromotion"], + &["eligible", "requires", "engineeringFacts"], + "CodeNexus adapter fact promotion", + )?; + if adapter["factPromotion"]["eligible"] != false + || adapter["factPromotion"]["requires"] != "evidence.admissibility-validate" + || adapter["factPromotion"]["engineeringFacts"] != json!([]) + { + return Err("CodeNexus adapter fact promotion contract is invalid".to_string()); + } + Ok(()) +} + +fn validate_native(native: &Value) -> Result<(), String> { + exact( + native, + &[ + "schema", + "providerMode", + "status", + "providerId", + "implementation", + "sourceRevision", + "expectedSnapshotIdentity", + "sourceSnapshotIdentity", + "collectedAt", + "observedAt", + "payload", + "activation", + "effects", + ], + "CodeNexus native result", + )?; + if native["schema"] != "code-intel-codenexus-native-result.v1" + || !matches!(native["providerMode"].as_str(), Some("full" | "lite")) + || !matches!( + native["status"].as_str(), + Some("current" | "partial" | "unavailable") + ) + || !nonempty(&native["providerId"]) + || !nonempty(&native["sourceRevision"]) + || !digest(&native["expectedSnapshotIdentity"]) + || !digest(&native["sourceSnapshotIdentity"]) + || native["collectedAt"].as_u64().is_none() + || native["observedAt"].as_u64().is_none() + || native["observedAt"].as_u64().unwrap() < native["collectedAt"].as_u64().unwrap() + { + return Err("CodeNexus native identity/status/time is invalid".to_string()); + } + exact( + &native["implementation"], + &["id", "version", "digest"], + "CodeNexus implementation", + )?; + if !nonempty(&native["implementation"]["id"]) + || !nonempty(&native["implementation"]["version"]) + || !digest(&native["implementation"]["digest"]) + { + return Err("CodeNexus implementation identity is invalid".to_string()); + } + let mode = native["providerMode"].as_str().unwrap(); + let activation = native["activation"].as_str().unwrap_or(""); + let provider_id = native["providerId"].as_str().unwrap(); + let implementation_id = native["implementation"]["id"].as_str().unwrap(); + if mode == "full" && activation != "primary" { + return Err("CodeNexus full provider must be primary".to_string()); + } + if mode == "full" + && (provider_id != "codenexus.full" || implementation_id == "invoke-codenexus-lite.ps1") + { + return Err("CodeNexus full provider identity is invalid".to_string()); + } + if mode == "lite" && !matches!(activation, "explicit_fallback" | "legacy_rollback") { + return Err("CodeNexus lite requires explicit fallback or legacy rollback".to_string()); + } + if mode == "lite" + && (provider_id != "codenexus.lite-compat" + || implementation_id != "invoke-codenexus-lite.ps1") + { + return Err("CodeNexus lite compatibility identity is invalid".to_string()); + } + validate_effects(&native["effects"], mode)?; + crate::capability::validate_artifact_ref_shape(&native["payload"])?; + if native["payload"]["artifactSchema"] != "code-intel-evidence-payload.v1" + || native["payload"]["type"] != "observed.evidence.payload" + || native["payload"]["consumedSnapshotIdentity"] != native["sourceSnapshotIdentity"] + { + return Err("CodeNexus payload contract/snapshot is invalid".to_string()); + } + Ok(()) +} + +fn validate_effects(value: &Value, mode: &str) -> Result<(), String> { + let values = value + .as_array() + .ok_or_else(|| "CodeNexus effects must be an array".to_string())?; + let allowed = if mode == "full" { + ["network_provider", "read_provider_artifact"].as_slice() + } else { + [ + "read_repository", + "read_git_history", + "read_sentrux_artifacts", + "write_compatibility_artifact", + ] + .as_slice() + }; + let mut seen = BTreeSet::new(); + for effect in values { + let effect = effect + .as_str() + .ok_or_else(|| "CodeNexus effect is invalid".to_string())?; + if !allowed.contains(&effect) || !seen.insert(effect) { + return Err("CodeNexus effects are invalid for provider mode".to_string()); + } + } + if values.is_empty() { + return Err("CodeNexus effects must not be empty".to_string()); + } + Ok(()) +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn nonempty(value: &Value) -> bool { + value.as_str().is_some_and(|text| !text.is_empty()) +} + +fn digest(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 64 + && text + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + }) +} diff --git a/crates/code-intel-cli/src/committed_evidence.rs b/crates/code-intel-cli/src/committed_evidence.rs new file mode 100644 index 0000000..2d9cde1 --- /dev/null +++ b/crates/code-intel-cli/src/committed_evidence.rs @@ -0,0 +1,118 @@ +use std::path::Path; + +use serde_json::{json, Value}; + +use crate::artifact_index; +use crate::artifact_ref::{self, VerifiedArtifact}; +use crate::snapshot; + +pub(crate) struct CommittedEvidence { + pub(crate) entry: Value, + pub(crate) refs: Vec, + pub(crate) verified: Vec, +} + +pub(crate) fn load(artifact_root: &Path, repo: &str) -> Result { + let index = artifact_index::rebuild(artifact_root).map_err(|error| match error { + artifact_index::IndexError::Contract(message) => EvidenceError::Contract(message), + artifact_index::IndexError::HostIo(message) => EvidenceError::HostIo(message), + })?; + let entry = index["entries"] + .as_array() + .and_then(|entries| entries.iter().find(|entry| entry["repo"] == repo)) + .cloned() + .ok_or_else(|| { + EvidenceError::Contract(format!( + "no committed authoritative run is indexed for repository: {repo}" + )) + })?; + let run = entry["run"].as_str().expect("A08 entry run"); + let run_root = artifact_root.join(repo).join(run); + let snapshot_identity = entry["snapshotIdentity"] + .as_str() + .expect("A08 entry snapshot identity"); + let refs = entry["artifactRefs"] + .as_array() + .expect("A08 entry Artifact Refs") + .clone(); + let verified = artifact_ref::verify_inputs( + &Value::Array(refs.clone()), + Some(&run_root), + snapshot_identity, + ) + .map_err(|error| match error { + artifact_ref::ArtifactError::Contract(message) => EvidenceError::Contract(message), + artifact_ref::ArtifactError::Io(message) => EvidenceError::HostIo(message), + })?; + Ok(CommittedEvidence { + entry, + refs, + verified, + }) +} + +impl CommittedEvidence { + pub(crate) fn snapshot_identity(&self) -> &str { + self.entry["snapshotIdentity"] + .as_str() + .expect("A08 entry snapshot identity") + } + + pub(crate) fn artifact(&self, artifact_type: &str) -> Option<(&Value, &VerifiedArtifact)> { + self.refs + .iter() + .zip(self.verified.iter()) + .find(|(artifact, _)| artifact["type"] == artifact_type) + } + + pub(crate) fn freshness(&self, repo_path: Option<&Path>) -> Result { + let recorded_identity = self.snapshot_identity(); + let Some(repo_path) = repo_path else { + return Ok(json!({ + "status":"unknown", + "recordedIdentity":recorded_identity, + "currentIdentity":Value::Null, + "workingTreePolicy":Value::Null, + "scope":[], + })); + }; + let snapshot_bytes = self + .artifact("repository.snapshot") + .map(|(_, verified)| verified.bytes()) + .ok_or_else(|| { + EvidenceError::Contract( + "freshness evaluation requires a committed repository snapshot artifact".into(), + ) + })?; + let recorded: Value = serde_json::from_slice(snapshot_bytes).map_err(|_| { + EvidenceError::Contract("repository snapshot artifact is invalid JSON".into()) + })?; + let policy = recorded["snapshot"]["workingTreePolicy"] + .as_str() + .expect("validated A02 working tree policy"); + let scopes = recorded["snapshot"]["scope"] + .as_array() + .expect("validated A02 scope") + .iter() + .map(|scope| scope.as_str().unwrap().to_string()) + .collect::>(); + let current = snapshot::build_for_dag(repo_path, policy, &scopes).map_err(|error| { + EvidenceError::Contract(format!("evaluate current snapshot: {error}")) + })?; + let current_identity = current["snapshot"]["identity"] + .as_str() + .expect("A02 current snapshot identity"); + Ok(json!({ + "status":if current_identity == recorded_identity { "current" } else { "stale" }, + "recordedIdentity":recorded_identity, + "currentIdentity":current_identity, + "workingTreePolicy":policy, + "scope":scopes, + })) + } +} + +pub(crate) enum EvidenceError { + Contract(String), + HostIo(String), +} diff --git a/crates/code-intel-cli/src/compatibility_retirement_gate.rs b/crates/code-intel-cli/src/compatibility_retirement_gate.rs new file mode 100644 index 0000000..05012fe --- /dev/null +++ b/crates/code-intel-cli/src/compatibility_retirement_gate.rs @@ -0,0 +1,889 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +use super::{AdapterArtifact, AdapterError, AdapterOutput}; +use crate::artifact_ref::{ + normalized_retirement_call_path, retirement_portable_paths, VerifiedArtifact, +}; +use crate::capability::sha256_hex; + +#[path = "authority.rs"] +mod authority; + +const MANIFEST_SCHEMA: &str = "code-intel-compatibility-retirement-manifest.v1"; +const EVIDENCE_SCHEMA: &str = "code-intel-compatibility-retirement-evidence.v1"; + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + let evaluated_at = evaluated_at(request)?; + let (manifest, evidence) = load_inputs(request, verified_inputs)?; + let decision = evaluate(&manifest, &evidence, evaluated_at)?; + publish_decision(out, decision) +} + +fn evaluated_at(request: &Value) -> Result { + let options = request["options"].as_object().ok_or_else(|| { + AdapterError::InvalidOptions("retirement gate options must be an object".into()) + })?; + if options.len() != 1 || !options.contains_key("evaluatedAt") { + return Err(AdapterError::InvalidOptions( + "compatibility.retirement-gate requires exactly options.evaluatedAt".into(), + )); + } + request["options"]["evaluatedAt"].as_u64().ok_or_else(|| { + AdapterError::InvalidOptions("retirement gate evaluatedAt must be an integer".into()) + }) +} + +fn load_inputs( + request: &Value, + verified_inputs: &[VerifiedArtifact], +) -> Result<(Value, BTreeMap), AdapterError> { + let manifest_input = verified_inputs + .iter() + .filter(|input| input.artifact_schema() == MANIFEST_SCHEMA) + .collect::>(); + if manifest_input.len() != 1 { + return Err(AdapterError::Contract( + "retirement gate requires exactly one A03-verified retirement manifest".into(), + )); + } + let manifest: Value = serde_json::from_slice(manifest_input[0].bytes()) + .map_err(|e| AdapterError::Contract(format!("parse retirement manifest: {e}")))?; + if manifest["snapshotIdentity"] != request["snapshot"]["identity"] { + return Err(AdapterError::Contract( + "retirement manifest snapshot differs from the A01 request".into(), + )); + } + let mut evidence = BTreeMap::::new(); + for input in verified_inputs { + if input.artifact_schema() == MANIFEST_SCHEMA { + continue; + } + if input.artifact_schema() != EVIDENCE_SCHEMA + || input.artifact_type() != "compatibility.retirement-evidence" + { + return Err(AdapterError::Contract( + "retirement gate accepts only its closed manifest and evidence contracts".into(), + )); + } + let value: Value = serde_json::from_slice(input.bytes()) + .map_err(|e| AdapterError::Contract(format!("parse retirement evidence: {e}")))?; + if value["snapshotIdentity"] != request["snapshot"]["identity"] { + return Err(AdapterError::Contract( + "retirement evidence payload snapshot differs from the A01 request".into(), + )); + } + if evidence.insert(input.sha256().to_string(), value).is_some() { + return Err(AdapterError::Contract( + "duplicate retirement evidence digest".into(), + )); + } + } + Ok((manifest, evidence)) +} + +fn publish_decision(out: &Path, decision: Value) -> Result { + let bytes = serde_json::to_vec(&decision) + .map_err(|e| AdapterError::Internal(format!("serialize retirement decision: {e}")))?; + fs::create_dir(out).map_err(|e| AdapterError::Io(format!("create retirement staging: {e}")))?; + fs::write(out.join("compatibility-retirement-decision.json"), &bytes) + .map_err(|e| AdapterError::Io(format!("write retirement decision: {e}")))?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-compatibility-retirement-decision.v1".into(), + artifact_type: "compatibility.retirement-decision".into(), + relative_path: "compatibility-retirement-decision.json".into(), + bytes, + }], + observed_effects: vec!["local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn evaluate( + manifest: &Value, + evidence: &BTreeMap, + evaluated_at: u64, +) -> Result { + let subject = &manifest["approvalSubject"]; + let retirement_id = text(manifest, "retirementId", "manifest")?; + let legacy = &subject["legacyBranch"]; + let legacy_capability = text(legacy, "capabilityId", "legacyBranch")?; + let legacy_branch = text(legacy, "branchId", "legacyBranch")?; + normalized_retirement_call_path(&legacy["callPath"], legacy_branch) + .map_err(AdapterError::Contract)?; + retirement_portable_paths(&legacy["affectedFiles"], "legacyBranch.affectedFiles") + .map_err(AdapterError::Contract)?; + let replacement = &subject["replacement"]; + let replacement_capability = text(replacement, "capabilityId", "replacement")?; + let dependencies = strings(&replacement["dependencies"], "replacement.dependencies")?; + let mut blockers = structural_blockers( + subject, + legacy_capability, + replacement_capability, + &dependencies, + ); + let mut referenced = BTreeSet::new(); + check_core_evidence( + subject, + retirement_id, + legacy_branch, + replacement_capability, + evidence, + &mut referenced, + &mut blockers, + )?; + check_compatibility_and_usage( + subject, + retirement_id, + legacy_branch, + replacement_capability, + evidence, + evaluated_at, + &mut referenced, + &mut blockers, + )?; + check_necessity_and_dependencies( + subject, + retirement_id, + legacy_branch, + replacement_capability, + &dependencies, + evidence, + &mut referenced, + &mut blockers, + )?; + let subject_sha = check_independent_approval( + manifest, + subject, + retirement_id, + legacy_branch, + replacement_capability, + evidence, + evaluated_at, + &mut referenced, + &mut blockers, + )?; + ensure_all_evidence_referenced(&referenced, evidence)?; + Ok(decision_document( + manifest, + retirement_id, + legacy, + replacement, + legacy_branch, + subject_sha, + referenced.len(), + blockers, + )) +} + +fn structural_blockers( + subject: &Value, + legacy_capability: &str, + replacement_capability: &str, + dependencies: &BTreeSet, +) -> Vec { + let mut blockers = Vec::new(); + if legacy_capability == replacement_capability { + blockers.push("replacement_is_legacy_capability".into()); + } + if dependencies.contains(legacy_capability) || dependencies.contains(replacement_capability) { + blockers.push("cyclic_replacement".into()); + } + if subject["lineReductionEvidence"] != false { + blockers.push("line_reduction_is_not_correctness_evidence".into()); + } + blockers +} + +#[allow(clippy::too_many_arguments)] +fn check_core_evidence( + subject: &Value, + retirement_id: &str, + legacy_branch: &str, + replacement_capability: &str, + evidence: &BTreeMap, + referenced: &mut BTreeSet, + blockers: &mut Vec, +) -> Result<(), AdapterError> { + let replacement = &subject["replacement"]; + let legacy = &subject["legacyBranch"]; + check( + ref_at(replacement, "atomEvidence", "replacement")?, + "replacement_atom", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |details| details["status"] == "production_ready" && details["outcome"] == "passed", + )?; + for (field, class) in [ + ("golden", "golden_parity"), + ("contract", "contract_parity"), + ("effects", "effect_parity"), + ] { + check( + ref_at(&subject["parity"], field, "parity")?, + class, + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |details| { + details["outcome"] == "passed" + && details["assertionCount"].as_u64().unwrap_or(0) > 0 + }, + )?; + } + check( + ref_at(subject, "registryReconciliation", "approvalSubject")?, + "registry_reconciliation", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + d["outcome"] == "passed" + && d["registryParticipantId"] == legacy["registryParticipantId"] + && d["replacementCapabilityId"] == replacement["capabilityId"] + && matches!(d["status"].as_str(), Some("declared" | "deleted")) + }, + )?; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn check_compatibility_and_usage( + subject: &Value, + retirement_id: &str, + legacy_branch: &str, + replacement_capability: &str, + evidence: &BTreeMap, + evaluated_at: u64, + referenced: &mut BTreeSet, + blockers: &mut Vec, +) -> Result<(), AdapterError> { + let (compatibility_start, compatibility_end) = check_compatibility_window( + subject, + retirement_id, + legacy_branch, + replacement_capability, + evidence, + evaluated_at, + referenced, + blockers, + )?; + check_rollback_and_usage( + subject, + retirement_id, + legacy_branch, + replacement_capability, + evidence, + compatibility_start, + compatibility_end, + referenced, + blockers, + ) +} + +#[allow(clippy::too_many_arguments)] +fn check_compatibility_window( + subject: &Value, + retirement_id: &str, + legacy_branch: &str, + replacement_capability: &str, + evidence: &BTreeMap, + evaluated_at: u64, + referenced: &mut BTreeSet, + blockers: &mut Vec, +) -> Result<(u64, u64), AdapterError> { + let compatibility_ref = ref_at(subject, "compatibilityWindow", "approvalSubject")?; + check( + compatibility_ref, + "compatibility_window", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + let start = d["startedAt"].as_u64().unwrap_or(u64::MAX); + let end = d["observedThrough"].as_u64().unwrap_or(0); + let days = d["minimumDays"].as_u64().unwrap_or(u64::MAX); + let checked = d["checkedAt"].as_u64().unwrap_or(u64::MAX); + let expires = d["expiresAt"].as_u64().unwrap_or(0); + d["outcome"] == "passed" + && end >= start + && end - start >= days.saturating_mul(86_400) + && checked <= evaluated_at + && expires >= evaluated_at + && expires >= checked + }, + )?; + let compatibility = evidence + .get(text( + compatibility_ref, + "sha256", + "compatibilityWindow ref", + )?) + .ok_or_else(|| AdapterError::Contract("compatibility window evidence is absent".into()))?; + let compatibility_start = compatibility["details"]["startedAt"] + .as_u64() + .unwrap_or(u64::MAX); + let compatibility_end = compatibility["details"]["observedThrough"] + .as_u64() + .unwrap_or(0); + Ok((compatibility_start, compatibility_end)) +} + +#[allow(clippy::too_many_arguments)] +fn check_rollback_and_usage( + subject: &Value, + retirement_id: &str, + legacy_branch: &str, + replacement_capability: &str, + evidence: &BTreeMap, + compatibility_start: u64, + compatibility_end: u64, + referenced: &mut BTreeSet, + blockers: &mut Vec, +) -> Result<(), AdapterError> { + let rollback = &subject["rollback"]; + let rollback_command = text(rollback, "command", "rollback")?; + check( + ref_at(rollback, "executionEvidence", "rollback")?, + "rollback_execution", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + d["outcome"] == "passed" + && d["exitCode"] == 0 + && d["command"] == rollback_command + && d["executedAt"].as_u64().is_some() + }, + )?; + check( + ref_at(subject, "usageObservation", "approvalSubject")?, + "usage_observation", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + let start = d["startedAt"].as_u64().unwrap_or(u64::MAX); + let end = d["endedAt"].as_u64().unwrap_or(0); + let total = d["totalInvocations"].as_u64().unwrap_or(0); + let legacy_count = d["legacyInvocations"].as_u64().unwrap_or(u64::MAX); + let replacement_count = d["replacementInvocations"].as_u64().unwrap_or(0); + d["outcome"] == "passed" + && start == compatibility_start + && end == compatibility_end + && end >= start + && replacement_count > 0 + && legacy_count == 0 + && total == legacy_count.saturating_add(replacement_count) + }, + )?; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn check_necessity_and_dependencies( + subject: &Value, + retirement_id: &str, + legacy_branch: &str, + replacement_capability: &str, + dependencies: &BTreeSet, + evidence: &BTreeMap, + referenced: &mut BTreeSet, + blockers: &mut Vec, +) -> Result<(), AdapterError> { + let necessity_trace_sha = sha256_hex( + &serde_json::to_vec(&json!({ + "retirementId":retirement_id, + "legacyBranchId":legacy_branch, + "replacementCapabilityId":replacement_capability + })) + .expect("retirement trace identity serializes"), + ); + check( + ref_at(subject, "necessityEvidence", "approvalSubject")?, + "c00_necessity", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + d["outcome"] == "passed" + && d["decision"] == "admit" + && d["changeId"] == retirement_id + && d["necessityTraceSha256"] == necessity_trace_sha + }, + )?; + let mut approved_dependencies = BTreeSet::new(); + for state in subject["dependencyStates"] + .as_array() + .ok_or_else(|| AdapterError::Contract("dependencyStates must be an array".into()))? + { + check( + state, + "dependency_approval", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + let dependency = d["dependencyId"].as_str().unwrap_or(""); + let valid = d["outcome"] == "passed" + && d["status"] == "approved" + && dependencies.contains(dependency); + if valid { + approved_dependencies.insert(dependency.to_string()); + } + valid + }, + )?; + } + if approved_dependencies != *dependencies { + blockers.push("dependency_approval_set_mismatch".into()); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn check_independent_approval( + manifest: &Value, + subject: &Value, + retirement_id: &str, + legacy_branch: &str, + replacement_capability: &str, + evidence: &BTreeMap, + evaluated_at: u64, + referenced: &mut BTreeSet, + blockers: &mut Vec, +) -> Result { + let legacy = &subject["legacyBranch"]; + let subject_bytes = serde_json::to_vec(subject) + .map_err(|e| AdapterError::Internal(format!("serialize approval subject: {e}")))?; + let subject_sha = sha256_hex(&subject_bytes); + check( + ref_at(manifest, "independentApproval", "manifest")?, + "independent_approval", + retirement_id, + legacy_branch, + replacement_capability, + evidence, + referenced, + blockers, + |d| { + let reviewer = d["reviewer"].as_str().unwrap_or(""); + let event = &d["authorityEvent"]; + let known = BTreeSet::from([subject_sha.clone()]); + let required = known.clone(); + let consumed = BTreeSet::new(); + d["outcome"] == "passed" + && d["approved"] == true + && d["subjectSha256"] == subject_sha + && !reviewer.is_empty() + && reviewer != legacy["owner"].as_str().unwrap_or("") + && event.pointer("/approver/id").and_then(Value::as_str) == Some(reviewer) + && authority::validate_signed_authority_event( + event, + evaluated_at, + &known, + &required, + &consumed, + ) + .is_ok() + }, + )?; + Ok(subject_sha) +} + +fn ensure_all_evidence_referenced( + referenced: &BTreeSet, + evidence: &BTreeMap, +) -> Result<(), AdapterError> { + if referenced.len() != evidence.len() { + return Err(AdapterError::Contract( + "retirement inputs contain unreferenced evidence".into(), + )); + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn decision_document( + manifest: &Value, + retirement_id: &str, + legacy: &Value, + replacement: &Value, + legacy_branch: &str, + subject_sha: String, + evidence_count: usize, + mut blockers: Vec, +) -> Value { + blockers.sort(); + blockers.dedup(); + let approved = blockers.is_empty(); + json!({ + "schema":"code-intel-compatibility-retirement-decision.v1", + "snapshotIdentity":manifest["snapshotIdentity"], + "retirementId":retirement_id, + "legacyBranch":legacy, + "replacement":replacement, + "approvalSubjectSha256":subject_sha, + "decision":if approved {"approved"} else {"blocked"}, + "blockers":blockers, + "authorityBoundary":"approval_only_no_deletion_authority", + "gainLedgerProjection":{ + "id":format!("retirement:{retirement_id}"), + "status":if approved {"approved-for-ticket"} else {"blocked"}, + "gain":format!("Retire legacy branch {legacy_branch} only through a separate deletion ticket"), + "evidenceCount":evidence_count + } + }) +} + +#[allow(clippy::too_many_arguments)] +fn check bool>( + reference: &Value, + class: &str, + retirement_id: &str, + legacy_branch: &str, + replacement: &str, + evidence: &BTreeMap, + referenced: &mut BTreeSet, + blockers: &mut Vec, + predicate: F, +) -> Result<(), AdapterError> { + let digest = text(reference, "sha256", "evidence ref")?; + if !referenced.insert(digest.to_string()) { + return Err(AdapterError::Contract( + "one evidence artifact cannot satisfy multiple retirement requirements".into(), + )); + } + let Some(value) = evidence.get(digest) else { + return Err(AdapterError::Contract(format!( + "referenced retirement evidence is absent: {digest}" + ))); + }; + if reference["artifactSchema"] != EVIDENCE_SCHEMA + || reference["type"] != "compatibility.retirement-evidence" + || value["evidenceClass"] != class + || value["retirementId"] != retirement_id + || value["legacyBranchId"] != legacy_branch + || value["replacementCapabilityId"] != replacement + { + blockers.push(format!("invalid_{class}")); + } else if !predicate(&value["details"]) { + blockers.push(format!("unproven_{class}")); + } + Ok(()) +} + +fn ref_at<'a>(value: &'a Value, field: &str, label: &str) -> Result<&'a Value, AdapterError> { + let reference = &value[field]; + if !reference.is_object() { + return Err(AdapterError::Contract(format!( + "{label}.{field} is missing" + ))); + } + Ok(reference) +} + +fn text<'a>(value: &'a Value, field: &str, label: &str) -> Result<&'a str, AdapterError> { + value[field] + .as_str() + .filter(|v| !v.is_empty()) + .ok_or_else(|| AdapterError::Contract(format!("{label}.{field} is missing"))) +} + +fn strings(value: &Value, label: &str) -> Result, AdapterError> { + let values = value + .as_array() + .ok_or_else(|| AdapterError::Contract(format!("{label} must be an array")))?; + let mut result = BTreeSet::new(); + for value in values { + let id = value + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| AdapterError::Contract(format!("{label} contains invalid id")))?; + if !result.insert(id.to_string()) { + return Err(AdapterError::Contract(format!( + "{label} contains duplicate ids" + ))); + } + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + const NOW: u64 = 3_000_000; + + fn signed_event(subject_sha: &str) -> Value { + let mut event = json!({ + "schema":"code-intel-authority-event.v1", + "id":"authority.retirement.ret-1", + "decision":"approved", + "approver":{"id":"code-intel-maintainers","role":"repository_governance"}, + "evidenceIds":[subject_sha], + "issuedAt":NOW - 10, + "expiresAt":NOW + 10 + }); + let digest = authority::authority_event_digest(&event).unwrap(); + event["attestation"] = json!({ + "scheme":"repository-governed-sha256-v1", + "digest":digest + }); + event + } + + fn evidence(class: &str, details: Value) -> (Value, String, Value) { + let value = json!({ + "schema":EVIDENCE_SCHEMA,"snapshotIdentity":"snap","id":format!("ev-{class}"), + "evidenceClass":class,"retirementId":"ret-1","legacyBranchId":"legacy.branch", + "replacementCapabilityId":"replacement.atom","details":details + }); + let bytes = serde_json::to_vec(&value).unwrap(); + let sha = sha256_hex(&bytes); + let reference = json!({ + "artifactSchema":EVIDENCE_SCHEMA,"type":"compatibility.retirement-evidence", + "path":format!("evidence/{class}.json"),"sha256":sha, + "consumedSnapshotIdentity":"snap" + }); + (reference, sha, value) + } + + fn fixture() -> (Value, BTreeMap) { + let mut values = BTreeMap::new(); + let mut add = |class: &str, details: Value| { + let (reference, sha, value) = evidence(class, details); + values.insert(sha, value); + reference + }; + let atom = add( + "replacement_atom", + json!({"status":"production_ready","outcome":"passed"}), + ); + let golden = add( + "golden_parity", + json!({"outcome":"passed","assertionCount":4}), + ); + let contract = add( + "contract_parity", + json!({"outcome":"passed","assertionCount":3}), + ); + let effects = add( + "effect_parity", + json!({"outcome":"passed","assertionCount":2}), + ); + let registry = add( + "registry_reconciliation", + json!({"outcome":"passed","registryParticipantId":"legacy.registry","replacementCapabilityId":"replacement.atom","status":"declared"}), + ); + let window = add( + "compatibility_window", + json!({"outcome":"passed","startedAt":1_000,"observedThrough":1_000+30*86_400,"minimumDays":30,"checkedAt":2_600_000,"expiresAt":NOW+100}), + ); + let rollback = add( + "rollback_execution", + json!({"outcome":"passed","command":"restore legacy.branch","executedAt":9_000,"exitCode":0}), + ); + let usage = add( + "usage_observation", + json!({"outcome":"passed","startedAt":1_000,"endedAt":1_000+30*86_400,"totalInvocations":20,"legacyInvocations":0,"replacementInvocations":20}), + ); + let trace_sha = sha256_hex( + &serde_json::to_vec(&json!({"retirementId":"ret-1","legacyBranchId":"legacy.branch","replacementCapabilityId":"replacement.atom"})).unwrap(), + ); + let necessity = add( + "c00_necessity", + json!({"outcome":"passed","decision":"admit","changeId":"ret-1","necessityTraceSha256":trace_sha}), + ); + let dependency = add( + "dependency_approval", + json!({"outcome":"passed","dependencyId":"D02","status":"approved","reviewer":"d02-reviewer"}), + ); + let subject = json!({ + "legacyBranch":{"capabilityId":"legacy.capability","branchId":"legacy.branch","callPath":"run-code-intel.ps1::legacy.branch","affectedFiles":["run-code-intel.ps1"],"owner":"owner-team","registryParticipantId":"legacy.registry"}, + "replacement":{"capabilityId":"replacement.atom","implementationId":"replacement.atom.compat","dependencies":["D02"],"atomEvidence":atom}, + "parity":{"golden":golden,"contract":contract,"effects":effects}, + "registryReconciliation":registry,"compatibilityWindow":window, + "rollback":{"command":"restore legacy.branch","executionEvidence":rollback}, + "usageObservation":usage,"necessityEvidence":necessity, + "dependencyStates":[dependency],"lineReductionEvidence":false + }); + let subject_sha = sha256_hex(&serde_json::to_vec(&subject).unwrap()); + let approval = add( + "independent_approval", + json!({"outcome":"passed","approved":true,"authorIndependent":true,"subjectSha256":subject_sha,"reviewer":"code-intel-maintainers","authorityEvent":signed_event(&subject_sha)}), + ); + ( + json!({"schema":MANIFEST_SCHEMA,"snapshotIdentity":"snap","retirementId":"ret-1","approvalSubject":subject,"independentApproval":approval}), + values, + ) + } + + #[test] + fn complete_content_bound_manifest_is_approved_without_deletion_authority() { + let (manifest, evidence) = fixture(); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert_eq!(decision["decision"], "approved"); + assert_eq!(decision["blockers"], json!([])); + assert_eq!( + decision["authorityBoundary"], + "approval_only_no_deletion_authority" + ); + assert_eq!( + decision["gainLedgerProjection"]["status"], + "approved-for-ticket" + ); + } + + #[test] + fn missing_rollback_execution_evidence_fails_closed() { + let (manifest, mut evidence) = fixture(); + let digest = manifest["approvalSubject"]["rollback"]["executionEvidence"]["sha256"] + .as_str() + .unwrap() + .to_string(); + evidence.remove(&digest); + assert!( + matches!(evaluate(&manifest, &evidence, NOW), Err(AdapterError::Contract(message)) if message.contains("evidence is absent")) + ); + } + + #[test] + fn pending_d02_and_cyclic_replacement_are_visible_blockers() { + let (mut manifest, mut evidence) = fixture(); + let dependency = manifest["approvalSubject"]["dependencyStates"][0]["sha256"] + .as_str() + .unwrap() + .to_string(); + evidence.get_mut(&dependency).unwrap()["details"]["status"] = json!("pending"); + manifest["approvalSubject"]["replacement"]["dependencies"] = json!(["legacy.capability"]); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert_eq!(decision["decision"], "blocked"); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("cyclic_replacement"))); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("unproven_dependency_approval"))); + } + + #[test] + fn tampering_or_line_count_claim_cannot_reuse_prior_approval() { + let (mut manifest, evidence) = fixture(); + manifest["approvalSubject"]["lineReductionEvidence"] = json!(true); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert_eq!(decision["decision"], "blocked"); + let blockers = decision["blockers"].as_array().unwrap(); + assert!(blockers.contains(&json!("line_reduction_is_not_correctness_evidence"))); + assert!(blockers.contains(&json!("unproven_independent_approval"))); + } + + fn evidence_digest(manifest: &Value, pointer: &str) -> String { + manifest + .pointer(pointer) + .and_then(|value| value["sha256"].as_str()) + .unwrap() + .to_string() + } + + #[test] + fn expired_compatibility_evidence_is_blocked_at_evaluation_time() { + let (manifest, mut evidence) = fixture(); + let digest = evidence_digest(&manifest, "/approvalSubject/compatibilityWindow"); + evidence.get_mut(&digest).unwrap()["details"]["expiresAt"] = json!(NOW - 1); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("unproven_compatibility_window"))); + } + + #[test] + fn dependency_approval_must_match_every_replacement_dependency() { + let (manifest, mut evidence) = fixture(); + let digest = evidence_digest(&manifest, "/approvalSubject/dependencyStates/0"); + evidence.get_mut(&digest).unwrap()["details"]["dependencyId"] = json!("unrelated"); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + let blockers = decision["blockers"].as_array().unwrap(); + assert!(blockers.contains(&json!("unproven_dependency_approval"))); + assert!(blockers.contains(&json!("dependency_approval_set_mismatch"))); + } + + #[test] + fn usage_requires_observed_replacement_calls_over_the_compatibility_window() { + let (manifest, mut evidence) = fixture(); + let digest = evidence_digest(&manifest, "/approvalSubject/usageObservation"); + evidence.get_mut(&digest).unwrap()["details"]["replacementInvocations"] = json!(0); + evidence.get_mut(&digest).unwrap()["details"]["totalInvocations"] = json!(0); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("unproven_usage_observation"))); + } + + #[test] + fn c00_necessity_trace_must_bind_the_retirement_record() { + let (manifest, mut evidence) = fixture(); + let digest = evidence_digest(&manifest, "/approvalSubject/necessityEvidence"); + evidence.get_mut(&digest).unwrap()["details"]["changeId"] = json!("other-retirement"); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("unproven_c00_necessity"))); + } + + #[test] + fn self_reported_independence_cannot_override_owner_or_authority_policy() { + let (mut manifest, mut evidence) = fixture(); + manifest["approvalSubject"]["legacyBranch"]["owner"] = json!("code-intel-maintainers"); + let subject_sha = sha256_hex(&serde_json::to_vec(&manifest["approvalSubject"]).unwrap()); + let digest = evidence_digest(&manifest, "/independentApproval"); + let details = &mut evidence.get_mut(&digest).unwrap()["details"]; + details["subjectSha256"] = json!(subject_sha); + details["authorIndependent"] = json!(true); + details["authorityEvent"] = signed_event(details["subjectSha256"].as_str().unwrap()); + let decision = evaluate(&manifest, &evidence, NOW).unwrap(); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("unproven_independent_approval"))); + } +} diff --git a/crates/code-intel-cli/src/compatibility_retirement_ticket.rs b/crates/code-intel-cli/src/compatibility_retirement_ticket.rs new file mode 100644 index 0000000..52b6534 --- /dev/null +++ b/crates/code-intel-cli/src/compatibility_retirement_ticket.rs @@ -0,0 +1,437 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use serde_json::Value; + +use crate::artifact_ref::{ + normalized_retirement_call_path, retirement_portable_paths, + validate_retirement_deletion_diff_value, VerifiedArtifact, +}; +use crate::capability::{reject_duplicate_json_keys, sha256_hex}; +use crate::capability_inventory::{AdapterArtifact, AdapterError, AdapterOutput}; + +const TEMPLATE_SCHEMA: &str = "code-intel-compatibility-retirement-ticket-template.v1"; + +pub(crate) fn execute( + request: &Value, + inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + let options = request["options"] + .as_object() + .ok_or_else(|| AdapterError::InvalidOptions("ticket options must be an object".into()))?; + if options.len() != 1 || !options.contains_key("evaluatedAt") { + return Err(AdapterError::InvalidOptions( + "ticket template requires exactly options.evaluatedAt".into(), + )); + } + let evaluated_at = options["evaluatedAt"] + .as_u64() + .ok_or_else(|| AdapterError::InvalidOptions("evaluatedAt must be an integer".into()))?; + let one = |schema: &str| -> Result<&VerifiedArtifact, AdapterError> { + let found = inputs + .iter() + .filter(|v| v.artifact_schema() == schema) + .collect::>(); + if found.len() == 1 { + Ok(found[0]) + } else { + Err(AdapterError::Contract(format!( + "ticket requires exactly one {schema}" + ))) + } + }; + if inputs.len() != 4 { + return Err(AdapterError::Contract( + "ticket requires exactly template, E00 manifest/decision, and deletion diff inputs" + .into(), + )); + } + let template = one(TEMPLATE_SCHEMA)?; + let manifest = one("code-intel-compatibility-retirement-manifest.v1")?; + let decision = one("code-intel-compatibility-retirement-decision.v1")?; + let deletion = one("code-intel-compatibility-retirement-deletion-diff.v1")?; + validate_template(template.bytes(), evaluated_at).map_err(AdapterError::Contract)?; + let ticket: Value = serde_json::from_slice(template.bytes()) + .map_err(|e| AdapterError::Contract(format!("parse ticket: {e}")))?; + let manifest_value: Value = serde_json::from_slice(manifest.bytes()) + .map_err(|e| AdapterError::Contract(format!("parse manifest: {e}")))?; + let decision_value: Value = serde_json::from_slice(decision.bytes()) + .map_err(|e| AdapterError::Contract(format!("parse decision: {e}")))?; + let deletion_value: Value = serde_json::from_slice(deletion.bytes()) + .map_err(|e| AdapterError::Contract(format!("parse deletion diff: {e}")))?; + validate_retirement_deletion_diff_value(&deletion_value).map_err(AdapterError::Contract)?; + let snapshot = &request["snapshot"]["identity"]; + if [ + &ticket["snapshotIdentity"], + &manifest_value["snapshotIdentity"], + &decision_value["snapshotIdentity"], + &deletion_value["snapshotIdentity"], + ] + .iter() + .any(|v| *v != snapshot) + { + return Err(AdapterError::Contract( + "ticket inputs must share the A01 snapshot".into(), + )); + } + if decision_value["decision"] != "approved" + || decision_value["authorityBoundary"] != "approval_only_no_deletion_authority" + { + return Err(AdapterError::Contract( + "ticket requires an approved E00 decision".into(), + )); + } + ref_digest( + &ticket["source"]["retirementDecision"], + decision.sha256(), + "E00 decision", + )?; + ref_digest( + &ticket["source"]["retirementManifest"], + manifest.sha256(), + "E00 manifest", + )?; + ref_digest( + &ticket["evidence"]["deletionDiff"], + deletion.sha256(), + "deletion diff", + )?; + let subject = &manifest_value["approvalSubject"]; + let subject_sha = sha256_hex(&serde_json::to_vec(subject).expect("JSON subject serializes")); + if decision_value["approvalSubjectSha256"] != subject_sha { + return Err(AdapterError::Contract( + "E00 decision is not content-bound to the consumed manifest".into(), + )); + } + let ticket_call_path = normalized_retirement_call_path( + &ticket["legacyBranch"]["callPath"], + ticket["legacyBranch"]["branchId"].as_str().unwrap_or(""), + ) + .map_err(AdapterError::Contract)?; + let approved_call_path = normalized_retirement_call_path( + &subject["legacyBranch"]["callPath"], + subject["legacyBranch"]["branchId"].as_str().unwrap_or(""), + ) + .map_err(AdapterError::Contract)?; + let ticket_files = retirement_portable_paths(&ticket["affectedFiles"], "ticket affectedFiles") + .map_err(AdapterError::Contract)?; + let approved_files = retirement_portable_paths( + &subject["legacyBranch"]["affectedFiles"], + "approved affectedFiles", + ) + .map_err(AdapterError::Contract)?; + if ticket["retirementId"] != manifest_value["retirementId"] + || ticket["retirementId"] != decision_value["retirementId"] + || ticket["legacyBranch"]["capabilityId"] != subject["legacyBranch"]["capabilityId"] + || ticket["legacyBranch"]["branchId"] != subject["legacyBranch"]["branchId"] + || ticket_call_path != approved_call_path + || ticket_files != approved_files + || ticket["replacement"] + != serde_json::json!({"capabilityId":subject["replacement"]["capabilityId"],"dependencies":subject["replacement"]["dependencies"]}) + { + return Err(AdapterError::Contract( + "ticket branch/replacement differs from the approved E00 subject".into(), + )); + } + for (ticket_field, subject_ref) in [ + ("golden", &subject["parity"]["golden"]), + ("contract", &subject["parity"]["contract"]), + ("effects", &subject["parity"]["effects"]), + ("usage", &subject["usageObservation"]), + ( + "rollbackRehearsal", + &subject["rollback"]["executionEvidence"], + ), + ] { + if ticket["evidence"][ticket_field] != *subject_ref { + return Err(AdapterError::Contract(format!( + "ticket {ticket_field} evidence differs from E00" + ))); + } + } + if deletion_value["retirementId"] != ticket["retirementId"] + || deletion_value["legacyBranchId"] != ticket["legacyBranch"]["branchId"] + || retirement_portable_paths(&deletion_value["affectedFiles"], "deletion affectedFiles") + .map_err(AdapterError::Contract)? + != approved_files + || deletion_value["deletionsOnly"] != true + { + return Err(AdapterError::Contract( + "deletion diff is not scoped to the ticket".into(), + )); + } + fs::create_dir(out).map_err(|e| AdapterError::Io(format!("create ticket staging: {e}")))?; + fs::write( + out.join("compatibility-retirement-ticket.json"), + template.bytes(), + ) + .map_err(|e| AdapterError::Io(format!("write ticket: {e}")))?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: TEMPLATE_SCHEMA.into(), + artifact_type: "compatibility.retirement-ticket-template".into(), + relative_path: "compatibility-retirement-ticket.json".into(), + bytes: template.bytes().to_vec(), + }], + observed_effects: vec!["local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn ref_digest(reference: &Value, actual: &str, label: &str) -> Result<(), AdapterError> { + if reference["sha256"] == actual { + Ok(()) + } else { + Err(AdapterError::Contract(format!( + "ticket {label} reference SHA-256 mismatch" + ))) + } +} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match lint_raw(raw) { + Ok(()) => { + println!("{{\"ok\":true}}"); + 0 + } + Err(message) => { + eprintln!("error: {message}"); + 65 + } + } +} + +fn lint_raw(raw: &[String]) -> Result<(), String> { + if raw.first().map(String::as_str) != Some("lint") { + return Err("expected compatibility retirement-ticket lint".into()); + } + let mut ticket = None; + let mut evaluated_at = None; + let mut i = 1; + while i < raw.len() { + match raw[i].as_str() { + "--ticket" if i + 1 < raw.len() => ticket = Some(raw[i + 1].clone()), + "--evaluated-at" if i + 1 < raw.len() => { + evaluated_at = Some( + raw[i + 1] + .parse::() + .map_err(|_| "--evaluated-at must be an integer")?, + ) + } + other => return Err(format!("unknown retirement-ticket lint argument: {other}")), + } + i += 2; + } + let path = ticket.ok_or("--ticket is required")?; + let evaluated_at = evaluated_at.ok_or("--evaluated-at is required")?; + let bytes = fs::read(Path::new(&path)).map_err(|e| format!("read ticket: {e}"))?; + validate_template(&bytes, evaluated_at) +} + +pub(crate) fn validate_template(bytes: &[u8], evaluated_at: u64) -> Result<(), String> { + let text = std::str::from_utf8(bytes).map_err(|e| format!("ticket is not UTF-8: {e}"))?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|e| format!("ticket is not JSON: {e}"))?; + validate_template_value(&value, evaluated_at) +} + +pub(crate) fn validate_template_value(value: &Value, evaluated_at: u64) -> Result<(), String> { + exact( + value, + &[ + "schema", + "snapshotIdentity", + "ticketId", + "retirementId", + "legacyBranch", + "replacement", + "affectedFiles", + "evidence", + "source", + "owner", + "verifier", + "observationExpiry", + "status", + "authorityBoundary", + ], + "ticket", + )?; + if value["schema"] != TEMPLATE_SCHEMA + || value["status"] != "draft" + || value["authorityBoundary"] != "template_only_no_approval_or_deletion_authority" + { + return Err("ticket schema/status/authority boundary is invalid".into()); + } + for key in [ + "snapshotIdentity", + "ticketId", + "retirementId", + "owner", + "verifier", + ] { + nonempty(&value[key], key)?; + } + if value["owner"] == value["verifier"] { + return Err("owner and verifier must be independent".into()); + } + if value["observationExpiry"] + .as_u64() + .is_none_or(|expiry| expiry < evaluated_at) + { + return Err("ticket observation evidence is expired".into()); + } + exact( + &value["legacyBranch"], + &["capabilityId", "branchId", "callPath"], + "legacyBranch", + )?; + for key in ["capabilityId", "branchId", "callPath"] { + nonempty(&value["legacyBranch"][key], key)?; + } + normalized_retirement_call_path( + &value["legacyBranch"]["callPath"], + value["legacyBranch"]["branchId"].as_str().unwrap_or(""), + )?; + exact( + &value["replacement"], + &["capabilityId", "dependencies"], + "replacement", + )?; + nonempty( + &value["replacement"]["capabilityId"], + "replacement.capabilityId", + )?; + unique_strings( + &value["replacement"]["dependencies"], + false, + "replacement.dependencies", + )?; + retirement_portable_paths(&value["affectedFiles"], "affectedFiles")?; + exact( + &value["evidence"], + &[ + "golden", + "contract", + "effects", + "usage", + "rollbackRehearsal", + "deletionDiff", + ], + "evidence", + )?; + for key in [ + "golden", + "contract", + "effects", + "usage", + "rollbackRehearsal", + ] { + artifact_ref( + &value["evidence"][key], + "code-intel-compatibility-retirement-evidence.v1", + "compatibility.retirement-evidence", + )?; + } + artifact_ref( + &value["evidence"]["deletionDiff"], + "code-intel-compatibility-retirement-deletion-diff.v1", + "compatibility.retirement-deletion-diff", + )?; + exact( + &value["source"], + &["retirementDecision", "retirementManifest"], + "source", + )?; + artifact_ref( + &value["source"]["retirementDecision"], + "code-intel-compatibility-retirement-decision.v1", + "compatibility.retirement-decision", + )?; + artifact_ref( + &value["source"]["retirementManifest"], + "code-intel-compatibility-retirement-manifest.v1", + "compatibility.retirement-manifest", + )?; + Ok(()) +} + +fn artifact_ref(value: &Value, schema: &str, kind: &str) -> Result<(), String> { + exact( + value, + &[ + "schema", + "artifactSchema", + "type", + "path", + "sha256", + "consumedSnapshotIdentity", + ], + "Artifact Ref", + )?; + if value["schema"] != "code-intel-artifact-ref.v1" + || value["artifactSchema"] != schema + || value["type"] != kind + || !value["path"] + .as_str() + .is_some_and(|v| !v.is_empty() && !v.contains('\\')) + || !value["consumedSnapshotIdentity"] + .as_str() + .is_some_and(|v| !v.is_empty()) + || !value["sha256"] + .as_str() + .is_some_and(|v| v.len() == 64 && v.bytes().all(|b| b.is_ascii_hexdigit())) + { + return Err("ticket Artifact Ref is invalid".into()); + } + Ok(()) +} + +fn unique_strings(value: &Value, paths: bool, label: &str) -> Result<(), String> { + let values = value + .as_array() + .ok_or_else(|| format!("{label} must be an array"))?; + if values.is_empty() { + return Err(format!("{label} must not be empty")); + } + let mut seen = BTreeSet::new(); + for value in values { + let text = value + .as_str() + .filter(|v| !v.is_empty()) + .ok_or_else(|| format!("{label} contains an invalid value"))?; + if paths + && (text.contains('\\') || text.starts_with('/') || text.split('/').any(|p| p == "..")) + { + return Err(format!("{label} contains a non-portable path")); + } + if !seen.insert(text) { + return Err(format!("{label} contains duplicates")); + } + } + Ok(()) +} + +fn nonempty(value: &Value, label: &str) -> Result<(), String> { + if value.as_str().is_some_and(|v| !v.trim().is_empty()) { + Ok(()) + } else { + Err(format!("{label} must be non-empty")) + } +} + +fn exact(value: &Value, expected: &[&str], label: &str) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} must use the closed contract")) + } +} diff --git a/crates/code-intel-cli/src/dag_coordinator.rs b/crates/code-intel-cli/src/dag_coordinator.rs new file mode 100644 index 0000000..2181f12 --- /dev/null +++ b/crates/code-intel-cli/src/dag_coordinator.rs @@ -0,0 +1,1058 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::error::Error; +use std::fmt; + +use serde_json::{json, Value}; + +pub const DAG_SCHEMA: &str = "code-intel-run-dag.v1"; +pub const RUN_STATE_SCHEMA: &str = "code-intel-run-state.v1"; +pub const RUN_MANIFEST_SCHEMA: &str = "code-intel-run-manifest.v1"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NodeSpec { + pub id: String, + pub capability: String, + pub request_identity: String, +} + +impl NodeSpec { + pub fn new( + id: impl Into, + capability: impl Into, + request_identity: impl Into, + ) -> Self { + Self { + id: id.into(), + capability: capability.into(), + request_identity: request_identity.into(), + } + } + + pub fn to_json(&self) -> Value { + json!({ + "id": self.id, + "capability": self.capability, + "requestIdentity": self.request_identity, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct EdgeSpec { + pub from: String, + pub to: String, +} + +impl EdgeSpec { + pub fn new(from: impl Into, to: impl Into) -> Self { + Self { + from: from.into(), + to: to.into(), + } + } + + fn to_json(&self) -> Value { + json!({"from": self.from, "to": self.to}) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DagSpec { + pub snapshot_identity: String, + pub max_concurrency: usize, + pub nodes: Vec, + pub edges: Vec, +} + +impl DagSpec { + pub fn new( + snapshot_identity: impl Into, + max_concurrency: usize, + nodes: Vec, + edges: Vec, + ) -> Self { + Self { + snapshot_identity: snapshot_identity.into(), + max_concurrency, + nodes, + edges, + } + } + + pub fn to_json(&self) -> Value { + let mut nodes = self.nodes.clone(); + nodes.sort_by(|left, right| left.id.cmp(&right.id)); + let mut edges = self.edges.clone(); + edges.sort(); + json!({ + "schema": DAG_SCHEMA, + "snapshotIdentity": self.snapshot_identity, + "maxConcurrency": self.max_concurrency, + "nodes": nodes.iter().map(NodeSpec::to_json).collect::>(), + "edges": edges.iter().map(EdgeSpec::to_json).collect::>(), + }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CoordinatorErrorKind { + InvalidSpec, + DuplicateNode, + UnknownNode, + DuplicateEdge, + Cycle, + InvalidTransition, + ResumeIdentityMismatch, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CoordinatorError { + kind: CoordinatorErrorKind, + message: String, +} + +impl CoordinatorError { + fn new(kind: CoordinatorErrorKind, message: impl Into) -> Self { + Self { + kind, + message: message.into(), + } + } + + pub fn kind(&self) -> CoordinatorErrorKind { + self.kind + } +} + +impl fmt::Display for CoordinatorError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl Error for CoordinatorError {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifiedArtifactRef { + artifact_schema: String, + artifact_type: String, + path: String, + sha256: String, + consumed_snapshot_identity: String, +} + +impl VerifiedArtifactRef { + #[cfg(test)] + pub(super) fn verified_for_test( + artifact_schema: impl Into, + artifact_type: impl Into, + path: impl Into, + sha256: impl Into, + consumed_snapshot_identity: impl Into, + ) -> Result { + let value = Self { + artifact_schema: artifact_schema.into(), + artifact_type: artifact_type.into(), + path: path.into(), + sha256: sha256.into(), + consumed_snapshot_identity: consumed_snapshot_identity.into(), + }; + value.validate()?; + Ok(value) + } + + pub(crate) fn from_a03( + path: impl Into, + verified: &crate::artifact_ref::VerifiedArtifact, + ) -> Result { + Self::from_a03_fields( + verified.artifact_schema().to_string(), + verified.artifact_type().to_string(), + path, + verified.sha256().to_string(), + verified.consumed_snapshot_identity().to_string(), + ) + } + + fn from_a03_fields( + artifact_schema: impl Into, + artifact_type: impl Into, + path: impl Into, + sha256: impl Into, + consumed_snapshot_identity: impl Into, + ) -> Result { + let value = Self { + artifact_schema: artifact_schema.into(), + artifact_type: artifact_type.into(), + path: path.into(), + sha256: sha256.into(), + consumed_snapshot_identity: consumed_snapshot_identity.into(), + }; + value.validate()?; + Ok(value) + } + + fn validate(&self) -> Result<(), CoordinatorError> { + if self.artifact_schema.is_empty() + || self.artifact_type.is_empty() + || self.path.is_empty() + || !valid_digest(&self.sha256) + || !valid_digest(&self.consumed_snapshot_identity) + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidSpec, + "verified Artifact Ref fields are invalid", + )); + } + Ok(()) + } + + pub fn path(&self) -> &str { + &self.path + } + + pub(crate) fn artifact_type(&self) -> &str { + &self.artifact_type + } + + pub(crate) fn to_json(&self) -> Value { + json!({ + "schema": "code-intel-artifact-ref.v1", + "artifactSchema": self.artifact_schema, + "type": self.artifact_type, + "path": self.path, + "sha256": self.sha256, + "consumedSnapshotIdentity": self.consumed_snapshot_identity, + }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DomainVerdict { + Pass, + Fail, + Unknown, + NotApplicable, +} + +impl DomainVerdict { + fn as_str(self) -> &'static str { + match self { + Self::Pass => "pass", + Self::Fail => "fail", + Self::Unknown => "unknown", + Self::NotApplicable => "not_applicable", + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ExecutionFailure { + Contract, + Unavailable, + Internal, + Io, +} + +impl ExecutionFailure { + fn as_str(self) -> &'static str { + match self { + Self::Contract => "contract", + Self::Unavailable => "unavailable", + Self::Internal => "internal", + Self::Io => "io", + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NodeOutcome { + Success { + verdict: DomainVerdict, + artifacts: Vec, + }, + DomainFail { + diagnostic: String, + artifacts: Vec, + }, + ProcessFailure { + failure: ExecutionFailure, + diagnostic: String, + }, +} + +impl NodeOutcome { + pub fn success(verdict: DomainVerdict, artifacts: Vec) -> Self { + if verdict == DomainVerdict::Fail { + Self::DomainFail { + diagnostic: "executor returned a domain fail verdict".to_string(), + artifacts, + } + } else { + Self::Success { verdict, artifacts } + } + } + + pub fn domain_fail(diagnostic: impl Into) -> Self { + Self::DomainFail { + diagnostic: diagnostic.into(), + artifacts: Vec::new(), + } + } + + pub fn domain_fail_with_artifacts( + diagnostic: impl Into, + artifacts: Vec, + ) -> Self { + Self::DomainFail { + diagnostic: diagnostic.into(), + artifacts, + } + } + + pub fn process_failure(failure: ExecutionFailure, diagnostic: impl Into) -> Self { + Self::ProcessFailure { + failure, + diagnostic: diagnostic.into(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NodeState { + Pending, + Running, + Succeeded { + verdict: DomainVerdict, + artifacts: Vec, + }, + DomainFailed { + diagnostic: String, + artifacts: Vec, + }, + ProcessFailed { + failure: ExecutionFailure, + diagnostic: String, + }, + DependencyBlocked { + blocked_by: Vec, + }, +} + +impl NodeState { + fn is_terminal(&self) -> bool { + matches!( + self, + Self::Succeeded { .. } + | Self::DomainFailed { .. } + | Self::ProcessFailed { .. } + | Self::DependencyBlocked { .. } + ) + } + + fn blocks_dependents(&self) -> bool { + matches!( + self, + Self::DomainFailed { .. } | Self::ProcessFailed { .. } | Self::DependencyBlocked { .. } + ) + } + + fn to_json(&self) -> Value { + match self { + Self::Pending => json!({"status":"pending"}), + Self::Running => json!({"status":"running"}), + Self::Succeeded { verdict, artifacts } => json!({ + "status":"succeeded", + "verdict":verdict.as_str(), + "artifacts":artifacts.iter().map(VerifiedArtifactRef::to_json).collect::>(), + }), + Self::DomainFailed { + diagnostic, + artifacts, + } => json!({ + "status":"domain_failed", + "verdict":"fail", + "diagnostic":diagnostic, + "artifacts":artifacts.iter().map(VerifiedArtifactRef::to_json).collect::>(), + }), + Self::ProcessFailed { + failure, + diagnostic, + } => json!({ + "status":"process_failed", + "failure":failure.as_str(), + "diagnostic":diagnostic, + }), + Self::DependencyBlocked { blocked_by } => json!({ + "status":"dependency_blocked", + "blockedBy":blocked_by, + }), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Dispatch { + pub node_id: String, + pub capability: String, + pub request_identity: String, + pub inputs: Vec, +} + +pub trait NodeExecutor: Sync { + fn execute(&self, dispatch: Dispatch) -> NodeOutcome; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RunCheckpoint { + pub schema: &'static str, + pub run_identity: String, + pub nodes: BTreeMap, +} + +impl RunCheckpoint { + pub fn to_json(&self) -> Value { + json!({ + "schema":self.schema, + "runIdentity":self.run_identity, + "nodes":self.nodes.iter().map(|(id,state)|(id.clone(),state.to_json())).collect::>(), + }) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RunOutcome(&'static str); + +impl RunOutcome { + pub fn as_str(&self) -> &'static str { + self.0 + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RunManifest { + pub schema: &'static str, + pub run_identity: String, + pub snapshot_identity: String, + pub outcome: RunOutcome, + pub nodes: BTreeMap, +} + +impl RunManifest { + pub fn to_json(&self) -> Value { + json!({ + "schema":self.schema, + "runIdentity":self.run_identity, + "snapshotIdentity":self.snapshot_identity, + "outcome":self.outcome.as_str(), + "nodes":self.nodes.iter().map(|(id,state)|(id.clone(),state.to_json())).collect::>(), + }) + } +} + +#[derive(Debug)] +pub struct Coordinator { + spec: DagSpec, + run_identity: String, + nodes: BTreeMap, + dependencies: BTreeMap>, + states: BTreeMap, +} + +impl Coordinator { + pub fn new(spec: DagSpec) -> Result { + let (nodes, dependencies) = validate_spec(&spec)?; + let states = nodes + .keys() + .map(|id| (id.clone(), NodeState::Pending)) + .collect(); + Ok(Self { + run_identity: identity_for(&spec), + spec, + nodes, + dependencies, + states, + }) + } + + pub fn resume(spec: DagSpec, checkpoint: RunCheckpoint) -> Result { + let mut coordinator = Self::new(spec)?; + if checkpoint.schema != RUN_STATE_SCHEMA + || checkpoint.run_identity != coordinator.run_identity + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + "checkpoint does not match the deterministic DAG identity", + )); + } + for id in checkpoint.nodes.keys() { + if !coordinator.nodes.contains_key(id) { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + format!("checkpoint contains unknown node: {id}"), + )); + } + } + coordinator.validate_checkpoint_history(&checkpoint.nodes)?; + for (id, state) in checkpoint.nodes { + let restored = match state { + NodeState::Running | NodeState::Pending => NodeState::Pending, + NodeState::DependencyBlocked { .. } => NodeState::Pending, + NodeState::Succeeded { verdict, artifacts } => { + if verdict == DomainVerdict::Fail + || artifacts.iter().any(|artifact| { + artifact.consumed_snapshot_identity + != coordinator.spec.snapshot_identity + }) + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + format!("checkpoint has invalid completed node: {id}"), + )); + } + NodeState::Succeeded { verdict, artifacts } + } + NodeState::DomainFailed { + diagnostic, + artifacts, + } => { + if diagnostic.trim().is_empty() + || artifacts.iter().any(|artifact| { + artifact.consumed_snapshot_identity + != coordinator.spec.snapshot_identity + }) + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + format!("checkpoint has invalid domain-failed node: {id}"), + )); + } + NodeState::DomainFailed { + diagnostic, + artifacts, + } + } + NodeState::ProcessFailed { + failure, + diagnostic, + } => { + if diagnostic.trim().is_empty() { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + format!("checkpoint has empty process diagnostic: {id}"), + )); + } + NodeState::ProcessFailed { + failure, + diagnostic, + } + } + }; + coordinator.states.insert(id, restored); + } + coordinator.propagate_blocked(); + Ok(coordinator) + } + + pub fn run_identity(&self) -> &str { + &self.run_identity + } + + pub fn state(&self, node_id: &str) -> Option<&NodeState> { + self.states.get(node_id) + } + + pub fn next_batch(&mut self) -> Result, CoordinatorError> { + self.propagate_blocked(); + let running = self + .states + .values() + .filter(|state| matches!(state, NodeState::Running)) + .count(); + let available = self.spec.max_concurrency.saturating_sub(running); + if available == 0 { + return Ok(Vec::new()); + } + let ready = self + .states + .iter() + .filter_map(|(id, state)| { + matches!(state, NodeState::Pending) + .then_some(id) + .filter(|id| self.dependencies_succeeded(id)) + }) + .take(available) + .cloned() + .collect::>(); + + let mut dispatches = Vec::with_capacity(ready.len()); + for id in ready { + let node = self.nodes.get(&id).expect("validated node"); + let mut inputs = Vec::new(); + for dependency in self.dependencies.get(&id).expect("validated dependencies") { + if let Some(NodeState::Succeeded { artifacts, .. }) = self.states.get(dependency) { + inputs.extend(artifacts.iter().cloned()); + } + } + self.states.insert(id.clone(), NodeState::Running); + dispatches.push(Dispatch { + node_id: id, + capability: node.capability.clone(), + request_identity: node.request_identity.clone(), + inputs, + }); + } + Ok(dispatches) + } + + pub fn record(&mut self, node_id: &str, outcome: NodeOutcome) -> Result<(), CoordinatorError> { + if !matches!(self.states.get(node_id), Some(NodeState::Running)) { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidTransition, + format!("node is not running: {node_id}"), + )); + } + let state = match outcome { + NodeOutcome::Success { verdict, artifacts } => { + if artifacts.iter().any(|artifact| { + artifact.consumed_snapshot_identity != self.spec.snapshot_identity + }) { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidTransition, + format!("node returned Artifact Ref for another snapshot: {node_id}"), + )); + } + if verdict == DomainVerdict::Fail { + NodeState::DomainFailed { + diagnostic: "executor returned a domain fail verdict".to_string(), + artifacts, + } + } else { + NodeState::Succeeded { verdict, artifacts } + } + } + NodeOutcome::DomainFail { + diagnostic, + artifacts, + } => { + if artifacts.iter().any(|artifact| { + artifact.consumed_snapshot_identity != self.spec.snapshot_identity + }) { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidTransition, + format!("node returned Artifact Ref for another snapshot: {node_id}"), + )); + } + NodeState::DomainFailed { + diagnostic: nonempty_diagnostic( + diagnostic, + "domain failure without diagnostic", + ), + artifacts, + } + } + NodeOutcome::ProcessFailure { + failure, + diagnostic, + } => NodeState::ProcessFailed { + failure, + diagnostic: nonempty_diagnostic(diagnostic, "process failure without diagnostic"), + }, + }; + self.states.insert(node_id.to_string(), state); + self.propagate_blocked(); + Ok(()) + } + + pub fn checkpoint(&self) -> RunCheckpoint { + let nodes = self + .states + .iter() + .map(|(id, state)| { + let persisted = if matches!(state, NodeState::Running) { + NodeState::Pending + } else { + state.clone() + }; + (id.clone(), persisted) + }) + .collect(); + RunCheckpoint { + schema: RUN_STATE_SCHEMA, + run_identity: self.run_identity.clone(), + nodes, + } + } + + pub fn is_terminal(&self) -> bool { + self.states.values().all(NodeState::is_terminal) + } + + pub fn run_to_completion( + mut self, + executor: &E, + ) -> Result { + while !self.is_terminal() { + let batch = self.next_batch()?; + if batch.is_empty() { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidTransition, + "DAG has unfinished nodes but no schedulable work", + )); + } + let mut results = std::thread::scope(|scope| { + batch + .into_iter() + .map(|dispatch| { + let id = dispatch.node_id.clone(); + scope.spawn(move || (id, executor.execute(dispatch))) + }) + .collect::>() + .into_iter() + .map(|handle| { + handle.join().map_err(|_| { + CoordinatorError::new( + CoordinatorErrorKind::InvalidTransition, + "node executor panicked", + ) + }) + }) + .collect::, _>>() + })?; + results.sort_by(|left, right| left.0.cmp(&right.0)); + for (id, outcome) in results { + self.record(&id, outcome)?; + } + } + Ok(self.manifest()) + } + + pub fn manifest(&self) -> RunManifest { + let outcome = if self + .states + .values() + .any(|state| matches!(state, NodeState::ProcessFailed { .. })) + { + RunOutcome("process_failed") + } else if self + .states + .values() + .any(|state| matches!(state, NodeState::DomainFailed { .. })) + { + RunOutcome("domain_failed") + } else if self.states.values().any(|state| { + matches!( + state, + NodeState::Succeeded { + verdict: DomainVerdict::Unknown, + .. + } + ) + }) { + RunOutcome("domain_unknown") + } else if self.is_terminal() { + RunOutcome("completed") + } else { + RunOutcome("incomplete") + }; + RunManifest { + schema: RUN_MANIFEST_SCHEMA, + run_identity: self.run_identity.clone(), + snapshot_identity: self.spec.snapshot_identity.clone(), + outcome, + nodes: self.states.clone(), + } + } + + fn dependencies_succeeded(&self, node_id: &str) -> bool { + self.dependencies + .get(node_id) + .expect("validated dependencies") + .iter() + .all(|dependency| { + matches!( + self.states.get(dependency), + Some(NodeState::Succeeded { .. }) + ) + }) + } + + fn validate_checkpoint_history( + &self, + checkpoint: &BTreeMap, + ) -> Result<(), CoordinatorError> { + for (id, state) in checkpoint { + let dependencies = self.dependencies.get(id).expect("validated dependencies"); + match state { + NodeState::Succeeded { .. } + | NodeState::DomainFailed { .. } + | NodeState::ProcessFailed { .. } + if !dependencies.iter().all(|dependency| { + matches!( + checkpoint.get(dependency), + Some(NodeState::Succeeded { .. }) + ) + }) => + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + format!("checkpoint finalized node before its dependencies: {id}"), + )); + } + NodeState::DependencyBlocked { blocked_by } => { + let expected = dependencies + .iter() + .filter(|dependency| { + checkpoint + .get(*dependency) + .is_some_and(NodeState::blocks_dependents) + }) + .cloned() + .collect::>(); + if expected.is_empty() || *blocked_by != expected { + return Err(CoordinatorError::new( + CoordinatorErrorKind::ResumeIdentityMismatch, + format!("checkpoint has inconsistent dependency block history: {id}"), + )); + } + } + _ => {} + } + } + Ok(()) + } + + fn propagate_blocked(&mut self) { + loop { + let blocked = self + .states + .iter() + .filter(|(_, state)| matches!(state, NodeState::Pending)) + .filter_map(|(id, _)| { + let blocked_by = self + .dependencies + .get(id) + .expect("validated dependencies") + .iter() + .filter(|dependency| { + self.states + .get(*dependency) + .is_some_and(NodeState::blocks_dependents) + }) + .cloned() + .collect::>(); + (!blocked_by.is_empty()).then_some((id.clone(), blocked_by)) + }) + .collect::>(); + if blocked.is_empty() { + break; + } + for (id, blocked_by) in blocked { + self.states + .insert(id, NodeState::DependencyBlocked { blocked_by }); + } + } + } +} + +fn validate_spec( + spec: &DagSpec, +) -> Result<(BTreeMap, BTreeMap>), CoordinatorError> { + if !valid_digest(&spec.snapshot_identity) + || spec.nodes.is_empty() + || spec.max_concurrency == 0 + || spec.max_concurrency > 256 + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidSpec, + "DAG snapshot, nodes, or concurrency bound is invalid", + )); + } + let mut nodes = BTreeMap::new(); + for node in &spec.nodes { + if !valid_id(&node.id) + || !valid_id(&node.capability) + || node.request_identity.trim().is_empty() + { + return Err(CoordinatorError::new( + CoordinatorErrorKind::InvalidSpec, + format!("invalid DAG node: {}", node.id), + )); + } + if nodes.insert(node.id.clone(), node.clone()).is_some() { + return Err(CoordinatorError::new( + CoordinatorErrorKind::DuplicateNode, + format!("duplicate DAG node: {}", node.id), + )); + } + } + let mut dependencies = nodes + .keys() + .map(|id| (id.clone(), Vec::new())) + .collect::>(); + let mut edges = BTreeSet::new(); + for edge in &spec.edges { + if !nodes.contains_key(&edge.from) || !nodes.contains_key(&edge.to) { + return Err(CoordinatorError::new( + CoordinatorErrorKind::UnknownNode, + format!( + "DAG edge references unknown node: {} -> {}", + edge.from, edge.to + ), + )); + } + if !edges.insert(edge.clone()) { + return Err(CoordinatorError::new( + CoordinatorErrorKind::DuplicateEdge, + format!("duplicate DAG edge: {} -> {}", edge.from, edge.to), + )); + } + dependencies + .get_mut(&edge.to) + .expect("known edge target") + .push(edge.from.clone()); + } + for values in dependencies.values_mut() { + values.sort(); + } + validate_acyclic(&nodes, &dependencies)?; + Ok((nodes, dependencies)) +} + +fn validate_acyclic( + nodes: &BTreeMap, + dependencies: &BTreeMap>, +) -> Result<(), CoordinatorError> { + let mut remaining = dependencies + .iter() + .map(|(id, values)| (id.clone(), values.len())) + .collect::>(); + let mut ready = remaining + .iter() + .filter_map(|(id, count)| (*count == 0).then_some(id.clone())) + .collect::>(); + let mut visited = 0; + while let Some(id) = ready.pop_first() { + visited += 1; + for (target, values) in dependencies { + if values.binary_search(&id).is_ok() { + let count = remaining.get_mut(target).expect("known target"); + *count -= 1; + if *count == 0 { + ready.insert(target.clone()); + } + } + } + } + if visited != nodes.len() { + return Err(CoordinatorError::new( + CoordinatorErrorKind::Cycle, + "DAG contains a dependency cycle", + )); + } + Ok(()) +} + +fn identity_for(spec: &DagSpec) -> String { + let canonical = serde_json::to_vec(&spec.to_json()).expect("DAG spec is JSON serializable"); + format!("dag-v1:{}", sha256_hex(&canonical)) +} + +fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0) + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (index, word) in chunk.chunks_exact(4).enumerate() { + w[index] = u32::from_be_bytes(word.try_into().expect("four-byte SHA-256 word")) + } + for index in 16..64 { + let s0 = w[index - 15].rotate_right(7) + ^ w[index - 15].rotate_right(18) + ^ (w[index - 15] >> 3); + let s1 = w[index - 2].rotate_right(17) + ^ w[index - 2].rotate_right(19) + ^ (w[index - 2] >> 10); + w[index] = w[index - 16] + .wrapping_add(s0) + .wrapping_add(w[index - 7]) + .wrapping_add(s1) + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for index in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let choose = (e & f) ^ (!e & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(choose) + .wrapping_add(K[index]) + .wrapping_add(w[index]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let majority = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(majority); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2) + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value) + } + } + h.iter().map(|value| format!("{value:08x}")).collect() +} + +fn valid_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn nonempty_diagnostic(value: String, fallback: &str) -> String { + if value.trim().is_empty() { + fallback.to_string() + } else { + value + } +} + +fn valid_id(value: &str) -> bool { + !value.is_empty() + && value.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || (index > 0 && matches!(byte, b'.' | b'_' | b'-')) + }) +} diff --git a/crates/code-intel-cli/src/dag_run.rs b/crates/code-intel-cli/src/dag_run.rs new file mode 100644 index 0000000..2913a40 --- /dev/null +++ b/crates/code-intel-cli/src/dag_run.rs @@ -0,0 +1,755 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde_json::{json, Value}; + +use crate::artifact_ref::{self, ArtifactError}; +use crate::capability::sha256_hex; +use crate::dag_coordinator::{ + Coordinator, DagSpec, Dispatch, DomainVerdict, EdgeSpec, ExecutionFailure, NodeExecutor, + NodeOutcome, NodeSpec, VerifiedArtifactRef, +}; +use crate::snapshot; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let cli = match Cli::parse(raw) { + Ok(cli) => cli, + Err(error) => { + eprintln!("{error}"); + return 64; + } + }; + match execute(cli) { + Ok(manifest) => { + println!( + "{}", + serde_json::to_string(&manifest).expect("run manifest serializes") + ); + match manifest["outcome"].as_str() { + Some("completed") => 0, + Some("domain_failed") => 10, + Some("domain_unknown") => 20, + Some("process_failed" | "incomplete") => 70, + _ => 70, + } + } + Err(error) => { + eprintln!("{}", error.message); + error.exit_code + } + } +} + +struct Cli { + repo: PathBuf, + out: PathBuf, + manifest: Option, + max_concurrency: usize, + working_tree_policy: String, + scopes: Vec, + diagnosis_inputs: Option, + seed_artifact_root: Option, + doctor_tool_path_prefix: Option, + session_evidence: Option, +} + +impl Cli { + fn parse(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("dag-coordinate") { + return Err("usage: run dag-coordinate --repo --out [--manifest ] [--max-concurrency ] [--working-tree-policy ] [--scope ]... [--session-evidence ] [--diagnosis-inputs --seed-artifact-root ] [--doctor-tool-path-prefix ]".into()); + } + let mut repo = None; + let mut out = None; + let mut manifest = None; + let mut max_concurrency = 2usize; + let mut working_tree_policy = "explicit_overlay".to_string(); + let mut scopes = Vec::new(); + let mut diagnosis_inputs = None; + let mut seed_artifact_root = None; + let mut doctor_tool_path_prefix = None; + let mut session_evidence = None; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--repo" + | "--out" + | "--manifest" + | "--max-concurrency" + | "--working-tree-policy" + | "--scope" + | "--diagnosis-inputs" + | "--seed-artifact-root" + | "--doctor-tool-path-prefix" + | "--session-evidence" + ) { + return Err(format!("unknown DAG run argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires one value"))?; + match flag { + "--repo" if repo.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate --repo".into()) + } + "--out" if out.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate --out".into()) + } + "--manifest" if manifest.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate --manifest".into()) + } + "--max-concurrency" => { + max_concurrency = value + .parse::() + .map_err(|_| "--max-concurrency must be an integer".to_string())?; + } + "--working-tree-policy" => working_tree_policy = value.clone(), + "--scope" => scopes.push(value.clone()), + "--diagnosis-inputs" + if diagnosis_inputs.replace(PathBuf::from(value)).is_some() => + { + return Err("duplicate --diagnosis-inputs".into()) + } + "--seed-artifact-root" + if seed_artifact_root.replace(PathBuf::from(value)).is_some() => + { + return Err("duplicate --seed-artifact-root".into()) + } + "--doctor-tool-path-prefix" + if doctor_tool_path_prefix + .replace(PathBuf::from(value)) + .is_some() => + { + return Err("duplicate --doctor-tool-path-prefix".into()) + } + "--session-evidence" + if session_evidence.replace(PathBuf::from(value)).is_some() => + { + return Err("duplicate --session-evidence".into()) + } + _ => {} + } + index += 2; + } + let repo = repo.ok_or("--repo is required")?; + if !repo.is_dir() { + return Err(format!( + "repository path is not a directory: {}", + repo.display() + )); + } + if scopes.is_empty() { + scopes.push(".".into()); + } + if diagnosis_inputs.is_some() != seed_artifact_root.is_some() { + return Err( + "--diagnosis-inputs and --seed-artifact-root must be provided together".into(), + ); + } + if diagnosis_inputs.is_some() && doctor_tool_path_prefix.is_some() { + return Err( + "--doctor-tool-path-prefix is valid only for the default DAG containing doctor" + .into(), + ); + } + if diagnosis_inputs.is_some() && session_evidence.is_some() { + return Err("--session-evidence is valid only for the default analysis DAG".into()); + } + if let Some(path) = &session_evidence { + if !path.is_file() { + return Err(format!( + "session evidence path is not a file: {}", + path.display() + )); + } + } + if let Some(prefix) = &mut doctor_tool_path_prefix { + if !prefix.is_dir() { + return Err(format!( + "--doctor-tool-path-prefix is not a directory: {}", + prefix.display() + )); + } + if prefix.is_relative() { + *prefix = std::env::current_dir() + .map_err(|error| format!("resolve current directory: {error}"))? + .join(&*prefix); + } + } + Ok(Self { + repo, + out: out.ok_or("--out is required")?, + manifest, + max_concurrency, + working_tree_policy, + scopes, + diagnosis_inputs, + seed_artifact_root, + doctor_tool_path_prefix, + session_evidence, + }) + } +} + +struct RunError { + exit_code: i32, + message: String, +} + +impl RunError { + fn contract(message: impl Into) -> Self { + Self { + exit_code: 65, + message: message.into(), + } + } + + fn io(message: impl Into) -> Self { + Self { + exit_code: 74, + message: message.into(), + } + } +} + +fn execute(cli: Cli) -> Result { + let snapshot_document = + snapshot::build_for_dag(&cli.repo, &cli.working_tree_policy, &cli.scopes) + .map_err(RunError::contract)?; + let snapshot_identity = snapshot_document["snapshot"]["identity"] + .as_str() + .expect("A02 snapshot has validated identity") + .to_string(); + let registry_path = cli.manifest.unwrap_or_else(default_registry); + let diagnosis_mode = cli.diagnosis_inputs.is_some(); + let (seeded_inputs, seed_artifact_root) = if let (Some(input_path), Some(artifact_root)) = + (&cli.diagnosis_inputs, &cli.seed_artifact_root) + { + let raw = fs::read(input_path).map_err(|error| { + RunError::io(format!("read seeded diagnosis Artifact Refs: {error}")) + })?; + let refs: Value = serde_json::from_slice(&raw).map_err(|error| { + RunError::contract(format!("parse seeded diagnosis Artifact Refs: {error}")) + })?; + let verified = artifact_ref::verify_inputs(&refs, Some(artifact_root), &snapshot_identity) + .map_err(|error| match error { + ArtifactError::Contract(message) => RunError::contract(message), + ArtifactError::Io(message) => RunError::io(message), + })?; + let ref_values = refs + .as_array() + .ok_or_else(|| RunError::contract("seeded diagnosis Artifact Refs must be an array"))?; + if ref_values.is_empty() { + return Err(RunError::contract( + "seeded diagnosis Artifact Refs must not be empty", + )); + } + let converted = ref_values + .iter() + .zip(verified.iter()) + .map(|(artifact, verified)| { + VerifiedArtifactRef::from_a03( + artifact["path"] + .as_str() + .expect("A03 verified Artifact Ref path"), + verified, + ) + .map_err(|error| RunError::contract(error.to_string())) + }) + .collect::, _>>()?; + (converted, Some(artifact_root.clone())) + } else { + (Vec::new(), None) + }; + let registry = read_registry(®istry_path)?; + let declarations = declarations(®istry)?; + let required = if diagnosis_mode { + vec!["diagnosis.hospital"] + } else { + vec![ + "repo.snapshot", + "doctor", + "inventory.rg", + "evidence.native-code", + "provider.graph-adapt", + "provider.sentrux-adapt", + "diagnosis.hospital", + ] + }; + for required in required { + if !declarations.contains_key(required) { + return Err(RunError::contract(format!( + "DAG capability is not registered: {required}" + ))); + } + } + fs::create_dir(&cli.out).map_err(|error| { + RunError::io(format!( + "exclusive DAG staging create {}: {error}", + cli.out.display() + )) + })?; + let session_inputs = match cli.session_evidence.as_deref() { + Some(path) => stage_session_evidence(&cli.out, path, &snapshot_identity)?, + None => Vec::new(), + }; + let request_identity = |capability: &str| format!("{capability}:{snapshot_identity}"); + let spec = if diagnosis_mode { + DagSpec::new( + &snapshot_identity, + 1, + vec![ + NodeSpec::new( + "seed.admitted-evidence", + "a03.seeded-inputs", + request_identity("a03.seeded-inputs"), + ), + NodeSpec::new( + "diagnosis.hospital", + "diagnosis.hospital", + request_identity("diagnosis.hospital"), + ), + ], + vec![EdgeSpec::new( + "seed.admitted-evidence", + "diagnosis.hospital", + )], + ) + } else { + let mut nodes = vec![ + NodeSpec::new( + "repo.snapshot", + "repo.snapshot", + request_identity("repo.snapshot"), + ), + NodeSpec::new("doctor", "doctor", request_identity("doctor")), + NodeSpec::new( + "inventory.rg", + "inventory.rg", + request_identity("inventory.rg"), + ), + NodeSpec::new( + "evidence.native-code", + "evidence.native-code", + request_identity("evidence.native-code"), + ), + NodeSpec::new( + "evidence.graph", + "provider.graph-adapt", + request_identity("provider.graph-adapt"), + ), + NodeSpec::new( + "evidence.sentrux", + "provider.sentrux-adapt", + request_identity("provider.sentrux-adapt"), + ), + NodeSpec::new( + "diagnosis.hospital", + "diagnosis.hospital", + request_identity("diagnosis.hospital"), + ), + ]; + let mut edges = vec![ + EdgeSpec::new("repo.snapshot", "doctor"), + EdgeSpec::new("repo.snapshot", "inventory.rg"), + EdgeSpec::new("inventory.rg", "evidence.native-code"), + EdgeSpec::new("repo.snapshot", "evidence.graph"), + EdgeSpec::new("repo.snapshot", "evidence.sentrux"), + EdgeSpec::new("evidence.graph", "diagnosis.hospital"), + EdgeSpec::new("evidence.sentrux", "diagnosis.hospital"), + ]; + if !session_inputs.is_empty() { + nodes.push(NodeSpec::new( + "verification.session-evidence", + "a03.session-evidence", + request_identity("a03.session-evidence"), + )); + edges.push(EdgeSpec::new( + "repo.snapshot", + "verification.session-evidence", + )); + } + DagSpec::new(&snapshot_identity, cli.max_concurrency, nodes, edges) + }; + let run_root = cli.out.clone(); + let executor = CapabilityEnvelopeExecutor { + binary: std::env::current_exe() + .map_err(|error| RunError::io(format!("locate code-intel executable: {error}")))?, + repo: cli.repo, + run_root: run_root.clone(), + registry_path, + snapshot: snapshot_document["snapshot"].clone(), + declarations, + seeded_inputs, + session_inputs, + seed_artifact_root, + doctor_tool_path_prefix: cli.doctor_tool_path_prefix, + }; + let manifest = Coordinator::new(spec) + .map_err(|error| RunError::contract(error.to_string()))? + .run_to_completion(&executor) + .map_err(|error| RunError::contract(error.to_string()))?; + let manifest = manifest.to_json(); + persist_commit_handoff(&run_root, &manifest, &snapshot_identity)?; + Ok(manifest) +} + +fn stage_session_evidence( + run_root: &Path, + source: &Path, + snapshot_identity: &str, +) -> Result, RunError> { + let metadata = fs::metadata(source) + .map_err(|error| RunError::io(format!("inspect session evidence: {error}")))?; + if !metadata.is_file() || metadata.len() > 128 * 1024 * 1024 { + return Err(RunError::contract( + "session evidence must be a regular file no larger than 128 MiB", + )); + } + let bytes = fs::read(source) + .map_err(|error| RunError::io(format!("read session evidence: {error}")))?; + let value: Value = serde_json::from_slice(&bytes) + .map_err(|error| RunError::contract(format!("parse session evidence: {error}")))?; + crate::session_evidence::validate_artifact_value(&value).map_err(RunError::contract)?; + if value["snapshot"]["identity"] != snapshot_identity { + return Err(RunError::contract( + "session evidence snapshot does not match the A09 run snapshot", + )); + } + let relative = "verification.session-evidence/session-evidence.json"; + let directory = run_root.join("verification.session-evidence"); + fs::create_dir(&directory) + .map_err(|error| RunError::io(format!("create session evidence staging: {error}")))?; + fs::write(run_root.join(relative), &bytes) + .map_err(|error| RunError::io(format!("stage session evidence: {error}")))?; + let reference = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-session-evidence.v1", + "type":"verification.session-evidence", + "path":relative, + "sha256":sha256_hex(&bytes), + "consumedSnapshotIdentity":snapshot_identity, + }); + let refs = json!([reference]); + let verified = + artifact_ref::verify_inputs(&refs, Some(run_root), snapshot_identity).map_err(|error| { + match error { + ArtifactError::Contract(message) => RunError::contract(message), + ArtifactError::Io(message) => RunError::io(message), + } + })?; + let converted = VerifiedArtifactRef::from_a03(relative, &verified[0]) + .map_err(|error| RunError::contract(error.to_string()))?; + Ok(vec![converted]) +} + +fn persist_commit_handoff( + run_root: &Path, + manifest: &Value, + snapshot_identity: &str, +) -> Result<(), RunError> { + let manifest_bytes = serde_json::to_vec(manifest) + .map_err(|error| RunError::contract(format!("serialize terminal run manifest: {error}")))?; + let manifest_name = "run-manifest.json"; + fs::write(run_root.join(manifest_name), &manifest_bytes) + .map_err(|error| RunError::io(format!("write terminal run manifest: {error}")))?; + let manifest_ref = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-run-manifest.v1", + "type":"run.manifest", + "path":manifest_name, + "sha256":sha256_hex(&manifest_bytes), + "consumedSnapshotIdentity":snapshot_identity, + }); + fs::write( + run_root.join("run-manifest-ref.json"), + serde_json::to_vec(&manifest_ref).expect("run manifest Artifact Ref serializes"), + ) + .map_err(|error| RunError::io(format!("write run manifest Artifact Ref: {error}"))) +} + +struct CapabilityEnvelopeExecutor { + binary: PathBuf, + repo: PathBuf, + run_root: PathBuf, + registry_path: PathBuf, + snapshot: Value, + declarations: BTreeMap, + seeded_inputs: Vec, + session_inputs: Vec, + seed_artifact_root: Option, + doctor_tool_path_prefix: Option, +} + +impl NodeExecutor for CapabilityEnvelopeExecutor { + fn execute(&self, dispatch: Dispatch) -> NodeOutcome { + match self.execute_node(dispatch) { + Ok(outcome) => outcome, + Err((failure, message)) => NodeOutcome::process_failure(failure, message), + } + } +} + +impl CapabilityEnvelopeExecutor { + fn execute_node(&self, dispatch: Dispatch) -> Result { + if dispatch.capability == "a03.seeded-inputs" { + if self.seeded_inputs.is_empty() { + return Err(( + ExecutionFailure::Contract, + "seeded diagnosis path requires at least one A03-verified Artifact Ref".into(), + )); + } + return Ok(NodeOutcome::success( + DomainVerdict::Pass, + self.seeded_inputs.clone(), + )); + } + if dispatch.capability == "a03.session-evidence" { + if self.session_inputs.len() != 1 { + return Err(( + ExecutionFailure::Contract, + "optional session verification requires one A03-verified artifact".into(), + )); + } + return Ok(NodeOutcome::success( + DomainVerdict::Pass, + self.session_inputs.clone(), + )); + } + let declaration = self.declarations.get(&dispatch.capability).ok_or_else(|| { + ( + ExecutionFailure::Contract, + format!("unregistered DAG capability: {}", dispatch.capability), + ) + })?; + let node_out = self.run_root.join(&dispatch.node_id); + let request_path = self + .run_root + .join(format!("{}.request.json", dispatch.node_id)); + let mut options = match dispatch.capability.as_str() { + "diagnosis.hospital" => json!({}), + "doctor" => json!({"repoPath":self.repo,"manifestPath":self.registry_path}), + _ => json!({"repoPath":self.repo}), + }; + if dispatch.capability == "doctor" { + if let Some(prefix) = &self.doctor_tool_path_prefix { + options["toolPathPrefix"] = json!(prefix); + } + } + if dispatch.capability == "provider.sentrux-adapt" { + if let Some(prefix) = &self.doctor_tool_path_prefix { + options["toolPathPrefix"] = json!(prefix); + } + } + let inputs = if dispatch.capability == "diagnosis.hospital" { + dispatch + .inputs + .iter() + .filter(|reference| reference.artifact_type() == "evidence.admission") + .map(VerifiedArtifactRef::to_json) + .collect::>() + } else { + dispatch + .inputs + .iter() + .map(VerifiedArtifactRef::to_json) + .collect::>() + }; + let request = json!({ + "schema":"code-intel-capability-request.v1", + "capability":dispatch.capability, + "contractVersion":1, + "implementation":declaration["implementation"], + "snapshot":self.snapshot, + "options":options, + "inputs":inputs, + "effectPolicy":{"allowedEffects":declaration["allowedEffects"]} + }); + fs::write( + &request_path, + serde_json::to_vec(&request).expect("A01 request serializes"), + ) + .map_err(|error| { + ( + ExecutionFailure::Io, + format!("write DAG capability request: {error}"), + ) + })?; + let mut command = Command::new(&self.binary); + command + .args(["capability", "exec", &dispatch.capability, "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&node_out) + .arg("--artifact-root") + .arg(if dispatch.capability == "diagnosis.hospital" { + self.seed_artifact_root.as_ref().unwrap_or(&self.run_root) + } else { + &self.run_root + }) + .arg("--manifest") + .arg(&self.registry_path); + let output = command.output().map_err(|error| { + ( + ExecutionFailure::Unavailable, + format!("launch A01 capability executor: {error}"), + ) + })?; + fs::write( + self.run_root + .join(format!("{}.result.json", dispatch.node_id)), + &output.stdout, + ) + .map_err(|error| { + ( + ExecutionFailure::Io, + format!("persist A01 result envelope: {error}"), + ) + })?; + let result: Value = serde_json::from_slice(&output.stdout).map_err(|error| { + ( + ExecutionFailure::Contract, + format!("A01 executor emitted invalid result JSON: {error}"), + ) + })?; + let result_verdict = result["verdict"].as_str().unwrap_or(""); + if !output.status.success() && result_verdict != "fail" { + return Err(( + failure_for_exit(output.status.code()), + diagnostics(&result, &output.stderr), + )); + } + if result["status"] != "completed" || !matches!(result_verdict, "pass" | "fail") { + return Err(( + ExecutionFailure::Contract, + "A01 success result has invalid status/verdict".into(), + )); + } + let mut rebased = result["artifacts"].clone(); + for artifact in rebased.as_array_mut().ok_or_else(|| { + ( + ExecutionFailure::Contract, + "A01 result artifacts must be an array".to_string(), + ) + })? { + let relative = artifact["path"].as_str().ok_or_else(|| { + ( + ExecutionFailure::Contract, + "A01 Artifact Ref path is missing".to_string(), + ) + })?; + artifact["path"] = json!(format!("{}/{relative}", dispatch.node_id)); + } + let expected_snapshot = self.snapshot["identity"] + .as_str() + .expect("A02 snapshot identity"); + let verified = + artifact_ref::verify_inputs(&rebased, Some(&self.run_root), expected_snapshot) + .map_err(map_artifact_error)?; + let refs = rebased + .as_array() + .expect("validated Artifact Ref array") + .iter() + .zip(verified.iter()) + .map(|(artifact, verified)| { + VerifiedArtifactRef::from_a03( + artifact["path"].as_str().expect("validated path"), + verified, + ) + .map_err(|error| (ExecutionFailure::Contract, error.to_string())) + }) + .collect::, _>>()?; + let domain_verdict = match result["domainVerdict"].as_str() { + Some("pass") => DomainVerdict::Pass, + Some("fail") => DomainVerdict::Fail, + Some("unknown") => DomainVerdict::Unknown, + Some("not_applicable") => DomainVerdict::NotApplicable, + other => { + return Err(( + ExecutionFailure::Contract, + format!("A01 result has invalid domainVerdict: {other:?}"), + )) + } + }; + if result_verdict == "fail" { + return Ok(NodeOutcome::domain_fail_with_artifacts( + diagnostics(&result, &output.stderr), + refs, + )); + } + Ok(NodeOutcome::success(domain_verdict, refs)) + } +} + +fn map_artifact_error(error: ArtifactError) -> (ExecutionFailure, String) { + match error { + ArtifactError::Contract(message) => (ExecutionFailure::Contract, message), + ArtifactError::Io(message) => (ExecutionFailure::Io, message), + } +} + +fn failure_for_exit(code: Option) -> ExecutionFailure { + match code { + Some(64 | 65) => ExecutionFailure::Contract, + Some(69) => ExecutionFailure::Unavailable, + Some(74) => ExecutionFailure::Io, + _ => ExecutionFailure::Internal, + } +} + +fn diagnostics(result: &Value, stderr: &[u8]) -> String { + result["diagnostics"] + .as_array() + .and_then(|values| values.first()) + .and_then(Value::as_str) + .filter(|message| !message.is_empty()) + .map(str::to_string) + .or_else(|| { + let message = String::from_utf8_lossy(stderr).trim().to_string(); + (!message.is_empty()).then_some(message) + }) + .unwrap_or_else(|| "capability execution failed without diagnostic".into()) +} + +fn read_registry(path: &Path) -> Result { + let bytes = fs::read(path) + .map_err(|error| RunError::io(format!("read DAG capability registry: {error}")))?; + serde_json::from_slice(&bytes) + .map_err(|error| RunError::contract(format!("parse DAG capability registry: {error}"))) +} + +fn declarations(registry: &Value) -> Result, RunError> { + let integrations = registry["integrations"] + .as_array() + .ok_or_else(|| RunError::contract("registry integrations must be an array"))?; + let mut declarations = BTreeMap::new(); + for integration in integrations { + let Some(declaration) = integration.get("capabilityDeclaration") else { + continue; + }; + let id = declaration["id"] + .as_str() + .ok_or_else(|| RunError::contract("registered capability declaration lacks id"))?; + if declarations + .insert(id.to_string(), declaration.clone()) + .is_some() + { + return Err(RunError::contract(format!( + "duplicate registered capability declaration: {id}" + ))); + } + } + Ok(declarations) +} + +fn default_registry() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json") +} diff --git a/crates/code-intel-cli/src/decision_gap.rs b/crates/code-intel-cli/src/decision_gap.rs new file mode 100644 index 0000000..6d94c09 --- /dev/null +++ b/crates/code-intel-cli/src/decision_gap.rs @@ -0,0 +1,375 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +const MAX_RULE_BYTES: u64 = 64 * 1024; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DetectError(String); + +impl fmt::Display for DetectError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for DetectError {} + +#[derive(Debug, Clone)] +pub(crate) struct RuleTable { + choice_kinds: BTreeSet, + fact_kinds: BTreeSet, +} + +pub(crate) fn load_rule_table(path: &Path) -> Result { + let metadata = fs::metadata(path) + .map_err(|error| DetectError(format!("inspect decision-gap rules: {error}")))?; + if !metadata.is_file() || metadata.len() > MAX_RULE_BYTES { + return Err(DetectError( + "decision-gap rules must be a bounded regular file".to_string(), + )); + } + let document: Value = serde_json::from_slice( + &fs::read(path) + .map_err(|error| DetectError(format!("read decision-gap rules: {error}")))?, + ) + .map_err(|error| DetectError(format!("parse decision-gap rules: {error}")))?; + exact_object( + &document, + "rule table", + &["schema", "choiceKinds", "factKinds"], + )?; + if document["schema"] != "code-intel-decision-gap-rules.v1" { + return Err(DetectError("decision-gap rule schema is invalid".into())); + } + let choice_kinds = string_set(&document["choiceKinds"], "choiceKinds", true)?; + let fact_kinds = string_set(&document["factKinds"], "factKinds", true)?; + if !choice_kinds.is_disjoint(&fact_kinds) { + return Err(DetectError( + "choiceKinds and factKinds must be disjoint".into(), + )); + } + Ok(RuleTable { + choice_kinds, + fact_kinds, + }) +} + +pub(crate) fn detect(request: &Value, rules: &RuleTable) -> Result { + exact_object(request, "request", &["schema", "branches"])?; + if request["schema"] != "code-intel-decision-gap-detection-request.v1" { + return Err(DetectError("decision-gap request schema is invalid".into())); + } + let branches = request["branches"] + .as_array() + .ok_or_else(|| DetectError("branches must be an array".into()))?; + let mut branch_inputs = BTreeMap::new(); + for (index, branch) in branches.iter().enumerate() { + let context = format!("branches[{index}]"); + exact_object(branch, &context, &["branchId", "status", "blockers"])?; + let id = nonempty_string(&branch["branchId"], &format!("{context}.branchId"))?; + let status = nonempty_string(&branch["status"], &format!("{context}.status"))?; + if !matches!(status, "completed" | "pending") { + return Err(DetectError(format!("{context}.status is invalid"))); + } + if branch_inputs.insert(id.to_string(), branch).is_some() { + return Err(DetectError(format!("duplicate branchId {id}"))); + } + } + + let known_branches = branch_inputs.keys().cloned().collect::>(); + let mut blocker_ids = BTreeSet::new(); + let mut gaps = BTreeMap::new(); + let mut fact_discovery = BTreeMap::new(); + let mut branch_gap_ids: BTreeMap> = BTreeMap::new(); + let mut branch_fact_ids: BTreeMap> = BTreeMap::new(); + + for (owner_branch, branch) in &branch_inputs { + let blockers = branch["blockers"].as_array().ok_or_else(|| { + DetectError(format!("branch {owner_branch}.blockers must be an array")) + })?; + for blocker in blockers { + let parsed = parse_blocker(blocker, &known_branches)?; + if !blocker_ids.insert(parsed.id.clone()) { + return Err(DetectError(format!("duplicate blocker id {}", parsed.id))); + } + if !rules.choice_kinds.contains(&parsed.kind) + && !rules.fact_kinds.contains(&parsed.kind) + { + return Err(DetectError(format!("unknown blocker kind {}", parsed.kind))); + } + let unresolved_facts = parsed + .facts + .iter() + .filter(|fact| fact["status"] == "missing") + .filter_map(|fact| fact["factId"].as_str()) + .chain(parsed.missing_fact_ids.iter().map(String::as_str)) + .map(str::to_string) + .collect::>(); + if rules.fact_kinds.contains(&parsed.kind) && unresolved_facts.is_empty() { + return Err(DetectError(format!( + "fact blocker {} must name an unresolved fact", + parsed.id + ))); + } + if !unresolved_facts.is_empty() { + let affected = if parsed.affected_branches.is_empty() { + BTreeSet::from([owner_branch.clone()]) + } else { + parsed.affected_branches.clone() + }; + for branch_id in &affected { + branch_fact_ids + .entry(branch_id.clone()) + .or_default() + .insert(parsed.id.clone()); + } + fact_discovery.insert( + parsed.id.clone(), + json!({ + "blockerId": parsed.id, + "kind": parsed.kind, + "missingFactIds": unresolved_facts, + "affectedBranches": affected, + }), + ); + continue; + } + if parsed.facts.is_empty() { + return Err(DetectError(format!( + "choice blocker {} must name discoverable facts checked", + parsed.id + ))); + } + if parsed.options.len() < 2 { + return Err(DetectError(format!( + "choice blocker {} must provide at least two options", + parsed.id + ))); + } + if !parsed.options.contains_key(&parsed.recommended_option_id) { + return Err(DetectError(format!( + "choice blocker {} recommends an unknown option", + parsed.id + ))); + } + if parsed.affected_branches.is_empty() { + return Err(DetectError(format!( + "choice blocker {} must affect at least one branch", + parsed.id + ))); + } + for branch_id in &parsed.affected_branches { + branch_gap_ids + .entry(branch_id.clone()) + .or_default() + .insert(parsed.id.clone()); + } + gaps.insert( + parsed.id.clone(), + json!({ + "schema": "code-intel-decision-gap.v1", + "id": parsed.id, + "kind": parsed.kind, + "blockedDecision": parsed.blocked_decision, + "discoverableFactsChecked": parsed.facts, + "options": parsed.options.into_values().collect::>(), + "recommendedAnswer": { + "kind": "proposal", + "optionId": parsed.recommended_option_id, + "rationale": parsed.recommendation_rationale, + }, + "affectedBranches": parsed.affected_branches, + "authorityRequired": true, + "authorityState": "unresolved", + "effects": [], + }), + ); + } + } + + let branch_results = branch_inputs + .into_iter() + .map(|(branch_id, branch)| { + let gap_ids = branch_gap_ids.remove(&branch_id).unwrap_or_default(); + let fact_ids = branch_fact_ids.remove(&branch_id).unwrap_or_default(); + let status = if !gap_ids.is_empty() { + "blocked_decision_gap" + } else if !fact_ids.is_empty() { + "fact_discovery_required" + } else { + branch["status"].as_str().unwrap() + }; + json!({ + "branchId": branch_id, + "status": status, + "blockedByGapIds": gap_ids, + "factDiscoveryBlockerIds": fact_ids, + }) + }) + .collect::>(); + + Ok(json!({ + "schema": "code-intel-decision-gap-detection-result.v1", + "status": "completed", + "gaps": gaps.into_values().collect::>(), + "factDiscovery": fact_discovery.into_values().collect::>(), + "branches": branch_results, + "answersRecorded": false, + "authorityEvents": [], + "adoptionDecisions": [], + "committedEngineeringPlans": [], + "engineeringFacts": [], + })) +} + +struct ParsedBlocker { + id: String, + kind: String, + blocked_decision: String, + facts: Vec, + missing_fact_ids: BTreeSet, + options: BTreeMap, + recommended_option_id: String, + recommendation_rationale: String, + affected_branches: BTreeSet, +} + +fn parse_blocker( + value: &Value, + known_branches: &BTreeSet, +) -> Result { + exact_object( + value, + "blocker", + &[ + "id", + "kind", + "blockedDecision", + "discoverableFactsChecked", + "missingFactIds", + "options", + "recommendedOptionId", + "recommendationRationale", + "affectedBranches", + ], + )?; + let id = nonempty_string(&value["id"], "blocker.id")?.to_string(); + let kind = nonempty_string(&value["kind"], "blocker.kind")?.to_string(); + let blocked_decision = + nonempty_string(&value["blockedDecision"], "blocker.blockedDecision")?.to_string(); + let missing_fact_ids = string_set(&value["missingFactIds"], "blocker.missingFactIds", false)?; + let affected_branches = string_set( + &value["affectedBranches"], + "blocker.affectedBranches", + false, + )?; + if let Some(unknown) = affected_branches + .iter() + .find(|branch| !known_branches.contains(*branch)) + { + return Err(DetectError(format!( + "blocker {id} references unknown branch {unknown}" + ))); + } + let facts = parse_facts(&value["discoverableFactsChecked"])?; + let options = parse_options(&value["options"])?; + Ok(ParsedBlocker { + id, + kind, + blocked_decision, + facts, + missing_fact_ids, + options, + recommended_option_id: nonempty_string( + &value["recommendedOptionId"], + "blocker.recommendedOptionId", + )? + .to_string(), + recommendation_rationale: nonempty_string( + &value["recommendationRationale"], + "blocker.recommendationRationale", + )? + .to_string(), + affected_branches, + }) +} + +fn parse_facts(value: &Value) -> Result, DetectError> { + let entries = value + .as_array() + .ok_or_else(|| DetectError("discoverableFactsChecked must be an array".into()))?; + let mut facts = BTreeMap::new(); + for entry in entries { + exact_object(entry, "fact check", &["factId", "status"])?; + let fact_id = nonempty_string(&entry["factId"], "fact check.factId")?; + let status = nonempty_string(&entry["status"], "fact check.status")?; + if !matches!(status, "resolved" | "missing") { + return Err(DetectError("fact check.status is invalid".into())); + } + if facts.insert(fact_id.to_string(), entry.clone()).is_some() { + return Err(DetectError(format!("duplicate factId {fact_id}"))); + } + } + Ok(facts.into_values().collect()) +} + +fn parse_options(value: &Value) -> Result, DetectError> { + let entries = value + .as_array() + .ok_or_else(|| DetectError("options must be an array".into()))?; + let mut options = BTreeMap::new(); + for entry in entries { + exact_object(entry, "option", &["id", "label", "consequence"])?; + let id = nonempty_string(&entry["id"], "option.id")?; + nonempty_string(&entry["label"], "option.label")?; + nonempty_string(&entry["consequence"], "option.consequence")?; + if options.insert(id.to_string(), entry.clone()).is_some() { + return Err(DetectError(format!("duplicate option id {id}"))); + } + } + Ok(options) +} + +fn exact_object(value: &Value, context: &str, keys: &[&str]) -> Result<(), DetectError> { + let object = value + .as_object() + .ok_or_else(|| DetectError(format!("{context} must be an object")))?; + let expected = keys.iter().copied().collect::>(); + let actual = object.keys().map(String::as_str).collect::>(); + if actual != expected { + return Err(DetectError(format!("{context} has invalid fields"))); + } + Ok(()) +} + +fn nonempty_string<'a>(value: &'a Value, context: &str) -> Result<&'a str, DetectError> { + value + .as_str() + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| DetectError(format!("{context} must be a non-empty string"))) +} + +fn string_set( + value: &Value, + context: &str, + require_nonempty: bool, +) -> Result, DetectError> { + let values = value + .as_array() + .ok_or_else(|| DetectError(format!("{context} must be an array")))?; + if require_nonempty && values.is_empty() { + return Err(DetectError(format!("{context} must not be empty"))); + } + let mut result = BTreeSet::new(); + for value in values { + let item = nonempty_string(value, context)?.to_string(); + if !result.insert(item.clone()) { + return Err(DetectError(format!("{context} contains duplicate {item}"))); + } + } + Ok(result) +} diff --git a/crates/code-intel-cli/src/decision_port.rs b/crates/code-intel-cli/src/decision_port.rs new file mode 100644 index 0000000..1893a6f --- /dev/null +++ b/crates/code-intel-cli/src/decision_port.rs @@ -0,0 +1,898 @@ +use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fmt; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +const MAX_MESSAGE_BYTES: u64 = 128 * 1024; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let (value, exit_code) = match run_cli(raw) { + Ok(value) => { + let exit_code = match value["status"].as_str() { + Some("resolved") => 0, + Some("pending") => 10, + Some("timeout") => 11, + Some("cancelled") => 12, + _ => 70, + }; + (value, exit_code) + } + Err(error) => { + eprintln!("decision request-response: {error}"); + ( + json!({ + "schema":"code-intel-decision-exchange-result.v1", + "status":"rejected", + "correlationId":null, + "gapId":null, + "acceptedAnswer":null, + "authorityProvenance":null, + "branches":[], + "effects":[], + "diagnostics":[error.to_string()], + }), + 65, + ) + } + }; + match serde_json::to_string_pretty(&value) { + Ok(text) => { + println!("{text}"); + exit_code + } + Err(error) => { + eprintln!("decision request-response: serialize result: {error}"); + 70 + } + } +} + +fn run_cli(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("request-response") { + return Err(DecisionPortError( + "expected request-response subcommand".into(), + )); + } + let mut request_path = None; + let mut response_path = None; + let mut cancellation_path = None; + let mut now = None; + let mut branches = Vec::new(); + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + let value = raw + .get(index + 1) + .ok_or_else(|| DecisionPortError(format!("{flag} requires a value")))?; + match flag { + "--request" if request_path.is_none() => request_path = Some(value.clone()), + "--response" if response_path.is_none() => response_path = Some(value.clone()), + "--cancel" if cancellation_path.is_none() => cancellation_path = Some(value.clone()), + "--now" if now.is_none() => { + now = + Some(value.parse::().map_err(|_| { + DecisionPortError("--now must be an unsigned integer".into()) + })?) + } + "--branch" => branches.push(value.clone()), + _ => { + return Err(DecisionPortError(format!( + "unknown or duplicate option {flag}" + ))) + } + } + index += 2; + } + if response_path.is_some() && cancellation_path.is_some() { + return Err(DecisionPortError( + "--response and --cancel are mutually exclusive".into(), + )); + } + let request_path = + request_path.ok_or_else(|| DecisionPortError("--request is required".into()))?; + let now = now.ok_or_else(|| DecisionPortError("--now is required".into()))?; + let request = read_cli_message(&request_path)?; + let mut port = NativeStructuredDecisionPort::default(); + if let Some(path) = response_path { + port.supply_response(read_cli_message(&path)?)?; + } + if let Some(path) = cancellation_path { + port.supply_cancellation(read_cli_message(&path)?)?; + } + let branch_refs = branches.iter().map(String::as_str).collect::>(); + DecisionExchange::default().advance(&request, &mut port, now, &branch_refs) +} + +fn read_cli_message(path: &str) -> Result { + if path != "-" { + return read_message(Path::new(path)); + } + let mut bytes = Vec::new(); + io::stdin() + .take(MAX_MESSAGE_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| DecisionPortError(format!("read stdin message: {error}")))?; + if bytes.len() as u64 > MAX_MESSAGE_BYTES { + return Err(DecisionPortError("stdin message exceeds size limit".into())); + } + parse_message(&bytes, "stdin") +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DecisionPortError(String); + +impl fmt::Display for DecisionPortError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for DecisionPortError {} + +#[derive(Debug, Clone)] +pub(crate) enum PortPoll { + Pending, + Response(Value), + Cancelled(Value), +} + +pub(crate) trait DecisionRequestResponsePort { + fn submit(&mut self, request: &Value) -> Result<(), DecisionPortError>; + fn poll(&mut self, correlation_id: &str) -> Result; +} + +#[derive(Debug, Clone)] +struct EvidenceRef { + expires_at: u64, +} + +#[derive(Debug, Clone)] +struct ParsedRequest { + correlation_id: String, + gap_id: String, + option_ids: BTreeSet, + evidence_refs: Vec, + authority_kind: String, + actor_ids: BTreeSet, + issued_at: u64, + expires_at: u64, + affected_branches: BTreeSet, +} + +#[derive(Debug, Default)] +pub(crate) struct DecisionExchange { + pending: BTreeMap, + terminal: BTreeSet, +} + +impl DecisionExchange { + pub(crate) fn advance( + &mut self, + request: &Value, + port: &mut P, + now: u64, + branches: &[&str], + ) -> Result { + let parsed = parse_request(request)?; + let branch_ids = validate_branches(branches, &parsed.affected_branches)?; + if now < parsed.issued_at { + return Err(DecisionPortError( + "processing clock precedes request issue time".into(), + )); + } + if self.terminal.contains(&parsed.correlation_id) { + return Err(DecisionPortError(format!( + "response replay for terminal correlation {}", + parsed.correlation_id + ))); + } + match self.pending.get(&parsed.correlation_id) { + Some(existing) if existing != request => { + return Err(DecisionPortError(format!( + "correlation {} was reused for a different request", + parsed.correlation_id + ))) + } + Some(_) => {} + None => { + port.submit(request)?; + self.pending + .insert(parsed.correlation_id.clone(), request.clone()); + } + } + + match port.poll(&parsed.correlation_id)? { + PortPoll::Pending if now >= parsed.expires_at => { + self.finish(&parsed.correlation_id); + Ok(result( + &parsed, + &branch_ids, + "timeout", + "blocked_timeout", + None, + None, + )) + } + PortPoll::Pending => Ok(result( + &parsed, + &branch_ids, + "pending", + "blocked_pending_response", + None, + None, + )), + PortPoll::Response(response) => { + if now >= parsed.expires_at { + self.finish(&parsed.correlation_id); + return Err(DecisionPortError( + "expired request cannot accept a response".into(), + )); + } + let (answer, provenance) = validate_response(&response, &parsed, now)?; + self.finish(&parsed.correlation_id); + Ok(result( + &parsed, + &branch_ids, + "resolved", + "ready", + Some(answer), + Some(provenance), + )) + } + PortPoll::Cancelled(cancellation) => { + if now >= parsed.expires_at { + self.finish(&parsed.correlation_id); + return Err(DecisionPortError( + "expired request cannot accept a cancellation".into(), + )); + } + let provenance = validate_cancellation(&cancellation, &parsed, now)?; + self.finish(&parsed.correlation_id); + Ok(result( + &parsed, + &branch_ids, + "cancelled", + "blocked_cancelled", + None, + Some(provenance), + )) + } + } + } + + fn finish(&mut self, correlation_id: &str) { + self.pending.remove(correlation_id); + self.terminal.insert(correlation_id.to_string()); + } +} + +fn result( + request: &ParsedRequest, + branches: &BTreeSet, + status: &str, + affected_status: &str, + answer: Option, + provenance: Option, +) -> Value { + let branch_results = branches + .iter() + .map(|branch| { + json!({ + "branchId": branch, + "status": if request.affected_branches.contains(branch) { + affected_status + } else { + "continues" + }, + }) + }) + .collect::>(); + json!({ + "schema": "code-intel-decision-exchange-result.v1", + "status": status, + "correlationId": request.correlation_id, + "gapId": request.gap_id, + "acceptedAnswer": answer, + "authorityProvenance": provenance, + "branches": branch_results, + "effects": [], + "diagnostics": [], + }) +} + +fn parse_request(value: &Value) -> Result { + exact_object( + value, + "decision request", + &[ + "schema", + "correlationId", + "gapId", + "question", + "recommendation", + "evidenceRefs", + "options", + "authorityNeeded", + "issuedAt", + "expiresAt", + "affectedBranches", + ], + )?; + if value["schema"] != "code-intel-decision-request.v1" { + return Err(DecisionPortError( + "decision request schema is invalid".into(), + )); + } + let correlation_id = identifier(&value["correlationId"], "correlationId")?.to_string(); + let gap_id = nonempty_string(&value["gapId"], "gapId")?.to_string(); + nonempty_string(&value["question"], "question")?; + + exact_object( + &value["recommendation"], + "recommendation", + &["optionId", "rationale"], + )?; + let recommended_option = nonempty_string( + &value["recommendation"]["optionId"], + "recommendation.optionId", + )?; + nonempty_string( + &value["recommendation"]["rationale"], + "recommendation.rationale", + )?; + + let option_values = value["options"] + .as_array() + .ok_or_else(|| DecisionPortError("options must be an array".into()))?; + if option_values.len() < 2 { + return Err(DecisionPortError( + "decision request requires at least two options".into(), + )); + } + let mut option_ids = BTreeSet::new(); + for option in option_values { + exact_object(option, "option", &["id", "label", "consequence"])?; + let id = nonempty_string(&option["id"], "option.id")?; + nonempty_string(&option["label"], "option.label")?; + nonempty_string(&option["consequence"], "option.consequence")?; + if !option_ids.insert(id.to_string()) { + return Err(DecisionPortError(format!("duplicate option id {id}"))); + } + } + if !option_ids.contains(recommended_option) { + return Err(DecisionPortError( + "recommendation names an unknown option".into(), + )); + } + + let evidence_values = value["evidenceRefs"] + .as_array() + .ok_or_else(|| DecisionPortError("evidenceRefs must be an array".into()))?; + if evidence_values.is_empty() { + return Err(DecisionPortError("evidenceRefs must not be empty".into())); + } + let mut evidence_ids = BTreeSet::new(); + let mut evidence_refs = Vec::new(); + for evidence in evidence_values { + exact_object( + evidence, + "evidence ref", + &["refId", "sha256", "observedAt", "expiresAt"], + )?; + let ref_id = nonempty_string(&evidence["refId"], "evidence ref.refId")?; + if !evidence_ids.insert(ref_id.to_string()) { + return Err(DecisionPortError(format!( + "duplicate evidence ref {ref_id}" + ))); + } + let sha256 = nonempty_string(&evidence["sha256"], "evidence ref.sha256")?; + if sha256.len() != 64 + || !sha256 + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(DecisionPortError( + "evidence ref.sha256 must be lowercase hexadecimal".into(), + )); + } + let observed_at = integer(&evidence["observedAt"], "evidence ref.observedAt")?; + let expires_at = integer(&evidence["expiresAt"], "evidence ref.expiresAt")?; + if observed_at >= expires_at { + return Err(DecisionPortError( + "evidence ref expiry must follow observation".into(), + )); + } + evidence_refs.push(EvidenceRef { expires_at }); + } + + exact_object( + &value["authorityNeeded"], + "authorityNeeded", + &["kind", "actorIds"], + )?; + let authority_kind = + nonempty_string(&value["authorityNeeded"]["kind"], "authorityNeeded.kind")?.to_string(); + let actor_ids = string_set( + &value["authorityNeeded"]["actorIds"], + "authorityNeeded.actorIds", + )?; + let issued_at = integer(&value["issuedAt"], "issuedAt")?; + let expires_at = integer(&value["expiresAt"], "expiresAt")?; + if issued_at >= expires_at { + return Err(DecisionPortError( + "request expiry must follow issue time".into(), + )); + } + let affected_branches = string_set(&value["affectedBranches"], "affectedBranches")?; + + Ok(ParsedRequest { + correlation_id, + gap_id, + option_ids, + evidence_refs, + authority_kind, + actor_ids, + issued_at, + expires_at, + affected_branches, + }) +} + +fn validate_response( + response: &Value, + request: &ParsedRequest, + now: u64, +) -> Result<(Value, Value), DecisionPortError> { + exact_object( + response, + "decision response", + &[ + "schema", + "correlationId", + "gapId", + "answer", + "actorProvenance", + "timestamp", + ], + )?; + if response["schema"] != "code-intel-decision-response.v1" { + return Err(DecisionPortError( + "decision response schema is invalid".into(), + )); + } + validate_binding(response, request, now)?; + let answer = &response["answer"]; + let answer_object = answer + .as_object() + .ok_or_else(|| DecisionPortError("answer must be an object".into()))?; + match answer_object.get("kind").and_then(Value::as_str) { + Some("choice") => { + exact_object(answer, "choice answer", &["kind", "optionId"])?; + let option_id = nonempty_string(&answer["optionId"], "answer.optionId")?; + if !request.option_ids.contains(option_id) { + return Err(DecisionPortError("answer selects an unknown option".into())); + } + } + Some("free-form") => { + exact_object(answer, "free-form answer", &["kind", "text"])?; + nonempty_string(&answer["text"], "answer.text")?; + } + _ => return Err(DecisionPortError("answer kind is invalid".into())), + } + Ok((answer.clone(), response["actorProvenance"].clone())) +} + +fn validate_cancellation( + cancellation: &Value, + request: &ParsedRequest, + now: u64, +) -> Result { + exact_object( + cancellation, + "decision cancellation", + &[ + "schema", + "correlationId", + "gapId", + "actorProvenance", + "timestamp", + "reason", + ], + )?; + if cancellation["schema"] != "code-intel-decision-cancellation.v1" { + return Err(DecisionPortError( + "decision cancellation schema is invalid".into(), + )); + } + nonempty_string(&cancellation["reason"], "cancellation.reason")?; + validate_binding(cancellation, request, now)?; + Ok(cancellation["actorProvenance"].clone()) +} + +fn validate_binding( + message: &Value, + request: &ParsedRequest, + now: u64, +) -> Result<(), DecisionPortError> { + if message["correlationId"].as_str() != Some(&request.correlation_id) { + return Err(DecisionPortError("response correlation mismatch".into())); + } + if message["gapId"].as_str() != Some(&request.gap_id) { + return Err(DecisionPortError("response gap mismatch".into())); + } + exact_object( + &message["actorProvenance"], + "actor provenance", + &["actorId", "authorityKind", "source"], + )?; + let actor_id = nonempty_string( + &message["actorProvenance"]["actorId"], + "actorProvenance.actorId", + )?; + let authority_kind = nonempty_string( + &message["actorProvenance"]["authorityKind"], + "actorProvenance.authorityKind", + )?; + nonempty_string( + &message["actorProvenance"]["source"], + "actorProvenance.source", + )?; + if !request.actor_ids.contains(actor_id) || authority_kind != request.authority_kind { + return Err(DecisionPortError( + "response actor lacks the requested authority".into(), + )); + } + let timestamp = integer(&message["timestamp"], "timestamp")?; + if timestamp > now { + return Err(DecisionPortError( + "response timestamp is in the future".into(), + )); + } + if timestamp < request.issued_at || timestamp > request.expires_at { + return Err(DecisionPortError( + "response timestamp is outside the request lifetime".into(), + )); + } + if request + .evidence_refs + .iter() + .any(|evidence| timestamp > evidence.expires_at) + { + return Err(DecisionPortError( + "response is bound to stale evidence".into(), + )); + } + Ok(()) +} + +fn validate_branches( + branches: &[&str], + affected: &BTreeSet, +) -> Result, DecisionPortError> { + let mut result = BTreeSet::new(); + for branch in branches { + if branch.trim().is_empty() || !result.insert((*branch).to_string()) { + return Err(DecisionPortError( + "branch list must contain unique non-empty ids".into(), + )); + } + } + if let Some(missing) = affected.iter().find(|branch| !result.contains(*branch)) { + return Err(DecisionPortError(format!( + "affected branch {missing} is absent from the DAG branch set" + ))); + } + Ok(result) +} + +#[derive(Debug, Default)] +pub(crate) struct InMemoryDecisionPort { + requests: BTreeMap, + events: VecDeque, +} + +impl InMemoryDecisionPort { + pub(crate) fn supply_response(&mut self, response: Value) -> Result<(), DecisionPortError> { + message_correlation(&response)?; + self.events.push_back(PortPoll::Response(response)); + Ok(()) + } + + pub(crate) fn supply_cancellation( + &mut self, + cancellation: Value, + ) -> Result<(), DecisionPortError> { + message_correlation(&cancellation)?; + self.events.push_back(PortPoll::Cancelled(cancellation)); + Ok(()) + } +} + +impl DecisionRequestResponsePort for InMemoryDecisionPort { + fn submit(&mut self, request: &Value) -> Result<(), DecisionPortError> { + store_request(&mut self.requests, request) + } + + fn poll(&mut self, _correlation_id: &str) -> Result { + Ok(self.events.pop_front().unwrap_or(PortPoll::Pending)) + } +} + +#[derive(Debug, Default)] +pub(crate) struct NativeStructuredDecisionPort { + requests: BTreeMap, + events: VecDeque, +} + +impl NativeStructuredDecisionPort { + pub(crate) fn supply_response(&mut self, response: Value) -> Result<(), DecisionPortError> { + message_correlation(&response)?; + self.events.push_back(PortPoll::Response(response)); + Ok(()) + } + + pub(crate) fn supply_cancellation( + &mut self, + cancellation: Value, + ) -> Result<(), DecisionPortError> { + message_correlation(&cancellation)?; + self.events.push_back(PortPoll::Cancelled(cancellation)); + Ok(()) + } + + pub(crate) fn submitted(&self) -> impl Iterator { + self.requests.values() + } +} + +impl DecisionRequestResponsePort for NativeStructuredDecisionPort { + fn submit(&mut self, request: &Value) -> Result<(), DecisionPortError> { + store_request(&mut self.requests, request) + } + + fn poll(&mut self, _correlation_id: &str) -> Result { + Ok(self.events.pop_front().unwrap_or(PortPoll::Pending)) + } +} + +#[derive(Debug, Default)] +pub(crate) struct PlainTextDecisionPort { + requests: BTreeMap, + outbox: Vec, + events: VecDeque, +} + +impl PlainTextDecisionPort { + pub(crate) fn supply_line(&mut self, line: &str) -> Result<(), DecisionPortError> { + let parts = line.split('\t').collect::>(); + if parts.len() != 8 || parts.iter().any(|part| part.trim().is_empty()) { + return Err(DecisionPortError( + "plain-text reply must contain eight non-empty tab-separated fields".into(), + )); + } + let timestamp = parts[7] + .parse::() + .map_err(|_| DecisionPortError("plain-text timestamp is invalid".into()))?; + let provenance = json!({ + "actorId": parts[4], + "authorityKind": parts[5], + "source": parts[6], + }); + let event = match parts[0] { + "choice" => PortPoll::Response(json!({ + "schema": "code-intel-decision-response.v1", + "correlationId": parts[1], + "gapId": parts[2], + "answer": {"kind": "choice", "optionId": parts[3]}, + "actorProvenance": provenance, + "timestamp": timestamp, + })), + "free-form" => PortPoll::Response(json!({ + "schema": "code-intel-decision-response.v1", + "correlationId": parts[1], + "gapId": parts[2], + "answer": {"kind": "free-form", "text": parts[3]}, + "actorProvenance": provenance, + "timestamp": timestamp, + })), + "cancel" => PortPoll::Cancelled(json!({ + "schema": "code-intel-decision-cancellation.v1", + "correlationId": parts[1], + "gapId": parts[2], + "actorProvenance": provenance, + "timestamp": timestamp, + "reason": parts[3], + })), + _ => return Err(DecisionPortError("plain-text reply kind is invalid".into())), + }; + self.events.push_back(event); + Ok(()) + } + + pub(crate) fn outbox(&self) -> &[String] { + &self.outbox + } +} + +impl DecisionRequestResponsePort for PlainTextDecisionPort { + fn submit(&mut self, request: &Value) -> Result<(), DecisionPortError> { + let parsed = parse_request(request)?; + store_request(&mut self.requests, request)?; + if !self.outbox.iter().any(|message| { + message.starts_with(&format!("DECISION REQUEST {}\n", parsed.correlation_id)) + }) { + let request_json = serde_json::to_string_pretty(request).map_err(|error| { + DecisionPortError(format!("render plain-text request: {error}")) + })?; + self.outbox.push(format!( + "DECISION REQUEST {}\n{}", + parsed.correlation_id, request_json + )); + } + Ok(()) + } + + fn poll(&mut self, _correlation_id: &str) -> Result { + Ok(self.events.pop_front().unwrap_or(PortPoll::Pending)) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct FileDecisionPort { + root: PathBuf, +} + +impl FileDecisionPort { + pub(crate) fn new(root: PathBuf) -> Self { + Self { root } + } + + fn path(&self, correlation_id: &str, suffix: &str) -> Result { + if correlation_id.is_empty() + || !correlation_id + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + { + return Err(DecisionPortError( + "correlation id is not portable for file transport".into(), + )); + } + Ok(self.root.join(format!("{correlation_id}.{suffix}.json"))) + } +} + +impl DecisionRequestResponsePort for FileDecisionPort { + fn submit(&mut self, request: &Value) -> Result<(), DecisionPortError> { + let parsed = parse_request(request)?; + fs::create_dir_all(&self.root) + .map_err(|error| DecisionPortError(format!("create file-port root: {error}")))?; + let path = self.path(&parsed.correlation_id, "request")?; + if path.exists() { + let existing = read_message(&path)?; + if existing != *request { + return Err(DecisionPortError( + "file port correlation already contains a different request".into(), + )); + } + return Ok(()); + } + let bytes = serde_json::to_vec_pretty(request) + .map_err(|error| DecisionPortError(format!("serialize request: {error}")))?; + fs::write(path, bytes) + .map_err(|error| DecisionPortError(format!("write file-port request: {error}"))) + } + + fn poll(&mut self, correlation_id: &str) -> Result { + let response_path = self.path(correlation_id, "response")?; + let cancel_path = self.path(correlation_id, "cancel")?; + match (response_path.is_file(), cancel_path.is_file()) { + (true, true) => Err(DecisionPortError( + "file port contains both response and cancellation".into(), + )), + (true, false) => Ok(PortPoll::Response(read_message(&response_path)?)), + (false, true) => Ok(PortPoll::Cancelled(read_message(&cancel_path)?)), + (false, false) => Ok(PortPoll::Pending), + } + } +} + +fn read_message(path: &Path) -> Result { + let metadata = fs::metadata(path) + .map_err(|error| DecisionPortError(format!("inspect port message: {error}")))?; + if !metadata.is_file() || metadata.len() > MAX_MESSAGE_BYTES { + return Err(DecisionPortError( + "port message must be a bounded regular file".into(), + )); + } + let bytes = + fs::read(path).map_err(|error| DecisionPortError(format!("read port message: {error}")))?; + parse_message(&bytes, "port") +} + +fn parse_message(bytes: &[u8], source: &str) -> Result { + let text = std::str::from_utf8(bytes) + .map_err(|error| DecisionPortError(format!("{source} message is not UTF-8: {error}")))?; + crate::capability::reject_duplicate_json_keys(text) + .map_err(|error| DecisionPortError(format!("{source} message {error}")))?; + serde_json::from_str(text) + .map_err(|error| DecisionPortError(format!("parse {source} message: {error}"))) +} + +fn store_request( + requests: &mut BTreeMap, + request: &Value, +) -> Result<(), DecisionPortError> { + let parsed = parse_request(request)?; + match requests.get(&parsed.correlation_id) { + Some(existing) if existing != request => Err(DecisionPortError( + "port correlation already contains a different request".into(), + )), + Some(_) => Ok(()), + None => { + requests.insert(parsed.correlation_id, request.clone()); + Ok(()) + } + } +} + +fn message_correlation(message: &Value) -> Result { + identifier(&message["correlationId"], "message.correlationId").map(str::to_string) +} + +fn exact_object(value: &Value, context: &str, keys: &[&str]) -> Result<(), DecisionPortError> { + let object = value + .as_object() + .ok_or_else(|| DecisionPortError(format!("{context} must be an object")))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = keys.iter().copied().collect::>(); + if actual != expected { + return Err(DecisionPortError(format!("{context} has invalid fields"))); + } + Ok(()) +} + +fn nonempty_string<'a>(value: &'a Value, context: &str) -> Result<&'a str, DecisionPortError> { + value + .as_str() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| DecisionPortError(format!("{context} must be a non-empty string"))) +} + +fn identifier<'a>(value: &'a Value, context: &str) -> Result<&'a str, DecisionPortError> { + let value = nonempty_string(value, context)?; + if !value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + { + return Err(DecisionPortError(format!("{context} is not portable"))); + } + Ok(value) +} + +fn integer(value: &Value, context: &str) -> Result { + value + .as_u64() + .ok_or_else(|| DecisionPortError(format!("{context} must be an unsigned integer"))) +} + +fn string_set(value: &Value, context: &str) -> Result, DecisionPortError> { + let values = value + .as_array() + .ok_or_else(|| DecisionPortError(format!("{context} must be an array")))?; + if values.is_empty() { + return Err(DecisionPortError(format!("{context} must not be empty"))); + } + let mut result = BTreeSet::new(); + for value in values { + let item = nonempty_string(value, context)?.to_string(); + if !result.insert(item.clone()) { + return Err(DecisionPortError(format!( + "{context} contains duplicate {item}" + ))); + } + } + Ok(result) +} diff --git a/crates/code-intel-cli/src/decision_record.rs b/crates/code-intel-cli/src/decision_record.rs new file mode 100644 index 0000000..12f530d --- /dev/null +++ b/crates/code-intel-cli/src/decision_record.rs @@ -0,0 +1,1042 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fs::{self, File, OpenOptions}; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Map, Value}; + +const MAX_RECORD_BYTES: u64 = 1024 * 1024; +const RECORD_SCHEMA: &str = "code-intel-decision-record.v1"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DecisionRecordError(String); + +impl fmt::Display for DecisionRecordError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for DecisionRecordError {} + +#[derive(Debug, Clone)] +pub(crate) struct DecisionRecordStore { + root: PathBuf, +} + +impl DecisionRecordStore { + pub(crate) fn new(root: PathBuf) -> Self { + Self { root } + } + + pub(crate) fn record(&self, resolution: &Value) -> Result { + let _lock = self.lock_store()?; + let loaded = self.load_locked()?; + let candidate = build_record(resolution)?; + let candidate_id = candidate["id"].as_str().unwrap(); + if let Some(existing) = loaded + .records + .iter() + .find(|record| record["id"] == candidate_id) + { + return Ok(outcome( + "replay", + false, + Some(existing.clone()), + None, + loaded.diagnostics, + )); + } + + let authority_id = candidate["authorityEvent"]["id"].as_str().unwrap(); + if loaded + .records + .iter() + .any(|record| record["authorityEvent"]["id"] == authority_id) + { + return Err(DecisionRecordError( + "authority event replay is rejected".to_string(), + )); + } + if loaded.records.iter().any(|record| { + record["gap"]["id"] == candidate["gap"]["id"] + && record["snapshotIdentity"] == candidate["snapshotIdentity"] + && record["evidenceBinding"]["digest"] == candidate["evidenceBinding"]["digest"] + }) { + return Err(DecisionRecordError( + "decision gap is already recorded for unchanged evidence".to_string(), + )); + } + + self.publish(&candidate)?; + Ok(outcome( + "recorded", + false, + Some(candidate), + None, + loaded.diagnostics, + )) + } + + pub(crate) fn replay(&self, query: &Value) -> Result { + validate_query(query)?; + let _lock = self.lock_store()?; + let loaded = self.load_locked()?; + let gap_id = query["gapId"].as_str().unwrap(); + let Some(record) = loaded + .records + .iter() + .filter(|record| record["gap"]["id"] == gap_id) + .max_by_key(|record| record["recordedAt"].as_u64().unwrap_or_default()) + else { + return Ok(outcome( + "reopen", + true, + None, + Some("no_record"), + loaded.diagnostics, + )); + }; + + if string_set(&query["affectedBranches"], "query affectedBranches")? + != string_set(&record["affectedBranches"], "record affectedBranches")? + { + return Err(DecisionRecordError( + "decision replay branch scope mismatch".to_string(), + )); + } + let query_evidence = validate_evidence_refs(&query["evidenceRefs"])?; + let now = unsigned(&query["now"], "query now")?; + if now < record["recordedAt"].as_u64().unwrap() + || query_evidence + .iter() + .any(|evidence| now < evidence["observedAt"].as_u64().unwrap()) + { + return Err(DecisionRecordError( + "decision replay time precedes the record or observed evidence".to_string(), + )); + } + if query["snapshotIdentity"] != record["snapshotIdentity"] { + return Ok(outcome( + "reopen", + true, + Some(record.clone()), + Some("snapshot_changed"), + loaded.diagnostics, + )); + } + let query_digest = digest_value(&Value::Array(query_evidence)); + if record["evidenceBinding"]["digest"] != query_digest { + return Ok(outcome( + "reopen", + true, + Some(record.clone()), + Some("evidence_changed"), + loaded.diagnostics, + )); + } + if now > record["freshness"]["evidenceExpiresAt"].as_u64().unwrap() { + return Ok(outcome( + "reopen", + true, + Some(record.clone()), + Some("evidence_stale"), + loaded.diagnostics, + )); + } + Ok(outcome( + "replay", + false, + Some(record.clone()), + None, + loaded.diagnostics, + )) + } + + fn ensure_root(&self) -> Result<(), DecisionRecordError> { + let existed = self.root.is_dir(); + fs::create_dir_all(&self.root).map_err(|error| { + DecisionRecordError(format!("create decision record store: {error}")) + })?; + if !existed { + if let Some(parent) = self.root.parent() { + crate::staged_artifact::sync_directory_path(parent).map_err(|error| { + DecisionRecordError(format!("sync decision store parent: {error}")) + })?; + } + } + crate::staged_artifact::sync_directory_path(&self.root) + .map_err(|error| DecisionRecordError(format!("sync decision store: {error}"))) + } + + fn lock_store(&self) -> Result { + self.ensure_root()?; + let lock_path = self.root.join(".decision-record.lock"); + let existed = lock_path.exists(); + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(&lock_path) + .map_err(|error| DecisionRecordError(format!("open decision store lock: {error}")))?; + if !existed { + file.sync_all().map_err(|error| { + DecisionRecordError(format!("sync decision store lock: {error}")) + })?; + crate::staged_artifact::sync_directory_path(&self.root).map_err(|error| { + DecisionRecordError(format!("sync decision store lock directory: {error}")) + })?; + } + file.lock() + .map_err(|error| DecisionRecordError(format!("lock decision store: {error}")))?; + Ok(file) + } + + fn load_locked(&self) -> Result { + let mut records = Vec::new(); + let mut diagnostics = Vec::new(); + let entries = fs::read_dir(&self.root) + .map_err(|error| DecisionRecordError(format!("read decision record store: {error}")))?; + for entry_result in entries { + let entry = match entry_result { + Ok(entry) => entry, + Err(error) => { + diagnostics.push(format!("decision store read_dir entry failed: {error}")); + continue; + } + }; + let name = entry.file_name().to_string_lossy().into_owned(); + if name == ".decision-record.lock" { + continue; + } + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(error) => { + diagnostics.push(format!( + "decision store entry {name} file_type failed: {error}" + )); + continue; + } + }; + if file_type.is_symlink() { + diagnostics.push(format!("ignored symlink decision store entry {name}")); + continue; + } + let path = entry.path(); + if file_type.is_dir() { + if name == ".staging" { + match fs::read_dir(&path) { + Ok(mut staged) => match staged.next() { + None => {} + Some(Ok(_)) => diagnostics + .push("ignored uncommitted decision staging directory".to_string()), + Some(Err(error)) => diagnostics + .push(format!("decision staging read_dir entry failed: {error}")), + }, + Err(error) => { + diagnostics.push(format!("decision staging read_dir failed: {error}")) + } + } + continue; + } + match load_committed_record(&path) { + Ok(record) => records.push(record), + Err(error) => diagnostics.push(format!( + "ignored invalid or unreadable committed decision run {name}: {error}" + )), + } + } else if file_type.is_file() { + let diagnosis = read_record(&path).and_then(|record| validate_record(&record)); + diagnostics.push(match diagnosis { + Ok(()) => format!("ignored uncommitted legacy decision record file {name}"), + Err(error) => { + format!("ignored invalid or unreadable decision store file {name}: {error}") + } + }); + } else { + diagnostics.push(format!("ignored unsupported decision store entry {name}")); + } + } + records.sort_by(|left, right| left["id"].as_str().cmp(&right["id"].as_str())); + Ok(Loaded { + records, + diagnostics, + }) + } + + fn publish(&self, record: &Value) -> Result<(), DecisionRecordError> { + let digest = record["bindingDigest"].as_str().unwrap(); + let bytes = serde_json::to_vec_pretty(record) + .map_err(|error| DecisionRecordError(format!("serialize decision record: {error}")))?; + validate_decision_record_artifact(&bytes).map_err(DecisionRecordError)?; + let snapshot = record["snapshotIdentity"].as_str().unwrap(); + let mut writer = crate::staged_artifact::StagedWriter::begin(&self.root, snapshot) + .map_err(|error| DecisionRecordError(format!("begin decision staging: {error}")))?; + let record_ref = writer + .stage( + &bytes, + crate::staged_artifact::ArtifactWriteContract { + artifact_schema: RECORD_SCHEMA, + artifact_type: "decision.record", + max_bytes: MAX_RECORD_BYTES, + validate_payload: validate_decision_record_artifact, + }, + ) + .map_err(|error| DecisionRecordError(format!("stage decision record: {error}")))? + .to_artifact_ref_value(); + let manifest = json!({ + "schema":"code-intel-run-manifest.v1", + "runIdentity":format!("dag-v1:{digest}"), + "snapshotIdentity":snapshot, + "outcome":"completed", + "nodes":{"decision_record":{"status":"succeeded","verdict":"pass","artifacts":[record_ref]}} + }); + let manifest_bytes = serde_json::to_vec(&manifest).map_err(|error| { + DecisionRecordError(format!("serialize decision manifest: {error}")) + })?; + let manifest_ref = writer + .stage( + &manifest_bytes, + crate::staged_artifact::ArtifactWriteContract { + artifact_schema: "code-intel-run-manifest.v1", + artifact_type: "run.manifest", + max_bytes: 8 * 1024 * 1024, + validate_payload: crate::run_commit::validate_run_manifest_bytes, + }, + ) + .map_err(|error| DecisionRecordError(format!("stage decision manifest: {error}")))? + .to_artifact_ref_value(); + let staged = writer + .seal() + .map_err(|error| DecisionRecordError(format!("seal decision staging: {error}")))?; + let committed = crate::run_commit::commit( + staged, + &manifest_ref, + &format!("decision-{digest}"), + crate::run_commit::CommitOptions::default(), + ) + .map_err(|error| DecisionRecordError(format!("commit decision run: {error}")))?; + let loaded = load_committed_record(&committed.final_path)?; + if loaded != *record { + return Err(DecisionRecordError( + "committed decision record verification mismatch".to_string(), + )); + } + Ok(()) + } +} + +struct Loaded { + records: Vec, + diagnostics: Vec, +} + +pub(crate) fn validate_decision_record_artifact(bytes: &[u8]) -> Result<(), String> { + if bytes.len() as u64 > MAX_RECORD_BYTES { + return Err("decision record exceeds size limit".to_string()); + } + crate::artifact_ref::validate_decision_record_schema(bytes)?; + let value: Value = serde_json::from_slice(bytes) + .map_err(|error| format!("decision record artifact is invalid JSON: {error}"))?; + validate_record(&value).map_err(|error| error.to_string()) +} + +fn load_committed_record(path: &Path) -> Result { + let (_, manifest) = crate::run_commit::validate_committed_run(path) + .map_err(|error| DecisionRecordError(format!("validate committed run: {error}")))?; + let mut refs = Vec::new(); + for node in manifest["nodes"].as_object().unwrap().values() { + if let Some(artifacts) = node["artifacts"].as_array() { + refs.extend(artifacts.iter().filter(|artifact| { + artifact["artifactSchema"] == RECORD_SCHEMA && artifact["type"] == "decision.record" + })); + } + } + if refs.len() != 1 { + return Err(DecisionRecordError( + "committed decision run must contain exactly one decision record artifact".to_string(), + )); + } + let artifact = refs[0]; + let relative = artifact["path"].as_str().ok_or_else(|| { + DecisionRecordError("decision record artifact path is invalid".to_string()) + })?; + let components = relative.split('/').collect::>(); + let stable = crate::stable_artifact::read_beneath(path, &components, MAX_RECORD_BYTES) + .map_err(|error| { + DecisionRecordError(format!("read committed decision record: {error:?}")) + })?; + if artifact["sha256"] != crate::capability::sha256_hex(&stable.bytes) { + return Err(DecisionRecordError( + "committed decision record digest mismatch".to_string(), + )); + } + validate_decision_record_artifact(&stable.bytes).map_err(DecisionRecordError)?; + serde_json::from_slice(&stable.bytes) + .map_err(|error| DecisionRecordError(format!("parse committed decision record: {error}"))) +} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match run_cli(raw) { + Ok(value) => { + println!("{}", serde_json::to_string_pretty(&value).unwrap()); + 0 + } + Err(error) => { + eprintln!("decision record: {error}"); + println!( + "{}", + serde_json::to_string(&json!({ + "schema":"code-intel-decision-record-operation-result.v1", + "status":"rejected","questionRequired":true,"record":null, + "reason":"contract_rejected","diagnostics":[error.to_string()] + })) + .unwrap() + ); + 65 + } + } +} + +fn run_cli(raw: &[String]) -> Result { + let operation = raw + .first() + .map(String::as_str) + .ok_or_else(|| DecisionRecordError("expected record or replay subcommand".to_string()))?; + let mut input = None; + let mut store = None; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + let value = raw + .get(index + 1) + .ok_or_else(|| DecisionRecordError(format!("{flag} requires a value")))?; + match flag { + "--resolution" if operation == "record" && input.is_none() => input = Some(value), + "--query" if operation == "replay" && input.is_none() => input = Some(value), + "--store" if store.is_none() => store = Some(value), + _ => { + return Err(DecisionRecordError(format!( + "unknown or duplicate option {flag}" + ))) + } + } + index += 2; + } + let input = read_json(Path::new(input.ok_or_else(|| { + DecisionRecordError(format!( + "--{} is required", + if operation == "record" { + "resolution" + } else { + "query" + } + )) + })?))?; + let store = DecisionRecordStore::new(PathBuf::from( + store.ok_or_else(|| DecisionRecordError("--store is required".to_string()))?, + )); + match operation { + "record" => store.record(&input), + "replay" => store.replay(&input), + _ => Err(DecisionRecordError( + "expected record or replay subcommand".to_string(), + )), + } +} + +fn build_record(resolution: &Value) -> Result { + exact( + resolution, + &[ + "schema", + "gap", + "request", + "response", + "authorityEvent", + "snapshotIdentity", + "recordedAt", + ], + "decision record request", + )?; + if resolution["schema"] != "code-intel-decision-record-request.v1" { + return Err(DecisionRecordError( + "decision record request schema is invalid".to_string(), + )); + } + let snapshot = digest(&resolution["snapshotIdentity"], "snapshotIdentity")?; + let recorded_at = unsigned(&resolution["recordedAt"], "recordedAt")?; + validate_gap(&resolution["gap"])?; + validate_request(&resolution["request"])?; + validate_response(&resolution["response"])?; + bind_exchange(resolution, recorded_at)?; + + let evidence = validate_evidence_refs(&resolution["request"]["evidenceRefs"])?; + let evidence_ids = evidence + .iter() + .map(|value| value["refId"].as_str().unwrap().to_string()) + .collect::>(); + let empty = BTreeSet::new(); + crate::authority::validate_authority_event( + &resolution["authorityEvent"], + recorded_at, + &evidence_ids, + &evidence_ids, + &empty, + ) + .map_err(DecisionRecordError)?; + let actor = &resolution["response"]["actorProvenance"]; + let approver = &resolution["authorityEvent"]["approver"]; + if actor["actorId"] != approver["id"] + || actor["authorityKind"] != approver["role"] + || actor["authorityKind"] != resolution["request"]["authorityNeeded"]["kind"] + { + return Err(DecisionRecordError( + "authority event approver does not bind the accepted response".to_string(), + )); + } + + let accepted = resolution["response"]["answer"].clone(); + let consequences = match accepted["kind"].as_str() { + Some("choice") => { + let option_id = accepted["optionId"].as_str().unwrap(); + vec![resolution["request"]["options"] + .as_array() + .unwrap() + .iter() + .find(|option| option["id"] == option_id) + .unwrap()["consequence"] + .clone()] + } + Some("free-form") => Vec::new(), + _ => unreachable!(), + }; + let evidence_expires_at = evidence + .iter() + .filter_map(|value| value["expiresAt"].as_u64()) + .min() + .unwrap(); + if recorded_at > evidence_expires_at { + return Err(DecisionRecordError( + "decision record cannot bind expired evidence".to_string(), + )); + } + let evidence_digest = digest_value(&Value::Array(evidence.clone())); + let binding = json!({ + "gap":resolution["gap"],"request":resolution["request"],"response":resolution["response"], + "authorityEvent":resolution["authorityEvent"],"snapshotIdentity":snapshot,"recordedAt":recorded_at + }); + let binding_digest = digest_value(&binding); + Ok(json!({ + "schema":RECORD_SCHEMA,"id":format!("decision-record-v1:{binding_digest}"),"bindingDigest":binding_digest, + "gap":resolution["gap"],"request":resolution["request"],"response":resolution["response"], + "evidenceBinding":{"refs":evidence,"digest":evidence_digest},"snapshotIdentity":snapshot, + "acceptedChoice":accepted,"authorityEvent":resolution["authorityEvent"],"consequences":consequences, + "affectedBranches":resolution["request"]["affectedBranches"],"recordedAt":recorded_at, + "freshness":{"evidenceExpiresAt":evidence_expires_at,"state":"current"}, + "reopenRule":{"evidenceDigestChanged":true,"snapshotChanged":true,"evidenceExpired":true} + })) +} + +fn validate_record(record: &Value) -> Result<(), DecisionRecordError> { + exact( + record, + &[ + "schema", + "id", + "bindingDigest", + "gap", + "request", + "response", + "evidenceBinding", + "snapshotIdentity", + "acceptedChoice", + "authorityEvent", + "consequences", + "affectedBranches", + "recordedAt", + "freshness", + "reopenRule", + ], + "decision record", + )?; + if record["schema"] != RECORD_SCHEMA { + return Err(DecisionRecordError( + "decision record schema is invalid".to_string(), + )); + } + let reconstructed = build_record(&json!({ + "schema":"code-intel-decision-record-request.v1","gap":record["gap"],"request":record["request"], + "response":record["response"],"authorityEvent":record["authorityEvent"], + "snapshotIdentity":record["snapshotIdentity"],"recordedAt":record["recordedAt"] + }))?; + if reconstructed != *record { + return Err(DecisionRecordError( + "decision record content binding is invalid".to_string(), + )); + } + Ok(()) +} + +fn validate_gap(gap: &Value) -> Result<(), DecisionRecordError> { + exact( + gap, + &[ + "schema", + "id", + "kind", + "blockedDecision", + "discoverableFactsChecked", + "options", + "recommendedAnswer", + "affectedBranches", + "authorityRequired", + "authorityState", + "effects", + ], + "decision gap", + )?; + if gap["schema"] != "code-intel-decision-gap.v1" + || !matches!( + gap["kind"].as_str(), + Some("intent" | "priority" | "resource_allocation" | "risk_acceptance" | "tradeoff") + ) + || gap["authorityRequired"] != true + || gap["authorityState"] != "unresolved" + || gap["effects"] != json!([]) + { + return Err(DecisionRecordError( + "decision gap contract is invalid".to_string(), + )); + } + nonempty(&gap["id"], "gap id")?; + nonempty(&gap["blockedDecision"], "blocked decision")?; + let facts = gap["discoverableFactsChecked"].as_array().ok_or_else(|| { + DecisionRecordError("discoverableFactsChecked must be an array".to_string()) + })?; + if facts.is_empty() { + return Err(DecisionRecordError( + "discoverableFactsChecked must not be empty".to_string(), + )); + } + let mut fact_ids = BTreeSet::new(); + for fact in facts { + exact(fact, &["factId", "status"], "fact check")?; + let id = nonempty(&fact["factId"], "fact id")?; + if fact["status"] != "resolved" || !fact_ids.insert(id.to_string()) { + return Err(DecisionRecordError( + "fact checks must be resolved and unique".to_string(), + )); + } + } + let options = validate_options(&gap["options"])?; + exact( + &gap["recommendedAnswer"], + &["kind", "optionId", "rationale"], + "recommended answer", + )?; + let recommended = nonempty(&gap["recommendedAnswer"]["optionId"], "recommended option")?; + if gap["recommendedAnswer"]["kind"] != "proposal" + || !options.contains(recommended) + || nonempty( + &gap["recommendedAnswer"]["rationale"], + "recommendation rationale", + ) + .is_err() + { + return Err(DecisionRecordError( + "recommended answer contract is invalid".to_string(), + )); + } + string_set(&gap["affectedBranches"], "gap affectedBranches")?; + Ok(()) +} + +fn validate_request(request: &Value) -> Result<(), DecisionRecordError> { + exact( + request, + &[ + "schema", + "correlationId", + "gapId", + "question", + "recommendation", + "evidenceRefs", + "options", + "authorityNeeded", + "issuedAt", + "expiresAt", + "affectedBranches", + ], + "decision request", + )?; + if request["schema"] != "code-intel-decision-request.v1" { + return Err(DecisionRecordError( + "decision request schema is invalid".to_string(), + )); + } + identifier(&request["correlationId"], "request correlationId")?; + nonempty(&request["gapId"], "request gapId")?; + nonempty(&request["question"], "request question")?; + exact( + &request["recommendation"], + &["optionId", "rationale"], + "request recommendation", + )?; + nonempty(&request["recommendation"]["optionId"], "recommended option")?; + nonempty( + &request["recommendation"]["rationale"], + "recommendation rationale", + )?; + let options = validate_options(&request["options"])?; + if !options.contains(request["recommendation"]["optionId"].as_str().unwrap()) { + return Err(DecisionRecordError( + "recommendation names unknown option".to_string(), + )); + } + validate_evidence_refs(&request["evidenceRefs"])?; + exact( + &request["authorityNeeded"], + &["kind", "actorIds"], + "authority needed", + )?; + nonempty(&request["authorityNeeded"]["kind"], "authority kind")?; + string_set( + &request["authorityNeeded"]["actorIds"], + "authority actorIds", + )?; + let issued = unsigned(&request["issuedAt"], "request issuedAt")?; + let expires = unsigned(&request["expiresAt"], "request expiresAt")?; + if issued >= expires { + return Err(DecisionRecordError( + "request expiry must follow issue".to_string(), + )); + } + string_set(&request["affectedBranches"], "request affectedBranches")?; + Ok(()) +} + +fn validate_response(response: &Value) -> Result<(), DecisionRecordError> { + exact( + response, + &[ + "schema", + "correlationId", + "gapId", + "answer", + "actorProvenance", + "timestamp", + ], + "decision response", + )?; + if response["schema"] != "code-intel-decision-response.v1" { + return Err(DecisionRecordError( + "decision response schema is invalid".to_string(), + )); + } + identifier(&response["correlationId"], "response correlationId")?; + nonempty(&response["gapId"], "response gapId")?; + match response["answer"]["kind"].as_str() { + Some("choice") => { + exact(&response["answer"], &["kind", "optionId"], "choice answer")?; + nonempty(&response["answer"]["optionId"], "answer optionId")?; + } + Some("free-form") => { + exact(&response["answer"], &["kind", "text"], "free-form answer")?; + nonempty(&response["answer"]["text"], "answer text")?; + } + _ => { + return Err(DecisionRecordError( + "response answer kind is invalid".to_string(), + )) + } + } + exact( + &response["actorProvenance"], + &["actorId", "authorityKind", "source"], + "actor provenance", + )?; + nonempty(&response["actorProvenance"]["actorId"], "actor id")?; + nonempty( + &response["actorProvenance"]["authorityKind"], + "actor authority kind", + )?; + nonempty(&response["actorProvenance"]["source"], "actor source")?; + unsigned(&response["timestamp"], "response timestamp")?; + Ok(()) +} + +fn bind_exchange(resolution: &Value, recorded_at: u64) -> Result<(), DecisionRecordError> { + let gap = &resolution["gap"]; + let request = &resolution["request"]; + let response = &resolution["response"]; + if gap["id"] != request["gapId"] || request["gapId"] != response["gapId"] { + return Err(DecisionRecordError( + "decision response gap binding mismatch".to_string(), + )); + } + if request["correlationId"] != response["correlationId"] { + return Err(DecisionRecordError( + "decision response correlation mismatch".to_string(), + )); + } + if gap["options"] != request["options"] + || gap["recommendedAnswer"]["optionId"] != request["recommendation"]["optionId"] + || gap["recommendedAnswer"]["rationale"] != request["recommendation"]["rationale"] + || gap["affectedBranches"] != request["affectedBranches"] + { + return Err(DecisionRecordError( + "gap and request binding mismatch".to_string(), + )); + } + if let Some(option) = response["answer"]["optionId"].as_str() { + let options = validate_options(&request["options"])?; + if !options.contains(option) { + return Err(DecisionRecordError( + "response selects unknown option".to_string(), + )); + } + } + let actor = response["actorProvenance"]["actorId"].as_str().unwrap(); + if !string_set( + &request["authorityNeeded"]["actorIds"], + "authority actorIds", + )? + .contains(actor) + || response["actorProvenance"]["authorityKind"] != request["authorityNeeded"]["kind"] + { + return Err(DecisionRecordError( + "response actor lacks requested authority".to_string(), + )); + } + let timestamp = response["timestamp"].as_u64().unwrap(); + if timestamp < request["issuedAt"].as_u64().unwrap() + || timestamp > request["expiresAt"].as_u64().unwrap() + || timestamp > recorded_at + || recorded_at > request["expiresAt"].as_u64().unwrap() + || request["evidenceRefs"] + .as_array() + .unwrap() + .iter() + .any(|evidence| timestamp > evidence["expiresAt"].as_u64().unwrap()) + { + return Err(DecisionRecordError( + "response is outside request or evidence freshness".to_string(), + )); + } + Ok(()) +} + +fn validate_query(query: &Value) -> Result<(), DecisionRecordError> { + exact( + query, + &[ + "schema", + "gapId", + "snapshotIdentity", + "evidenceRefs", + "affectedBranches", + "now", + ], + "decision replay query", + )?; + if query["schema"] != "code-intel-decision-replay-query.v1" { + return Err(DecisionRecordError( + "decision replay query schema is invalid".to_string(), + )); + } + nonempty(&query["gapId"], "query gapId")?; + digest(&query["snapshotIdentity"], "query snapshotIdentity")?; + let evidence = validate_evidence_refs(&query["evidenceRefs"])?; + string_set(&query["affectedBranches"], "query affectedBranches")?; + let now = unsigned(&query["now"], "query now")?; + if evidence + .iter() + .any(|reference| now < reference["observedAt"].as_u64().unwrap()) + { + return Err(DecisionRecordError( + "decision replay time precedes observed evidence".to_string(), + )); + } + Ok(()) +} + +fn validate_options(value: &Value) -> Result, DecisionRecordError> { + let options = value + .as_array() + .ok_or_else(|| DecisionRecordError("options must be an array".to_string()))?; + if options.len() < 2 { + return Err(DecisionRecordError( + "at least two options are required".to_string(), + )); + } + let mut ids = BTreeSet::new(); + for option in options { + exact(option, &["id", "label", "consequence"], "decision option")?; + let id = nonempty(&option["id"], "option id")?; + nonempty(&option["label"], "option label")?; + nonempty(&option["consequence"], "option consequence")?; + if !ids.insert(id.to_string()) { + return Err(DecisionRecordError("duplicate option id".to_string())); + } + } + Ok(ids) +} + +fn validate_evidence_refs(value: &Value) -> Result, DecisionRecordError> { + let values = value + .as_array() + .ok_or_else(|| DecisionRecordError("evidenceRefs must be an array".to_string()))?; + if values.is_empty() { + return Err(DecisionRecordError( + "evidenceRefs must not be empty".to_string(), + )); + } + let mut refs = BTreeMap::new(); + for evidence in values { + exact( + evidence, + &["refId", "sha256", "observedAt", "expiresAt"], + "evidence ref", + )?; + let id = nonempty(&evidence["refId"], "evidence refId")?; + digest(&evidence["sha256"], "evidence sha256")?; + let observed = unsigned(&evidence["observedAt"], "evidence observedAt")?; + let expires = unsigned(&evidence["expiresAt"], "evidence expiresAt")?; + if observed >= expires { + return Err(DecisionRecordError( + "evidence expiry must follow observation".to_string(), + )); + } + if refs.insert(id.to_string(), evidence.clone()).is_some() { + return Err(DecisionRecordError("duplicate evidence refId".to_string())); + } + } + Ok(refs.into_values().collect()) +} + +fn outcome( + status: &str, + question_required: bool, + record: Option, + reason: Option<&str>, + diagnostics: Vec, +) -> Value { + json!({ + "schema":"code-intel-decision-record-operation-result.v1","status":status, + "questionRequired":question_required,"record":record,"reason":reason,"diagnostics":diagnostics + }) +} + +fn read_record(path: &Path) -> Result { + let metadata = fs::metadata(path) + .map_err(|error| DecisionRecordError(format!("inspect record: {error}")))?; + if !metadata.is_file() || metadata.len() > MAX_RECORD_BYTES { + return Err(DecisionRecordError( + "record must be a bounded regular file".to_string(), + )); + } + read_json(path) +} + +fn read_json(path: &Path) -> Result { + let bytes = + fs::read(path).map_err(|error| DecisionRecordError(format!("read JSON: {error}")))?; + if bytes.len() as u64 > MAX_RECORD_BYTES { + return Err(DecisionRecordError("JSON exceeds size limit".to_string())); + } + let text = std::str::from_utf8(&bytes) + .map_err(|_| DecisionRecordError("JSON must be UTF-8".to_string()))?; + crate::capability::reject_duplicate_json_keys(text).map_err(DecisionRecordError)?; + serde_json::from_str(text).map_err(|_| DecisionRecordError("invalid JSON".to_string())) +} + +fn digest_value(value: &Value) -> String { + crate::capability::sha256_hex(&serde_json::to_vec(&canonical(value)).unwrap()) +} + +fn canonical(value: &Value) -> Value { + match value { + Value::Object(object) => { + let mut sorted = object.iter().collect::>(); + sorted.sort_by(|left, right| left.0.cmp(right.0)); + let mut result = Map::new(); + for (key, value) in sorted { + result.insert(key.clone(), canonical(value)); + } + Value::Object(result) + } + Value::Array(values) => Value::Array(values.iter().map(canonical).collect()), + _ => value.clone(), + } +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), DecisionRecordError> { + let object = value + .as_object() + .ok_or_else(|| DecisionRecordError(format!("{label} must be an object")))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(DecisionRecordError(format!("{label} fields are invalid"))) + } +} + +fn nonempty<'a>(value: &'a Value, label: &str) -> Result<&'a str, DecisionRecordError> { + value + .as_str() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| DecisionRecordError(format!("{label} is invalid"))) +} + +fn identifier<'a>(value: &'a Value, label: &str) -> Result<&'a str, DecisionRecordError> { + let value = nonempty(value, label)?; + if value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + { + Ok(value) + } else { + Err(DecisionRecordError(format!("{label} is not portable"))) + } +} + +fn digest<'a>(value: &'a Value, label: &str) -> Result<&'a str, DecisionRecordError> { + let value = nonempty(value, label)?; + if value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + { + Ok(value) + } else { + Err(DecisionRecordError(format!( + "{label} must be lowercase SHA-256" + ))) + } +} + +fn unsigned(value: &Value, label: &str) -> Result { + value + .as_u64() + .ok_or_else(|| DecisionRecordError(format!("{label} must be an unsigned integer"))) +} + +fn string_set(value: &Value, label: &str) -> Result, DecisionRecordError> { + let values = value + .as_array() + .ok_or_else(|| DecisionRecordError(format!("{label} must be an array")))?; + if values.is_empty() { + return Err(DecisionRecordError(format!("{label} must not be empty"))); + } + let mut result = BTreeSet::new(); + for value in values { + let item = nonempty(value, label)?.to_string(); + if !result.insert(item) { + return Err(DecisionRecordError(format!("{label} contains duplicates"))); + } + } + Ok(result) +} diff --git a/crates/code-intel-cli/src/delivery_light_speed.rs b/crates/code-intel-cli/src/delivery_light_speed.rs new file mode 100644 index 0000000..54a17bd --- /dev/null +++ b/crates/code-intel-cli/src/delivery_light_speed.rs @@ -0,0 +1,717 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +use crate::artifact_ref::VerifiedArtifact; +use crate::capability::sha256_hex; +use crate::capability_inventory::{AdapterArtifact, AdapterError, AdapterOutput}; + +const METHOD_CARDS: [&str; 2] = ["critical-path-pert", "value-stream-queue-delay"]; +const METHOD_CATALOG_BYTES: &[u8] = + include_bytes!("../../../orchestration/methods/catalog.v1.json"); +const CRITICAL_PATH_CARD_BYTES: &[u8] = + include_bytes!("../../../orchestration/methods/cards/critical-path-pert.v1.json"); +const VALUE_STREAM_CARD_BYTES: &[u8] = + include_bytes!("../../../orchestration/methods/cards/value-stream-queue-delay.v1.json"); + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + if request["options"] + .as_object() + .map_or(true, |options| !options.is_empty()) + { + return Err(AdapterError::InvalidOptions( + "delivery.light-speed-measure accepts no options".into(), + )); + } + let inputs = Inputs::parse(request, verified_inputs)?; + let input = inputs.timing; + let timing: Value = serde_json::from_slice(input.bytes()) + .map_err(|error| AdapterError::Contract(format!("parse run timing events: {error}")))?; + if timing["measurementSnapshotIdentity"] != request["snapshot"]["identity"] + || input.consumed_snapshot_identity() + != request["snapshot"]["identity"].as_str().unwrap_or_default() + { + return Err(AdapterError::Contract( + "run timing measurement does not match the request snapshot".into(), + )); + } + let report = evaluate(&timing, input.sha256(), &inputs, request)?; + let bytes = serde_json::to_vec(&report).map_err(|error| { + AdapterError::Internal(format!("serialize light-speed report: {error}")) + })?; + let markdown = render(&report).into_bytes(); + publish(out, "light-speed-report.json", &bytes)?; + publish(out, "light-speed-report.md", &markdown)?; + Ok(AdapterOutput { + artifacts: vec![ + AdapterArtifact { + artifact_schema: "code-intel-delivery-light-speed.v1".into(), + artifact_type: "delivery.light-speed-report".into(), + relative_path: "light-speed-report.json".into(), + bytes, + }, + AdapterArtifact { + artifact_schema: "code-intel-delivery-light-speed-markdown.v1".into(), + artifact_type: "delivery.light-speed-report-view".into(), + relative_path: "light-speed-report.md".into(), + bytes: markdown, + }, + ], + observed_effects: vec!["local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +struct Inputs<'a> { + timing: &'a VerifiedArtifact, + commits: Vec<(&'a Value, Value)>, + manifests: Vec<(&'a Value, Value)>, + method_provenance: Value, +} + +impl<'a> Inputs<'a> { + fn parse(request: &'a Value, verified: &'a [VerifiedArtifact]) -> Result { + let refs = request["inputs"] + .as_array() + .ok_or_else(|| contract("request inputs must be an array"))?; + if refs.len() != verified.len() || verified.len() != 8 { + return Err(contract("delivery.light-speed-measure requires timing, two commits, two manifests, method catalog, and two method cards as A03 inputs")); + } + let mut timing = None; + let mut commits = Vec::new(); + let mut manifests = Vec::new(); + let mut catalog = None; + let mut cards = BTreeMap::new(); + for (reference, artifact) in refs.iter().zip(verified) { + match (artifact.artifact_schema(), artifact.artifact_type()) { + ("code-intel-run-timing-events.v1", "delivery.run-timing-events") => { + if timing.replace(artifact).is_some() { + return Err(contract("duplicate timing input")); + } + } + ("code-intel-run-commit.v1", "run.commit") => { + commits.push((reference, parse_json(artifact, "run commit")?)) + } + ("code-intel-run-manifest.v1", "run.manifest") => { + manifests.push((reference, parse_json(artifact, "run manifest")?)) + } + ("code-intel-method-catalog.v1", "method.catalog") => { + if catalog.replace((reference, artifact)).is_some() { + return Err(contract("duplicate method catalog input")); + } + } + ("code-intel-method-card.v1", "method.card") => { + let card = parse_json(artifact, "method card")?; + let id = card["id"] + .as_str() + .ok_or_else(|| contract("method card id is invalid"))?; + if cards + .insert(id.to_string(), (reference, artifact)) + .is_some() + { + return Err(contract("duplicate method card input")); + } + } + _ => return Err(contract("unexpected delivery light-speed input contract")), + } + } + let timing = timing.ok_or_else(|| contract("missing timing input"))?; + if commits.len() != 2 || manifests.len() != 2 || cards.len() != 2 { + return Err(contract( + "delivery light-speed input cardinality is invalid", + )); + } + let (catalog_ref, catalog_artifact) = + catalog.ok_or_else(|| contract("missing method catalog input"))?; + require_managed_bytes(catalog_artifact, METHOD_CATALOG_BYTES, "method catalog")?; + let mut card_shas = Vec::new(); + let mut card_refs = Vec::new(); + for (id, expected) in [ + (METHOD_CARDS[0], CRITICAL_PATH_CARD_BYTES), + (METHOD_CARDS[1], VALUE_STREAM_CARD_BYTES), + ] { + let (reference, artifact) = cards + .get(id) + .ok_or_else(|| contract(format!("missing method card {id}")))?; + require_managed_bytes(artifact, expected, id)?; + card_shas.push(artifact.sha256()); + card_refs.push((*reference).clone()); + } + let method_provenance = json!({ + "catalogRef":catalog_ref, + "catalogSha256":catalog_artifact.sha256(), + "methodCardRefs":card_refs, + "methodCardSha256":card_shas + }); + Ok(Self { + timing, + commits, + manifests, + method_provenance, + }) + } + + fn bind_commit(&self, commit_ref: &Value) -> Result<&Value, AdapterError> { + let (_, commit) = self + .commits + .iter() + .find(|(reference, _)| *reference == commit_ref) + .ok_or_else(|| { + contract("timing commitRef was not supplied as an A03-verified input") + })?; + let manifest_ref = self + .manifests + .iter() + .find(|(reference, _)| { + reference["path"] == commit["manifest"]["path"] + && reference["sha256"] == commit["manifest"]["sha256"] + }) + .ok_or_else(|| { + contract("A07 commit manifest object was not supplied as an A03-verified input") + })?; + let manifest = &manifest_ref.1; + if manifest["runIdentity"] != commit["runIdentity"] + || manifest["snapshotIdentity"] != commit["snapshotIdentity"] + { + return Err(contract( + "A07 commit does not match its A03-verified run manifest", + )); + } + Ok(commit) + } +} + +fn parse_json(artifact: &VerifiedArtifact, label: &str) -> Result { + serde_json::from_slice(artifact.bytes()) + .map_err(|error| contract(format!("parse {label}: {error}"))) +} + +fn require_managed_bytes( + artifact: &VerifiedArtifact, + expected: &[u8], + label: &str, +) -> Result<(), AdapterError> { + if artifact.sha256() != sha256_hex(expected) || artifact.bytes() != expected { + return Err(contract(format!( + "{label} differs from the managed C01 method evidence" + ))); + } + Ok(()) +} + +fn evaluate( + input: &Value, + source_sha256: &str, + inputs: &Inputs<'_>, + request: &Value, +) -> Result { + exact( + input, + &[ + "schema", + "measurementSnapshotIdentity", + "telemetry", + "baseline", + "current", + ], + "run timing events", + )?; + exact( + &input["telemetry"], + &["mode", "clock", "externalPlatform"], + "telemetry policy", + )?; + if input["schema"] != "code-intel-run-timing-events.v1" + || !digest(&input["measurementSnapshotIdentity"]) + || input["telemetry"]["mode"] != "local_opt_in" + || input["telemetry"]["clock"] != "monotonic_elapsed_ms" + || input["telemetry"]["externalPlatform"] != false + { + return Err(contract( + "run timing events must be local opt-in monotonic telemetry", + )); + } + let baseline_commit = inputs.bind_commit(&input["baseline"]["commitRef"])?; + let current_commit = inputs.bind_commit(&input["current"]["commitRef"])?; + if current_commit["snapshotIdentity"] != request["snapshot"]["identity"] { + return Err(contract( + "current A07 commit does not match the request snapshot", + )); + } + let baseline = measure_trace( + &input["baseline"], + baseline_commit, + "baseline", + source_sha256, + )?; + let current = measure_trace(&input["current"], current_commit, "current", source_sha256)?; + if baseline.run_identity == current.run_identity { + return Err(contract("baseline and current committed runs must differ")); + } + let delta = delta(&baseline.document, ¤t.document); + Ok(json!({ + "schema":"code-intel-delivery-light-speed.v1", + "measurementSnapshotIdentity":input["measurementSnapshotIdentity"], + "authority":"derived_measurement_no_schedule_commitment", + "method":{ + "methodCardIds":METHOD_CARDS, + "provenance":inputs.method_provenance, + "valueStreamFormula":"leadTimeMs = max(completedAtMs) - min(startedAtMs); touch/category time is the sum of explicit event intervals", + "queueFormula":"queueMs is the duration of explicit queue events; it is not inferred from mandatory verification", + "criticalPathFormula":"maximum-duration predecessor closure; every join predecessor and ancestor is included once; lexical event-id order breaks equal-duration ties", + "deltaFormula":"current minus baseline in milliseconds" + }, + "rules":rules(), + "baseline":baseline.document, + "current":current.document, + "delta":delta, + "limitations":[ + "local opt-in events are measured only after an A07 commit marker is bound into each trace", + "attribution reports observed intervals and does not promise a delivery date, staffing level, or schedule", + "resource contention, arrival rates, and capacity remain unknown unless represented by explicit committed events" + ] + })) +} + +struct TraceMeasurement { + run_identity: String, + document: Value, +} + +#[derive(Clone)] +struct Event<'a> { + value: &'a Value, + id: &'a str, + kind: &'a str, + subject: &'a str, + start: u64, + end: u64, + predecessors: Vec<&'a str>, + pointer: String, +} + +fn measure_trace( + trace: &Value, + commit: &Value, + label: &str, + source_sha256: &str, +) -> Result { + exact(trace, &["commitRef", "events"], &format!("{label} trace"))?; + let values = trace["events"] + .as_array() + .filter(|events| !events.is_empty()) + .ok_or_else(|| contract(format!("{label} events must be a non-empty array")))?; + let mut events = Vec::with_capacity(values.len()); + let mut ids = BTreeSet::new(); + for (index, value) in values.iter().enumerate() { + exact( + value, + &[ + "id", + "kind", + "subject", + "startedAtMs", + "completedAtMs", + "mandatory", + "coordinationNeed", + "predecessors", + ], + &format!("{label} event"), + )?; + let id = nonempty(value, "id", "event id")?; + let kind = nonempty(value, "kind", "event kind")?; + let subject = nonempty(value, "subject", "event subject")?; + let start = value["startedAtMs"] + .as_u64() + .ok_or_else(|| contract("event startedAtMs is invalid"))?; + let end = value["completedAtMs"] + .as_u64() + .filter(|end| *end > start) + .ok_or_else(|| contract("event completedAtMs must be after startedAtMs"))?; + if !ids.insert(id) || !valid_id(id) { + return Err(contract("event ids must be unique stable ids")); + } + let mandatory = value["mandatory"] + .as_bool() + .ok_or_else(|| contract("event mandatory is invalid"))?; + validate_classification(value, kind, mandatory)?; + let predecessors = value["predecessors"] + .as_array() + .ok_or_else(|| contract("event predecessors must be an array"))? + .iter() + .map(|item| { + item.as_str() + .filter(|item| valid_id(item)) + .ok_or_else(|| contract("event predecessor is invalid")) + }) + .collect::, _>>()?; + if predecessors.iter().collect::>().len() != predecessors.len() { + return Err(contract("event predecessors must be unique")); + } + events.push(Event { + value, + id, + kind, + subject, + start, + end, + predecessors, + pointer: format!("/{label}/events/{index}"), + }); + } + events.sort_by_key(|event| (event.start, event.id)); + let by_id = events + .iter() + .map(|event| (event.id, event.clone())) + .collect::>(); + for event in &events { + for predecessor in &event.predecessors { + let prior = by_id + .get(predecessor) + .ok_or_else(|| contract("event predecessor is missing"))?; + if prior.end > event.start { + return Err(contract( + "event predecessors must complete before the dependent event starts", + )); + } + } + } + let categories = classify(&events); + let critical_ids = critical_path(&events, &by_id)?; + let critical_events = critical_ids + .iter() + .map(|id| by_id.get(id.as_str()).unwrap().clone()) + .collect::>(); + let critical = classify(&critical_events); + let first = events.iter().map(|event| event.start).min().unwrap(); + let last = events.iter().map(|event| event.end).max().unwrap(); + let touch = events.iter().map(duration).sum::(); + let provenance = provenance(&categories, source_sha256); + let run_identity = commit["runIdentity"].as_str().unwrap().to_string(); + let document = json!({ + "commitRef":trace["commitRef"], + "commit":commit, + "leadTimeMs":last-first, + "touchTimeMs":touch, + "categories":categories.values(), + "avoidableDelayMs":categories.avoidable(), + "criticalPath":{ + "eventIds":critical_ids, + "durationMs":critical_events.iter().map(duration).sum::(), + "irreducibleTechnicalWorkMs":critical.irreducible, + "mandatoryVerificationMs":critical.verification, + "requiredCoordinationMs":critical.required_coordination, + "queueMs":critical.queue, + "handoffMs":critical.handoff, + "repeatedUnderstandingMs":critical.repeated_understanding, + "reworkMs":critical.rework, + "unnecessaryCoordinationMs":critical.unnecessary_coordination, + "avoidableDelayMs":critical.avoidable() + }, + "provenance":provenance + }); + Ok(TraceMeasurement { + run_identity, + document, + }) +} + +#[derive(Default)] +struct Categories { + irreducible: u64, + verification: u64, + required_coordination: u64, + queue: u64, + handoff: u64, + repeated_understanding: u64, + rework: u64, + unnecessary_coordination: u64, + ids: BTreeMap<&'static str, Vec>, + pointers: BTreeMap<&'static str, Vec>, +} + +impl Categories { + fn add(&mut self, category: &'static str, event: &Event<'_>) { + let value = duration(event); + match category { + "irreducibleTechnicalWorkMs" => self.irreducible += value, + "mandatoryVerificationMs" => self.verification += value, + "requiredCoordinationMs" => self.required_coordination += value, + "queueMs" => self.queue += value, + "handoffMs" => self.handoff += value, + "repeatedUnderstandingMs" => self.repeated_understanding += value, + "reworkMs" => self.rework += value, + "unnecessaryCoordinationMs" => self.unnecessary_coordination += value, + _ => unreachable!(), + } + self.ids + .entry(category) + .or_default() + .push(event.id.to_string()); + self.pointers + .entry(category) + .or_default() + .push(event.pointer.clone()); + } + + fn avoidable(&self) -> u64 { + self.queue + + self.handoff + + self.repeated_understanding + + self.rework + + self.unnecessary_coordination + } + + fn values(&self) -> Value { + json!({ + "irreducibleTechnicalWorkMs":self.irreducible, + "mandatoryVerificationMs":self.verification, + "requiredCoordinationMs":self.required_coordination, + "queueMs":self.queue, + "handoffMs":self.handoff, + "repeatedUnderstandingMs":self.repeated_understanding, + "reworkMs":self.rework, + "unnecessaryCoordinationMs":self.unnecessary_coordination + }) + } +} + +fn classify(events: &[Event<'_>]) -> Categories { + let mut categories = Categories::default(); + let mut understood = BTreeSet::new(); + for event in events { + let category = match event.kind { + "technical_work" => "irreducibleTechnicalWorkMs", + "test" | "verification" => "mandatoryVerificationMs", + "queue" => "queueMs", + "handoff" => "handoffMs", + "rework" => "reworkMs", + "understanding" if understood.insert(event.subject) => "irreducibleTechnicalWorkMs", + "understanding" => "repeatedUnderstandingMs", + "coordination" if event.value["coordinationNeed"] == "required" => { + "requiredCoordinationMs" + } + "coordination" => "unnecessaryCoordinationMs", + _ => unreachable!(), + }; + categories.add(category, event); + } + categories +} + +fn critical_path<'a>( + events: &[Event<'a>], + by_id: &BTreeMap<&'a str, Event<'a>>, +) -> Result, AdapterError> { + let mut closures: BTreeMap<&str, BTreeSet> = BTreeMap::new(); + for event in events { + let mut closure = BTreeSet::new(); + for predecessor in &event.predecessors { + let predecessor_closure = closures + .get(predecessor) + .ok_or_else(|| contract("event graph is cyclic or not predecessor-closed"))?; + closure.extend(predecessor_closure.iter().cloned()); + } + closure.insert(event.id.to_string()); + closures.insert(event.id, closure); + } + let selected = closures + .values() + .map(|closure| { + let duration_ms = closure + .iter() + .map(|id| duration(by_id.get(id.as_str()).expect("closure ids are verified"))) + .sum::(); + (duration_ms, closure) + }) + .max_by(|left, right| { + left.0 + .cmp(&right.0) + .then_with(|| right.1.iter().cmp(left.1.iter())) + }) + .map(|(_, closure)| closure) + .ok_or_else(|| contract("critical path requires events"))?; + Ok(events + .iter() + .filter(|event| selected.contains(event.id)) + .map(|event| event.id.to_string()) + .collect()) +} + +fn provenance(categories: &Categories, source_sha256: &str) -> Value { + let mut result = serde_json::Map::new(); + for (category, rule) in [ + ( + "irreducibleTechnicalWorkMs", + "irreducible-and-first-understanding", + ), + ( + "mandatoryVerificationMs", + "mandatory-verification-protection", + ), + ("requiredCoordinationMs", "coordination-necessity"), + ("queueMs", "explicit-queue-attribution"), + ("handoffMs", "explicit-handoff-attribution"), + ("repeatedUnderstandingMs", "repeat-understanding-by-subject"), + ("reworkMs", "explicit-rework-attribution"), + ("unnecessaryCoordinationMs", "coordination-necessity"), + ] { + result.insert( + category.into(), + json!({ + "sourceArtifactSha256":source_sha256, + "jsonPointers":categories.pointers.get(category).cloned().unwrap_or_default(), + "eventIds":categories.ids.get(category).cloned().unwrap_or_default(), + "ruleId":rule + }), + ); + } + Value::Object(result) +} + +fn delta(baseline: &Value, current: &Value) -> Value { + let difference = |current: u64, baseline: u64| i128::from(current) - i128::from(baseline); + let field = + |document: &Value, pointer: &str| document.pointer(pointer).unwrap().as_u64().unwrap(); + json!({ + "formula":"current_minus_baseline_ms", + "leadTimeMs":difference(field(current,"/leadTimeMs"),field(baseline,"/leadTimeMs")), + "touchTimeMs":difference(field(current,"/touchTimeMs"),field(baseline,"/touchTimeMs")), + "irreducibleTechnicalWorkMs":difference(field(current,"/categories/irreducibleTechnicalWorkMs"),field(baseline,"/categories/irreducibleTechnicalWorkMs")), + "mandatoryVerificationMs":difference(field(current,"/categories/mandatoryVerificationMs"),field(baseline,"/categories/mandatoryVerificationMs")), + "requiredCoordinationMs":difference(field(current,"/categories/requiredCoordinationMs"),field(baseline,"/categories/requiredCoordinationMs")), + "queueMs":difference(field(current,"/categories/queueMs"),field(baseline,"/categories/queueMs")), + "handoffMs":difference(field(current,"/categories/handoffMs"),field(baseline,"/categories/handoffMs")), + "repeatedUnderstandingMs":difference(field(current,"/categories/repeatedUnderstandingMs"),field(baseline,"/categories/repeatedUnderstandingMs")), + "reworkMs":difference(field(current,"/categories/reworkMs"),field(baseline,"/categories/reworkMs")), + "unnecessaryCoordinationMs":difference(field(current,"/categories/unnecessaryCoordinationMs"),field(baseline,"/categories/unnecessaryCoordinationMs")), + "avoidableDelayMs":difference(field(current,"/avoidableDelayMs"),field(baseline,"/avoidableDelayMs")) + }) +} + +fn rules() -> Value { + json!([ + {"id":"explicit-queue-attribution","methodCardIds":METHOD_CARDS,"rule":"only explicit queue intervals contribute to queueMs"}, + {"id":"explicit-handoff-attribution","methodCardIds":METHOD_CARDS,"rule":"only explicit handoff intervals contribute to handoffMs"}, + {"id":"repeat-understanding-by-subject","methodCardIds":METHOD_CARDS,"rule":"the first understanding interval per subject is required; later intervals for that subject are repeated understanding"}, + {"id":"explicit-rework-attribution","methodCardIds":METHOD_CARDS,"rule":"explicit rework intervals are reported separately"}, + {"id":"coordination-necessity","methodCardIds":METHOD_CARDS,"rule":"coordinationNeed required is protected; unnecessary is avoidable"}, + {"id":"mandatory-verification-protection","methodCardIds":METHOD_CARDS,"rule":"test and verification intervals must be mandatory and never contribute to avoidable delay"}, + {"id":"predecessor-critical-path","methodCardIds":METHOD_CARDS,"rule":"critical path is the maximum-duration full predecessor closure with deterministic lexical tie-breaking"} + ]) +} + +fn validate_classification(value: &Value, kind: &str, mandatory: bool) -> Result<(), AdapterError> { + if !matches!( + kind, + "technical_work" + | "test" + | "verification" + | "queue" + | "handoff" + | "understanding" + | "rework" + | "coordination" + ) { + return Err(contract("event kind is unknown")); + } + let coordination = value["coordinationNeed"].as_str(); + match kind { + "test" | "verification" if !mandatory || coordination.is_some() => Err(contract( + "test/verification events must be mandatory and have no coordinationNeed", + )), + "test" | "verification" => Ok(()), + "coordination" + if !matches!(coordination, Some("required" | "unnecessary")) + || mandatory != (coordination == Some("required")) => + { + Err(contract( + "coordination must declare required/mandatory or unnecessary/non-mandatory", + )) + } + "coordination" => Ok(()), + _ if mandatory || !value["coordinationNeed"].is_null() => Err(contract( + "only mandatory verification and required coordination may be mandatory", + )), + _ => Ok(()), + } +} + +fn render(report: &Value) -> String { + format!( + "# Delivery Light-Speed Measurement\n\n- Authority: derived measurement; no schedule commitment\n- Baseline lead time: {} ms\n- Current lead time: {} ms\n- Lead-time delta (current - baseline): {} ms\n- Baseline avoidable delay: {} ms\n- Current avoidable delay: {} ms\n- Avoidable-delay delta (current - baseline): {} ms\n- Baseline mandatory verification protected: {} ms\n- Current mandatory verification protected: {} ms\n", + report["baseline"]["leadTimeMs"], + report["current"]["leadTimeMs"], + report["delta"]["leadTimeMs"], + report["baseline"]["avoidableDelayMs"], + report["current"]["avoidableDelayMs"], + report["delta"]["avoidableDelayMs"], + report["baseline"]["categories"]["mandatoryVerificationMs"], + report["current"]["categories"]["mandatoryVerificationMs"] + ) +} + +fn publish(out: &Path, relative: &str, bytes: &[u8]) -> Result<(), AdapterError> { + fs::create_dir_all(out) + .map_err(|error| AdapterError::Io(format!("create light-speed output: {error}")))?; + let path = out.join(relative); + if path.exists() { + return Err(AdapterError::Io(format!( + "refusing to overwrite light-speed artifact: {relative}" + ))); + } + fs::write(path, bytes).map_err(|error| AdapterError::Io(format!("write {relative}: {error}"))) +} + +fn duration(event: &Event<'_>) -> u64 { + event.end - event.start +} + +fn nonempty<'a>(value: &'a Value, field: &str, context: &str) -> Result<&'a str, AdapterError> { + value[field] + .as_str() + .filter(|text| !text.is_empty()) + .ok_or_else(|| contract(format!("{context} is invalid"))) +} + +fn exact(value: &Value, fields: &[&str], context: &str) -> Result<(), AdapterError> { + let actual = value + .as_object() + .ok_or_else(|| contract(format!("{context} must be an object")))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual != expected { + return Err(contract(format!("{context} fields are not exact"))); + } + Ok(()) +} + +fn valid_id(value: &str) -> bool { + !value.is_empty() + && value.len() <= 128 + && value.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"._-".contains(&byte) + }) +} + +fn digest(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 64 + && text + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + }) +} + +fn contract(message: impl Into) -> AdapterError { + AdapterError::Contract(message.into()) +} diff --git a/crates/code-intel-cli/src/doctor_adapter.rs b/crates/code-intel-cli/src/doctor_adapter.rs new file mode 100644 index 0000000..8bdddb0 --- /dev/null +++ b/crates/code-intel-cli/src/doctor_adapter.rs @@ -0,0 +1,512 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde_json::{json, Value}; + +use super::{AdapterArtifact, AdapterError, AdapterOutput}; +use crate::artifact_ref::VerifiedArtifact; +use crate::capability::sha256_hex; + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + validate_snapshot_input(request, verified_inputs)?; + let options = Options::parse(request)?; + let bootstrap = run_bootstrap(&options)?; + let manifest = validate_manifest(&options.manifest_path)?; + let document = adapt(request, &options, &bootstrap, &manifest)?; + let domain_failure = diagnosis(&document); + let domain_verdict = if domain_failure.is_some() { + crate::capability_inventory::AdapterDomainVerdict::Fail + } else { + crate::capability_inventory::AdapterDomainVerdict::Pass + }; + let bytes = serde_json::to_vec(&document).map_err(|error| { + AdapterError::Internal(format!("serialize doctor observation: {error}")) + })?; + publish(out, "doctor-observation.json", &bytes)?; + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-doctor-observation.v1".into(), + artifact_type: "doctor.observation".into(), + relative_path: "doctor-observation.json".into(), + bytes, + }], + observed_effects: vec!["repo_read".into(), "local_write".into()], + domain_verdict, + domain_failure, + }) +} + +struct Options { + repo_path: PathBuf, + config_path: Option, + manifest_path: PathBuf, + platform: String, + require_repowise: bool, + require_understand: bool, + tool_path_prefix: Option, +} + +impl Options { + fn parse(request: &Value) -> Result { + let options = request["options"].as_object().ok_or_else(|| { + AdapterError::InvalidOptions("doctor options must be an object".into()) + })?; + let allowed = [ + "repoPath", + "configPath", + "manifestPath", + "platform", + "requireRepowise", + "requireUnderstand", + "toolPathPrefix", + ] + .into_iter() + .collect::>(); + if options.keys().any(|key| !allowed.contains(key.as_str())) { + return Err(AdapterError::InvalidOptions( + "doctor accepts only repoPath/configPath/manifestPath/platform/requireRepowise/requireUnderstand/toolPathPrefix" + .into(), + )); + } + let repo_path = required_existing_directory(options.get("repoPath"), "options.repoPath")?; + let config_path = optional_existing_file(options.get("configPath"), "options.configPath")?; + let manifest_path = match options.get("manifestPath") { + Some(value) => required_existing_file(Some(value), "options.manifestPath")?, + None => pipeline_root() + .join("orchestration") + .join("integrations.json"), + }; + if !manifest_path.is_file() { + return Err(AdapterError::InvalidOptions(format!( + "options.manifestPath is not a file: {}", + manifest_path.display() + ))); + } + let platform = options + .get("platform") + .map(|value| { + value + .as_str() + .filter(|value| matches!(*value, "auto" | "windows" | "macos" | "linux")) + .map(str::to_string) + .ok_or_else(|| { + AdapterError::InvalidOptions( + "options.platform must be auto/windows/macos/linux".into(), + ) + }) + }) + .transpose()? + .unwrap_or_else(|| "auto".into()); + Ok(Self { + repo_path, + config_path, + manifest_path, + platform, + require_repowise: optional_bool( + options.get("requireRepowise"), + true, + "requireRepowise", + )?, + require_understand: optional_bool( + options.get("requireUnderstand"), + false, + "requireUnderstand", + )?, + tool_path_prefix: options + .get("toolPathPrefix") + .map(|value| required_existing_directory(Some(value), "options.toolPathPrefix")) + .transpose()?, + }) + } +} + +fn validate_snapshot_input( + request: &Value, + inputs: &[VerifiedArtifact], +) -> Result<(), AdapterError> { + if inputs.len() != 1 + || inputs[0].artifact_schema() != "code-intel-repository-snapshot.v1" + || inputs[0].artifact_type() != "repository.snapshot" + { + return Err(AdapterError::Contract( + "doctor requires exactly one verified repository.snapshot Artifact Ref".into(), + )); + } + let snapshot: Value = serde_json::from_slice(inputs[0].bytes()).map_err(|error| { + AdapterError::Contract(format!( + "verified repository snapshot is invalid JSON: {error}" + )) + })?; + if snapshot.pointer("/snapshot/identity") != request.pointer("/snapshot/identity") { + return Err(AdapterError::Contract( + "doctor repository.snapshot payload differs from request snapshot".into(), + )); + } + Ok(()) +} + +fn run_bootstrap(options: &Options) -> Result { + let script = pipeline_root().join("check-code-intel-tools.ps1"); + if !script.is_file() { + return Err(AdapterError::Unavailable(format!( + "doctor bootstrap adapter is unavailable: {}", + script.display() + ))); + } + let mut command = Command::new("pwsh"); + command + .args(["-NoLogo", "-NoProfile", "-File"]) + .arg(&script) + .arg("-RepoPath") + .arg(&options.repo_path) + .arg("-Platform") + .arg(&options.platform) + .arg(format!("-RequireRepowise:${}", options.require_repowise)) + .arg(format!( + "-RequireUnderstand:${}", + options.require_understand + )) + .arg("-Json"); + if let Some(config) = &options.config_path { + command.arg("-Config").arg(config); + } + if let Some(prefix) = &options.tool_path_prefix { + let mut paths = vec![prefix.clone()]; + paths.extend(std::env::split_paths( + &std::env::var_os("PATH").unwrap_or_default(), + )); + let path = std::env::join_paths(paths).map_err(|error| { + AdapterError::InvalidOptions(format!("compose options.toolPathPrefix PATH: {error}")) + })?; + command + .env_remove("PATH") + .env_remove("Path") + .env("PATH", path); + } + let output = command + .output() + .map_err(|error| AdapterError::Unavailable(format!("start doctor bootstrap: {error}")))?; + let value: Value = serde_json::from_slice(&output.stdout).map_err(|error| { + AdapterError::Contract(format!( + "doctor bootstrap stdout is not one JSON observation: {error}" + )) + })?; + if value["schema"] != "code-intel-doctor-bootstrap-observation.v1" + || value["authority"] != "observation_only" + || !value["ok"].is_boolean() + { + return Err(AdapterError::Contract( + "doctor bootstrap observation lacks the non-authoritative v1 contract".into(), + )); + } + Ok(value) +} + +fn validate_manifest(path: &Path) -> Result { + let binary = std::env::current_exe() + .map_err(|error| AdapterError::Io(format!("locate code-intel executable: {error}")))?; + let output = Command::new(binary) + .args(["orchestrate", "--action", "Validate", "--manifest"]) + .arg(path) + .arg("--json") + .output() + .map_err(|error| { + AdapterError::Unavailable(format!("run manifest reconciliation: {error}")) + })?; + serde_json::from_slice(&output.stdout).map_err(|error| { + AdapterError::Contract(format!( + "manifest reconciliation stdout is not one JSON document: {error}" + )) + }) +} + +fn adapt( + request: &Value, + options: &Options, + raw: &Value, + manifest: &Value, +) -> Result { + let tools = raw + .pointer("/checks/tools") + .and_then(Value::as_array) + .ok_or_else(|| { + AdapterError::Contract("doctor bootstrap checks.tools must be an array".into()) + })?; + let tool_observations = tools + .iter() + .map(|tool| { + json!({ + "name": string(tool, "name"), + "required": boolean(tool, "required"), + "presence": if boolean(tool, "found") { "present" } else { "missing" }, + "readiness": if boolean(tool, "found") { "ready" } else { "unavailable" }, + "conformance": "not_evaluated", + "admissibility": "not_evaluated" + }) + }) + .collect::>(); + let tool_present = |name: &str| { + tools + .iter() + .any(|tool| string(tool, "name") == name && boolean(tool, "found")) + }; + let sentrux_core = raw + .pointer("/checks/sentrux/core/found") + .and_then(Value::as_bool) + .unwrap_or(false); + let sentrux_pro = raw + .pointer("/checks/sentrux/pro/found") + .and_then(Value::as_bool) + .unwrap_or(false); + let graph_source = raw + .pointer("/checks/graphProvider/sourceFound") + .and_then(Value::as_bool) + .unwrap_or(false); + let graph_cargo = raw + .pointer("/checks/graphProvider/cargoFound") + .and_then(Value::as_bool) + .unwrap_or(false); + let graph_binary = raw + .pointer("/checks/graphProvider/binaryFound") + .and_then(Value::as_bool) + .unwrap_or(false); + let policy = json!({ + "platform": options.platform, + "requireRepowise": options.require_repowise, + "requireUnderstand": options.require_understand + }); + let policy_bytes = serde_json::to_vec(&policy) + .map_err(|error| AdapterError::Internal(format!("serialize doctor policy: {error}")))?; + let registry_ok = manifest + .pointer("/registryAudit/ok") + .and_then(Value::as_bool) + .unwrap_or(false); + let manifest_ok = manifest["ok"].as_bool().unwrap_or(false) && registry_ok; + Ok(json!({ + "schema":"code-intel-doctor-observation.v1", + "snapshotIdentity":request["snapshot"]["identity"], + "environmentPolicy":{"policy":policy,"sha256":sha256_hex(&policy_bytes)}, + "bootstrap":{"schema":raw["schema"],"authority":"observation_only","ready":raw["ok"]}, + "repository":{"presence":if raw.pointer("/checks/repo/exists").and_then(Value::as_bool).unwrap_or(false) { "present" } else { "missing" },"readiness":if raw.pointer("/checks/repo/exists").and_then(Value::as_bool).unwrap_or(false) { "ready" } else { "unavailable" },"conformance":"not_evaluated","admissibility":"not_evaluated"}, + "tools":tool_observations, + "providers":[ + {"id":"repowise","presence":if tool_present("repowise") {"present"} else {"missing"},"readiness":if tool_present("repowise") {"ready"} else {"unavailable"},"conformance":"not_evaluated","admissibility":"not_evaluated"}, + {"id":"sentrux","presence":if tool_present("sentrux") {"present"} else {"missing"},"readiness":if sentrux_core && sentrux_pro {"ready"} else {"unavailable"},"conformance":if tool_present("sentrux") && sentrux_core && sentrux_pro {"conforming"} else if tool_present("sentrux") {"nonconforming"} else {"not_evaluated"},"admissibility":"not_evaluated"}, + {"id":"graph.code-intel","presence":if graph_source && graph_cargo {"present"} else {"missing"},"readiness":if graph_source && graph_cargo && graph_binary {"ready"} else {"unavailable"},"conformance":if graph_source && graph_cargo {"conforming"} else {"not_evaluated"},"admissibility":"not_evaluated"} + ], + "manifest":{"reconciled":manifest_ok,"registryReconciled":registry_ok,"findingCount":manifest["errors"].as_array().map_or(0, Vec::len)}, + "diagnostics":{"bootstrapReady":raw["ok"],"manifestReady":manifest_ok}, + "engineeringFacts":[] + })) +} + +fn diagnosis(document: &Value) -> Option { + let bootstrap_ready = document + .pointer("/diagnostics/bootstrapReady") + .and_then(Value::as_bool) + .unwrap_or(false); + let manifest_ready = document + .pointer("/diagnostics/manifestReady") + .and_then(Value::as_bool) + .unwrap_or(false); + let nonconforming = document["providers"].as_array().is_some_and(|providers| { + providers + .iter() + .any(|provider| provider["conformance"] == "nonconforming") + }); + let mut causes = Vec::new(); + if !bootstrap_ready { + causes.push("bootstrap readiness failed"); + } + if nonconforming { + causes.push("provider conformance failed"); + } + if !manifest_ready { + causes.push("manifest reconciliation failed"); + } + (!causes.is_empty()).then(|| format!("doctor diagnosis: {}", causes.join("; "))) +} + +fn publish(out: &Path, relative: &str, bytes: &[u8]) -> Result<(), AdapterError> { + fs::create_dir(out).map_err(|error| { + AdapterError::Io(format!( + "exclusive doctor staging create {}: {error}", + out.display() + )) + })?; + let path = out.join(relative); + fs::write(&path, bytes).map_err(|error| { + AdapterError::Io(format!("write doctor artifact {}: {error}", path.display())) + }) +} + +fn pipeline_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("..") +} + +fn required_existing_directory(value: Option<&Value>, name: &str) -> Result { + let path = value + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .ok_or_else(|| AdapterError::InvalidOptions(format!("{name} must be a non-empty path")))?; + if !path.is_dir() { + return Err(AdapterError::InvalidOptions(format!( + "{name} is not a directory: {}", + path.display() + ))); + } + Ok(path) +} + +fn required_existing_file(value: Option<&Value>, name: &str) -> Result { + let path = value + .and_then(Value::as_str) + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .ok_or_else(|| AdapterError::InvalidOptions(format!("{name} must be a non-empty path")))?; + if !path.is_file() { + return Err(AdapterError::InvalidOptions(format!( + "{name} is not a file: {}", + path.display() + ))); + } + Ok(path) +} + +fn optional_existing_file( + value: Option<&Value>, + name: &str, +) -> Result, AdapterError> { + value + .map(|value| required_existing_file(Some(value), name)) + .transpose() +} + +fn optional_bool(value: Option<&Value>, default: bool, name: &str) -> Result { + value + .map(|value| { + value.as_bool().ok_or_else(|| { + AdapterError::InvalidOptions(format!("options.{name} must be boolean")) + }) + }) + .transpose() + .map(|value| value.unwrap_or(default)) +} + +fn string<'a>(value: &'a Value, field: &str) -> &'a str { + value.get(field).and_then(Value::as_str).unwrap_or("") +} + +fn boolean(value: &Value, field: &str) -> bool { + value.get(field).and_then(Value::as_bool).unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn options() -> Options { + Options { + repo_path: PathBuf::from("fixture"), + config_path: None, + manifest_path: PathBuf::from("integrations.json"), + platform: "windows".into(), + require_repowise: true, + require_understand: false, + tool_path_prefix: None, + } + } + + fn request() -> Value { + json!({"snapshot":{"identity":"a".repeat(64)}}) + } + + fn bootstrap() -> Value { + json!({ + "schema":"code-intel-doctor-bootstrap-observation.v1", + "authority":"observation_only", + "ok":false, + "checks":{ + "repo":{"exists":true,"path":"C:/secret/repo"}, + "tools":[ + {"name":"rg","required":true,"found":true,"source":"C:/secret/bin/rg.exe"}, + {"name":"repowise","required":true,"found":true,"source":"C:/secret/bin/repowise.exe"}, + {"name":"sentrux","required":true,"found":true,"source":"C:/secret/bin/sentrux.exe"} + ], + "sentrux":{ + "core":{"found":false,"output":"Authorization: Bearer super-secret-token"}, + "pro":{"found":true,"output":"password=hunter2"} + }, + "graphProvider":{"sourceFound":true,"cargoFound":true,"binaryFound":true,"command":"secret"} + } + }) + } + + #[test] + fn adaptation_separates_health_boundaries_redacts_and_fails_domain() { + let manifest = json!({"ok":false,"errors":["drift"],"registryAudit":{"ok":false}}); + let document = adapt(&request(), &options(), &bootstrap(), &manifest).unwrap(); + let text = serde_json::to_string(&document).unwrap(); + + assert_eq!(document["providers"][0]["presence"], "present"); + assert_eq!(document["providers"][0]["conformance"], "not_evaluated"); + assert_eq!(document["providers"][0]["admissibility"], "not_evaluated"); + assert_eq!(document["providers"][1]["presence"], "present"); + assert_eq!(document["providers"][1]["conformance"], "nonconforming"); + assert_eq!(document["manifest"]["reconciled"], false); + assert_eq!(document["engineeringFacts"], json!([])); + assert!(!text.contains("super-secret-token")); + assert!(!text.contains("hunter2")); + assert!(!text.contains("C:/secret")); + assert!(diagnosis(&document).is_some()); + } + + #[test] + fn adaptation_is_byte_replayable_for_the_same_observations_and_policy() { + let manifest = json!({"ok":true,"errors":[],"registryAudit":{"ok":true}}); + let left = + serde_json::to_vec(&adapt(&request(), &options(), &bootstrap(), &manifest).unwrap()) + .unwrap(); + let right = + serde_json::to_vec(&adapt(&request(), &options(), &bootstrap(), &manifest).unwrap()) + .unwrap(); + assert_eq!(left, right); + } + + #[test] + fn registry_toolchain_digests_bind_the_adapter_and_dispatch_sources() { + let root = pipeline_root(); + let registry: Value = serde_json::from_slice( + &fs::read(root.join("orchestration/integrations.json")).unwrap(), + ) + .unwrap(); + let declaration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|integration| { + integration.pointer("/capabilityDeclaration/id") == Some(&json!("doctor")) + }) + .unwrap(); + let declared = declaration["capabilityDeclaration"]["implementation"]["toolchainDigests"] + .as_array() + .unwrap(); + for relative in [ + "crates/code-intel-cli/src/doctor_adapter.rs", + "crates/code-intel-cli/src/capability_inventory.rs", + ] { + let actual = sha256_hex(&fs::read(root.join(relative)).unwrap()); + assert!( + declared.iter().any(|digest| digest == &json!(actual)), + "stale doctor toolchain digest for {relative}" + ); + } + } +} diff --git a/crates/code-intel-cli/src/evidence_query.rs b/crates/code-intel-cli/src/evidence_query.rs new file mode 100644 index 0000000..eb54265 --- /dev/null +++ b/crates/code-intel-cli/src/evidence_query.rs @@ -0,0 +1,288 @@ +use std::collections::BTreeSet; +use std::path::PathBuf; + +use serde_json::{json, Value}; + +use crate::committed_evidence::{self, EvidenceError}; + +const DEFAULT_LIMIT: usize = 20; +const MAX_LIMIT: usize = 100; +const PREVIEW_CHARS: usize = 400; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match Cli::parse(raw).and_then(execute) { + Ok(result) => { + println!("{}", serde_json::to_string(&result).unwrap()); + 0 + } + Err(QueryError::Contract(message)) => { + eprintln!("{message}"); + 65 + } + Err(QueryError::HostIo(message)) => { + eprintln!("{message}"); + 74 + } + } +} + +struct Cli { + artifact_root: PathBuf, + repo: String, + repo_path: Option, + artifact_schema: Option, + artifact_type: Option, + contains: Option, + limit: usize, +} + +impl Cli { + fn parse(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("query") { + return Err(QueryError::Contract("usage: artifact query --artifact-root --repo [--repo-path ] [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>]".into())); + } + let mut artifact_root = None; + let mut repo = None; + let mut repo_path = None; + let mut artifact_schema = None; + let mut artifact_type = None; + let mut contains = None; + let mut limit = DEFAULT_LIMIT; + let mut limit_seen = false; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--artifact-root" + | "--repo" + | "--repo-path" + | "--artifact-schema" + | "--type" + | "--contains" + | "--limit" + ) { + return Err(QueryError::Contract(format!( + "unknown artifact query argument: {flag}" + ))); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| QueryError::Contract(format!("{flag} requires one value")))?; + match flag { + "--artifact-root" => { + set_once(&mut artifact_root, PathBuf::from(value), "--artifact-root")? + } + "--repo" => set_once(&mut repo, value.clone(), "--repo")?, + "--repo-path" => set_once(&mut repo_path, PathBuf::from(value), "--repo-path")?, + "--artifact-schema" => { + set_once(&mut artifact_schema, value.clone(), "--artifact-schema")? + } + "--type" => set_once(&mut artifact_type, value.clone(), "--type")?, + "--contains" => set_once(&mut contains, value.clone(), "--contains")?, + "--limit" => { + if limit_seen { + return Err(QueryError::Contract("duplicate --limit".into())); + } + limit_seen = true; + limit = value.parse::().map_err(|_| { + QueryError::Contract("--limit must be an integer in 1..=100".into()) + })?; + if !(1..=MAX_LIMIT).contains(&limit) { + return Err(QueryError::Contract( + "--limit must be an integer in 1..=100".into(), + )); + } + } + _ => unreachable!(), + } + index += 2; + } + let artifact_root = artifact_root + .ok_or_else(|| QueryError::Contract("--artifact-root is required".into()))?; + if !artifact_root.is_dir() { + return Err(QueryError::Contract( + "artifact root must be an existing directory".into(), + )); + } + let repo = repo.ok_or_else(|| QueryError::Contract("--repo is required".into()))?; + if repo_path.as_ref().is_some_and(|path| !path.is_dir()) { + return Err(QueryError::Contract( + "--repo-path must be an existing directory".into(), + )); + } + Ok(Self { + artifact_root, + repo, + repo_path, + artifact_schema, + artifact_type, + contains, + limit, + }) + } +} + +fn set_once(slot: &mut Option, value: T, flag: &str) -> Result<(), QueryError> { + if slot.replace(value).is_some() { + Err(QueryError::Contract(format!("duplicate {flag}"))) + } else { + Ok(()) + } +} + +fn execute(cli: Cli) -> Result { + let evidence = + committed_evidence::load(&cli.artifact_root, &cli.repo).map_err(map_evidence_error)?; + let entry = &evidence.entry; + let run = entry["run"].as_str().expect("A08 entry run"); + let snapshot_identity = evidence.snapshot_identity(); + let freshness = evidence + .freshness(cli.repo_path.as_deref()) + .map_err(map_evidence_error)?; + let run_outcome = entry["outcome"].as_str().expect("A08 entry outcome"); + let needle = cli.contains.as_ref().map(|value| value.to_lowercase()); + let available_artifact_types = evidence + .refs + .iter() + .filter_map(|artifact| artifact["type"].as_str()) + .map(str::to_string) + .collect::>(); + let mut contract_matches = 0usize; + let mut matches = Vec::new(); + for (artifact, verified) in evidence.refs.iter().zip(evidence.verified.iter()) { + if cli + .artifact_schema + .as_ref() + .is_some_and(|schema| artifact["artifactSchema"] != *schema) + || cli + .artifact_type + .as_ref() + .is_some_and(|kind| artifact["type"] != *kind) + { + continue; + } + contract_matches += 1; + let text = String::from_utf8_lossy(verified.bytes()); + if needle + .as_ref() + .is_some_and(|needle| !text.to_lowercase().contains(needle)) + { + continue; + } + let matched_by = matched_by(&cli); + matches.push(json!({ + "artifactRef":artifact, + "matchedBy":matched_by, + "preview":preview(&text), + "previewTruncated":text.chars().count() > PREVIEW_CHARS, + "explanation":format!( + "A07-committed bytes passed registered schema, digest, and snapshot verification; matched {}.", + matched_by.join(", ") + ), + })); + if matches.len() == cli.limit { + break; + } + } + let contract_requested = cli.artifact_schema.is_some() || cli.artifact_type.is_some(); + let requested_evidence_status = if !contract_requested { + "not_requested" + } else if contract_matches == 0 { + "unavailable" + } else { + "available" + }; + let mut unknowns = Vec::new(); + if run_outcome != "completed" { + unknowns.push(format!( + "authoritative run outcome is {run_outcome}; published artifacts may be partial" + )); + } + if requested_evidence_status == "unavailable" { + unknowns.push("the requested artifact contract is absent from this run".to_string()); + } + if freshness["status"] == "stale" { + unknowns.push("the committed snapshot is stale relative to the requested checkout".into()); + } else if freshness["status"] == "unknown" { + unknowns.push("freshness is unknown because no checkout was supplied".into()); + } + let coverage_status = + if run_outcome == "completed" && requested_evidence_status != "unavailable" { + "complete" + } else { + "partial" + }; + let confidence = if coverage_status == "complete" && freshness["status"] == "current" { + "high" + } else { + "limited" + }; + Ok(json!({ + "schema":"code-intel-evidence-query.v1", + "repo":cli.repo, + "run":run, + "runIdentity":entry["runIdentity"], + "runOutcome":run_outcome, + "authority":{"status":"committed","indexSchema":"code-intel-artifact-index.v1"}, + "snapshotIdentity":snapshot_identity, + "freshness":freshness, + "coverage":{ + "status":coverage_status, + "availableArtifactTypes":available_artifact_types, + "requestedEvidenceStatus":requested_evidence_status, + "unknowns":unknowns, + }, + "confidence":confidence, + "query":{ + "artifactSchema":cli.artifact_schema, + "type":cli.artifact_type, + "contains":cli.contains, + "limit":cli.limit, + }, + "matches":matches, + })) +} + +fn matched_by(cli: &Cli) -> Vec<&'static str> { + let mut values = Vec::new(); + if cli.artifact_schema.is_some() { + values.push("artifact_schema"); + } + if cli.artifact_type.is_some() { + values.push("artifact_type"); + } + if cli.contains.is_some() { + values.push("content"); + } + if values.is_empty() { + values.push("all"); + } + values +} + +fn preview(text: &str) -> String { + text.chars() + .take(PREVIEW_CHARS) + .map(|character| { + if character.is_control() && !matches!(character, '\n' | '\r' | '\t') { + '\u{fffd}' + } else { + character + } + }) + .collect() +} + +fn map_evidence_error(error: EvidenceError) -> QueryError { + match error { + EvidenceError::Contract(message) => QueryError::Contract(message), + EvidenceError::HostIo(message) => QueryError::HostIo(message), + } +} + +enum QueryError { + Contract(String), + HostIo(String), +} diff --git a/crates/code-intel-cli/src/file_boundary.rs b/crates/code-intel-cli/src/file_boundary.rs new file mode 100644 index 0000000..9f2abec --- /dev/null +++ b/crates/code-intel-cli/src/file_boundary.rs @@ -0,0 +1,295 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde_json::{json, Value}; + +pub(crate) fn resolve(request: &Value) -> Result { + exact( + request, + &[ + "schema", + "targetFile", + "expectedSnapshotIdentity", + "policy", + "document", + ], + "file boundary request", + )?; + if request["schema"] != "code-intel-file-boundary-request.v1" + || !digest(&request["expectedSnapshotIdentity"]) + { + return Err("file boundary request identity is invalid".to_string()); + } + + let target = normalize_relative_path(&request["targetFile"], "target file")?; + let policy = &request["policy"]; + exact( + policy, + &["evaluatedAt", "maxAgeSeconds"], + "file boundary freshness policy", + )?; + let evaluated_at = policy["evaluatedAt"] + .as_u64() + .ok_or_else(|| "file boundary evaluatedAt is invalid".to_string())?; + let max_age_seconds = policy["maxAgeSeconds"] + .as_u64() + .filter(|value| *value > 0) + .ok_or_else(|| "file boundary maxAgeSeconds must be positive".to_string())?; + + let document = &request["document"]; + validate_document(document)?; + let expected_snapshot = request["expectedSnapshotIdentity"].as_str().unwrap(); + let source_snapshot = document["snapshotIdentity"].as_str().unwrap(); + if expected_snapshot != source_snapshot { + return Err("file boundary document snapshot mismatch".to_string()); + } + let observed_at = document["observedAt"].as_u64().unwrap(); + if observed_at > evaluated_at { + return Err("file boundary observation is from the future".to_string()); + } + if evaluated_at - observed_at > max_age_seconds { + return Err("file boundary observation is stale".to_string()); + } + + let mut by_match_key = BTreeMap::new(); + for entry in document["entries"].as_array().unwrap() { + let normalized = validate_entry(entry)?; + let match_key = normalized.to_ascii_lowercase(); + if by_match_key + .insert(match_key, (normalized, entry)) + .is_some() + { + return Err("file boundary document contains duplicate or ambiguous paths".to_string()); + } + } + + let source = &document["source"]; + let provenance = json!({ + "sourceKind":source["kind"], + "sourcePath":normalize_relative_path(&source["path"], "boundary source path")?, + "sourceSha256":source["sha256"], + "observedAt":observed_at + }); + let mut diagnostics = Vec::new(); + for construct in document["unsupportedConstructs"].as_array().unwrap() { + diagnostics.push(json!({ + "code":"unsupported_construct", + "reference":construct + })); + } + + let key = target.to_ascii_lowercase(); + let Some((matched_path, entry)) = by_match_key.get(&key) else { + diagnostics.push(json!({ + "code":"no_matching_boundary", + "reference":target + })); + return Ok(json!({ + "schema":"code-intel-file-boundary-result.v1", + "status":"unknown", + "resolution":"exact_path", + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":source_snapshot, + "normalizedTargetFile":target, + "freshness":"current", + "completeness":"unknown", + "boundary":Value::Null, + "provenance":provenance, + "diagnostics":diagnostics + })); + }; + + let completeness = if diagnostics.is_empty() { + "complete" + } else { + "partial" + }; + Ok(json!({ + "schema":"code-intel-file-boundary-result.v1", + "status":"resolved", + "resolution":"exact_path", + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":source_snapshot, + "normalizedTargetFile":target, + "freshness":"current", + "completeness":completeness, + "boundary":{ + "path":matched_path, + "role":entry["role"], + "forbid":entry["forbid"], + "gotcha":entry["gotcha"], + "checks":entry["checks"] + }, + "provenance":provenance, + "diagnostics":diagnostics + })) +} + +fn validate_document(document: &Value) -> Result<(), String> { + exact( + document, + &[ + "schema", + "snapshotIdentity", + "observedAt", + "source", + "entries", + "unsupportedConstructs", + ], + "file boundary document", + )?; + if document["schema"] != "code-intel-file-boundary-document.v1" + || !digest(&document["snapshotIdentity"]) + || document["observedAt"].as_u64().is_none() + || document["entries"].as_array().is_none() + || document["unsupportedConstructs"].as_array().is_none() + { + return Err("file boundary document identity or collections are invalid".to_string()); + } + exact( + &document["source"], + &["kind", "path", "sha256"], + "file boundary source", + )?; + if document["source"]["kind"] != "local_boundary_document" + || !digest(&document["source"]["sha256"]) + { + return Err("file boundary source identity is invalid".to_string()); + } + normalize_relative_path(&document["source"]["path"], "boundary source path")?; + + let mut unsupported = BTreeSet::new(); + for item in document["unsupportedConstructs"].as_array().unwrap() { + let value = nonempty_string(item, "unsupported construct")?; + if !unsupported.insert(value) { + return Err("file boundary unsupported constructs must be unique".to_string()); + } + } + Ok(()) +} + +fn validate_entry(entry: &Value) -> Result { + exact( + entry, + &["path", "role", "forbid", "gotcha", "checks"], + "file boundary entry", + )?; + let path = normalize_relative_path(&entry["path"], "file boundary entry path")?; + if path.contains('*') || path.contains('?') || path.contains('[') || path.contains(']') { + return Err("file boundary selectors must be exact paths".to_string()); + } + if !(entry["role"].is_null() + || entry["role"] + .as_str() + .is_some_and(|value| !value.trim().is_empty())) + { + return Err("file boundary role is invalid".to_string()); + } + + let mut ids = BTreeSet::new(); + validate_rules(&entry["forbid"], "forbid", &mut ids)?; + validate_rules(&entry["gotcha"], "gotcha", &mut ids)?; + validate_checks(&entry["checks"], &mut ids)?; + Ok(path) +} + +fn validate_rules(value: &Value, label: &str, ids: &mut BTreeSet) -> Result<(), String> { + let rules = value + .as_array() + .ok_or_else(|| format!("file boundary {label} rules must be an array"))?; + for rule in rules { + exact( + rule, + &["id", "summary"], + &format!("file boundary {label} rule"), + )?; + let id = validate_rule_id(&rule["id"])?; + nonempty_string(&rule["summary"], &format!("file boundary {label} summary"))?; + if !ids.insert(id) { + return Err("file boundary rule IDs must be unique per entry".to_string()); + } + } + Ok(()) +} + +fn validate_checks(value: &Value, ids: &mut BTreeSet) -> Result<(), String> { + let checks = value + .as_array() + .ok_or_else(|| "file boundary checks must be an array".to_string())?; + for check in checks { + exact(check, &["id", "command"], "file boundary check")?; + let id = validate_rule_id(&check["id"])?; + nonempty_string(&check["command"], "file boundary check command")?; + if !ids.insert(id) { + return Err("file boundary rule IDs must be unique per entry".to_string()); + } + } + Ok(()) +} + +fn validate_rule_id(value: &Value) -> Result { + let id = nonempty_string(value, "file boundary rule ID")?; + if id.len() > 128 + || !id + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) + { + return Err("file boundary rule ID is invalid".to_string()); + } + Ok(id) +} + +fn normalize_relative_path(value: &Value, label: &str) -> Result { + let raw = nonempty_string(value, label)?; + if raw.contains('\0') || raw.starts_with('/') || raw.starts_with('\\') { + return Err(format!("{label} must be a safe repository-relative path")); + } + let replaced = raw.replace('\\', "/"); + if replaced.len() >= 2 && replaced.as_bytes()[1] == b':' { + return Err(format!("{label} must be a safe repository-relative path")); + } + let mut parts = Vec::new(); + for part in replaced.split('/') { + match part { + "" | "." => {} + ".." => return Err(format!("{label} must not escape the repository")), + value => parts.push(value), + } + } + if parts.is_empty() { + return Err(format!("{label} must not be empty")); + } + Ok(parts.join("/")) +} + +fn nonempty_string(value: &Value, label: &str) -> Result { + value + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .ok_or_else(|| format!("{label} is invalid")) +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn digest(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 64 + && text + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + }) +} diff --git a/crates/code-intel-cli/src/graph.rs b/crates/code-intel-cli/src/graph.rs index 6aaffc7..d497d8e 100644 --- a/crates/code-intel-cli/src/graph.rs +++ b/crates/code-intel-cli/src/graph.rs @@ -1,10 +1,12 @@ -use crate::Result; use serde_json::{json, Value}; use std::collections::HashSet; +use std::error::Error; use std::fs; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; +type Result = std::result::Result>; + pub struct Options<'a> { pub repo: &'a Path, pub language: &'a str, @@ -480,7 +482,13 @@ fn truncate(value: &str, max: usize) -> String { if value.len() <= max { return value.to_string(); } - format!("{}...", &value[..max]) + let end = value + .char_indices() + .map(|(index, _)| index) + .take_while(|index| *index <= max) + .last() + .unwrap_or(0); + format!("{}...", &value[..end]) } fn normalize_path(path: impl AsRef) -> String { @@ -518,6 +526,15 @@ mod tests { fs::remove_dir_all(repo).unwrap(); } + #[test] + fn truncates_unicode_on_a_character_boundary() { + let value = "交易账户与行情连接是两个独立概念"; + + let truncated = truncate(value, 10); + + assert_eq!(truncated, "交易账..."); + } + fn unique_temp_dir() -> std::path::PathBuf { let mut dir = std::env::temp_dir(); dir.push(format!( diff --git a/crates/code-intel-cli/src/graph_adapter.rs b/crates/code-intel-cli/src/graph_adapter.rs new file mode 100644 index 0000000..d8529f8 --- /dev/null +++ b/crates/code-intel-cli/src/graph_adapter.rs @@ -0,0 +1,273 @@ +use std::collections::BTreeSet; + +use serde_json::{json, Value}; + +pub(crate) fn translate( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> Result { + validate_native(native)?; + if max_age_seconds == 0 { + return Err("graph freshness policy max age must be positive".to_string()); + } + + let mode = native["providerMode"].as_str().unwrap(); + let status = native["status"].as_str().unwrap(); + let observed_at = native["observedAt"].as_u64().unwrap(); + let expected = native["expectedSnapshotIdentity"].as_str().unwrap(); + let consumed = native["sourceSnapshotIdentity"].as_str().unwrap(); + let snapshot_current = expected == consumed; + let time_current = observed_at <= evaluated_at && evaluated_at - observed_at <= max_age_seconds; + let freshness = if !snapshot_current { + "snapshot_mismatch" + } else if time_current { + "current" + } else { + "stale" + }; + let completeness = if status == "current" { + "complete" + } else { + "partial" + }; + let failure = match status { + "current" => json!({"kind":"none"}), + "partial" => json!({"kind":"domain_unknown","message":"architecture graph is partial"}), + "missing" => { + json!({"kind":"provider_unavailable","message":"architecture graph is missing"}) + } + _ => unreachable!("validated graph status"), + }; + let fallback_identity = native["fallback"] + .as_object() + .map(|fallback| fallback["identity"].clone()); + let provenance = json!({ + "sourceRevision":native["sourceRevision"], + "observedAt":observed_at + }); + let request = json!({ + "schema":"code-intel-evidence-admissibility-request.v1", + "expectedSnapshotIdentity":native["expectedSnapshotIdentity"], + "policy":{"evaluatedAt":evaluated_at,"maxAgeSeconds":max_age_seconds}, + "observation":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{ + "id":if mode == "internal" { "architecture-graph.internal" } else { "architecture-graph.external-fallback" }, + "implementation":native["implementation"] + }, + "source":{"revision":native["sourceRevision"]}, + "consumedSnapshotIdentity":native["sourceSnapshotIdentity"], + "observedAt":observed_at, + "completeness":completeness, + "claimedComplete":completeness == "complete", + "payload":native["payload"], + "provenance":{ + "collectionId":format!("graph-{mode}-{}-{observed_at}", native["sourceRevision"].as_str().unwrap()), + "command":if mode == "internal" { "code-intel graph adapter:internal" } else { "code-intel graph adapter:explicit-fallback" }, + "startedAt":native["collectedAt"], + "completedAt":observed_at + }, + "failure":failure + } + }); + + Ok(json!({ + "schema":"code-intel-graph-adapter-result.v1", + "port":{ + "schema":"code-intel-architecture-graph-port.v1", + "status":status, + "completeness":completeness, + "freshness":freshness, + "expectedSnapshotIdentity":expected, + "sourceSnapshotIdentity":consumed, + "provider":{ + "mode":mode, + "implementationId":native["implementation"]["id"], + "fallbackIdentity":fallback_identity + }, + "provenance":provenance, + "payload":native["payload"], + "anatomyUsable":false + }, + "evidence":{"request":request}, + "factPromotion":{"eligible":false,"requires":"evidence.admissibility-validate","engineeringFacts":[]} + })) +} + +pub(crate) fn validate_admitted_payload(payload: &Value, adapter: &Value) -> Result<(), String> { + exact(payload, &["schema", "data"], "graph evidence payload")?; + if payload["schema"] != "code-intel-evidence-payload.v1" { + return Err("graph evidence payload schema is invalid".to_string()); + } + let graph = &payload["data"]["architectureGraph"]; + exact( + graph, + &[ + "schema", + "snapshotIdentity", + "provider", + "provenance", + "completeness", + "graph", + ], + "architecture graph port payload", + )?; + if graph["schema"] != "code-intel-architecture-graph-evidence.v1" + || graph["snapshotIdentity"] != adapter["port"]["sourceSnapshotIdentity"] + || graph["completeness"] != adapter["port"]["completeness"] + { + return Err("architecture graph port snapshot/completeness mismatch".to_string()); + } + exact( + &graph["provider"], + &["mode", "implementationId", "fallbackIdentity"], + "architecture graph provider", + )?; + if graph["provider"] != adapter["port"]["provider"] { + return Err("architecture graph provider/fallback identity mismatch".to_string()); + } + exact( + &graph["provenance"], + &["sourceRevision", "observedAt"], + "architecture graph provenance", + )?; + if graph["provenance"]["sourceRevision"] != adapter["port"]["provenance"]["sourceRevision"] + || graph["provenance"]["observedAt"] != adapter["port"]["provenance"]["observedAt"] + { + return Err("architecture graph provenance mismatch".to_string()); + } + match adapter["port"]["status"].as_str().unwrap() { + "missing" if !graph["graph"].is_null() => { + Err("missing architecture graph must carry null graph data".to_string()) + } + "current" if !graph_document(&graph["graph"]) => { + Err("current architecture graph data is invalid".to_string()) + } + "partial" if !(graph["graph"].is_null() || graph_document(&graph["graph"])) => { + Err("partial architecture graph data is invalid".to_string()) + } + _ => Ok(()), + } +} + +fn graph_document(value: &Value) -> bool { + let Some(object) = value.as_object() else { + return false; + }; + let actual = object.keys().map(String::as_str).collect::>(); + let required = ["schema", "summary", "nodes", "edges", "symbols"] + .into_iter() + .collect::>(); + required.is_subset(&actual) + && value["schema"] == "code-intel-understand-graph.v1" + && value["summary"].is_object() + && value["nodes"].is_array() + && value["edges"].is_array() + && value["symbols"].is_array() +} + +fn validate_native(native: &Value) -> Result<(), String> { + exact( + native, + &[ + "schema", + "providerMode", + "status", + "implementation", + "sourceRevision", + "expectedSnapshotIdentity", + "sourceSnapshotIdentity", + "collectedAt", + "observedAt", + "payload", + "fallback", + ], + "graph provider native result", + )?; + if native["schema"] != "code-intel-graph-provider-native.v1" + || !matches!( + native["providerMode"].as_str(), + Some("internal" | "external") + ) + || !matches!( + native["status"].as_str(), + Some("current" | "partial" | "missing") + ) + || !digest(&native["expectedSnapshotIdentity"]) + || !digest(&native["sourceSnapshotIdentity"]) + || native["collectedAt"].as_u64().is_none() + || native["observedAt"].as_u64().is_none() + || native["observedAt"].as_u64().unwrap() < native["collectedAt"].as_u64().unwrap() + { + return Err("graph provider native identity/status/time is invalid".to_string()); + } + exact( + &native["implementation"], + &["id", "version", "digest"], + "graph provider implementation", + )?; + if !nonempty(&native["implementation"]["id"]) + || !nonempty(&native["implementation"]["version"]) + || !digest(&native["implementation"]["digest"]) + || !nonempty(&native["sourceRevision"]) + { + return Err("graph provider implementation/source is invalid".to_string()); + } + crate::capability::validate_artifact_ref_shape(&native["payload"])?; + if native["payload"]["artifactSchema"] != "code-intel-evidence-payload.v1" + || native["payload"]["type"] != "observed.evidence.payload" + || native["payload"]["consumedSnapshotIdentity"] != native["sourceSnapshotIdentity"] + { + return Err("graph provider payload contract/snapshot is invalid".to_string()); + } + if native["providerMode"] == "internal" { + if !native["fallback"].is_null() { + return Err("internal graph provider cannot declare fallback identity".to_string()); + } + } else { + exact( + &native["fallback"], + &["identity", "activation", "reason"], + "external graph fallback", + )?; + if !nonempty(&native["fallback"]["identity"]) + || !nonempty(&native["fallback"]["reason"]) + || !matches!( + native["fallback"]["activation"].as_str(), + Some("explicit_fallback" | "legacy_rollback") + ) + { + return Err("external graph requires explicit fallback/rollback identity".to_string()); + } + } + Ok(()) +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn nonempty(value: &Value) -> bool { + value.as_str().is_some_and(|text| !text.is_empty()) +} + +fn digest(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 64 + && text + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + }) +} diff --git a/crates/code-intel-cli/src/hospital_diagnosis.rs b/crates/code-intel-cli/src/hospital_diagnosis.rs new file mode 100644 index 0000000..f5531bc --- /dev/null +++ b/crates/code-intel-cli/src/hospital_diagnosis.rs @@ -0,0 +1,409 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +use crate::artifact_ref::VerifiedArtifact; +use crate::capability_inventory::{AdapterArtifact, AdapterError, AdapterOutput}; + +#[derive(Default)] +struct Signals { + local_tool_failure: bool, + provider_quota: bool, + graph_seen: bool, + graph_current: bool, + structural_seen: bool, + structural_trusted: bool, + structural_rules: bool, + structural_failure: bool, + native_seen: bool, + modernization_debt: bool, + top_target: Option, + admissions: BTreeMap, +} + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + let options = request + .get("options") + .and_then(Value::as_object) + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if !options.is_empty() { + return Err(AdapterError::InvalidOptions( + "diagnosis.hospital accepts no options".into(), + )); + } + if verified_inputs.is_empty() { + return Err(AdapterError::Contract( + "diagnosis.hospital requires A04 admission Artifact Refs".into(), + )); + } + let mut signals = Signals::default(); + for input in verified_inputs { + if input.artifact_schema() != "code-intel-evidence-admissibility-result.v1" + || input.artifact_type() != "evidence.admission" + { + return Err(AdapterError::Contract( + "diagnosis.hospital consumes only A04 admission Artifact Refs".into(), + )); + } + consume_admission(input, &mut signals)?; + } + let machine = diagnose(request, &signals); + let domain_verdict = match machine["domainVerdict"].as_str() { + Some("pass") => crate::capability_inventory::AdapterDomainVerdict::Pass, + Some("fail") => crate::capability_inventory::AdapterDomainVerdict::Fail, + Some("unknown") => crate::capability_inventory::AdapterDomainVerdict::Unknown, + Some("not_applicable") => crate::capability_inventory::AdapterDomainVerdict::NotApplicable, + other => { + return Err(AdapterError::Contract(format!( + "hospital diagnosis has unsupported domain verdict: {other:?}" + ))) + } + }; + let domain_failure = + (domain_verdict == crate::capability_inventory::AdapterDomainVerdict::Fail).then(|| { + machine["triage"]["primary_diagnosis"] + .as_str() + .unwrap_or("hospital domain failure") + .to_string() + }); + let surgery = machine["surgery_plan"].clone(); + let hospital_bytes = serde_json::to_vec(&machine) + .map_err(|error| AdapterError::Internal(format!("serialize hospital report: {error}")))?; + let surgery_bytes = serde_json::to_vec(&surgery) + .map_err(|error| AdapterError::Internal(format!("serialize surgery plan: {error}")))?; + let hospital_markdown = render_hospital(&machine).into_bytes(); + let surgery_markdown = render_surgery(&surgery).into_bytes(); + fs::create_dir_all(out) + .map_err(|error| AdapterError::Io(format!("create hospital output directory: {error}")))?; + for (name, bytes) in [ + ("hospital-report.json", hospital_bytes.as_slice()), + ("hospital.md", hospital_markdown.as_slice()), + ("surgery-plan.json", surgery_bytes.as_slice()), + ("surgery-plan.md", surgery_markdown.as_slice()), + ] { + fs::write(out.join(name), bytes) + .map_err(|error| AdapterError::Io(format!("write {name}: {error}")))?; + } + Ok(AdapterOutput { + artifacts: vec![ + artifact( + "code-intel-hospital.v1", + "diagnosis.hospital", + "hospital-report.json", + hospital_bytes, + ), + artifact( + "code-intel-hospital-markdown.v1", + "diagnosis.hospital-view", + "hospital.md", + hospital_markdown, + ), + artifact( + "code-intel-surgery-plan.v1", + "diagnosis.surgery-plan", + "surgery-plan.json", + surgery_bytes, + ), + artifact( + "code-intel-surgery-plan-markdown.v1", + "diagnosis.surgery-plan-view", + "surgery-plan.md", + surgery_markdown, + ), + ], + observed_effects: vec!["local_write".into()], + domain_verdict, + domain_failure, + }) +} + +fn artifact(schema: &str, kind: &str, path: &str, bytes: Vec) -> AdapterArtifact { + AdapterArtifact { + artifact_schema: schema.into(), + artifact_type: kind.into(), + relative_path: path.into(), + bytes, + } +} + +fn consume_admission(input: &VerifiedArtifact, signals: &mut Signals) -> Result<(), AdapterError> { + let admission: Value = serde_json::from_slice(input.bytes()) + .map_err(|error| AdapterError::Contract(format!("parse A04 admission: {error}")))?; + if admission["status"] != "admitted" { + return Err(AdapterError::Contract( + "non-admitted evidence cannot enter diagnosis.hospital".into(), + )); + } + let provider = admission["evidence"]["provider"]["id"] + .as_str() + .ok_or_else(|| AdapterError::Contract("A04 admission lacks provider id".into()))?; + let identity = admission["admissionIdentity"] + .as_str() + .ok_or_else(|| AdapterError::Contract("A04 admission lacks identity".into()))?; + if signals + .admissions + .insert(provider.to_string(), identity.to_string()) + .is_some() + { + return Err(AdapterError::Contract(format!( + "duplicate admitted modality: {provider}" + ))); + } + let verdict = admission["domainVerdict"].as_str().unwrap_or("unknown"); + let failure = admission["evidence"]["failure"]["kind"] + .as_str() + .unwrap_or("domain_unknown"); + signals.local_tool_failure |= failure == "local_tool_error"; + let data = &admission["verifiedPayload"]["data"]; + if data.get("repowise").is_some() { + require_provider_modality(provider, "repowise")?; + } + signals.provider_quota |= matches!(provider, "repowise.docs" | "repowise.index") + && (failure == "provider_unavailable" || data["repowise"]["status"] == "quota"); + if let Some(graph) = data.get("architectureGraph") { + require_provider_modality(provider, "architecture_graph")?; + if signals.graph_seen { + return Err(AdapterError::Contract( + "duplicate admitted authoritative modality: architecture_graph".into(), + )); + } + signals.graph_seen = true; + signals.graph_current = verdict == "observed" + && graph["completeness"] == "complete" + && graph["graph"].is_object(); + } + if let Some(structural) = data.get("structuralEvidence") { + require_provider_modality(provider, "structural_evidence")?; + if signals.structural_seen { + return Err(AdapterError::Contract( + "duplicate admitted authoritative modality: structural_evidence".into(), + )); + } + signals.structural_seen = true; + let rules = structural["rules"].as_array(); + signals.structural_rules = rules.is_some_and(|items| !items.is_empty()); + signals.structural_trusted = verdict == "observed" + && structural["completeness"] == "complete" + && rules.is_some_and(|items| { + items + .iter() + .all(|rule| matches!(rule["verdict"].as_str(), Some("pass" | "fail"))) + }); + signals.structural_failure = + rules.is_some_and(|items| items.iter().any(|rule| rule["verdict"] == "fail")); + } + if let Some(native) = data.get("nativeCode") { + require_provider_modality(provider, "native_code")?; + if signals.native_seen { + return Err(AdapterError::Contract( + "duplicate admitted enrichment modality: native_code".into(), + )); + } + signals.native_seen = true; + signals.modernization_debt |= native["modernizationDebt"] == true; + signals.top_target = native["topTarget"] + .as_str() + .filter(|target| !target.is_empty()) + .map(str::to_string); + } + Ok(()) +} + +fn require_provider_modality(provider: &str, modality: &str) -> Result<(), AdapterError> { + let matches = match modality { + "repowise" => matches!(provider, "repowise.docs" | "repowise.index"), + "architecture_graph" => provider == "architecture-graph.internal", + "structural_evidence" => provider == "structural-evidence.sentrux", + "native_code" => provider == "native-code-evidence", + _ => false, + }; + if matches { + Ok(()) + } else { + Err(AdapterError::Contract(format!( + "provider identity cannot supply admitted modality {modality}" + ))) + } +} + +fn diagnose(request: &Value, s: &Signals) -> Value { + let (status, diagnosis, next_protocol, disposition, domain_verdict) = if s.local_tool_failure { + ( + "unknown", + "local tool failure", + "triage", + "admit", + "unknown", + ) + } else if s.provider_quota { + ( + "unknown", + "provider quota exhausted", + "triage", + "admit", + "unknown", + ) + } else if s.structural_seen && s.structural_trusted && s.structural_failure { + ( + "red", + "architecture gate failure", + "govern", + "admit", + "fail", + ) + } else if !s.graph_seen || !s.graph_current { + ( + "unknown", + "architecture graph missing", + "diagnose", + "admit", + "unknown", + ) + } else if !s.structural_seen || !s.structural_trusted { + ( + "unknown", + "authoritative structural evidence unavailable", + "diagnose", + "admit", + "unknown", + ) + } else if !s.structural_rules { + ( + "amber", + "ungoverned structural scope", + "govern", + "admit", + "fail", + ) + } else if s.modernization_debt { + ( + "amber", + "known modernization debt", + "surgery_plan", + "admit", + "fail", + ) + } else { + ("green", "clean snapshot", "post_op", "observe", "pass") + }; + let treatment = treatment(diagnosis, s.top_target.as_deref()); + let surgery_status = if next_protocol == "surgery_plan" && s.top_target.is_some() { + "planned" + } else { + "not_required" + }; + let evidence = s + .admissions + .iter() + .map(|(provider, admission)| json!({"provider":provider,"admissionIdentity":admission})) + .collect::>(); + json!({ + "schema":"code-intel-hospital.v1", + "domainVerdict":domain_verdict, + "generatedAt":null, + "repo":request["snapshot"]["repoIdentity"], + "mode":"atom", + "artifacts":{"runDir":"","report":"hospital-report.json","summary":"","understanding":""}, + "triage":{ + "status":status, + "disposition":disposition, + "primary_diagnosis":diagnosis, + "overall_score":null, + "next_protocol":next_protocol, + "research_status":"not_applicable", + "research_required":false, + "exit_criteria":[], + "admission_reason":admission_reason(diagnosis), + "discharge_criteria":[ + "all required authoritative modalities are admitted and current", + "structural rules contain no failing verdict", + "post-op verification reports no regression" + ] + }, + "state_machine":{"schema":"code-intel-hospital-state-machine.v1","current_state":next_protocol,"disposition":disposition,"next_protocol":next_protocol,"states":["triage","diagnose","govern","surgery_plan","post_op","discharge_ready"],"transitions":[]}, + "modalities":evidence, + "policies":{"precedence":["local tool failure","provider quota exhausted","architecture gate failure","architecture graph missing","authoritative structural evidence unavailable","ungoverned structural scope","known modernization debt","clean snapshot"]}, + "report_quality":{"overall_score":null,"diagnostic_score":null,"governance_score":null,"dimensions":[]}, + "diagnosis":{"findings":[diagnosis],"impression":diagnosis,"risk":status,"evidence":evidence}, + "treatment":{"plan":treatment,"follow_up":["Rerun diagnosis.hospital with current admitted evidence."]}, + "protocols":[], + "tools":{}, + "surgery_plan":{ + "schema":"code-intel-surgery-plan.v1", + "status":surgery_status, + "admission":{"disposition":disposition,"diagnosis":diagnosis,"reason":admission_reason(diagnosis)}, + "primary_target":{"file":s.top_target,"name":null,"source_anchor":null,"complexity":null,"scenario":null,"scenario_action":null,"codenexus_file":null}, + "operating_plan":if surgery_status == "planned" { vec!["Open the admitted primary target before editing.","Make one bounded repair and preserve behavior."] } else { Vec::<&str>::new() }, + "verification":["Rerun the smallest affected test.","Re-admit current structural evidence before discharge."], + "discharge_criteria":["the admitted structural verdict is pass"] + } + }) +} + +fn admission_reason(diagnosis: &str) -> &'static str { + match diagnosis { + "local tool failure" => "Local execution failed before diagnosis could be trusted.", + "provider quota exhausted" => "Provider quota prevented complete evidence collection.", + "architecture gate failure" => "Admitted authoritative structural rules contain a failure.", + "architecture graph missing" => "A current admitted architecture graph is unavailable.", + "authoritative structural evidence unavailable" => { + "Required authoritative structural evidence is missing or unknown." + } + "ungoverned structural scope" => { + "No authoritative structural rules govern the selected scope." + } + "known modernization debt" => "Admitted evidence identifies bounded modernization debt.", + _ => "No active inpatient diagnosis is present.", + } +} + +fn treatment(diagnosis: &str, target: Option<&str>) -> Vec { + let mut plan = vec![match diagnosis { + "local tool failure" => "Fix local tool errors before interpreting architecture signals.".into(), + "provider quota exhausted" => "Restore provider quota or use a complete admitted local evidence path before interpreting the result.".into(), + "architecture gate failure" => "Repair the first failing admitted structural rule without weakening its threshold.".into(), + "architecture graph missing" => "Produce and admit a current-snapshot architecture graph.".into(), + "authoritative structural evidence unavailable" => "Produce and admit complete authoritative structural evidence.".into(), + "ungoverned structural scope" => "Add and admit structural rules for the selected scope.".into(), + "known modernization debt" => "Repair the first admitted modernization target and verify behavior.".into(), + _ => "Keep this admitted evidence set as the clean comparison baseline.".into(), + }]; + if let Some(target) = target { + plan.push(format!("Start the bounded review at {target}.")); + } + plan +} + +fn render_hospital(value: &Value) -> String { + format!( + "# Code Intel Hospital Report\n\n- Status: {}\n- Disposition: {}\n- Primary diagnosis: {}\n- Next protocol: {}\n\n## Treatment\n{}\n", + value["triage"]["status"].as_str().unwrap_or("unknown"), + value["triage"]["disposition"].as_str().unwrap_or("admit"), + value["triage"]["primary_diagnosis"].as_str().unwrap_or("unknown"), + value["triage"]["next_protocol"].as_str().unwrap_or("triage"), + value["treatment"]["plan"] + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_str) + .map(|item| format!("- {item}")) + .collect::>() + .join("\n") + ) +} + +fn render_surgery(value: &Value) -> String { + format!( + "# Code Intel Surgery Plan\n\n- Status: {}\n- Diagnosis: {}\n", + value["status"].as_str().unwrap_or("not_required"), + value["admission"]["diagnosis"] + .as_str() + .unwrap_or("unknown") + ) +} diff --git a/crates/code-intel-cli/src/internalization_record.rs b/crates/code-intel-cli/src/internalization_record.rs new file mode 100644 index 0000000..531fac5 --- /dev/null +++ b/crates/code-intel-cli/src/internalization_record.rs @@ -0,0 +1,785 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::ops::Index; + +use serde_json::{json, Value}; + +const SUBJECT_KINDS: [&str; 5] = [ + "design_reference", + "evidence_provider", + "method_implementation", + "adapted_capability", + "selectively_owned_implementation", +]; +const ADOPTION_RUNGS: [&str; 7] = [ + "invoke", + "adapt", + "depend", + "vendor", + "fork", + "port", + "reimplement", +]; +const LIFECYCLE_STATES: [&str; 6] = [ + "research", + "production_enabled", + "rollback", + "replaced", + "retired", + "out_of_scope", +]; +const TRANSITIONS: [(&str, &str); 10] = [ + ("research", "production_enabled"), + ("research", "out_of_scope"), + ("research", "retired"), + ("production_enabled", "rollback"), + ("production_enabled", "replaced"), + ("production_enabled", "retired"), + ("rollback", "production_enabled"), + ("rollback", "replaced"), + ("rollback", "retired"), + ("replaced", "retired"), +]; + +pub(crate) struct Evaluation { + value: Value, + evaluated_record: Value, +} + +impl Index<&str> for Evaluation { + type Output = Value; + + fn index(&self, key: &str) -> &Self::Output { + &self.value[key] + } +} + +impl fmt::Display for Evaluation { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + self.value.fmt(formatter) + } +} + +pub(crate) fn evaluate_record( + record: &Value, + evaluated_at: u64, + known_evidence_ids: &[String], + consumed_authority_event_ids: &[String], +) -> Result { + validate_shape(record)?; + let known = unique_input_set(known_evidence_ids, "known evidence")?; + let consumed = unique_input_set(consumed_authority_event_ids, "consumed authority events")?; + let all_evidence = record_evidence_ids(record)?; + let mut diagnostics = Vec::new(); + + for (label, evidence) in evidence_classes(record) { + assess_evidence(label, evidence, evaluated_at, &known, &mut diagnostics)?; + } + for modification in record["ownedModifications"].as_array().unwrap() { + assess_ids( + "owned modification", + &modification["evidenceIds"], + &known, + &mut diagnostics, + )?; + } + assess_ids( + "lifecycle", + &record["lifecycle"]["evidenceIds"], + &known, + &mut diagnostics, + )?; + if record["update"]["nextCheckAt"].as_u64().unwrap() < evaluated_at { + diagnostics.push("update check is overdue".to_string()); + } + + let lifecycle = &record["lifecycle"]; + let current = lifecycle["status"].as_str().unwrap(); + let previous = lifecycle["previousStatus"].as_str(); + let changed = previous + .map(|value| value != current) + .unwrap_or(current != "research"); + let mut lifecycle_diagnostics = Vec::new(); + let mut validated_event_id = None; + if changed { + let from = previous.unwrap_or("research"); + if !TRANSITIONS.contains(&(from, current)) { + lifecycle_diagnostics.push(format!( + "lifecycle transition {from}->{current} is not allowed" + )); + } else if lifecycle["effectiveAt"].as_u64().unwrap() > evaluated_at { + lifecycle_diagnostics.push("lifecycle transition is future-dated".to_string()); + } else if lifecycle["authorityEvent"].is_null() { + lifecycle_diagnostics.push("lifecycle transition requires A05 authority".to_string()); + } else { + let repository_sign_off = record + .pointer("/authorityRequirements/repositoryGovernedAttestation") + .and_then(Value::as_bool) + .unwrap_or(false); + let validate = if repository_sign_off { + crate::authority::validate_signed_authority_event + } else { + crate::authority::validate_authority_event + }; + let required_evidence = if repository_sign_off { + lifecycle["evidenceIds"] + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap().to_string()) + .collect() + } else { + all_evidence.clone() + }; + match validate( + &lifecycle["authorityEvent"], + evaluated_at, + &known, + &required_evidence, + &consumed, + ) { + Ok(id) => validated_event_id = Some(id), + Err(message) => lifecycle_diagnostics.push(message), + } + } + } else if !lifecycle["authorityEvent"].is_null() { + lifecycle_diagnostics + .push("unchanged lifecycle must not consume an authority event".to_string()); + } + + match current { + "rollback" if record["rollback"]["evidence"]["evidenceIds"] == json!([]) => { + lifecycle_diagnostics.push("rollback state requires rollback evidence".to_string()) + } + "replaced" if lifecycle["replacementRecordId"].is_null() => lifecycle_diagnostics + .push("replaced state requires a replacement record id".to_string()), + "retired" if record["retirement"]["status"] != "completed" => lifecycle_diagnostics + .push("retired state requires completed retirement evidence".to_string()), + _ => {} + } + diagnostics.extend(lifecycle_diagnostics.iter().cloned()); + diagnostics.sort(); + diagnostics.dedup(); + let lifecycle_accepted = lifecycle_diagnostics.is_empty(); + let consumed_event_id = if lifecycle_accepted && diagnostics.is_empty() { + validated_event_id + } else { + None + }; + let production_enabled = + current == "production_enabled" && lifecycle_accepted && diagnostics.is_empty(); + + Ok(Evaluation { + value: json!({ + "schema":"code-intel-internalization-evaluation.v1", + "recordId":record["id"], + "status":current, + "researchAllowed":true, + "productionEnabled":production_enabled, + "lifecycleAccepted":lifecycle_accepted, + "consumedAuthorityEventId":consumed_event_id, + "diagnostics":diagnostics, + "engineeringFacts":[] + }), + evaluated_record: record.clone(), + }) +} + +pub(crate) fn record_evidence_ids(record: &Value) -> Result, String> { + validate_shape(record)?; + let mut result = BTreeSet::new(); + for (label, evidence) in evidence_classes(record) { + extend_ids(&mut result, &evidence["evidenceIds"], label)?; + } + for modification in record["ownedModifications"].as_array().unwrap() { + extend_ids( + &mut result, + &modification["evidenceIds"], + "owned modification evidence", + )?; + } + extend_ids( + &mut result, + &record["lifecycle"]["evidenceIds"], + "lifecycle evidence", + )?; + Ok(result) +} + +pub(crate) fn project_reuse_record( + record: &Value, + evaluation: &Evaluation, +) -> Result { + validate_shape(record)?; + if evaluation.evaluated_record != *record { + return Err("internalization evaluation does not match record".to_string()); + } + Ok(json!({ + "schema":"code-intel-reuse-record.v1", + "id":record["id"], + "projectId":record["projectId"], + "subject":record["subject"]["name"], + "subjectKind":record["subject"]["kind"], + "source":record["subject"]["source"]["uri"], + "sourceRevision":record["subject"]["source"]["revision"], + "license":record["subject"]["license"], + "adoptionRung":record["adoption"]["rung"], + "ownedBoundary":record["adoption"]["ownedBoundary"], + "necessityEvidence":record["adoption"]["necessityEvidence"], + "compatibilityEvidence":record["adoption"]["compatibilityEvidence"], + "conformanceEvidence":record["adoption"]["conformanceEvidence"], + "economics":record["economics"], + "assurance":record["assurance"], + "ownedModifications":record["ownedModifications"], + "update":record["update"], + "rollback":record["rollback"], + "exit":record["exit"], + "retirement":record["retirement"], + "lifecycle":record["lifecycle"]["status"], + "researchAllowed":evaluation["researchAllowed"], + "productionEnabled":evaluation["productionEnabled"], + "diagnostics":evaluation["diagnostics"], + "provenance":record["provenance"], + "engineeringFacts":[] + })) +} + +pub(crate) fn project_notice_provenance( + record: &Value, + evaluation: &Evaluation, +) -> Result { + validate_shape(record)?; + if evaluation.evaluated_record != *record { + return Err("internalization evaluation does not match record".to_string()); + } + let obligations = record["subject"]["license"]["obligations"] + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap()) + .collect::>() + .join("; "); + let notice = format!( + "{} — source {} at revision {}; license {}; obligations: {}.", + record["subject"]["name"].as_str().unwrap(), + record["subject"]["source"]["uri"].as_str().unwrap(), + record["subject"]["source"]["revision"].as_str().unwrap(), + record["subject"]["license"]["id"].as_str().unwrap(), + obligations + ); + Ok(json!({ + "schema":"code-intel-notice-provenance.v1", + "recordId":record["id"], + "noticeText":notice, + "provenance":{ + "source":record["subject"]["source"]["uri"], + "revision":record["subject"]["source"]["revision"], + "license":record["subject"]["license"], + "ownedModifications":record["ownedModifications"], + "recordedAt":record["provenance"]["recordedAt"], + "recordedBy":record["provenance"]["recordedBy"] + } + })) +} + +#[derive(Default)] +pub(crate) struct RecordStore { + records: BTreeMap, +} + +impl RecordStore { + pub(crate) fn insert(&mut self, record: Value) -> Result<(), String> { + validate_shape(&record)?; + let id = record["id"].as_str().unwrap().to_string(); + if self.records.contains_key(&id) { + return Err(format!("duplicate internalization record: {id}")); + } + self.records.insert(id, record); + Ok(()) + } + + pub(crate) fn project_reuse_records( + &self, + evaluated_at: u64, + known_evidence_ids: &[String], + consumed_authority_event_ids: &[String], + ) -> Result, String> { + self.records + .values() + .map(|record| { + let evaluation = evaluate_record( + record, + evaluated_at, + known_evidence_ids, + consumed_authority_event_ids, + )?; + project_reuse_record(record, &evaluation) + }) + .collect() + } +} + +fn validate_shape(record: &Value) -> Result<(), String> { + let mut fields = vec![ + "schema", + "id", + "projectId", + "subject", + "adoption", + "economics", + "assurance", + "update", + "ownedModifications", + "rollback", + "exit", + "retirement", + "lifecycle", + "provenance", + ]; + if record.get("operationTrace").is_some() { + fields.push("operationTrace"); + } + if record.get("authorityRequirements").is_some() { + fields.push("authorityRequirements"); + } + exact(record, &fields, "internalization record")?; + if record["schema"] != "code-intel-internalization-record.v1" { + return Err("internalization record schema is invalid".to_string()); + } + nonempty(&record["id"], "record id")?; + nonempty(&record["projectId"], "project id")?; + validate_subject(&record["subject"])?; + validate_adoption(&record["adoption"])?; + if let Some(requirements) = record.get("authorityRequirements") { + exact( + requirements, + &["repositoryGovernedAttestation"], + "authority requirements", + )?; + requirements["repositoryGovernedAttestation"] + .as_bool() + .ok_or("repositoryGovernedAttestation is invalid")?; + } + if let Some(trace) = record.get("operationTrace") { + validate_operation_trace(trace)?; + } + validate_economics(&record["economics"])?; + exact( + &record["assurance"], + &["maintenanceEvidence", "securityEvidence"], + "assurance", + )?; + validate_evidence_shape(&record["assurance"]["maintenanceEvidence"], "maintenance")?; + validate_evidence_shape(&record["assurance"]["securityEvidence"], "security")?; + validate_update(&record["update"])?; + validate_owned_modifications(&record["ownedModifications"])?; + validate_rollback(&record["rollback"])?; + validate_exit(&record["exit"])?; + validate_retirement(&record["retirement"])?; + validate_lifecycle(&record["lifecycle"])?; + exact( + &record["provenance"], + &["recordedAt", "recordedBy"], + "provenance", + )?; + record["provenance"]["recordedAt"] + .as_u64() + .ok_or("recordedAt is invalid")?; + nonempty(&record["provenance"]["recordedBy"], "recordedBy")?; + Ok(()) +} + +fn validate_operation_trace(value: &Value) -> Result<(), String> { + let entries = value.as_array().ok_or("operationTrace must be an array")?; + if entries.is_empty() { + return Err("operationTrace must not be empty".to_string()); + } + let mut identities = BTreeSet::new(); + for entry in entries { + exact( + entry, + &[ + "integrationId", + "operation", + "command", + "implementationIdentity", + "source", + "conformance", + ], + "operation trace entry", + )?; + let integration = entry["integrationId"] + .as_str() + .ok_or("operation trace integrationId is invalid")?; + let operation = entry["operation"] + .as_str() + .ok_or("operation trace operation is invalid")?; + if integration.is_empty() + || operation.is_empty() + || !identities.insert((integration, operation)) + { + return Err( + "operation trace integration/operation is invalid or duplicate".to_string(), + ); + } + nonempty(&entry["command"], "operation trace command")?; + exact( + &entry["implementationIdentity"], + &["providerId", "implementationId", "activation"], + "operation trace implementationIdentity", + )?; + for field in ["providerId", "implementationId", "activation"] { + nonempty( + &entry["implementationIdentity"][field], + &format!("operation trace {field}"), + )?; + } + for field in ["source", "conformance"] { + let expected = if field == "source" { + vec!["path", "sha256"] + } else { + vec!["path", "sha256", "testName"] + }; + exact( + &entry[field], + &expected, + &format!("operation trace {field}"), + )?; + nonempty( + &entry[field]["path"], + &format!("operation trace {field} path"), + )?; + let digest = entry[field]["sha256"] + .as_str() + .ok_or_else(|| format!("operation trace {field} sha256 is invalid"))?; + if digest.len() != 64 + || !digest + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(format!("operation trace {field} sha256 is invalid")); + } + if field == "conformance" { + nonempty( + &entry[field]["testName"], + "operation trace conformance testName", + )?; + } + } + } + Ok(()) +} + +fn validate_subject(value: &Value) -> Result<(), String> { + exact(value, &["name", "kind", "source", "license"], "subject")?; + nonempty(&value["name"], "subject name")?; + let kind = value["kind"].as_str().ok_or("subject kind is invalid")?; + if !SUBJECT_KINDS.contains(&kind) { + return Err("subject kind is unknown".to_string()); + } + exact(&value["source"], &["uri", "revision"], "source")?; + nonempty(&value["source"]["uri"], "source uri")?; + nonempty(&value["source"]["revision"], "source revision")?; + exact(&value["license"], &["id", "obligations"], "license")?; + nonempty(&value["license"]["id"], "license id")?; + nonempty_strings( + &value["license"]["obligations"], + "license obligations", + true, + )?; + Ok(()) +} + +fn validate_adoption(value: &Value) -> Result<(), String> { + exact( + value, + &[ + "rung", + "ownedBoundary", + "necessityEvidence", + "compatibilityEvidence", + "conformanceEvidence", + ], + "adoption", + )?; + let rung = value["rung"].as_str().ok_or("adoption rung is invalid")?; + if !ADOPTION_RUNGS.contains(&rung) { + return Err("adoption rung is unknown".to_string()); + } + nonempty_strings(&value["ownedBoundary"], "owned boundary", true)?; + for (field, label) in [ + ("necessityEvidence", "necessity"), + ("compatibilityEvidence", "compatibility"), + ("conformanceEvidence", "conformance"), + ] { + validate_evidence_shape(&value[field], label)?; + } + Ok(()) +} + +fn validate_economics(value: &Value) -> Result<(), String> { + exact( + value, + &["benefit", "cost", "benefitEvidence", "costEvidence"], + "economics", + )?; + validate_measurement(&value["benefit"], "measured benefit")?; + validate_measurement(&value["cost"], "measured cost")?; + validate_evidence_shape(&value["benefitEvidence"], "benefit")?; + validate_evidence_shape(&value["costEvidence"], "cost")?; + Ok(()) +} + +fn validate_measurement(value: &Value, label: &str) -> Result<(), String> { + exact(value, &["metric", "value", "unit"], label)?; + nonempty(&value["metric"], &format!("{label} metric"))?; + let amount = value["value"] + .as_f64() + .ok_or_else(|| format!("{label} value is invalid"))?; + if !amount.is_finite() || amount < 0.0 { + return Err(format!("{label} value is invalid")); + } + nonempty(&value["unit"], &format!("{label} unit")) +} + +fn validate_update(value: &Value) -> Result<(), String> { + exact(value, &["policy", "nextCheckAt", "evidence"], "update")?; + nonempty(&value["policy"], "update policy")?; + value["nextCheckAt"] + .as_u64() + .ok_or("update nextCheckAt is invalid")?; + validate_evidence_shape(&value["evidence"], "update") +} + +fn validate_owned_modifications(value: &Value) -> Result<(), String> { + let values = value + .as_array() + .ok_or("ownedModifications must be an array")?; + for modification in values { + exact( + modification, + &["path", "description", "evidenceIds"], + "owned modification", + )?; + nonempty(&modification["path"], "owned modification path")?; + nonempty( + &modification["description"], + "owned modification description", + )?; + nonempty_strings( + &modification["evidenceIds"], + "owned modification evidenceIds", + false, + )?; + } + Ok(()) +} + +fn validate_rollback(value: &Value) -> Result<(), String> { + exact(value, &["strategy", "evidence"], "rollback")?; + nonempty(&value["strategy"], "rollback strategy")?; + validate_evidence_shape(&value["evidence"], "rollback") +} + +fn validate_exit(value: &Value) -> Result<(), String> { + exact( + value, + &["strategy", "replacementCriteria", "evidence"], + "exit", + )?; + nonempty(&value["strategy"], "exit strategy")?; + nonempty_strings(&value["replacementCriteria"], "replacement criteria", true)?; + validate_evidence_shape(&value["evidence"], "exit") +} + +fn validate_retirement(value: &Value) -> Result<(), String> { + exact(value, &["status", "triggers", "evidence"], "retirement")?; + if !matches!( + value["status"].as_str(), + Some("active" | "candidate" | "approved" | "completed" | "out_of_scope") + ) { + return Err("retirement status is invalid".to_string()); + } + nonempty_strings(&value["triggers"], "retirement triggers", true)?; + validate_evidence_shape(&value["evidence"], "retirement") +} + +fn validate_lifecycle(value: &Value) -> Result<(), String> { + exact( + value, + &[ + "previousStatus", + "status", + "effectiveAt", + "replacementRecordId", + "evidenceIds", + "authorityEvent", + ], + "lifecycle", + )?; + if !value["previousStatus"].is_null() + && !value["previousStatus"] + .as_str() + .is_some_and(|state| LIFECYCLE_STATES.contains(&state)) + { + return Err("previous lifecycle status is invalid".to_string()); + } + if !value["status"] + .as_str() + .is_some_and(|state| LIFECYCLE_STATES.contains(&state)) + { + return Err("lifecycle status is invalid".to_string()); + } + value["effectiveAt"] + .as_u64() + .ok_or("lifecycle effectiveAt is invalid")?; + if !value["replacementRecordId"].is_null() { + nonempty(&value["replacementRecordId"], "replacement record id")?; + } + nonempty_strings(&value["evidenceIds"], "lifecycle evidenceIds", false)?; + if !value["authorityEvent"].is_null() && !value["authorityEvent"].is_object() { + return Err("lifecycle authorityEvent is invalid".to_string()); + } + Ok(()) +} + +fn validate_evidence_shape(value: &Value, label: &str) -> Result<(), String> { + exact( + value, + &["evidenceIds", "checkedAt", "expiresAt"], + &format!("{label} evidence"), + )?; + nonempty_strings( + &value["evidenceIds"], + &format!("{label} evidenceIds"), + false, + )?; + let checked = value["checkedAt"] + .as_u64() + .ok_or_else(|| format!("{label} checkedAt is invalid"))?; + let expires = value["expiresAt"] + .as_u64() + .ok_or_else(|| format!("{label} expiresAt is invalid"))?; + if expires < checked { + return Err(format!("{label} evidence expiry precedes check")); + } + Ok(()) +} + +fn evidence_classes(record: &Value) -> Vec<(&'static str, &Value)> { + vec![ + ("necessity", &record["adoption"]["necessityEvidence"]), + ( + "compatibility", + &record["adoption"]["compatibilityEvidence"], + ), + ("conformance", &record["adoption"]["conformanceEvidence"]), + ("benefit", &record["economics"]["benefitEvidence"]), + ("cost", &record["economics"]["costEvidence"]), + ("maintenance", &record["assurance"]["maintenanceEvidence"]), + ("security", &record["assurance"]["securityEvidence"]), + ("update", &record["update"]["evidence"]), + ("rollback", &record["rollback"]["evidence"]), + ("exit", &record["exit"]["evidence"]), + ("retirement", &record["retirement"]["evidence"]), + ] +} + +fn assess_evidence( + label: &str, + evidence: &Value, + evaluated_at: u64, + known: &BTreeSet, + diagnostics: &mut Vec, +) -> Result<(), String> { + assess_ids(label, &evidence["evidenceIds"], known, diagnostics)?; + let checked = evidence["checkedAt"].as_u64().unwrap(); + let expires = evidence["expiresAt"].as_u64().unwrap(); + if checked > evaluated_at { + diagnostics.push(format!("{label} evidence is future-dated")); + } + if expires < evaluated_at { + diagnostics.push(format!("{label} evidence is expired")); + } + Ok(()) +} + +fn assess_ids( + label: &str, + value: &Value, + known: &BTreeSet, + diagnostics: &mut Vec, +) -> Result<(), String> { + let ids = string_set(value, &format!("{label} evidenceIds"))?; + if ids.is_empty() { + diagnostics.push(format!("{label} evidence is missing")); + } else if !ids.is_subset(known) { + diagnostics.push(format!("{label} evidence references unknown evidence")); + } + Ok(()) +} + +fn exact(value: &Value, expected: &[&str], label: &str) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn nonempty(value: &Value, label: &str) -> Result<(), String> { + if value.as_str().is_some_and(|value| !value.is_empty()) { + Ok(()) + } else { + Err(format!("{label} is invalid")) + } +} + +fn nonempty_strings(value: &Value, label: &str, require_one: bool) -> Result<(), String> { + let values = value + .as_array() + .ok_or_else(|| format!("{label} must be an array"))?; + if require_one && values.is_empty() { + return Err(format!("{label} must not be empty")); + } + let mut seen = BTreeSet::new(); + for value in values { + let string = value + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| format!("{label} contains an invalid value"))?; + if !seen.insert(string) { + return Err(format!("{label} contains duplicate values")); + } + } + Ok(()) +} + +fn string_set(value: &Value, label: &str) -> Result, String> { + nonempty_strings(value, label, false)?; + Ok(value + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap().to_string()) + .collect()) +} + +fn extend_ids(result: &mut BTreeSet, value: &Value, label: &str) -> Result<(), String> { + result.extend(string_set(value, label)?); + Ok(()) +} + +fn unique_input_set(values: &[String], label: &str) -> Result, String> { + let result = values.iter().cloned().collect::>(); + if result.len() != values.len() || result.iter().any(|value| value.is_empty()) { + Err(format!("{label} contains invalid or duplicate ids")) + } else { + Ok(result) + } +} diff --git a/crates/code-intel-cli/src/main.rs b/crates/code-intel-cli/src/main.rs index 5b1d926..474b90b 100644 --- a/crates/code-intel-cli/src/main.rs +++ b/crates/code-intel-cli/src/main.rs @@ -6,13 +6,40 @@ use std::process; use serde_json::Value; +mod admissibility; +mod artifact_index; +mod artifact_ref; mod artifacts; +mod authority; +mod capability; +mod capability_inventory; +mod change_impact; +mod codenexus_adapter; +mod committed_evidence; +mod compatibility_retirement_ticket; +mod dag_coordinator; +mod dag_run; +mod decision_port; +mod decision_record; +mod evidence_query; +mod file_boundary; mod graph; +mod method_catalog; +mod model_channels; mod orchestration; +mod ponytail_gate; +mod project_orientation_benchmark; mod providers; mod routes; +mod run_commit; +mod runtime_ci_evidence; mod sentrux; mod sentrux_analysis; +mod session_evidence; +mod snapshot; +mod stable_artifact; +mod staged_artifact; +mod survival_scan; type Result = std::result::Result>; @@ -116,6 +143,33 @@ mod sentrux_contract_tests { assert_eq!(debt["summary"]["newDebt"], 1); assert_eq!(debt["summary"]["blocking"], 1); } + + #[test] + fn raw_routes_preserve_specific_dispatch_precedence_and_argument_offsets() { + let decision_record = vec!["decision".into(), "record".into(), "--store".into()]; + let decision_default = vec!["decision".into(), "request-response".into()]; + let artifact_index = vec!["artifact".into(), "index".into(), "--repo".into()]; + + let record_route = resolve_raw_route(&decision_record).expect("decision record route"); + let default_route = resolve_raw_route(&decision_default).expect("decision default route"); + let artifact_route = resolve_raw_route(&artifact_index).expect("artifact index route"); + + assert_eq!(record_route.subcommand, Some("record")); + assert_eq!(record_route.argument_offset, 1); + assert_eq!(default_route.subcommand, None); + assert_eq!(default_route.argument_offset, 1); + assert_eq!(artifact_route.subcommand, Some("index")); + assert_eq!(artifact_route.argument_offset, 1); + } + + #[test] + fn legacy_parser_commands_are_not_intercepted_by_raw_routes() { + let doctor = vec!["doctor".into(), "--json".into()]; + let provider_list = vec!["provider".into(), "--action".into(), "List".into()]; + + assert!(resolve_raw_route(&doctor).is_none()); + assert!(resolve_raw_route(&provider_list).is_none()); + } } #[derive(Debug)] @@ -145,12 +199,256 @@ struct ResumeSummary { } fn main() { + let raw: Vec = env::args().skip(1).collect(); + if let Some(exit_code) = dispatch_raw_command(&raw) { + process::exit(exit_code); + } if let Err(err) = run() { eprintln!("error: {err}"); process::exit(1); } } +type RawRunner = fn(&[String]) -> i32; + +struct RawRoute { + command: &'static str, + subcommand: Option<&'static str>, + argument_offset: usize, + runner: RawRunner, +} + +const RAW_ROUTES: &[RawRoute] = &[ + RawRoute { + command: "compatibility", + subcommand: Some("retirement-ticket"), + argument_offset: 2, + runner: compatibility_retirement_ticket::run_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("repowise-adapt"), + argument_offset: 2, + runner: providers::run_repowise_adapt_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("graph-adapt"), + argument_offset: 2, + runner: providers::run_graph_adapt_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("sentrux-adapt"), + argument_offset: 2, + runner: providers::run_sentrux_adapt_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("session-adapt"), + argument_offset: 2, + runner: session_evidence::run_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("codenexus-adapt"), + argument_offset: 2, + runner: providers::run_codenexus_adapt_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("file-boundary"), + argument_offset: 2, + runner: run_file_boundary_raw, + }, + RawRoute { + command: "provider", + subcommand: Some("runtime-ci-evidence"), + argument_offset: 2, + runner: run_runtime_ci_raw, + }, + RawRoute { + command: "repository", + subcommand: Some("survival-scan"), + argument_offset: 2, + runner: survival_scan::run_raw, + }, + RawRoute { + command: "artifact", + subcommand: Some("index"), + argument_offset: 1, + runner: artifact_index::run_raw, + }, + RawRoute { + command: "artifact", + subcommand: Some("query"), + argument_offset: 1, + runner: evidence_query::run_raw, + }, + RawRoute { + command: "change", + subcommand: Some("impact"), + argument_offset: 1, + runner: change_impact::run_raw, + }, + RawRoute { + command: "decision", + subcommand: Some("record"), + argument_offset: 1, + runner: decision_record::run_raw, + }, + RawRoute { + command: "decision", + subcommand: Some("replay"), + argument_offset: 1, + runner: decision_record::run_raw, + }, + RawRoute { + command: "run", + subcommand: Some("commit"), + argument_offset: 1, + runner: run_commit::run_raw, + }, + RawRoute { + command: "capability", + subcommand: None, + argument_offset: 1, + runner: capability::run_raw, + }, + RawRoute { + command: "model", + subcommand: None, + argument_offset: 1, + runner: model_channels::run_raw, + }, + RawRoute { + command: "benchmark", + subcommand: None, + argument_offset: 1, + runner: project_orientation_benchmark::run_raw, + }, + RawRoute { + command: "snapshot", + subcommand: None, + argument_offset: 1, + runner: snapshot::run_raw, + }, + RawRoute { + command: "evidence", + subcommand: None, + argument_offset: 1, + runner: admissibility::run_raw, + }, + RawRoute { + command: "decision", + subcommand: None, + argument_offset: 1, + runner: decision_port::run_raw, + }, + RawRoute { + command: "run", + subcommand: None, + argument_offset: 1, + runner: dag_run::run_raw, + }, + RawRoute { + command: "governance", + subcommand: None, + argument_offset: 1, + runner: ponytail_gate::run_raw, + }, +]; + +fn dispatch_raw_command(raw: &[String]) -> Option { + let route = resolve_raw_route(raw)?; + Some((route.runner)(&raw[route.argument_offset..])) +} + +fn resolve_raw_route(raw: &[String]) -> Option<&'static RawRoute> { + let command = raw.first()?; + RAW_ROUTES.iter().find(|route| { + route.command == command + && route.subcommand.map_or(true, |subcommand| { + raw.get(1).map(String::as_str) == Some(subcommand) + }) + }) +} + +fn raw_option(raw: &[String], name: &str) -> std::result::Result { + let positions = raw + .iter() + .enumerate() + .filter_map(|(index, value)| (value == name).then_some(index)) + .collect::>(); + if positions.len() != 1 { + return Err(format!("{name} must appear exactly once")); + } + raw.get(positions[0] + 1) + .filter(|value| !value.starts_with("--")) + .map(PathBuf::from) + .ok_or_else(|| format!("{name} requires a value")) +} + +fn write_provider_result(out: &Path, value: &Value) -> std::result::Result<(), String> { + if let Some(parent) = out.parent() { + fs::create_dir_all(parent).map_err(|error| format!("create output directory: {error}"))?; + } + let bytes = + serde_json::to_vec_pretty(value).map_err(|error| format!("serialize output: {error}"))?; + fs::write(out, bytes).map_err(|error| format!("write output: {error}")) +} + +fn run_file_boundary_raw(raw: &[String]) -> i32 { + let result = (|| -> std::result::Result<(), String> { + if raw.len() != 4 { + return Err("file-boundary requires --request --out ".into()); + } + let request = raw_option(raw, "--request")?; + let out = raw_option(raw, "--out")?; + let bytes = fs::read(request).map_err(|error| format!("read request: {error}"))?; + let text = + std::str::from_utf8(&bytes).map_err(|_| "file boundary request must be UTF-8 JSON")?; + capability::reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|error| format!("parse request: {error}"))?; + write_provider_result(&out, &file_boundary::resolve(&value)?) + })(); + match result { + Ok(()) => 0, + Err(error) => { + eprintln!("error: {error}"); + 65 + } + } +} + +fn run_runtime_ci_raw(raw: &[String]) -> i32 { + let result = (|| -> std::result::Result<(), String> { + if raw.len() != 6 { + return Err( + "runtime-ci-evidence requires --artifact-root --request --out " + .into(), + ); + } + let artifact_root = raw_option(raw, "--artifact-root")?; + let request = raw_option(raw, "--request")?; + let out = raw_option(raw, "--out")?; + let bytes = fs::read(request).map_err(|error| format!("read request: {error}"))?; + let value = runtime_ci_evidence::parse_request_bytes(&bytes)?; + write_provider_result( + &out, + &runtime_ci_evidence::ingest_request(&artifact_root, &value)?, + ) + })(); + match result { + Ok(()) => 0, + Err(error) => { + eprintln!("error: {error}"); + 65 + } + } +} + fn run() -> Result<()> { let args = parse_args(env::args().skip(1).collect())?; match args.command.as_str() { @@ -1104,8 +1402,28 @@ fn print_help() { println!(" doctor [--artifact-root ] [--json]"); println!(" graph --repo [--language zh] [--full] [--write] [--json]"); println!(" provider [--action List|Plan|Validate|Invoke] [--provider repowise|understand] [--operation ] [--repo ] [--language zh] [--write] [--json]"); + println!(" provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds "); + println!(" provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds "); + println!(" provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds "); + println!(" provider session-adapt --repo --trace [--hotspots ] [--out ] [--working-tree-policy head_only|explicit_overlay]"); + println!(" provider file-boundary --request --out "); + println!(" provider runtime-ci-evidence --artifact-root --request --out "); println!(" route [--action List|Plan|Validate] [--provider repowise|understand] [--operation ] [--repo ] [--json]"); println!(" sentrux "); + println!(" capability exec --request --out [--artifact-root ] [--manifest ]"); + println!(" model inventory-validate --request [--out ]"); + println!(" model route --request [--out ]"); + println!(" snapshot identity --repo --working-tree-policy [--scope ]..."); + println!(" evidence validate --request --artifact-root "); + println!(" artifact index --artifact-root [--output ] [--operation rebuild|incremental] [--existing ]"); + println!(" artifact query --artifact-root --repo [--repo-path ] [--artifact-schema ] [--type ] [--contains ] [--limit <1..100>]"); + println!(" change impact --artifact-root --repo --repo-path --changed [--changed ]..."); + println!(" decision request-response --request [--response |--cancel ] --now --branch ..."); + println!(" decision record --resolution --store "); + println!(" decision replay --query --store "); + println!(" run dag-coordinate --repo --out [--manifest ] [--max-concurrency ] [--session-evidence ]"); + println!(" run commit --source-root --authority-root --manifest-ref --final-name "); + println!(" governance ponytail-gate --request "); println!(" orchestrate [--action Validate|List|Plan] [--repo ] [--mode lite|normal|full] [--capability ] [--manifest ] [--json]"); } diff --git a/crates/code-intel-cli/src/method_catalog.rs b/crates/code-intel-cli/src/method_catalog.rs new file mode 100644 index 0000000..387fafc --- /dev/null +++ b/crates/code-intel-cli/src/method_catalog.rs @@ -0,0 +1,536 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fs; +use std::path::{Component, Path}; + +use serde_json::Value; + +const MAX_DOCUMENT_BYTES: u64 = 256 * 1024; +const CARD_FIELDS: [&str; 17] = [ + "schema", + "id", + "version", + "name", + "problemSignals", + "requiredEvidence", + "assumptions", + "deterministicSteps", + "outputs", + "confidenceRules", + "cost", + "contraindications", + "implementationPorts", + "source", + "applicabilityBoundary", + "relatedMethodIds", + "executionPolicy", +]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CatalogError(String); + +impl fmt::Display for CatalogError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for CatalogError {} + +#[derive(Debug, Clone)] +pub(crate) struct MethodCatalog { + cards: Vec, +} + +impl MethodCatalog { + pub(crate) fn cards(&self) -> &[Value] { + &self.cards + } +} + +pub(crate) fn load_catalog(methods_root: &Path) -> Result { + let index = read_json(&methods_root.join("catalog.v1.json"))?; + validate_index(&index)?; + let mut documents = Vec::new(); + for entry in index["cards"].as_array().unwrap() { + let relative = entry["path"].as_str().unwrap(); + validate_relative_card_path(relative)?; + documents.push(( + relative.to_string(), + read_json(&methods_root.join(relative))?, + )); + } + let expected = documents + .iter() + .map(|(path, _)| path.clone()) + .collect::>(); + let cards_dir = methods_root.join("cards"); + let mut actual = BTreeSet::new(); + for entry in fs::read_dir(&cards_dir).map_err(|error| { + CatalogError(format!( + "read method card directory {}: {error}", + cards_dir.display() + )) + })? { + let entry = + entry.map_err(|error| CatalogError(format!("read method card entry: {error}")))?; + let file_type = entry + .file_type() + .map_err(|error| CatalogError(format!("inspect method card entry: {error}")))?; + if !file_type.is_file() + || entry.path().extension().and_then(|value| value.to_str()) != Some("json") + { + return Err(CatalogError(format!( + "method card directory contains an unregistered non-card entry {}", + entry.path().display() + ))); + } + actual.insert(format!("cards/{}", entry.file_name().to_string_lossy())); + } + if actual != expected { + return Err(CatalogError( + "catalog card paths differ from checked-in JSON card files".to_string(), + )); + } + validate_documents(&index, &documents) +} + +pub(crate) fn validate_documents( + index: &Value, + documents: &[(String, Value)], +) -> Result { + validate_index(index)?; + let entries = index["cards"].as_array().unwrap(); + if entries.len() != documents.len() { + return Err(CatalogError( + "catalog entry count differs from loaded card count".to_string(), + )); + } + + let mut by_path = BTreeMap::new(); + for (path, card) in documents { + validate_relative_card_path(path)?; + if by_path.insert(path.as_str(), card).is_some() { + return Err(CatalogError(format!("duplicate loaded card path {path}"))); + } + } + + let mut cards = Vec::with_capacity(entries.len()); + let mut ids = BTreeSet::new(); + for entry in entries { + let path = entry["path"].as_str().unwrap(); + let expected_id = entry["id"].as_str().unwrap(); + let card = by_path + .get(path) + .ok_or_else(|| CatalogError(format!("catalog path {path} was not loaded")))?; + validate_card(card)?; + let actual_id = card["id"].as_str().unwrap(); + if actual_id != expected_id { + return Err(CatalogError(format!( + "catalog id {expected_id} does not match card id {actual_id}" + ))); + } + if !ids.insert(actual_id.to_string()) { + return Err(CatalogError(format!("duplicate method id {actual_id}"))); + } + cards.push((*card).clone()); + } + + for card in &cards { + let id = card["id"].as_str().unwrap(); + for related in strings(&card["relatedMethodIds"], "relatedMethodIds", false)? { + if related == id || !ids.contains(related) { + return Err(CatalogError(format!( + "{id}.relatedMethodIds references unknown or self method {related}" + ))); + } + } + } + Ok(MethodCatalog { cards }) +} + +fn validate_index(index: &Value) -> Result<(), CatalogError> { + exact_object( + index, + "catalog", + &["schema", "catalogVersion", "selectionPolicy", "cards"], + )?; + require_exact_string(index, "schema", "code-intel-method-catalog.v1", "catalog")?; + require_nonempty_string(index, "catalogVersion", "catalog")?; + require_exact_string(index, "selectionPolicy", "none_catalog_only", "catalog")?; + let cards = nonempty_array(&index["cards"], "catalog.cards")?; + let mut previous: Option<&str> = None; + let mut ids = BTreeSet::new(); + let mut paths = BTreeSet::new(); + for (position, entry) in cards.iter().enumerate() { + let context = format!("catalog.cards[{position}]"); + exact_object(entry, &context, &["id", "path"])?; + let id = require_portable_id(entry, "id", &context)?; + let path = require_nonempty_string(entry, "path", &context)?; + validate_relative_card_path(path)?; + if path != format!("cards/{id}.v1.json") { + return Err(CatalogError(format!( + "{context}.path must be the versioned path for method id {id}" + ))); + } + if previous.is_some_and(|prior| prior >= id) { + return Err(CatalogError( + "catalog cards must be strictly sorted by stable id".to_string(), + )); + } + previous = Some(id); + if !ids.insert(id) || !paths.insert(path) { + return Err(CatalogError(format!( + "duplicate catalog id or path at {context}" + ))); + } + } + Ok(()) +} + +fn validate_card(card: &Value) -> Result<(), CatalogError> { + exact_object(card, "card", &CARD_FIELDS)?; + require_exact_string(card, "schema", "code-intel-method-card.v1", "card")?; + let method_id = require_portable_id(card, "id", "card")?; + require_nonempty_string(card, "version", method_id)?; + require_nonempty_string(card, "name", method_id)?; + require_exact_string( + card, + "executionPolicy", + "catalog_only_no_selection_or_execution", + method_id, + )?; + + let evidence = + validate_described_ids(&card["requiredEvidence"], method_id, "requiredEvidence")?; + validate_described_ids(&card["problemSignals"], method_id, "problemSignals")?; + let outputs = validate_described_ids(&card["outputs"], method_id, "outputs")?; + strings( + &card["assumptions"], + &format!("{method_id}.assumptions"), + true, + )?; + strings( + &card["contraindications"], + &format!("{method_id}.contraindications"), + true, + )?; + validate_steps(card, method_id, &evidence, &outputs)?; + validate_confidence(card, method_id)?; + validate_cost(card, method_id)?; + validate_ports(card, method_id)?; + validate_source(card, method_id)?; + validate_boundary(card, method_id)?; + strings( + &card["relatedMethodIds"], + &format!("{method_id}.relatedMethodIds"), + false, + )?; + Ok(()) +} + +fn validate_described_ids( + value: &Value, + method_id: &str, + field: &str, +) -> Result, CatalogError> { + let entries = nonempty_array(value, &format!("{method_id}.{field}"))?; + let mut ids = BTreeSet::new(); + for (position, entry) in entries.iter().enumerate() { + let context = format!("{method_id}.{field}[{position}]"); + exact_object(entry, &context, &["id", "description"])?; + let id = require_portable_id(entry, "id", &context)?; + require_nonempty_string(entry, "description", &context)?; + if !ids.insert(id.to_string()) { + return Err(CatalogError(format!( + "duplicate id {id} in {method_id}.{field}" + ))); + } + } + Ok(ids) +} + +fn validate_steps( + card: &Value, + method_id: &str, + evidence: &BTreeSet, + outputs: &BTreeSet, +) -> Result<(), CatalogError> { + let steps = nonempty_array( + &card["deterministicSteps"], + &format!("{method_id}.deterministicSteps"), + )?; + let mut prior_steps = BTreeSet::new(); + let mut produced = BTreeSet::new(); + for (position, step) in steps.iter().enumerate() { + let context = format!("{method_id}.deterministicSteps[{position}]"); + exact_object(step, &context, &["id", "action", "requires", "produces"])?; + let id = require_portable_id(step, "id", &context)?; + if prior_steps.contains(id) { + return Err(CatalogError(format!( + "duplicate step id {id} in {method_id}" + ))); + } + require_nonempty_string(step, "action", &context)?; + for requirement in strings(&step["requires"], &format!("{context}.requires"), false)? { + if let Some(evidence_id) = requirement.strip_prefix("evidence:") { + if !evidence.contains(evidence_id) { + return Err(CatalogError(format!( + "{context} references unknown evidence {evidence_id}" + ))); + } + } else if let Some(step_id) = requirement.strip_prefix("step:") { + if !prior_steps.contains(step_id) { + return Err(CatalogError(format!( + "{context} references unknown or forward step {step_id}" + ))); + } + } else { + return Err(CatalogError(format!( + "{context} requirement must use evidence: or step:" + ))); + } + } + for output in strings(&step["produces"], &format!("{context}.produces"), true)? { + if !outputs.contains(output) { + return Err(CatalogError(format!( + "{context} produces unknown output {output}" + ))); + } + produced.insert(output.to_string()); + } + prior_steps.insert(id.to_string()); + } + if &produced != outputs { + return Err(CatalogError(format!( + "{method_id} deterministic steps must produce every declared output" + ))); + } + Ok(()) +} + +fn validate_confidence(card: &Value, method_id: &str) -> Result<(), CatalogError> { + let rules = nonempty_array( + &card["confidenceRules"], + &format!("{method_id}.confidenceRules"), + )?; + let mut levels = BTreeSet::new(); + for (position, rule) in rules.iter().enumerate() { + let context = format!("{method_id}.confidenceRules[{position}]"); + exact_object(rule, &context, &["level", "whenAll"])?; + let level = require_nonempty_string(rule, "level", &context)?; + if !["unknown", "low", "medium", "high"].contains(&level) || !levels.insert(level) { + return Err(CatalogError(format!( + "invalid or duplicate confidence level in {context}" + ))); + } + strings(&rule["whenAll"], &format!("{context}.whenAll"), true)?; + } + Ok(()) +} + +fn validate_cost(card: &Value, method_id: &str) -> Result<(), CatalogError> { + let cost = &card["cost"]; + let context = format!("{method_id}.cost"); + exact_object(cost, &context, &["relative", "drivers"])?; + let relative = require_nonempty_string(cost, "relative", &context)?; + if !["low", "medium", "high"].contains(&relative) { + return Err(CatalogError(format!("{context}.relative is invalid"))); + } + strings(&cost["drivers"], &format!("{context}.drivers"), true)?; + Ok(()) +} + +fn validate_ports(card: &Value, method_id: &str) -> Result<(), CatalogError> { + let ports = nonempty_array( + &card["implementationPorts"], + &format!("{method_id}.implementationPorts"), + )?; + let mut ids = BTreeSet::new(); + for (position, port) in ports.iter().enumerate() { + let context = format!("{method_id}.implementationPorts[{position}]"); + exact_object(port, &context, &["id", "kind", "contract"])?; + let id = require_portable_id(port, "id", &context)?; + if !ids.insert(id) { + return Err(CatalogError(format!("duplicate implementation port {id}"))); + } + let kind = require_nonempty_string(port, "kind", &context)?; + if ![ + "manual", + "deterministic_tool", + "statistical_engine", + "modeling_tool", + ] + .contains(&kind) + { + return Err(CatalogError(format!( + "invalid implementation port kind in {context}" + ))); + } + strings(&port["contract"], &format!("{context}.contract"), true)?; + } + Ok(()) +} + +fn validate_source(card: &Value, method_id: &str) -> Result<(), CatalogError> { + let source = &card["source"]; + let context = format!("{method_id}.source"); + exact_object(source, &context, &["title", "version", "reference"])?; + for field in ["title", "version", "reference"] { + require_nonempty_string(source, field, &context)?; + } + Ok(()) +} + +fn validate_boundary(card: &Value, method_id: &str) -> Result<(), CatalogError> { + let boundary = &card["applicabilityBoundary"]; + let context = format!("{method_id}.applicabilityBoundary"); + exact_object(boundary, &context, &["inScope", "outOfScope"])?; + strings(&boundary["inScope"], &format!("{context}.inScope"), true)?; + strings( + &boundary["outOfScope"], + &format!("{context}.outOfScope"), + true, + )?; + Ok(()) +} + +fn read_json(path: &Path) -> Result { + let metadata = fs::metadata(path) + .map_err(|error| CatalogError(format!("inspect {}: {error}", path.display())))?; + if !metadata.is_file() || metadata.len() > MAX_DOCUMENT_BYTES { + return Err(CatalogError(format!( + "method catalog document {} is not a bounded regular file", + path.display() + ))); + } + let bytes = fs::read(path) + .map_err(|error| CatalogError(format!("read {}: {error}", path.display())))?; + serde_json::from_slice(&bytes) + .map_err(|error| CatalogError(format!("parse {}: {error}", path.display()))) +} + +fn validate_relative_card_path(path: &str) -> Result<(), CatalogError> { + let value = Path::new(path); + let components = value.components().collect::>(); + let file_name = match components.as_slice() { + [Component::Normal(parent), Component::Normal(file)] + if parent.to_str() == Some("cards") => + { + file.to_str() + } + _ => None, + }; + let Some(method_id) = file_name.and_then(|name| name.strip_suffix(".v1.json")) else { + return Err(CatalogError(format!( + "invalid relative method card path {path}" + ))); + }; + if method_id.is_empty() + || method_id.len() > 96 + || !method_id + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') + { + return Err(CatalogError(format!( + "invalid relative method card path {path}" + ))); + } + Ok(()) +} + +fn exact_object(value: &Value, context: &str, fields: &[&str]) -> Result<(), CatalogError> { + let object = value + .as_object() + .ok_or_else(|| CatalogError(format!("{context} must be an object")))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual != expected { + let missing = expected.difference(&actual).copied().collect::>(); + let unknown = actual.difference(&expected).copied().collect::>(); + return Err(CatalogError(format!( + "{context} fields differ from v1 schema; missing={missing:?}; unknown={unknown:?}" + ))); + } + Ok(()) +} + +fn require_exact_string( + value: &Value, + field: &str, + expected: &str, + context: &str, +) -> Result<(), CatalogError> { + let actual = require_nonempty_string(value, field, context)?; + if actual != expected { + return Err(CatalogError(format!( + "{context}.{field} must equal {expected}" + ))); + } + Ok(()) +} + +fn require_nonempty_string<'a>( + value: &'a Value, + field: &str, + context: &str, +) -> Result<&'a str, CatalogError> { + value[field] + .as_str() + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| CatalogError(format!("{context}.{field} must be a non-empty string"))) +} + +fn require_portable_id<'a>( + value: &'a Value, + field: &str, + context: &str, +) -> Result<&'a str, CatalogError> { + let id = require_nonempty_string(value, field, context)?; + if id.len() > 96 + || !id + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') + { + return Err(CatalogError(format!( + "{context}.{field} is not a portable id" + ))); + } + Ok(id) +} + +fn nonempty_array<'a>(value: &'a Value, context: &str) -> Result<&'a [Value], CatalogError> { + value + .as_array() + .filter(|items| !items.is_empty()) + .map(Vec::as_slice) + .ok_or_else(|| CatalogError(format!("{context} must be a non-empty array"))) +} + +fn strings<'a>( + value: &'a Value, + context: &str, + require_nonempty: bool, +) -> Result, CatalogError> { + let array = value + .as_array() + .ok_or_else(|| CatalogError(format!("{context} must be an array")))?; + if require_nonempty && array.is_empty() { + return Err(CatalogError(format!("{context} must not be empty"))); + } + let mut seen = BTreeSet::new(); + let mut result = Vec::with_capacity(array.len()); + for item in array { + let text = item + .as_str() + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| CatalogError(format!("{context} must contain non-empty strings")))?; + if !seen.insert(text) { + return Err(CatalogError(format!( + "{context} contains duplicate value {text}" + ))); + } + result.push(text); + } + Ok(result) +} diff --git a/crates/code-intel-cli/src/method_select.rs b/crates/code-intel-cli/src/method_select.rs new file mode 100644 index 0000000..1fa1308 --- /dev/null +++ b/crates/code-intel-cli/src/method_select.rs @@ -0,0 +1,528 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +use crate::admissibility; +use crate::method_catalog::MethodCatalog; + +const MAX_RULE_BYTES: u64 = 256 * 1024; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct SelectError(String); + +impl fmt::Display for SelectError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +impl std::error::Error for SelectError {} + +#[derive(Debug, Clone)] +struct ContraRule { + signal_id: String, + card_text: String, +} + +#[derive(Debug, Clone)] +struct Rule { + method_id: String, + signal_ids: BTreeSet, + contraindications: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct BoundFact { + signal_ids: BTreeSet, + evidence_kinds: BTreeSet, +} + +#[derive(Debug, Clone)] +pub(crate) struct RuleTable { + rules: Vec, +} + +pub(crate) fn load_rule_table( + path: &Path, + catalog: &MethodCatalog, +) -> Result { + let metadata = fs::metadata(path) + .map_err(|error| SelectError(format!("inspect method selection rules: {error}")))?; + if !metadata.is_file() || metadata.len() > MAX_RULE_BYTES { + return Err(SelectError( + "method selection rules must be a bounded regular file".to_string(), + )); + } + let document: Value = serde_json::from_slice( + &fs::read(path) + .map_err(|error| SelectError(format!("read method selection rules: {error}")))?, + ) + .map_err(|error| SelectError(format!("parse method selection rules: {error}")))?; + validate_rules(&document, catalog) +} + +fn validate_rules(document: &Value, catalog: &MethodCatalog) -> Result { + exact_object(document, "rule table", &["schema", "rules"])?; + if document["schema"] != "code-intel-method-selection-rules.v1" { + return Err(SelectError( + "method selection rule schema is invalid".to_string(), + )); + } + let cards = catalog + .cards() + .iter() + .map(|card| (card["id"].as_str().unwrap(), card)) + .collect::>(); + let entries = nonempty_array(&document["rules"], "rule table.rules")?; + let mut rules = Vec::with_capacity(entries.len()); + let mut previous: Option<&str> = None; + for (index, entry) in entries.iter().enumerate() { + let context = format!("rules[{index}]"); + exact_object( + entry, + &context, + &["methodId", "signalIds", "contraindications"], + )?; + let method_id = nonempty_string(&entry["methodId"], &format!("{context}.methodId"))?; + if previous.is_some_and(|prior| prior >= method_id) { + return Err(SelectError( + "method selection rules must be strictly sorted by methodId".to_string(), + )); + } + previous = Some(method_id); + let card = cards.get(method_id).ok_or_else(|| { + SelectError(format!("rule references unknown C01 method {method_id}")) + })?; + let declared_signals = card["problemSignals"] + .as_array() + .unwrap() + .iter() + .map(|item| item["id"].as_str().unwrap()) + .collect::>(); + let signal_ids = string_set(&entry["signalIds"], &format!("{context}.signalIds"), true)?; + if signal_ids + .iter() + .any(|signal| !declared_signals.contains(signal.as_str())) + { + return Err(SelectError(format!( + "{context} positive signals must reference C01 problemSignals" + ))); + } + let declared_contraindications = card["contraindications"] + .as_array() + .unwrap() + .iter() + .map(|item| item.as_str().unwrap()) + .collect::>(); + let contra_entries = entry["contraindications"] + .as_array() + .ok_or_else(|| SelectError(format!("{context}.contraindications must be an array")))?; + let mut contraindications = Vec::new(); + let mut contra_signals = BTreeSet::new(); + for (position, contra) in contra_entries.iter().enumerate() { + let contra_context = format!("{context}.contraindications[{position}]"); + exact_object(contra, &contra_context, &["signalId", "cardText"])?; + let signal_id = + nonempty_string(&contra["signalId"], &format!("{contra_context}.signalId"))?; + let card_text = + nonempty_string(&contra["cardText"], &format!("{contra_context}.cardText"))?; + if !contra_signals.insert(signal_id) || !declared_contraindications.contains(card_text) + { + return Err(SelectError(format!( + "{contra_context} must uniquely reference a C01 contraindication" + ))); + } + contraindications.push(ContraRule { + signal_id: signal_id.to_string(), + card_text: card_text.to_string(), + }); + } + rules.push(Rule { + method_id: method_id.to_string(), + signal_ids, + contraindications, + }); + } + if rules.len() != cards.len() { + return Err(SelectError( + "rule table must contain exactly one rule for every C01 method card".to_string(), + )); + } + Ok(RuleTable { rules }) +} + +pub(crate) fn select( + request: &Value, + artifact_root: &Path, + catalog: &MethodCatalog, + table: &RuleTable, +) -> Result { + validate_request(request)?; + let snapshot = request["snapshotIdentity"].as_str().unwrap(); + let evaluated_at = request["evaluatedAt"].as_u64().unwrap(); + let max_age = request["maxEvidenceAgeSeconds"].as_u64().unwrap(); + let admissions = validate_admissions( + &request["admissions"], + snapshot, + evaluated_at, + max_age, + artifact_root, + )?; + let (signals, available, source_admissions) = validate_facts(&request["facts"], &admissions)?; + let gaps = string_set(&request["evidenceGaps"], "evidenceGaps", false)?; + if available.iter().any(|kind| gaps.contains(kind)) { + return Err(SelectError( + "an evidence kind cannot be both available and an evidence gap".to_string(), + )); + } + + let cards = catalog + .cards() + .iter() + .map(|card| (card["id"].as_str().unwrap(), card)) + .collect::>(); + let mut matches = Vec::new(); + for rule in &table.rules { + let matched = rule + .signal_ids + .intersection(&signals) + .cloned() + .collect::>(); + if matched.is_empty() { + continue; + } + let card = cards[rule.method_id.as_str()]; + let missing = card["requiredEvidence"] + .as_array() + .unwrap() + .iter() + .map(|item| item["id"].as_str().unwrap()) + .filter(|kind| !available.contains(*kind) || gaps.contains(*kind)) + .map(str::to_string) + .collect::>(); + let triggered = rule + .contraindications + .iter() + .filter(|contra| signals.contains(&contra.signal_id)) + .map(|contra| contra.card_text.clone()) + .collect::>(); + let outcome = if !triggered.is_empty() { + "none" + } else if !missing.is_empty() { + "unknown" + } else { + "proposal" + }; + let confidence = if outcome != "proposal" { + "unknown" + } else if matched.len() > 1 { + "high" + } else { + "medium" + }; + matches.push(json!({ + "methodId":rule.method_id, + "outcome":outcome, + "matchedSignals":matched, + "missingEvidence":missing, + "cost":card["cost"], + "triggeredContraindications":triggered, + "declaredContraindications":card["contraindications"], + "confidenceRules":card["confidenceRules"], + "selectionConfidence":confidence, + "matchScore":matched.len() + })); + } + let outcome = if matches.iter().any(|item| item["outcome"] == "proposal") { + "proposal" + } else if matches.iter().any(|item| item["outcome"] == "unknown") { + "unknown" + } else { + "none" + }; + let top_score = matches + .iter() + .filter(|item| item["outcome"] == "proposal") + .filter_map(|item| item["matchScore"].as_u64()) + .max(); + let tie = top_score.is_some_and(|score| { + matches + .iter() + .filter(|item| item["outcome"] == "proposal" && item["matchScore"] == score) + .count() + > 1 + }); + Ok(json!({ + "schema":"code-intel-method-selection-result.v1", + "outcome":outcome, + "tie":tie, + "matches":matches, + "sourceAdmissionIds":source_admissions, + "executionPolicy":"advisory_proposal_only_no_execution_or_decision" + })) +} + +fn validate_request(request: &Value) -> Result<(), SelectError> { + exact_object( + request, + "request", + &[ + "schema", + "snapshotIdentity", + "evaluatedAt", + "maxEvidenceAgeSeconds", + "admissions", + "facts", + "evidenceGaps", + ], + )?; + if request["schema"] != "code-intel-method-selection-request.v1" + || !digest(&request["snapshotIdentity"]) + || request["evaluatedAt"].as_u64().is_none() + || !request["maxEvidenceAgeSeconds"] + .as_u64() + .is_some_and(|value| value > 0) + || !request["admissions"].is_array() + || !request["facts"].is_array() + || !request["evidenceGaps"].is_array() + { + return Err(SelectError( + "method selection request is invalid".to_string(), + )); + } + Ok(()) +} + +fn validate_admissions( + value: &Value, + snapshot: &str, + evaluated_at: u64, + max_age: u64, + artifact_root: &Path, +) -> Result>, SelectError> { + let array = value.as_array().unwrap(); + let mut admissions = BTreeMap::new(); + for (index, envelope) in array.iter().enumerate() { + let context = format!("admissions[{index}]"); + exact_object(envelope, &context, &["request", "result"])?; + let request = &envelope["request"]; + if request["expectedSnapshotIdentity"] != snapshot + || request["policy"]["evaluatedAt"].as_u64() != Some(evaluated_at) + || request["policy"]["maxAgeSeconds"].as_u64() != Some(max_age) + { + return Err(SelectError(format!( + "{context} A04 request differs from the C02 snapshot/freshness policy" + ))); + } + let validated = + admissibility::validate_for_consumer(request, artifact_root).map_err(|message| { + SelectError(format!("{context} A04 validation failed: {message}")) + })?; + if validated.result() != &envelope["result"] { + return Err(SelectError(format!( + "{context} A04 result is forged or does not match runtime validation" + ))); + } + if validated.result()["status"] != "admitted" + || validated.result()["domainVerdict"] != "observed" + { + return Err(SelectError(format!( + "{context} is not admitted observed evidence" + ))); + } + let identity = validated.result()["admissionIdentity"] + .as_str() + .expect("A04 admitted result has an identity") + .to_string(); + let facts = payload_facts(validated.payload(), &context)?; + if facts.is_empty() { + return Err(SelectError(format!("{context} is an unused admission"))); + } + if admissions.insert(identity.clone(), facts).is_some() { + return Err(SelectError(format!( + "duplicate admission identity {identity}" + ))); + } + } + Ok(admissions) +} + +fn payload_facts( + payload: &Value, + context: &str, +) -> Result, SelectError> { + let data = payload["data"] + .as_object() + .ok_or_else(|| SelectError(format!("{context} A04 payload data is invalid")))?; + let values = data + .get("methodSelectionFacts") + .and_then(Value::as_array) + .ok_or_else(|| { + SelectError(format!( + "{context} A04 payload has no methodSelectionFacts evidence" + )) + })?; + let mut facts = BTreeMap::new(); + for (index, fact) in values.iter().enumerate() { + let fact_context = format!("{context}.payload.methodSelectionFacts[{index}]"); + exact_object(fact, &fact_context, &["id", "signalIds", "evidenceKinds"])?; + let id = nonempty_string(&fact["id"], &format!("{fact_context}.id"))?.to_string(); + let bound = BoundFact { + signal_ids: string_set( + &fact["signalIds"], + &format!("{fact_context}.signalIds"), + true, + )?, + evidence_kinds: string_set( + &fact["evidenceKinds"], + &format!("{fact_context}.evidenceKinds"), + true, + )?, + }; + if facts.insert(id.clone(), bound).is_some() { + return Err(SelectError(format!( + "{fact_context} duplicates payload fact {id}" + ))); + } + } + Ok(facts) +} + +fn validate_facts( + value: &Value, + admissions: &BTreeMap>, +) -> Result<(BTreeSet, BTreeSet, Vec), SelectError> { + let mut fact_ids = BTreeSet::new(); + let mut signals = BTreeSet::new(); + let mut evidence = BTreeSet::new(); + let mut used_admissions = BTreeSet::new(); + for (index, fact) in value.as_array().unwrap().iter().enumerate() { + let context = format!("facts[{index}]"); + exact_object( + fact, + &context, + &["id", "signalIds", "evidenceKinds", "admissionIds"], + )?; + let id = nonempty_string(&fact["id"], &format!("{context}.id"))?; + if !fact_ids.insert(id) { + return Err(SelectError(format!("duplicate fact id {id}"))); + } + let fact_signals = string_set(&fact["signalIds"], &format!("{context}.signalIds"), true)?; + let fact_evidence = string_set( + &fact["evidenceKinds"], + &format!("{context}.evidenceKinds"), + true, + )?; + for admission in string_set( + &fact["admissionIds"], + &format!("{context}.admissionIds"), + true, + )? { + let admitted_facts = admissions.get(&admission).ok_or_else(|| { + SelectError(format!( + "{context} references unknown admission {admission}" + )) + })?; + let bound = admitted_facts.get(id).ok_or_else(|| { + SelectError(format!( + "{context} fact {id} is not present in A04 admitted payload {admission}" + )) + })?; + if bound.signal_ids != fact_signals || bound.evidence_kinds != fact_evidence { + return Err(SelectError(format!( + "{context} signal/evidence labels differ from A04 admitted payload {admission}" + ))); + } + used_admissions.insert(admission); + } + signals.extend(fact_signals); + evidence.extend(fact_evidence); + } + let admitted = admissions.keys().cloned().collect::>(); + if used_admissions != admitted { + let unused = admitted + .difference(&used_admissions) + .cloned() + .collect::>(); + return Err(SelectError(format!( + "unused admission identities are not referenced by facts: {}", + unused.join(",") + ))); + } + Ok((signals, evidence, used_admissions.into_iter().collect())) +} + +fn exact_object(value: &Value, context: &str, fields: &[&str]) -> Result<(), SelectError> { + allowed_object(value, context, fields, &[]) +} + +fn allowed_object( + value: &Value, + context: &str, + required: &[&str], + optional: &[&str], +) -> Result<(), SelectError> { + let object = value + .as_object() + .ok_or_else(|| SelectError(format!("{context} must be an object")))?; + let actual = object.keys().map(String::as_str).collect::>(); + let required = required.iter().copied().collect::>(); + let optional = optional.iter().copied().collect::>(); + let allowed = required.union(&optional).copied().collect::>(); + if !required.is_subset(&actual) || !actual.is_subset(&allowed) { + return Err(SelectError(format!( + "{context} fields differ from v1 contract" + ))); + } + Ok(()) +} + +fn nonempty_array<'a>(value: &'a Value, context: &str) -> Result<&'a [Value], SelectError> { + value + .as_array() + .filter(|items| !items.is_empty()) + .map(Vec::as_slice) + .ok_or_else(|| SelectError(format!("{context} must be a non-empty array"))) +} + +fn string_set( + value: &Value, + context: &str, + require_nonempty: bool, +) -> Result, SelectError> { + let array = value + .as_array() + .ok_or_else(|| SelectError(format!("{context} must be an array")))?; + if require_nonempty && array.is_empty() { + return Err(SelectError(format!("{context} must not be empty"))); + } + let mut set = BTreeSet::new(); + for item in array { + let text = nonempty_string(item, context)?; + if !set.insert(text.to_string()) { + return Err(SelectError(format!("{context} contains duplicate {text}"))); + } + } + Ok(set) +} + +fn nonempty_string<'a>(value: &'a Value, context: &str) -> Result<&'a str, SelectError> { + value + .as_str() + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| SelectError(format!("{context} must be a non-empty string"))) +} + +fn digest(value: &Value) -> bool { + value.as_str().is_some_and(is_digest) +} + +fn is_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) +} diff --git a/crates/code-intel-cli/src/model_channels.rs b/crates/code-intel-cli/src/model_channels.rs new file mode 100644 index 0000000..b77387f --- /dev/null +++ b/crates/code-intel-cli/src/model_channels.rs @@ -0,0 +1,735 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Map, Value}; + +const FAILURE_CATEGORIES: [&str; 9] = [ + "consent_required", + "model_unavailable", + "provider_unavailable", + "provider_quota", + "config_error", + "local_tool_error", + "adapter_protocol_error", + "external_data_forbidden", + "paid_usage_forbidden", +]; +const COST_SCOPES: [&str; 4] = [ + "local_compute", + "subscription_cli", + "free_or_internal_quota", + "metered_api", +]; +const CONSENT_STATUSES: [&str; 3] = ["unanswered", "granted", "denied"]; +const INVENTORY_DIAGNOSTICS: [&str; 26] = [ + "candidate_verified", + "version_probe_passed", + "fallback_candidate_selected", + "candidate_timed_out", + "candidate_verification_failed", + "endpoint_configured", + "endpoint_not_configured", + "model_declared_by_user", + "model_not_declared", + "endpoint_value_not_collected", + "model_catalog_observed", + "auth_method_api_key", + "auth_method_oauth", + "auth_method_subscription", + "auth_method_unknown", + "installation_present", + "installation_not_found", + "config_present_content_not_read", + "config_not_found", + "presence_only", + "credential_values_not_collected", + "endpoint_values_not_collected", + "endpoint_not_probed", + "endpoint_probe_passed", + "endpoint_probe_failed", + "model_not_in_catalog", +]; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let parsed = match parse_cli(raw) { + Ok(parsed) => parsed, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let request = match read_json(&parsed.request) { + Ok(request) => request, + Err((code, message)) => { + eprintln!("{message}"); + return code; + } + }; + let result = match parsed.operation.as_str() { + "inventory-validate" => validate_inventory(&request).map(|_| request), + "route" => route(&request), + _ => unreachable!("operation was validated by parse_cli"), + }; + let result = match result { + Ok(result) => result, + Err(message) => { + eprintln!("{message}"); + return 65; + } + }; + let output = serde_json::to_vec_pretty(&result).expect("model channel result serializes"); + if let Some(path) = parsed.out.as_deref() { + if let Err(error) = fs::write(path, &output) { + eprintln!("cannot write {}: {error}", path.display()); + return 74; + } + } + println!("{}", String::from_utf8(output).expect("JSON is UTF-8")); + if result.get("status").and_then(Value::as_str) == Some("consent_required") { + 2 + } else { + 0 + } +} + +struct Cli { + operation: String, + request: PathBuf, + out: Option, +} + +fn parse_cli(raw: &[String]) -> Result { + let operation = raw + .first() + .map(String::as_str) + .ok_or("usage: model --request [--out ]")?; + if !matches!(operation, "inventory-validate" | "route") { + return Err(format!("unknown model operation: {operation}")); + } + let mut request = None; + let mut out = None; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!(flag, "--request" | "--out") { + return Err(format!("unknown model argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + let slot = if flag == "--request" { + &mut request + } else { + &mut out + }; + if slot.replace(PathBuf::from(value)).is_some() { + return Err(format!("duplicate model argument: {flag}")); + } + index += 2; + } + Ok(Cli { + operation: operation.to_string(), + request: request.ok_or("model operation requires --request")?, + out, + }) +} + +fn read_json(path: &Path) -> Result { + let bytes = + fs::read(path).map_err(|error| (74, format!("cannot read {}: {error}", path.display())))?; + if bytes.len() > 8 * 1024 * 1024 { + return Err((64, "model request exceeds 8 MiB".into())); + } + let text = std::str::from_utf8(&bytes) + .map_err(|error| (64, format!("model request is not UTF-8: {error}")))?; + crate::capability::reject_duplicate_json_keys(text).map_err(|error| (64, error))?; + serde_json::from_str(text).map_err(|error| (64, format!("invalid JSON: {error}"))) +} + +fn validate_inventory(value: &Value) -> Result<(), String> { + let object = exact_object(value, &["schema", "candidates", "configurationBrokers"])?; + exact_string( + object, + "schema", + "code-intel-model-channel-inventory-result.v1", + )?; + let candidates = exact_array(object, "candidates")?; + let mut ids = std::collections::BTreeSet::new(); + for candidate in candidates { + let candidate = exact_object( + candidate, + &[ + "id", + "channelKind", + "provider", + "model", + "costScope", + "endpointConfigured", + "discovered", + "executableVerified", + "authPresent", + "modelAvailable", + "externalEgress", + "source", + "diagnostics", + ], + )?; + let id = portable_id(candidate, "id")?; + if !ids.insert(id) { + return Err("candidate ids must be unique".into()); + } + enum_string( + candidate, + "channelKind", + &[ + "local_compatible", + "ollama", + "claude_cli", + "opencode_cli", + "codex_cli", + ], + )?; + nullable_string(candidate, "provider")?; + nullable_string(candidate, "model")?; + enum_string(candidate, "costScope", &COST_SCOPES)?; + bool_field(candidate, "endpointConfigured")?; + bool_field(candidate, "discovered")?; + bool_field(candidate, "executableVerified")?; + enum_string( + candidate, + "authPresent", + &["unknown", "present", "absent", "not_applicable"], + )?; + enum_string( + candidate, + "modelAvailable", + &["unknown", "available", "unavailable"], + )?; + bool_field(candidate, "externalEgress")?; + enum_string( + candidate, + "source", + &["user_input", "local_discovery", "cc_switch", "cli_config"], + )?; + enum_string_array(candidate, "diagnostics", &INVENTORY_DIAGNOSTICS)?; + } + let brokers = exact_array(object, "configurationBrokers")?; + let mut broker_ids = std::collections::BTreeSet::new(); + for broker in brokers { + let broker = exact_object( + broker, + &["id", "kind", "discovered", "configPresent", "diagnostics"], + )?; + let id = portable_id(broker, "id")?; + if !broker_ids.insert(id) { + return Err("configuration broker ids must be unique".into()); + } + enum_string(broker, "kind", &["cc_switch", "manual_config"])?; + bool_field(broker, "discovered")?; + bool_field(broker, "configPresent")?; + enum_string_array(broker, "diagnostics", &INVENTORY_DIAGNOSTICS)?; + } + Ok(()) +} + +pub(crate) fn route(request: &Value) -> Result { + let object = exact_object(request, &["schema", "inventory", "policy", "workload"])?; + exact_string(object, "schema", "code-intel-model-routing-request.v1")?; + let inventory = object + .get("inventory") + .expect("exact object contains inventory"); + validate_inventory(inventory)?; + let policy = exact_object( + object.get("policy").expect("exact object contains policy"), + &[ + "consumptionAuthorization", + "externalData", + "paidSpend", + "selection", + ], + )?; + let consumption = exact_object( + policy + .get("consumptionAuthorization") + .expect("exact policy"), + &["status", "scopes"], + )?; + let consumption_status = enum_string(consumption, "status", &CONSENT_STATUSES)?; + let authorized_scopes = enum_string_array(consumption, "scopes", &COST_SCOPES)?; + let external_status = status_object(policy, "externalData")?; + let paid_status = status_object(policy, "paidSpend")?; + let selection = exact_object( + policy.get("selection").expect("exact policy"), + &["pinnedAdapter", "fallbackPolicy"], + )?; + let pinned = nullable_portable_id(selection, "pinnedAdapter")?; + let fallback = enum_string(selection, "fallbackPolicy", &["denied", "allowed"])?; + let workload = exact_object( + object + .get("workload") + .expect("exact object contains workload"), + &["requiresExternalData"], + )?; + let requires_external = bool_field(workload, "requiresExternalData")?; + + let candidates = inventory["candidates"].as_array().expect("validated array"); + let mut ordered: Vec<&Value> = Vec::with_capacity(candidates.len()); + if let Some(pinned_id) = pinned { + if let Some(candidate) = candidates + .iter() + .find(|candidate| candidate["id"] == pinned_id) + { + ordered.push(candidate); + } + } + ordered.extend( + candidates + .iter() + .filter(|candidate| pinned.map_or(true, |pinned_id| candidate["id"] != pinned_id)), + ); + let pinned_seen = pinned.map(|id| candidates.iter().any(|candidate| candidate["id"] == id)); + let mut attempts = Vec::new(); + let mut selected = None; + let mut pinned_failed = pinned_seen == Some(false); + for candidate in ordered { + let id = candidate["id"].as_str().expect("validated id"); + let is_pinned = pinned == Some(id); + if pinned.is_some() && !is_pinned && (fallback == "denied" || !pinned_failed) { + attempts.push(attempt( + id, + "discovered", + false, + Some("config_error"), + "fallback_not_authorized", + )); + continue; + } + let (state, category, reason) = evaluate_candidate( + candidate, + consumption_status, + &authorized_scopes, + external_status, + paid_status, + requires_external, + ); + let eligible = category.is_none(); + attempts.push(attempt(id, state, eligible, category, reason)); + if eligible && selected.is_none() { + selected = Some(json!({ + "candidateId": id, + "channelKind": candidate["channelKind"], + "provider": candidate["provider"], + "model": candidate["model"], + "costScope": candidate["costScope"], + "readinessState": "ready" + })); + break; + } + if is_pinned { + pinned_failed = true; + } + } + if pinned_seen == Some(false) { + attempts.insert( + 0, + attempt( + pinned.unwrap(), + "discovered", + false, + Some("config_error"), + "pinned_adapter_not_found", + ), + ); + } + let consent_blocked = attempts.iter().any(|attempt| { + attempt["failureCategory"] == "consent_required" + || attempt["failureCategory"] == "external_data_forbidden" + || attempt["failureCategory"] == "paid_usage_forbidden" + }); + let status = if selected.is_some() { + "ready" + } else if consent_blocked { + "consent_required" + } else { + "deterministic_degraded" + }; + let manual_action = match status { + "consent_required" => Value::String("obtain_explicit_authorization".into()), + "deterministic_degraded" => Value::String("provide_or_enable_model_channel".into()), + _ => Value::Null, + }; + Ok(json!({ + "schema": "code-intel-model-routing-result.v1", + "status": status, + "selected": selected, + "authorization": { + "consumptionAuthorization": { + "status": consumption_status, + "scopes": authorized_scopes + }, + "externalData": {"status": external_status}, + "paidSpend": {"status": paid_status} + }, + "attempts": attempts, + "manualAction": manual_action + })) +} + +fn evaluate_candidate<'a>( + candidate: &'a Value, + consumption_status: &str, + authorized_scopes: &[&str], + external_status: &str, + paid_status: &str, + requires_external: bool, +) -> (&'static str, Option<&'static str>, &'static str) { + if !candidate["discovered"].as_bool().unwrap() { + return ("discovered", Some("provider_unavailable"), "not_discovered"); + } + if candidate["channelKind"] == "local_compatible" + && !candidate["endpointConfigured"].as_bool().unwrap() + { + return ( + "executable_verified", + Some("config_error"), + "endpoint_not_configured", + ); + } + if !candidate["executableVerified"].as_bool().unwrap() { + return ( + "executable_verified", + Some("local_tool_error"), + "executable_not_verified", + ); + } + if candidate["authPresent"] == "absent" { + return ( + "auth_present", + Some("provider_unavailable"), + "authentication_absent", + ); + } + if candidate["authPresent"] == "unknown" { + return ( + "auth_present", + Some("provider_unavailable"), + "authentication_unverified", + ); + } + if candidate["modelAvailable"] != "available" { + return ( + "model_available", + Some("model_unavailable"), + "model_not_available", + ); + } + if requires_external + && candidate["externalEgress"].as_bool().unwrap() + && external_status != "granted" + { + return ( + "egress_allowed", + Some("external_data_forbidden"), + "external_data_not_authorized", + ); + } + let scope = candidate["costScope"].as_str().unwrap(); + if consumption_status != "granted" || !authorized_scopes.contains(&scope) { + return ( + "spend_allowed", + Some("consent_required"), + "consumption_scope_not_authorized", + ); + } + if scope == "metered_api" && paid_status != "granted" { + return ( + "spend_allowed", + Some("paid_usage_forbidden"), + "paid_spend_not_authorized", + ); + } + ("ready", None, "eligible") +} + +fn attempt(id: &str, state: &str, eligible: bool, category: Option<&str>, reason: &str) -> Value { + debug_assert!(category.map_or(true, |value| FAILURE_CATEGORIES.contains(&value))); + json!({ + "candidateId": id, + "readinessState": state, + "eligible": eligible, + "failureCategory": category, + "reason": reason + }) +} + +fn status_object<'a>(parent: &'a Map, field: &str) -> Result<&'a str, String> { + let object = exact_object(parent.get(field).expect("exact policy"), &["status"])?; + enum_string(object, "status", &CONSENT_STATUSES) +} + +fn exact_object<'a>(value: &'a Value, keys: &[&str]) -> Result<&'a Map, String> { + let object = value.as_object().ok_or("expected an object")?; + if object.len() != keys.len() || keys.iter().any(|key| !object.contains_key(*key)) { + return Err(format!("object must contain exactly: {}", keys.join(", "))); + } + Ok(object) +} + +fn exact_string<'a>( + object: &'a Map, + key: &str, + expected: &str, +) -> Result<&'a str, String> { + let value = object + .get(key) + .and_then(Value::as_str) + .ok_or_else(|| format!("{key} must be a string"))?; + if value != expected { + return Err(format!("{key} must equal {expected}")); + } + Ok(value) +} +fn nonempty_string<'a>(object: &'a Map, key: &str) -> Result<&'a str, String> { + let value = object + .get(key) + .and_then(Value::as_str) + .filter(|v| !v.is_empty()) + .ok_or_else(|| format!("{key} must be a non-empty string"))?; + Ok(value) +} +fn portable_id<'a>(object: &'a Map, key: &str) -> Result<&'a str, String> { + let value = nonempty_string(object, key)?; + if value.len() > 128 + || !value.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_alphanumeric() || (index > 0 && matches!(byte, b'.' | b'_' | b'-')) + }) + { + return Err(format!("{key} must be a portable identifier")); + } + Ok(value) +} +fn nullable_string<'a>( + object: &'a Map, + key: &str, +) -> Result, String> { + match object.get(key) { + Some(Value::Null) => Ok(None), + Some(Value::String(value)) if !value.is_empty() => Ok(Some(value)), + _ => Err(format!("{key} must be null or a non-empty string")), + } +} +fn nullable_portable_id<'a>( + object: &'a Map, + key: &str, +) -> Result, String> { + match nullable_string(object, key)? { + None => Ok(None), + Some(value) + if value.len() <= 128 + && value.bytes().enumerate().all(|(index, byte)| { + byte.is_ascii_alphanumeric() + || (index > 0 && matches!(byte, b'.' | b'_' | b'-')) + }) => + { + Ok(Some(value)) + } + Some(_) => Err(format!("{key} must be a portable identifier")), + } +} +fn enum_string<'a>( + object: &'a Map, + key: &str, + allowed: &[&str], +) -> Result<&'a str, String> { + let value = object + .get(key) + .and_then(Value::as_str) + .ok_or_else(|| format!("{key} must be a string"))?; + if !allowed.contains(&value) { + return Err(format!("{key} is outside the closed vocabulary")); + } + Ok(value) +} +fn bool_field(object: &Map, key: &str) -> Result { + object + .get(key) + .and_then(Value::as_bool) + .ok_or_else(|| format!("{key} must be boolean")) +} +fn exact_array<'a>(object: &'a Map, key: &str) -> Result<&'a Vec, String> { + object + .get(key) + .and_then(Value::as_array) + .ok_or_else(|| format!("{key} must be an array")) +} +fn enum_string_array<'a>( + object: &'a Map, + key: &str, + allowed: &[&str], +) -> Result, String> { + let array = exact_array(object, key)?; + let mut result = Vec::new(); + let mut unique = std::collections::BTreeSet::new(); + for value in array { + let value = value + .as_str() + .ok_or_else(|| format!("{key} must contain strings"))?; + if !allowed.contains(&value) || !unique.insert(value) { + return Err(format!( + "{key} must contain unique closed-vocabulary values" + )); + } + result.push(value); + } + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn candidate(id: &str, scope: &str) -> Value { + json!({"id":id,"channelKind":"ollama","provider":"ollama","model":"qwen","costScope":scope,"endpointConfigured":true,"discovered":true,"executableVerified":true,"authPresent":"not_applicable","modelAvailable":"available","externalEgress":false,"source":"local_discovery","diagnostics":[]}) + } + fn request( + candidates: Vec, + consumption: &str, + scopes: Vec<&str>, + pinned: Value, + fallback: &str, + ) -> Value { + json!({"schema":"code-intel-model-routing-request.v1","inventory":{"schema":"code-intel-model-channel-inventory-result.v1","candidates":candidates,"configurationBrokers":[]},"policy":{"consumptionAuthorization":{"status":consumption,"scopes":scopes},"externalData":{"status":"denied"},"paidSpend":{"status":"denied"},"selection":{"pinnedAdapter":pinned,"fallbackPolicy":fallback}},"workload":{"requiresExternalData":false}}) + } + #[test] + fn local_channel_requires_explicit_compute_authorization() { + let result = route(&request( + vec![candidate("ollama", "local_compute")], + "unanswered", + vec![], + Value::Null, + "denied", + )) + .unwrap(); + assert_eq!(result["status"], "consent_required"); + assert_eq!(result["attempts"][0]["readinessState"], "spend_allowed"); + } + #[test] + fn authorized_local_channel_is_ready() { + let result = route(&request( + vec![candidate("ollama", "local_compute")], + "granted", + vec!["local_compute"], + Value::Null, + "denied", + )) + .unwrap(); + assert_eq!(result["status"], "ready"); + assert_eq!(result["selected"]["candidateId"], "ollama"); + } + #[test] + fn pinned_adapter_does_not_fallback_without_permission() { + let mut unavailable = candidate("pinned", "subscription_cli"); + unavailable["modelAvailable"] = json!("unavailable"); + let result = route(&request( + vec![unavailable, candidate("other", "local_compute")], + "granted", + vec!["subscription_cli", "local_compute"], + json!("pinned"), + "denied", + )) + .unwrap(); + assert_eq!(result["status"], "deterministic_degraded"); + assert!(result["selected"].is_null()); + assert_eq!(result["attempts"][1]["reason"], "fallback_not_authorized"); + } + #[test] + fn ready_pinned_adapter_is_evaluated_before_inventory_order() { + let result = route(&request( + vec![ + candidate("other", "local_compute"), + candidate("pinned", "local_compute"), + ], + "granted", + vec!["local_compute"], + json!("pinned"), + "allowed", + )) + .unwrap(); + assert_eq!(result["selected"]["candidateId"], "pinned"); + assert_eq!(result["attempts"][0]["candidateId"], "pinned"); + } + #[test] + fn missing_pinned_adapter_falls_back_only_when_allowed() { + let result = route(&request( + vec![candidate("other", "local_compute")], + "granted", + vec!["local_compute"], + json!("missing"), + "allowed", + )) + .unwrap(); + assert_eq!(result["status"], "ready"); + assert_eq!(result["selected"]["candidateId"], "other"); + assert_eq!(result["attempts"][0]["reason"], "pinned_adapter_not_found"); + } + #[test] + fn unknown_authentication_is_not_ready() { + let mut channel = candidate("claude", "subscription_cli"); + channel["channelKind"] = json!("claude_cli"); + channel["authPresent"] = json!("unknown"); + let result = route(&request( + vec![channel], + "granted", + vec!["subscription_cli"], + Value::Null, + "denied", + )) + .unwrap(); + assert_eq!(result["status"], "deterministic_degraded"); + assert_eq!(result["attempts"][0]["readinessState"], "auth_present"); + } + #[test] + fn metered_api_needs_separate_paid_spend_consent() { + let result = route(&request( + vec![candidate("api", "metered_api")], + "granted", + vec!["metered_api"], + Value::Null, + "denied", + )) + .unwrap(); + assert_eq!(result["status"], "consent_required"); + assert_eq!( + result["attempts"][0]["failureCategory"], + "paid_usage_forbidden" + ); + } + #[test] + fn repository_data_on_external_channel_needs_separate_egress_consent() { + let mut channel = candidate("claude", "subscription_cli"); + channel["channelKind"] = json!("claude_cli"); + channel["authPresent"] = json!("present"); + channel["externalEgress"] = json!(true); + let mut request = request( + vec![channel], + "granted", + vec!["subscription_cli"], + Value::Null, + "denied", + ); + request["workload"]["requiresExternalData"] = json!(true); + let result = route(&request).unwrap(); + assert_eq!(result["status"], "consent_required"); + assert_eq!( + result["attempts"][0]["failureCategory"], + "external_data_forbidden" + ); + } + #[test] + fn inventory_rejects_unknown_fields() { + let mut value = json!({"schema":"code-intel-model-channel-inventory-result.v1","candidates":[],"configurationBrokers":[]}); + value["secret"] = json!("must-not-be-accepted"); + assert!(validate_inventory(&value).is_err()); + } +} diff --git a/crates/code-intel-cli/src/native_code_evidence.rs b/crates/code-intel-cli/src/native_code_evidence.rs new file mode 100644 index 0000000..ede7f97 --- /dev/null +++ b/crates/code-intel-cli/src/native_code_evidence.rs @@ -0,0 +1,749 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +use super::{AdapterArtifact, AdapterError, AdapterOutput}; +use crate::artifact_ref::VerifiedArtifact; +use crate::capability::sha256_hex; +use crate::snapshot; + +const SUPPORTED_HEURISTICS: [&str; 7] = [ + "powershell", + "python", + "javascript", + "typescript", + "rust", + "go", + "java", +]; + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + let options = request["options"] + .as_object() + .ok_or_else(|| AdapterError::InvalidOptions("options must be an object".into()))?; + if options.len() != 1 || !options.contains_key("repoPath") { + return Err(AdapterError::InvalidOptions( + "evidence.native-code accepts only options.repoPath".into(), + )); + } + let repo = options["repoPath"] + .as_str() + .filter(|value| !value.is_empty()) + .map(Path::new) + .ok_or_else(|| AdapterError::InvalidOptions("options.repoPath must be non-empty".into()))?; + let inventory = match verified_inputs { + [inventory] if inventory.artifact_type() == "inventory.files" => inventory, + _ => { + return Err(AdapterError::Contract( + "evidence.native-code requires exactly one A03-verified inventory.files input" + .into(), + )) + } + }; + let lease = snapshot::begin_consumption(repo, &request["snapshot"]) + .map_err(|message| AdapterError::Contract(format!("snapshot consumption: {message}")))?; + let paths = inventory_paths(inventory.bytes())?; + let mut files = Vec::new(); + let mut symbols = Vec::new(); + let mut chunks = Vec::new(); + let mut mappings = Vec::new(); + let mut imports = Vec::new(); + let mut unsupported_files = Vec::new(); + + for relative in paths { + let full = safe_join(repo, &relative)?; + if !full.is_file() { + return Err(AdapterError::Contract(format!( + "inventory path is not a readable file: {relative}" + ))); + } + let bytes = fs::read(&full).map_err(|error| { + AdapterError::Io(format!("read native code evidence {relative}: {error}")) + })?; + let language = language(&relative); + let hash = sha256_hex(&bytes); + let content = match std::str::from_utf8(&bytes) { + Ok(content) => content, + Err(_) if !SUPPORTED_HEURISTICS.contains(&language) => { + unsupported_files.push(relative.clone()); + files.push(json!({ + "path":relative, + "language":language, + "bytes":bytes.len(), + "lines":0, + "textHash":hash, + "source":"native-minimal" + })); + chunks.push(json!({ + "id":format!("{relative}#file"), + "file":relative, + "startLine":1, + "endLine":1, + "kind":"file", + "containsSymbols":[], + "textHash":hash, + "source":"native-minimal" + })); + continue; + } + Err(_) => { + return Err(AdapterError::Contract(format!( + "supported source file is not UTF-8 text: {relative}" + ))) + } + }; + let lines = lines(content); + if !SUPPORTED_HEURISTICS.contains(&language) { + unsupported_files.push(relative.clone()); + } + files.push(json!({ + "path":relative, + "language":language, + "bytes":bytes.len(), + "lines":lines.len(), + "textHash":hash, + "source":"native-minimal" + })); + let file_symbols = extract_symbols(&relative, language, &lines); + let chunk_id = format!("{relative}#file"); + chunks.push(json!({ + "id":chunk_id, + "file":relative, + "startLine":1, + "endLine":lines.len().max(1), + "kind":"file", + "containsSymbols":file_symbols.iter().map(|symbol| symbol["id"].clone()).collect::>(), + "textHash":hash, + "source":"native-minimal" + })); + for symbol in &file_symbols { + mappings.push(json!({ + "symbolId":symbol["id"], + "chunkId":chunk_id, + "relation":"contained_by", + "confidence":0.55 + })); + } + symbols.extend(file_symbols); + imports.extend(extract_imports(&relative, language, &lines)); + } + lease + .verify_after(repo) + .map_err(|message| AdapterError::Contract(format!("snapshot changed: {message}")))?; + + canonicalize_evidence_arrays( + &mut files, + &mut symbols, + &mut chunks, + &mut mappings, + &mut imports, + &mut unsupported_files, + ); + let ranking = ranking(&files, &symbols, &imports); + let coverage = json!({ + "schema":"code-evidence-coverage.v1", + "producer":"native-minimal", + "parserKind":"line-heuristic", + "supportedHeuristics":SUPPORTED_HEURISTICS, + "unsupportedFiles":unsupported_files, + "symbolPrecision":"heuristic", + "importPrecision":"heuristic", + "relationshipPrecision":"unknown", + "callGraph":"unknown", + "effects":["repo_read","local_write"] + }); + let coco_outcome = json!({ + "schema":"code-evidence-adapter-outcome.v1", + "adapter":"cocoindex-code", + "enabled":false, + "required":false, + "status":"skipped", + "fatal":false, + "reasonCode":"reviewed_deletion", + "reason":"cocoindex-code is a reviewed retirement tombstone; legacy configuration cannot restore discovery or invocation.", + "command":"" + }); + let scorecard = json!({ + "schema":"code-evidence-scorecard.v1", + "status":"ok", + "nativeMinimal":true, + "adapters":[coco_outcome.clone()], + "metrics":{ + "files":files.len(), + "symbols":symbols.len(), + "chunks":chunks.len(), + "imports":imports.len(), + "symbolContainmentRate":if symbols.is_empty() { Value::Null } else { json!(1.0) }, + "fallbackChunkRate":1.0 + } + }); + let agent_views = render_agent_views(&ranking, &files); + let documents = vec![ + ( + "code-evidence/merged/full/files.json", + "code-evidence-files.v1", + "code_evidence.files", + json!({"schema":"code-evidence-files.v1","files":files}), + ), + ( + "code-evidence/merged/full/symbols.json", + "code-evidence-symbols.v1", + "code_evidence.symbols", + json!({"schema":"code-evidence-symbols.v1","symbols":symbols}), + ), + ( + "code-evidence/merged/full/chunks.json", + "code-evidence-chunks.v1", + "code_evidence.chunks", + json!({"schema":"code-evidence-chunks.v1","chunks":chunks}), + ), + ( + "code-evidence/merged/full/symbol-chunks.json", + "code-evidence-symbol-chunks.v1", + "code_evidence.symbol_chunks", + json!({"schema":"code-evidence-symbol-chunks.v1","mappings":mappings}), + ), + ( + "code-evidence/merged/full/imports.json", + "code-evidence-imports.v1", + "code_evidence.imports", + json!({"schema":"code-evidence-imports.v1","imports":imports}), + ), + ( + "code-evidence/merged/scorecard.json", + "code-evidence-scorecard.v1", + "code_evidence.scorecard", + scorecard, + ), + ( + "code-evidence/coverage.json", + "code-evidence-coverage.v1", + "code_evidence.coverage", + coverage, + ), + ( + "code-evidence/merged/agent/ranking.json", + "agent-code-slice-ranking.v1", + "code_evidence.agent_slice", + ranking, + ), + ]; + let mut artifacts = Vec::new(); + for (relative_path, artifact_schema, artifact_type, document) in documents { + let bytes = serde_json::to_vec(&document).map_err(|error| { + AdapterError::Internal(format!("serialize {artifact_schema}: {error}")) + })?; + publish(out, relative_path, &bytes)?; + artifacts.push(AdapterArtifact { + artifact_schema: artifact_schema.into(), + artifact_type: artifact_type.into(), + relative_path: relative_path.into(), + bytes, + }); + } + let coco_bytes = serde_json::to_vec(&coco_outcome) + .map_err(|error| AdapterError::Internal(format!("serialize cocoindex outcome: {error}")))?; + publish( + out, + "code-evidence/adapters/cocoindex-code/outcome.json", + &coco_bytes, + )?; + for (relative, content) in agent_views { + publish(out, relative, content.as_bytes())?; + } + Ok(AdapterOutput { + artifacts, + observed_effects: vec!["repo_read".into(), "local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn canonicalize_evidence_arrays( + files: &mut [Value], + symbols: &mut [Value], + chunks: &mut [Value], + mappings: &mut [Value], + imports: &mut [Value], + unsupported_files: &mut [String], +) { + files.sort_by(|left, right| string_field(left, "path").cmp(string_field(right, "path"))); + symbols.sort_by(|left, right| { + string_field(left, "file") + .cmp(string_field(right, "file")) + .then_with(|| u64_field(left, "startLine").cmp(&u64_field(right, "startLine"))) + .then_with(|| string_field(left, "kind").cmp(string_field(right, "kind"))) + .then_with(|| string_field(left, "name").cmp(string_field(right, "name"))) + }); + chunks.sort_by(|left, right| { + string_field(left, "file") + .cmp(string_field(right, "file")) + .then_with(|| u64_field(left, "startLine").cmp(&u64_field(right, "startLine"))) + .then_with(|| u64_field(left, "endLine").cmp(&u64_field(right, "endLine"))) + .then_with(|| string_field(left, "id").cmp(string_field(right, "id"))) + }); + mappings.sort_by(|left, right| { + string_field(left, "symbolId") + .cmp(string_field(right, "symbolId")) + .then_with(|| string_field(left, "chunkId").cmp(string_field(right, "chunkId"))) + }); + imports.sort_by(|left, right| { + string_field(left, "file") + .cmp(string_field(right, "file")) + .then_with(|| u64_field(left, "line").cmp(&u64_field(right, "line"))) + .then_with(|| string_field(left, "target").cmp(string_field(right, "target"))) + }); + unsupported_files.sort(); +} + +fn string_field<'a>(value: &'a Value, field: &str) -> &'a str { + value[field].as_str().unwrap_or("") +} + +fn u64_field(value: &Value, field: &str) -> u64 { + value[field].as_u64().unwrap_or_default() +} + +fn render_agent_views(ranking: &Value, files: &[Value]) -> Vec<(&'static str, String)> { + let ranked_lines = ranking["files"] + .as_array() + .expect("ranking files") + .iter() + .take(20) + .map(|file| { + let reasons = file["reasons"] + .as_array() + .expect("ranking reasons") + .iter() + .filter_map(Value::as_str) + .collect::>() + .join(","); + format!( + "- {} score={} reasons={reasons}", + file["path"].as_str().expect("ranking path"), + file["score"].as_u64().expect("ranking score") + ) + }) + .collect::>() + .join("\n"); + let entrypoints = file_lines(files, entrypoint, 20); + let tests = file_lines(files, test_file, 30); + vec![ + ( + "code-evidence/merged/agent/index.md", + "# Agent Code Map\n\n## Status\n- Code Evidence Layer: ok\n- Native minimal layer: enabled\n- Parser coverage: line-heuristic\n- Call graph precision: unknown\n- Ranking: [ranking.json](ranking.json)\n- Native retrieval slice: [native-retrieval](slices/native-retrieval.md)\n\n## Full Dumps\n- [files](../full/files.json)\n- [symbols](../full/symbols.json)\n- [chunks](../full/chunks.json)\n- [symbol chunks](../full/symbol-chunks.json)\n- [imports](../full/imports.json)\n".into(), + ), + ( + "code-evidence/merged/agent/slices/native-retrieval.md", + format!("# Native Retrieval Slice\n\n- Strategy: native-evidence-default\n- Source: Code Evidence files/symbols/imports only\n\n## Ranked Files\n{ranked_lines}\n"), + ), + ( + "code-evidence/merged/agent/slices/entrypoints.md", + format!("# Entrypoints\n\n{entrypoints}\n"), + ), + ( + "code-evidence/merged/agent/slices/tests.md", + format!("# Tests\n\n{tests}\n"), + ), + ( + "code-evidence/merged/agent/slices/risk-hotspots.md", + "# Risk Hotspots\n\n- Native minimal layer does not calculate complexity.\n- Treat file-sized chunks as fallback evidence.\n- Call graph and cross-file relationship precision are unknown.\n".into(), + ), + ] +} + +fn file_lines(files: &[Value], predicate: fn(&str) -> bool, limit: usize) -> String { + files + .iter() + .filter(|file| predicate(file["path"].as_str().expect("file path"))) + .take(limit) + .map(|file| { + format!( + "- {} ({})", + file["path"].as_str().expect("file path"), + file["language"].as_str().expect("file language") + ) + }) + .collect::>() + .join("\n") +} + +fn inventory_paths(bytes: &[u8]) -> Result, AdapterError> { + let text = std::str::from_utf8(bytes) + .map_err(|_| AdapterError::Contract("inventory.files must be UTF-8 paths".into()))?; + let mut paths = text + .split(['\0', '\n']) + .map(|path| path.trim_end_matches('\r').replace('\\', "/")) + .filter(|path| !path.is_empty()) + .collect::>(); + // File traversal order is not semantic and `rg --files` may vary with filesystem enumeration. + // Publish one portable canonical order instead of preserving an incidental legacy traversal. + paths.sort(); + paths.dedup(); + Ok(paths) +} + +fn safe_join(repo: &Path, relative: &str) -> Result { + let path = Path::new(relative); + if path.is_absolute() + || path.components().any(|component| { + matches!( + component, + std::path::Component::ParentDir | std::path::Component::Prefix(_) + ) + }) + { + return Err(AdapterError::Contract(format!( + "inventory path escapes repository: {relative}" + ))); + } + Ok(repo.join(path)) +} + +fn lines(content: &str) -> Vec<&str> { + if content.is_empty() { + Vec::new() + } else { + content + .split('\n') + .map(|line| line.trim_end_matches('\r')) + .collect() + } +} + +fn language(path: &str) -> &'static str { + match Path::new(path) + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("") + .to_ascii_lowercase() + .as_str() + { + "ps1" | "psm1" => "powershell", + "py" => "python", + "js" | "jsx" | "mjs" | "cjs" => "javascript", + "ts" | "tsx" => "typescript", + "rs" => "rust", + "go" => "go", + "java" => "java", + "cs" => "csharp", + _ => "text", + } +} + +fn extract_symbols(path: &str, language: &str, lines: &[&str]) -> Vec { + lines + .iter() + .enumerate() + .filter_map(|(index, line)| { + symbol_candidate(language, line).map(|(kind, name)| { + json!({ + "id":format!("{path}#{kind}:{name}"), + "kind":kind, + "name":name, + "file":path, + "startLine":index + 1, + "endLine":index + 1, + "language":language, + "confidence":0.55, + "source":"native-minimal" + }) + }) + }) + .collect() +} + +fn symbol_candidate<'a>(language: &str, line: &'a str) -> Option<(&'static str, &'a str)> { + let line = line.trim_start(); + match language { + "powershell" => word_after_ci(line, "function ").map(|name| ("function", name)), + "python" => word_after(line, "def ") + .map(|name| ("function", name)) + .or_else(|| word_after(line, "class ").map(|name| ("class", name))), + "javascript" | "typescript" => { + let line = line.strip_prefix("export ").unwrap_or(line); + let line = line.strip_prefix("async ").unwrap_or(line); + word_after(line, "function ") + .map(|name| ("function", name)) + .or_else(|| word_after(line, "class ").map(|name| ("class", name))) + .or_else(|| word_after(line, "interface ").map(|name| ("interface", name))) + .or_else(|| arrow_name(line).map(|name| ("function", name))) + } + "rust" => { + let line = line.strip_prefix("pub ").unwrap_or(line); + let line = line.strip_prefix("async ").unwrap_or(line); + word_after(line, "fn ").map(|name| ("function", name)) + } + "go" => { + let rest = line.strip_prefix("func ")?; + let rest = if rest.starts_with('(') { + rest.split_once(')')?.1.trim_start() + } else { + rest + }; + identifier(rest).map(|name| ("function", name)) + } + "java" => { + let mut rest = line; + for prefix in ["public ", "private ", "protected "] { + rest = rest.strip_prefix(prefix).unwrap_or(rest); + } + word_after(rest, "class ") + .map(|name| ("class", name)) + .or_else(|| word_after(rest, "interface ").map(|name| ("interface", name))) + .or_else(|| word_after(rest, "enum ").map(|name| ("enum", name))) + } + _ => None, + } +} + +fn word_after<'a>(line: &'a str, prefix: &str) -> Option<&'a str> { + identifier(line.strip_prefix(prefix)?) +} + +fn word_after_ci<'a>(line: &'a str, prefix: &str) -> Option<&'a str> { + line.get(..prefix.len()) + .filter(|actual| actual.eq_ignore_ascii_case(prefix))?; + identifier(&line[prefix.len()..]) +} + +fn identifier(text: &str) -> Option<&str> { + let end = text + .char_indices() + .take_while(|(_, character)| { + character.is_ascii_alphanumeric() || matches!(character, '_' | '$' | '-' | ':') + }) + .map(|(index, character)| index + character.len_utf8()) + .last()?; + Some(&text[..end]) +} + +fn arrow_name(line: &str) -> Option<&str> { + let line = ["const ", "let ", "var "] + .iter() + .find_map(|prefix| line.strip_prefix(prefix))?; + let (name, value) = line.split_once('=')?; + value.contains("=>").then(|| name.trim()).filter(|name| { + !name.is_empty() + && name.chars().all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '_' | '$') + }) + }) +} + +fn extract_imports(path: &str, language: &str, lines: &[&str]) -> Vec { + lines + .iter() + .enumerate() + .filter_map(|(index, line)| { + import_target(language, line).map(|target| { + json!({ + "file":path, + "line":index + 1, + "target":target, + "language":language, + "confidence":0.6, + "source":"native-minimal" + }) + }) + }) + .collect() +} + +fn import_target<'a>(language: &str, line: &'a str) -> Option<&'a str> { + let trimmed = line.trim_start(); + if matches!(language, "javascript" | "typescript") { + if let Some(target) = quoted_after(trimmed, "from") { + return Some(target); + } + if let Some(start) = trimmed.find("require(") { + return first_quoted(&trimmed[start + "require(".len()..]); + } + } + if language == "python" { + if let Some(rest) = trimmed.strip_prefix("from ") { + return rest.split_ascii_whitespace().next(); + } + if let Some(rest) = trimmed.strip_prefix("import ") { + return rest.split_ascii_whitespace().next(); + } + } + if language == "rust" { + return trimmed + .strip_prefix("use ") + .and_then(|rest| rest.strip_suffix(';')) + .map(str::trim); + } + if language == "go" { + return trimmed.strip_prefix("import ").and_then(first_quoted); + } + if let Some(rest) = trimmed.strip_prefix("#include") { + let rest = rest.trim_start(); + if let Some(value) = first_quoted(rest) { + return Some(value); + } + return rest.strip_prefix('<')?.split_once('>').map(|pair| pair.0); + } + None +} + +fn quoted_after<'a>(line: &'a str, token: &str) -> Option<&'a str> { + let start = line.find(token)? + token.len(); + first_quoted(&line[start..]) +} + +fn first_quoted(text: &str) -> Option<&str> { + let (start, quote) = text + .char_indices() + .find(|(_, character)| matches!(character, '\'' | '"'))?; + let rest = &text[start + quote.len_utf8()..]; + let end = rest.find(quote)?; + Some(&rest[..end]) +} + +fn ranking(files: &[Value], symbols: &[Value], imports: &[Value]) -> Value { + let mut symbols_by_file: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); + let mut imports_by_file: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); + for symbol in symbols { + symbols_by_file + .entry(symbol["file"].as_str().unwrap()) + .or_default() + .push(symbol["name"].as_str().unwrap()); + } + for import in imports { + imports_by_file + .entry(import["file"].as_str().unwrap()) + .or_default() + .push(import["target"].as_str().unwrap()); + } + let ranked = files + .iter() + .map(|file| { + let path = file["path"].as_str().unwrap(); + let mut score = 0u64; + let mut reasons = Vec::new(); + if entrypoint(path) { + reasons.push("entrypoint"); + score += 40; + } + if test_file(path) { + reasons.push("test"); + score += 35; + } + let file_symbols = symbols_by_file.get(path).cloned().unwrap_or_default(); + let file_imports = imports_by_file.get(path).cloned().unwrap_or_default(); + if !file_symbols.is_empty() { + reasons.push("symbols"); + score += (5 * file_symbols.len() as u64).min(20); + } + if !file_imports.is_empty() { + reasons.push("imports"); + score += (5 * file_imports.len() as u64).min(15); + } + if score == 0 { + reasons.push("inventory"); + score = 1; + } + json!({ + "path":path, + "language":file["language"], + "score":score, + "reasons":reasons, + "symbols":legacy_collapsed_strings(&file_symbols), + "imports":legacy_collapsed_strings(&file_imports) + }) + }) + .collect::>(); + json!({ + "schema":"agent-code-slice-ranking.v1", + "strategy":"native-evidence-default", + "files":ranked + }) +} + +fn legacy_collapsed_strings(values: &[&str]) -> Value { + match values { + [] => Value::Null, + [value] => Value::String((*value).to_string()), + values => Value::Array( + values + .iter() + .map(|value| Value::String((*value).to_string())) + .collect(), + ), + } +} + +fn entrypoint(path: &str) -> bool { + let file = path.rsplit('/').next().unwrap_or(path); + ["index.", "main.", "app.", "server.", "cli."] + .iter() + .any(|prefix| file.starts_with(prefix)) +} + +fn test_file(path: &str) -> bool { + let file = path.rsplit('/').next().unwrap_or(path); + file.contains("test.") + || file.contains("spec.") + || path.starts_with("test/") + || path.starts_with("tests/") + || path.starts_with("spec/") + || path.contains("/test/") + || path.contains("/tests/") + || path.contains("/spec/") +} + +fn publish(out: &Path, relative: &str, bytes: &[u8]) -> Result<(), AdapterError> { + let path = out.join(relative); + let parent = path.parent().ok_or_else(|| { + AdapterError::Internal(format!("artifact has no parent directory: {relative}")) + })?; + fs::create_dir_all(parent) + .map_err(|error| AdapterError::Io(format!("create artifact directory: {error}")))?; + if path.exists() { + return Err(AdapterError::Io(format!( + "refusing to overwrite native code evidence artifact: {relative}" + ))); + } + fs::write(&path, bytes).map_err(|error| AdapterError::Io(format!("write {relative}: {error}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn registry_toolchain_digest_is_the_declared_source_sha256() { + let manifest_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/integrations.json"); + let manifest: Value = serde_json::from_slice(&fs::read(manifest_path).unwrap()).unwrap(); + let integration = manifest["integrations"] + .as_array() + .unwrap() + .iter() + .find(|item| item["id"] == "evidence.native-code") + .unwrap(); + assert_eq!( + integration["toolchainDigestEvidence"], + json!({ + "algorithm":"sha256", + "inputs":["crates/code-intel-cli/src/native_code_evidence.rs"] + }) + ); + let declared = integration["capabilityDeclaration"]["implementation"]["toolchainDigests"] + [0] + .as_str() + .unwrap(); + assert_eq!( + declared, + sha256_hex(include_bytes!("native_code_evidence.rs")) + ); + } +} diff --git a/crates/code-intel-cli/src/orchestration.rs b/crates/code-intel-cli/src/orchestration.rs index dd00489..39e3a11 100644 --- a/crates/code-intel-cli/src/orchestration.rs +++ b/crates/code-intel-cli/src/orchestration.rs @@ -73,6 +73,11 @@ pub(crate) fn run(options: &Options<'_>) -> Result<()> { } } + let registry_audit = reconcile_production_registry(&root, &manifest, &integration_ids); + if registry_audit.enforce { + errors.extend(registry_audit.findings.iter().cloned()); + } + let mut selected = integrations .iter() .filter(|integration| integration_matches_capability(integration, options.capability)) @@ -103,6 +108,7 @@ pub(crate) fn run(options: &Options<'_>) -> Result<()> { "manifest": manifest_path, "policy": manifest.get("policy").cloned().unwrap_or(Value::Null), "errors": errors, + "registryAudit": registry_audit.output(), "stages": sorted_stages, "integrations": if action == "Validate" { Vec::::new() } else { plan.clone() }, "plan": if action == "Plan" { plan.clone() } else { Vec::::new() } @@ -186,10 +192,475 @@ fn normalize_action(value: &str) -> Result { "validate" => Ok("Validate".to_string()), "list" => Ok("List".to_string()), "plan" => Ok("Plan".to_string()), + "audit" => Ok("Audit".to_string()), other => Err(format!("unsupported orchestration action: {other}").into()), } } +struct RegistryAudit { + configured: bool, + enforce: bool, + findings: Vec, + participants: Vec, +} + +impl RegistryAudit { + fn output(&self) -> Value { + serde_json::json!({ + "configured": self.configured, + "mode": if self.enforce { "enforce" } else { "report" }, + "ok": self.findings.is_empty(), + "findings": self.findings, + "participants": self.participants, + }) + } +} + +#[derive(Clone, Copy)] +struct ProductionParticipant { + capability_id: &'static str, + source: &'static str, + marker: &'static str, +} + +const PRODUCTION_PARTICIPANTS: [ProductionParticipant; 12] = [ + ProductionParticipant { + capability_id: "doctor", + source: "invoke-code-intel.ps1", + marker: "$doctor = Join-Path $root \"check-code-intel-tools.ps1\"", + }, + ProductionParticipant { + capability_id: "diagnosis.hospital", + source: "run-code-intel.ps1", + marker: "$hospitalReport = New-CodeIntelHospitalReport", + }, + ProductionParticipant { + capability_id: "pack.repomix", + source: "run-code-intel.ps1", + marker: "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\"", + }, + ProductionParticipant { + capability_id: "evidence.native-code", + source: "run-code-intel.ps1", + marker: "$codeEvidence = New-CodeEvidenceLayer -RepoPath", + }, + ProductionParticipant { + capability_id: "evidence.cocoindex-code", + source: "run-code-intel.ps1", + marker: "$adapterConfig = Get-JsonProperty $adapters \"cocoindex-code\" $null", + }, + ProductionParticipant { + capability_id: "research.github-solution", + source: "run-code-intel.ps1", + marker: + "$githubResearchScript = Join-Path $PSScriptRoot \"Invoke-GitHubSolutionResearch.ps1\"", + }, + ProductionParticipant { + capability_id: "memory.repowise", + source: "run-code-intel.ps1", + marker: "$scopedRepowiseScript = Join-Path $PSScriptRoot \"Invoke-ScopedRepowise.ps1\"", + }, + ProductionParticipant { + capability_id: "graph.code-intel-understand", + source: "run-code-intel.ps1", + marker: "$knowledgeGraph = Join-Path $understandDir \"knowledge-graph.json\"", + }, + ProductionParticipant { + capability_id: "structure.sentrux", + source: "run-code-intel.ps1", + marker: "$sentruxAgentTool = Join-Path $PSScriptRoot \"Invoke-SentruxAgentTool.ps1\"", + }, + ProductionParticipant { + capability_id: "localization.codenexus-lite", + source: "run-code-intel.ps1", + marker: "$codeNexusLiteTool = Join-Path $PSScriptRoot \"Invoke-CodeNexusLite.ps1\"", + }, + ProductionParticipant { + capability_id: "run.commit", + source: "run-code-intel.ps1", + marker: "& $rustCli run commit", + }, + ProductionParticipant { + capability_id: "artifact.index-committed-only", + source: "invoke-code-intel.ps1", + marker: "$indexer = Join-Path $root \"update-code-intel-index.ps1\"", + }, +]; + +fn reconcile_production_registry( + root: &Path, + manifest: &Value, + integration_ids: &HashSet, +) -> RegistryAudit { + let Some(config) = manifest.get("productionRegistry") else { + return empty_registry_audit(); + }; + + let configured_mode = string_field(config, "mode").unwrap_or_default(); + let enforce = configured_mode != "report"; + let production_files = string_array_field(config, "productionFiles"); + let declarations = array_values(config, "participants"); + let mut findings = Vec::new(); + validate_registry_mode(&configured_mode, &mut findings); + let sources = load_production_sources(root, &production_files, &mut findings); + let declarations_by_id = index_participant_declarations(&declarations, &mut findings); + let participant_output = PRODUCTION_PARTICIPANTS + .into_iter() + .filter_map(|participant| { + audit_production_participant( + participant, + &production_files, + &sources, + &declarations_by_id, + integration_ids, + &mut findings, + ) + }) + .collect(); + audit_unknown_declarations(&declarations, &mut findings); + + RegistryAudit { + configured: true, + enforce, + findings, + participants: participant_output, + } +} + +fn empty_registry_audit() -> RegistryAudit { + RegistryAudit { + configured: false, + enforce: false, + findings: Vec::new(), + participants: Vec::new(), + } +} + +fn validate_registry_mode(configured_mode: &str, findings: &mut Vec) { + if !matches!(configured_mode, "report" | "enforce") { + findings.push(format!( + "production registry has unsupported mode: {configured_mode}" + )); + } +} + +fn load_production_sources( + root: &Path, + production_files: &[String], + findings: &mut Vec, +) -> HashMap { + let mut sources = HashMap::new(); + for relative in production_files { + match fs::read_to_string(root.join(relative)) { + Ok(text) => record_production_source(&mut sources, relative, text, findings), + Err(error) => findings.push(format!( + "production registry source missing or unreadable: {relative}: {error}" + )), + } + } + sources +} + +fn record_production_source( + sources: &mut HashMap, + relative: &str, + text: String, + findings: &mut Vec, +) { + if sources.insert(relative.to_string(), text).is_some() { + findings.push(format!( + "production registry lists source more than once: {relative}" + )); + } +} + +fn index_participant_declarations<'a>( + declarations: &'a [Value], + findings: &mut Vec, +) -> HashMap { + let mut declarations_by_id = HashMap::new(); + for declaration in declarations { + let id = string_field(declaration, "capabilityId").unwrap_or_default(); + if id.trim().is_empty() { + findings.push("production participant declaration has empty capabilityId".to_string()); + } else if declarations_by_id.insert(id.clone(), declaration).is_some() { + findings.push(format!( + "duplicate production participant declaration: {id}" + )); + } + } + declarations_by_id +} + +fn audit_production_participant( + participant: ProductionParticipant, + production_files: &[String], + sources: &HashMap, + declarations_by_id: &HashMap, + integration_ids: &HashSet, + findings: &mut Vec, +) -> Option { + audit_participant_source(participant, production_files, findings); + let hits = sources + .get(participant.source) + .map(|text| production_call_sites(participant.source, text, participant.marker)) + .unwrap_or_default(); + let Some(declaration) = declarations_by_id.get(participant.capability_id) else { + audit_missing_declaration(participant, &hits, findings); + return None; + }; + let status = string_field(declaration, "status").unwrap_or_default(); + audit_participant_lifecycle( + declaration, + participant, + &status, + &hits, + integration_ids, + findings, + ); + Some(participant_audit_output(participant, status, hits)) +} + +fn audit_participant_source( + participant: ProductionParticipant, + production_files: &[String], + findings: &mut Vec, +) { + if !production_files + .iter() + .any(|path| path == participant.source) + { + findings.push(format!( + "production participant {} source is not audited: {}", + participant.capability_id, participant.source + )); + } +} + +fn audit_missing_declaration( + participant: ProductionParticipant, + hits: &[String], + findings: &mut Vec, +) { + if hits.is_empty() { + findings.push(format!( + "required production participant has no declaration: {}", + participant.capability_id + )); + } else { + findings.push(format!( + "undeclared production invocation: {} marker '{}'", + participant.capability_id, participant.marker + )); + } +} + +fn audit_participant_lifecycle( + declaration: &Value, + participant: ProductionParticipant, + status: &str, + hits: &[String], + integration_ids: &HashSet, + findings: &mut Vec, +) { + match status { + "deleted" => { + audit_deleted_participant(declaration, participant, hits, integration_ids, findings) + } + "declared" => { + audit_declared_participant(declaration, participant, hits, integration_ids, findings) + } + _ => findings.push(format!( + "production participant {} has unsupported status: {status}", + participant.capability_id + )), + } +} + +fn audit_deleted_participant( + declaration: &Value, + participant: ProductionParticipant, + hits: &[String], + integration_ids: &HashSet, + findings: &mut Vec, +) { + let capability_id = participant.capability_id; + let deletion = declaration.get("reviewedDeletion").unwrap_or(&Value::Null); + for field in ["reviewer", "reviewedAt", "evidence"] { + if string_field(deletion, field).is_none_or(|value| value.trim().is_empty()) { + findings.push(format!( + "reviewed deletion for {capability_id} is missing {field}" + )); + } + } + if !hits.is_empty() { + findings.push(format!( + "deleted production participant is still invoked: {capability_id}" + )); + } + if integration_ids.contains(capability_id) { + findings.push(format!( + "deleted production participant remains in integrations registry: {capability_id}" + )); + } + require_call_site_declaration(declaration, participant, findings); +} + +fn audit_declared_participant( + declaration: &Value, + participant: ProductionParticipant, + hits: &[String], + integration_ids: &HashSet, + findings: &mut Vec, +) { + let capability_id = participant.capability_id; + if !integration_ids.contains(capability_id) { + findings.push(format!( + "production participant is not in integrations registry: {capability_id}" + )); + } + audit_declared_scalar_metadata(declaration, capability_id, findings); + audit_declared_array_metadata(declaration, capability_id, findings); + audit_declared_dependencies(declaration, capability_id, integration_ids, findings); + audit_declared_artifacts(declaration, capability_id, findings); + audit_declared_call_sites(participant, hits, findings); + require_call_site_declaration(declaration, participant, findings); +} + +fn audit_declared_scalar_metadata( + declaration: &Value, + capability_id: &str, + findings: &mut Vec, +) { + for field in ["envelope", "owner"] { + if string_field(declaration, field).is_none_or(|value| value.trim().is_empty()) { + findings.push(format!( + "production participant {capability_id} is missing {field} metadata" + )); + } + } +} + +fn audit_declared_array_metadata( + declaration: &Value, + capability_id: &str, + findings: &mut Vec, +) { + for field in ["dependencies", "effects", "artifacts"] { + if !is_string_array(declaration, field) { + findings.push(format!( + "production participant {capability_id} is missing {field} metadata" + )); + } + } +} + +fn audit_declared_dependencies( + declaration: &Value, + capability_id: &str, + integration_ids: &HashSet, + findings: &mut Vec, +) { + for dependency in string_array_field(declaration, "dependencies") { + if !integration_ids.contains(&dependency) { + findings.push(format!( + "production participant {capability_id} references unknown dependency: {dependency}" + )); + } + } +} + +fn audit_declared_artifacts(declaration: &Value, capability_id: &str, findings: &mut Vec) { + if string_array_field(declaration, "artifacts").is_empty() { + findings.push(format!( + "production participant {capability_id} has empty artifacts metadata" + )); + } +} + +fn audit_declared_call_sites( + participant: ProductionParticipant, + hits: &[String], + findings: &mut Vec, +) { + if hits.is_empty() { + findings.push(format!( + "orphan production declaration: {} marker '{}' not found", + participant.capability_id, participant.marker + )); + } else if hits.len() != 1 { + findings.push(format!( + "production participant {} must have exactly one call site, found {}", + participant.capability_id, + hits.len() + )); + } +} + +fn participant_audit_output( + participant: ProductionParticipant, + status: String, + hits: Vec, +) -> Value { + serde_json::json!({ + "capabilityId": participant.capability_id, + "status": status, + "source": participant.source, + "marker": participant.marker, + "callSites": hits, + }) +} + +fn audit_unknown_declarations(declarations: &[Value], findings: &mut Vec) { + for declaration in declarations { + let id = string_field(declaration, "capabilityId").unwrap_or_default(); + if !PRODUCTION_PARTICIPANTS + .iter() + .any(|participant| participant.capability_id == id) + { + findings.push(format!( + "orphan production declaration: unknown capability {id}" + )); + } + } +} + +fn production_call_sites(source: &str, text: &str, marker: &str) -> Vec { + text.lines() + .enumerate() + .filter(|(_, line)| line.contains(marker)) + .map(|(index, _)| format!("{source}:{}", index + 1)) + .collect() +} + +fn require_call_site_declaration( + declaration: &Value, + participant: ProductionParticipant, + findings: &mut Vec, +) { + let capability_id = participant.capability_id; + let Some(call_site) = declaration.get("callSite") else { + findings.push(format!( + "production participant {capability_id} is missing callSite metadata" + )); + return; + }; + let declared_source = string_field(call_site, "source").unwrap_or_default(); + let declared_anchor = string_field(call_site, "anchor").unwrap_or_default(); + if declared_source != participant.source || declared_anchor != participant.marker { + findings.push(format!( + "production participant {capability_id} callSite contract drift" + )); + } +} + +fn is_string_array(value: &Value, key: &str) -> bool { + value + .get(key) + .and_then(Value::as_array) + .is_some_and(|items| items.iter().all(Value::is_string)) +} + fn normalize_mode(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "lite" => Ok("lite".to_string()), @@ -458,4 +929,255 @@ mod tests { let err = run(&options).expect_err("missing entrypoint should fail"); assert!(err.to_string().contains("orchestration validation failed")); } + + #[test] + fn registry_audit_rejects_undeclared_repomix_invocation() { + let dir = orchestration_fixture_dir("registry-undeclared-repomix"); + touch( + &dir.join("run-code-intel.ps1"), + "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\"\n", + ); + let manifest = json!({ + "productionRegistry": { + "mode": "enforce", + "productionFiles": ["run-code-intel.ps1"], + "participants": [] + } + }); + + let audit = reconcile_production_registry(&dir, &manifest, &HashSet::new()); + + assert!(audit + .findings + .iter() + .any(|finding| finding.contains("undeclared production invocation: pack.repomix"))); + } + + #[test] + fn registry_audit_rejects_registered_participant_without_dependency_or_effect_metadata() { + let dir = orchestration_fixture_dir("registry-incomplete-repomix"); + touch( + &dir.join("run-code-intel.ps1"), + "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\"\n", + ); + let manifest = json!({ + "productionRegistry": { + "mode": "enforce", + "productionFiles": ["run-code-intel.ps1"], + "participants": [{ + "capabilityId": "pack.repomix", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\"" + }, + "envelope": "code-intel-capability-envelope.v1", + "owner": "code-intel-pipeline", + "artifacts": ["repomix-output.*"] + }] + } + }); + let ids = HashSet::from(["pack.repomix".to_string()]); + + let audit = reconcile_production_registry(&dir, &manifest, &ids); + + assert!(audit + .findings + .iter() + .any(|finding| finding.contains("pack.repomix is missing dependencies metadata"))); + assert!(audit + .findings + .iter() + .any(|finding| finding.contains("pack.repomix is missing effects metadata"))); + } + + #[test] + fn registry_audit_rejects_duplicate_call_site_and_contract_anchor_drift() { + let dir = orchestration_fixture_dir("registry-call-site-drift"); + let anchor = "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\""; + touch( + &dir.join("run-code-intel.ps1"), + &format!("{anchor}\n{anchor}\n"), + ); + let manifest = json!({ + "productionRegistry": { + "mode": "enforce", + "productionFiles": ["run-code-intel.ps1"], + "participants": [{ + "capabilityId": "pack.repomix", + "status": "declared", + "callSite": {"source": "run-code-intel.ps1", "anchor": "wrong"}, + "envelope": "code-intel-capability-envelope.v1", + "owner": "code-intel-pipeline", + "dependencies": [], + "effects": [], + "artifacts": ["repomix-summary.json"] + }] + } + }); + let ids = HashSet::from(["pack.repomix".to_string()]); + + let audit = reconcile_production_registry(&dir, &manifest, &ids); + + assert!(audit.findings.iter().any(|finding| { + finding.contains("pack.repomix must have exactly one call site, found 2") + })); + assert!(audit + .findings + .iter() + .any(|finding| finding.contains("pack.repomix callSite contract drift"))); + } + + #[test] + fn registry_audit_rejects_every_required_participant_metadata_field() { + let dir = orchestration_fixture_dir("registry-required-metadata"); + let anchor = "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\""; + touch(&dir.join("run-code-intel.ps1"), &format!("{anchor}\n")); + let ids = HashSet::from(["pack.repomix".to_string()]); + + for field in [ + "callSite", + "envelope", + "owner", + "dependencies", + "effects", + "artifacts", + ] { + let mut declaration = json!({ + "capabilityId": "pack.repomix", + "status": "declared", + "callSite": {"source": "run-code-intel.ps1", "anchor": anchor}, + "envelope": "code-intel-capability-envelope.v1", + "owner": "code-intel-pipeline", + "dependencies": [], + "effects": [], + "artifacts": ["repomix-summary.json"] + }); + declaration.as_object_mut().unwrap().remove(field); + let manifest = json!({ + "productionRegistry": { + "mode": "enforce", + "productionFiles": ["run-code-intel.ps1"], + "participants": [declaration] + } + }); + + let audit = reconcile_production_registry(&dir, &manifest, &ids); + + assert!( + audit + .findings + .iter() + .any(|finding| { finding.contains("pack.repomix") && finding.contains(field) }), + "missing {field} must fail closed: {:?}", + audit.findings + ); + } + } + + #[test] + fn registry_audit_report_and_enforce_modes_are_explicit() { + let dir = orchestration_fixture_dir("registry-modes"); + touch(&dir.join("run-code-intel.ps1"), "# no production calls\n"); + let mut manifest = json!({ + "productionRegistry": { + "mode": "report", + "productionFiles": ["run-code-intel.ps1"], + "participants": [] + } + }); + + let report = reconcile_production_registry(&dir, &manifest, &HashSet::new()); + assert!(!report.enforce); + assert!(!report.findings.is_empty()); + + manifest["productionRegistry"]["mode"] = Value::String("enforce".to_string()); + let enforce = reconcile_production_registry(&dir, &manifest, &HashSet::new()); + assert!(enforce.enforce); + assert_eq!(report.findings, enforce.findings); + } + + #[test] + fn registry_audit_rejects_unknown_orphan_declaration() { + let dir = orchestration_fixture_dir("registry-orphan"); + touch(&dir.join("run-code-intel.ps1"), "# no production calls\n"); + let manifest = json!({ + "productionRegistry": { + "mode": "enforce", + "productionFiles": ["run-code-intel.ps1"], + "participants": [{"capabilityId": "unknown.future-tool", "status": "declared"}] + } + }); + + let audit = reconcile_production_registry(&dir, &manifest, &HashSet::new()); + + assert!(audit.findings.iter().any(|finding| { + finding + .contains("orphan production declaration: unknown capability unknown.future-tool") + })); + } + + #[test] + fn registry_audit_accepts_reviewed_deletion_only_after_call_site_is_removed() { + let dir = orchestration_fixture_dir("registry-reviewed-deletion"); + touch(&dir.join("run-code-intel.ps1"), "# call site removed\n"); + let anchor = "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\""; + let manifest = json!({ + "productionRegistry": { + "mode": "enforce", + "productionFiles": ["run-code-intel.ps1"], + "participants": [{ + "capabilityId": "pack.repomix", + "status": "deleted", + "callSite": {"source": "run-code-intel.ps1", "anchor": anchor}, + "reviewedDeletion": { + "reviewer": "verifier", + "reviewedAt": "2026-07-13T00:00:00Z", + "evidence": "fixture:call-site-removed" + } + }] + } + }); + + let audit = reconcile_production_registry(&dir, &manifest, &HashSet::new()); + + assert!( + audit + .findings + .iter() + .all(|finding| !finding.contains("pack.repomix")), + "reviewed deletion should close the participant exactly: {:?}", + audit.findings + ); + } + + #[test] + fn checked_in_production_registry_reconciles_eleven_calls_and_one_reviewed_deletion() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest = read_json(&root.join("orchestration/integrations.json")).unwrap(); + let ids = array_values(&manifest, "integrations") + .iter() + .filter_map(|integration| string_field(integration, "id")) + .collect::>(); + + let audit = reconcile_production_registry(&root, &manifest, &ids); + + assert!(audit.configured); + assert!(audit.enforce); + assert!(audit.findings.is_empty(), "{:?}", audit.findings); + assert_eq!(audit.participants.len(), 12); + for participant in audit.participants { + let expected = if participant["status"] == "deleted" { + 0 + } else { + 1 + }; + assert_eq!( + participant["callSites"].as_array().unwrap().len(), + expected, + "{} must reconcile to its declared lifecycle", + participant["capabilityId"] + ); + } + } } diff --git a/crates/code-intel-cli/src/ponytail_gate.rs b/crates/code-intel-cli/src/ponytail_gate.rs new file mode 100644 index 0000000..12d2ae4 --- /dev/null +++ b/crates/code-intel-cli/src/ponytail_gate.rs @@ -0,0 +1,589 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +use crate::authority; +#[cfg(not(test))] +use crate::capability::reject_duplicate_json_keys; + +#[cfg(test)] +fn reject_duplicate_json_keys(_: &str) -> Result<(), String> { + Ok(()) +} + +const MAX_REQUEST_BYTES: u64 = 1024 * 1024; + +const CHANGE_KINDS: [&str; 7] = [ + "artifact", + "dependency", + "abstraction", + "file", + "test", + "documentation", + "process", +]; +const OPERATIONS: [&str; 3] = ["add", "delete", "reuse"]; +const CURRENT_VALUE_SOURCES: [&str; 6] = [ + "operator_requested_outcome", + "committed_engineering_plan_deliverable", + "verified_defect_or_risk", + "required_contract_or_gate", + "evidence_closing_spike", + "approved_debt_reduction", +]; +const RUNGS: [&str; 7] = [ + "do_nothing", + "repository_reuse", + "standard_library", + "platform_native", + "installed_dependency", + "one_liner", + "smallest_local_implementation", +]; +const NON_FILTERABLE: [&str; 7] = [ + "verification", + "evidence", + "safety", + "error_handling", + "accessibility", + "data_loss_prevention", + "artifact_contract", +]; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let request_path = match parse_cli(raw) { + Ok(path) => path, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let request = match read_request(&request_path) { + Ok(request) => request, + Err((exit, message)) => { + eprintln!("{message}"); + return exit; + } + }; + match evaluate(&request) { + Ok(result) => { + println!( + "{}", + serde_json::to_string(&result).expect("Ponytail result serializes") + ); + if result["enforcedBlock"] == true { + 2 + } else { + 0 + } + } + Err(message) => { + eprintln!("{message}"); + 65 + } + } +} + +fn parse_cli(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("ponytail-gate") { + return Err("usage: governance ponytail-gate --request ".to_string()); + } + if raw.len() != 3 || raw[1] != "--request" || raw[2].is_empty() { + return Err("usage: governance ponytail-gate --request ".to_string()); + } + Ok(PathBuf::from(&raw[2])) +} + +fn read_request(path: &Path) -> Result { + let bytes = if path == Path::new("-") { + let mut bytes = Vec::new(); + io::stdin() + .take(MAX_REQUEST_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| (74, format!("read Ponytail request stdin: {error}")))?; + bytes + } else { + let metadata = fs::metadata(path) + .map_err(|error| (74, format!("inspect Ponytail request: {error}")))?; + if !metadata.is_file() { + return Err((65, "Ponytail request must be a regular file".to_string())); + } + if metadata.len() > MAX_REQUEST_BYTES { + return Err((65, "Ponytail request exceeds size limit".to_string())); + } + fs::read(path).map_err(|error| (74, format!("read Ponytail request: {error}")))? + }; + if bytes.len() as u64 > MAX_REQUEST_BYTES { + return Err((65, "Ponytail request exceeds size limit".to_string())); + } + let text = std::str::from_utf8(&bytes) + .map_err(|error| (65, format!("Ponytail request is not UTF-8: {error}")))?; + reject_duplicate_json_keys(text).map_err(|error| (65, error))?; + serde_json::from_str(text) + .map_err(|error| (65, format!("invalid Ponytail request JSON: {error}"))) +} + +pub(crate) fn policy_document() -> Value { + json!({ + "schema":"code-intel-ponytail-gate-policy.v1", + "changeKinds":CHANGE_KINDS, + "operations":OPERATIONS, + "allowedCurrentValueSources":CURRENT_VALUE_SOURCES, + "forbiddenValueSources":["future_maybe"], + "firstSufficientSolutionRungs":RUNGS, + "nonFilterableRequirements":NON_FILTERABLE, + "bypassAuthoritySchema":"code-intel-authority-event.v1", + "rules":{ + "currentValue":"every change must name one allowed current value source backed by known evidence", + "firstSufficient":"every rung below the selected rung must be rejected once with known evidence", + "protectedRequirements":"verification, evidence, safety, and the documented engineering boundaries cannot be filtered out", + "bypass":"only a scoped, approved, unexpired, unreplayed A05 authority event covering value-source, lower-rung, and all required trace evidence may bypass a value or rung rejection", + "modes":"report_only retains rejection traces without blocking; enforce blocks when any rejection remains" + } + }) +} + +pub(crate) fn evaluate(request: &Value) -> Result { + validate_request(request)?; + let mode = request["mode"].as_str().unwrap(); + let evaluated_at = request["evaluatedAt"].as_u64().unwrap(); + let known = string_set(&request["knownEvidenceIds"], "knownEvidenceIds")?; + let consumed = string_set( + &request["consumedAuthorityEventIds"], + "consumedAuthorityEventIds", + )?; + let changes = request["changes"].as_array().unwrap(); + let duplicate_changes = duplicates(changes.iter().filter_map(|change| change["id"].as_str())); + let duplicate_events = duplicates(changes.iter().filter_map(|change| { + change + .pointer("/bypass/authorityEvent/id") + .and_then(Value::as_str) + })); + + let mut newly_consumed = BTreeSet::new(); + let results = changes + .iter() + .enumerate() + .map(|(index, change)| { + evaluate_change( + change, + index, + evaluated_at, + &known, + &consumed, + &duplicate_changes, + &duplicate_events, + &mut newly_consumed, + ) + }) + .collect::>(); + let would_reject = results + .iter() + .filter(|result| result["status"] == "rejected") + .count(); + let mut all_consumed = consumed; + all_consumed.extend(newly_consumed); + Ok(json!({ + "schema":"code-intel-ponytail-gate-result.v1", + "status":"completed", + "mode":mode, + "wouldReject":would_reject, + "enforcedBlock":mode == "enforce" && would_reject > 0, + "traceRetained":true, + "consumedAuthorityEventIds":all_consumed, + "changes":results + })) +} + +#[allow(clippy::too_many_arguments)] +fn evaluate_change( + change: &Value, + index: usize, + evaluated_at: u64, + known: &BTreeSet, + consumed: &BTreeSet, + duplicate_changes: &BTreeSet, + duplicate_events: &BTreeSet, + newly_consumed: &mut BTreeSet, +) -> Value { + let id = change["id"] + .as_str() + .map(str::to_string) + .unwrap_or_else(|| format!("invalid-change-{index}")); + let hard_rejection = validate_change_shape(change).and_then(|_| { + let removed = removed_protections(change)?; + if !removed.is_empty() { + Err(format!( + "non-filterable requirements would be removed: {}", + removed.into_iter().collect::>().join(", ") + )) + } else { + Ok(()) + } + }); + let hard_rejection = if duplicate_changes.contains(&id) { + Err("duplicate change id".to_string()) + } else { + hard_rejection + }; + let policy_rejection = hard_rejection + .as_ref() + .map(|_| validate_policy(change, known)) + .unwrap_or(Ok(())); + let bypass = change.get("bypass"); + + let (status, authority_event_id, diagnostics) = match (hard_rejection, policy_rejection) { + (Err(message), _) => ("rejected", None, vec![message]), + (Ok(()), Ok(())) if bypass.is_none() => ("accepted", None, Vec::new()), + (Ok(()), Ok(())) => ( + "rejected", + None, + vec!["bypass is not allowed without a policy rejection".to_string()], + ), + (Ok(()), Err(message)) => match validate_bypass( + bypass, + &id, + change, + evaluated_at, + known, + consumed, + duplicate_events, + newly_consumed, + ) { + Ok(event_id) => ("bypassed", Some(event_id), vec![message]), + Err(bypass_error) => ( + "rejected", + None, + vec![message, format!("bypass rejected: {bypass_error}")], + ), + }, + }; + json!({ + "change":change, + "status":status, + "authorityEventId":authority_event_id, + "diagnostics":diagnostics + }) +} + +fn validate_request(request: &Value) -> Result<(), String> { + exact( + request, + &[ + "schema", + "mode", + "evaluatedAt", + "knownEvidenceIds", + "consumedAuthorityEventIds", + "changes", + ], + "Ponytail gate request", + )?; + if request["schema"] != "code-intel-ponytail-gate-request.v1" { + return Err("Ponytail gate request schema is invalid".to_string()); + } + if !matches!(request["mode"].as_str(), Some("report_only" | "enforce")) { + return Err("Ponytail gate mode is invalid".to_string()); + } + request["evaluatedAt"] + .as_u64() + .ok_or("evaluatedAt must be a non-negative integer")?; + string_set(&request["knownEvidenceIds"], "knownEvidenceIds")?; + string_set( + &request["consumedAuthorityEventIds"], + "consumedAuthorityEventIds", + )?; + let changes = request["changes"] + .as_array() + .ok_or("changes must be an array")?; + if changes.is_empty() { + return Err("changes must not be empty".to_string()); + } + for (index, change) in changes.iter().enumerate() { + validate_change_shape(change) + .map_err(|error| format!("changes[{index}] is schema-invalid: {error}"))?; + } + Ok(()) +} + +fn validate_change_shape(change: &Value) -> Result<(), String> { + exact_optional( + change, + &[ + "id", + "kind", + "operation", + "valueSource", + "requiredEvidenceIds", + "firstSufficientRung", + "lowerRungs", + "removedProtections", + ], + &["bypass"], + "change", + )?; + nonempty(&change["id"], "change id")?; + let kind = change["kind"].as_str().ok_or("change kind is invalid")?; + if !CHANGE_KINDS.contains(&kind) { + return Err("change kind is unknown".to_string()); + } + let operation = change["operation"] + .as_str() + .ok_or("change operation is invalid")?; + if !OPERATIONS.contains(&operation) { + return Err("change operation is unknown".to_string()); + } + exact( + &change["valueSource"], + &["kind", "id", "evidenceIds"], + "value source", + )?; + let source_kind = change["valueSource"]["kind"] + .as_str() + .ok_or("value source kind is invalid")?; + if source_kind != "future_maybe" && !CURRENT_VALUE_SOURCES.contains(&source_kind) { + return Err("value source kind is not in the request schema".to_string()); + } + nonempty(&change["valueSource"]["id"], "value source id")?; + if string_set( + &change["valueSource"]["evidenceIds"], + "value source evidenceIds", + )? + .is_empty() + { + return Err("value source evidenceIds must not be empty".to_string()); + } + if string_set(&change["requiredEvidenceIds"], "requiredEvidenceIds")?.is_empty() { + return Err("requiredEvidenceIds must not be empty".to_string()); + } + let selected_rung = change["firstSufficientRung"] + .as_str() + .ok_or("first sufficient rung is invalid")?; + if !RUNGS.contains(&selected_rung) { + return Err("first sufficient rung is not in the request schema".to_string()); + } + let lower_rungs = change["lowerRungs"] + .as_array() + .ok_or("lowerRungs must be an array")?; + let mut unique_lower_rungs = BTreeSet::new(); + for (index, lower_rung) in lower_rungs.iter().enumerate() { + let identity = serde_json::to_string(lower_rung) + .map_err(|error| format!("serialize lowerRungs[{index}]: {error}"))?; + if !unique_lower_rungs.insert(identity) { + return Err("duplicate lowerRungs entries are schema-invalid".to_string()); + } + exact( + lower_rung, + &["rung", "reason", "evidenceIds"], + &format!("lowerRungs[{index}]"), + )?; + let rung = lower_rung["rung"].as_str().ok_or("lower rung is invalid")?; + if !RUNGS.contains(&rung) { + return Err("lower rung is not in the request schema".to_string()); + } + nonempty(&lower_rung["reason"], "lower rung reason")?; + if string_set(&lower_rung["evidenceIds"], "lower rung evidenceIds")?.is_empty() { + return Err("lower rung evidenceIds must not be empty".to_string()); + } + } + removed_protections(change) + .map_err(|error| format!("removedProtections is schema-invalid: {error}"))?; + if let Some(bypass) = change.get("bypass") { + validate_bypass_shape(bypass)?; + } + Ok(()) +} + +fn validate_bypass_shape(bypass: &Value) -> Result<(), String> { + exact(bypass, &["changeId", "authorityEvent"], "bypass")?; + nonempty(&bypass["changeId"], "bypass changeId")?; + let event = &bypass["authorityEvent"]; + exact( + event, + &[ + "schema", + "id", + "decision", + "approver", + "evidenceIds", + "issuedAt", + "expiresAt", + ], + "authority event", + )?; + if event["schema"] != "code-intel-authority-event.v1" || event["decision"] != "approved" { + return Err("authority event constants are invalid".to_string()); + } + nonempty(&event["id"], "authority event id")?; + exact(&event["approver"], &["id", "role"], "authority approver")?; + nonempty(&event["approver"]["id"], "authority approver id")?; + nonempty(&event["approver"]["role"], "authority approver role")?; + if string_set(&event["evidenceIds"], "authority event evidenceIds")?.is_empty() { + return Err("authority event evidenceIds must not be empty".to_string()); + } + event["issuedAt"] + .as_u64() + .ok_or("authority event issuedAt is invalid")?; + event["expiresAt"] + .as_u64() + .ok_or("authority event expiresAt is invalid")?; + Ok(()) +} + +fn validate_policy(change: &Value, known: &BTreeSet) -> Result<(), String> { + let source = &change["valueSource"]; + let evidence = string_set(&source["evidenceIds"], "value source evidenceIds")?; + if evidence.is_empty() || !evidence.is_subset(known) { + return Err("value source evidence is missing or unknown".to_string()); + } + let required = string_set(&change["requiredEvidenceIds"], "requiredEvidenceIds")?; + if required.is_empty() || !required.is_subset(known) { + return Err("required trace evidence is missing or unknown".to_string()); + } + let source_kind = source["kind"] + .as_str() + .ok_or("value source kind is invalid")?; + if !CURRENT_VALUE_SOURCES.contains(&source_kind) { + return Err(format!("value source is not current: {source_kind}")); + } + let selected = change["firstSufficientRung"] + .as_str() + .and_then(|rung| RUNGS.iter().position(|candidate| *candidate == rung)) + .ok_or("first sufficient rung is invalid")?; + let lower = change["lowerRungs"].as_array().unwrap(); + if lower.len() != selected { + return Err("lower rung trace is incomplete or duplicated".to_string()); + } + for (index, entry) in lower.iter().enumerate() { + exact(entry, &["rung", "reason", "evidenceIds"], "lower rung")?; + if entry["rung"] != RUNGS[index] { + return Err("lower rungs must enumerate every earlier rung in order".to_string()); + } + nonempty(&entry["reason"], "lower rung reason")?; + let rung_evidence = string_set(&entry["evidenceIds"], "lower rung evidenceIds")?; + if rung_evidence.is_empty() || !rung_evidence.is_subset(known) { + return Err("lower rung evidence is missing or unknown".to_string()); + } + } + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn validate_bypass( + bypass: Option<&Value>, + change_id: &str, + change: &Value, + evaluated_at: u64, + known: &BTreeSet, + consumed: &BTreeSet, + duplicate_events: &BTreeSet, + newly_consumed: &mut BTreeSet, +) -> Result { + let bypass = bypass.ok_or("policy rejection has no authority bypass")?; + exact(bypass, &["changeId", "authorityEvent"], "bypass")?; + if bypass["changeId"] != change_id { + return Err("authority bypass is scoped to another change".to_string()); + } + let event = &bypass["authorityEvent"]; + let event_id = event["id"] + .as_str() + .ok_or("authority event id is invalid")?; + if duplicate_events.contains(event_id) || newly_consumed.contains(event_id) { + return Err("authority event duplicate use is rejected".to_string()); + } + let mut required = string_set( + &change["valueSource"]["evidenceIds"], + "value source evidenceIds", + )?; + required.extend(string_set( + &change["requiredEvidenceIds"], + "requiredEvidenceIds", + )?); + for lower_rung in change["lowerRungs"].as_array().unwrap() { + required.extend(string_set( + &lower_rung["evidenceIds"], + "lower rung evidenceIds", + )?); + } + let event_id = + authority::validate_authority_event(event, evaluated_at, known, &required, consumed)?; + newly_consumed.insert(event_id.clone()); + Ok(event_id) +} + +fn removed_protections(change: &Value) -> Result, String> { + let removed = string_set(&change["removedProtections"], "removedProtections")?; + if removed + .iter() + .any(|protection| !NON_FILTERABLE.contains(&protection.as_str())) + { + return Err("removedProtections contains an unknown requirement".to_string()); + } + Ok(removed) +} + +fn exact(value: &Value, expected: &[&str], label: &str) -> Result<(), String> { + exact_optional(value, expected, &[], label) +} + +fn exact_optional( + value: &Value, + required: &[&str], + optional: &[&str], + label: &str, +) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let required = required.iter().copied().collect::>(); + let allowed = required + .iter() + .copied() + .chain(optional.iter().copied()) + .collect::>(); + if required.is_subset(&actual) && actual.is_subset(&allowed) { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn string_set(value: &Value, label: &str) -> Result, String> { + let values = value + .as_array() + .ok_or_else(|| format!("{label} must be an array"))?; + let mut result = BTreeSet::new(); + for value in values { + let item = value + .as_str() + .filter(|item| !item.is_empty()) + .ok_or_else(|| format!("{label} contains an invalid id"))?; + if !result.insert(item.to_string()) { + return Err(format!("{label} contains duplicate ids")); + } + } + Ok(result) +} + +fn duplicates<'a>(values: impl Iterator) -> BTreeSet { + let mut counts = BTreeMap::new(); + for value in values { + *counts.entry(value.to_string()).or_insert(0usize) += 1; + } + counts + .into_iter() + .filter_map(|(value, count)| (count > 1).then_some(value)) + .collect() +} + +fn nonempty(value: &Value, label: &str) -> Result<(), String> { + if value.as_str().is_some_and(|value| !value.is_empty()) { + Ok(()) + } else { + Err(format!("{label} is invalid")) + } +} diff --git a/crates/code-intel-cli/src/project_orientation.rs b/crates/code-intel-cli/src/project_orientation.rs new file mode 100644 index 0000000..9b1cbcc --- /dev/null +++ b/crates/code-intel-cli/src/project_orientation.rs @@ -0,0 +1,405 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +use super::{AdapterArtifact, AdapterError, AdapterOutput}; +use crate::artifact_ref::VerifiedArtifact; + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + if request["options"] + .as_object() + .map_or(true, |options| !options.is_empty()) + { + return Err(AdapterError::InvalidOptions( + "project.orientation accepts no options".into(), + )); + } + let inputs = Inputs::parse(verified_inputs)?; + let orientation = compose(request, &inputs)?; + let json_bytes = serde_json::to_vec(&orientation).map_err(|error| { + AdapterError::Internal(format!("serialize project orientation: {error}")) + })?; + let markdown_bytes = render_summary(&orientation).into_bytes(); + publish(out, "project-orientation.json", &json_bytes)?; + publish(out, "project-orientation.md", &markdown_bytes)?; + Ok(AdapterOutput { + artifacts: vec![ + AdapterArtifact { + artifact_schema: "code-intel-project-orientation.v1".into(), + artifact_type: "project.orientation".into(), + relative_path: "project-orientation.json".into(), + bytes: json_bytes, + }, + AdapterArtifact { + artifact_schema: "code-intel-project-orientation-summary.v1".into(), + artifact_type: "materialized_view.project_orientation_summary".into(), + relative_path: "project-orientation.md".into(), + bytes: markdown_bytes, + }, + ], + observed_effects: vec!["local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +struct Inputs<'a> { + snapshot: (&'a VerifiedArtifact, Value), + inventory: (&'a VerifiedArtifact, Vec), + survival: (&'a VerifiedArtifact, Value), + native_files: (&'a VerifiedArtifact, Value), + coverage: (&'a VerifiedArtifact, Value), + ranking: (&'a VerifiedArtifact, Value), +} + +impl<'a> Inputs<'a> { + fn parse(inputs: &'a [VerifiedArtifact]) -> Result { + if inputs.len() != 6 { + return Err(contract( + "project.orientation requires exactly six A03-verified inputs", + )); + } + let find = |kind: &str| { + let matches = inputs + .iter() + .filter(|input| input.artifact_type() == kind) + .collect::>(); + match matches.as_slice() { + [input] => Ok(*input), + [] => Err(contract(format!( + "project.orientation input is missing: {kind}" + ))), + _ => Err(contract(format!( + "project.orientation input is duplicated: {kind}" + ))), + } + }; + let snapshot = find("repository.snapshot")?; + let inventory = find("inventory.files")?; + let survival = find("repository.survival-scan")?; + let native_files = find("code_evidence.files")?; + let coverage = find("code_evidence.coverage")?; + let ranking = find("code_evidence.agent_slice")?; + require_schema(snapshot, "code-intel-repository-snapshot.v1")?; + require_schema(inventory, "code-intel-file-inventory.v1")?; + require_schema(survival, "code-intel-repository-survival-scan-result.v1")?; + require_schema(native_files, "code-evidence-files.v1")?; + require_schema(coverage, "code-evidence-coverage.v1")?; + require_schema(ranking, "agent-code-slice-ranking.v1")?; + Ok(Self { + snapshot: (snapshot, parse_json(snapshot, "repository snapshot")?), + inventory: (inventory, inventory_paths(inventory.bytes())?), + survival: (survival, parse_json(survival, "survival scan")?), + native_files: (native_files, parse_json(native_files, "native files")?), + coverage: (coverage, parse_json(coverage, "native coverage")?), + ranking: (ranking, parse_json(ranking, "native ranking")?), + }) + } +} + +fn compose(request: &Value, inputs: &Inputs<'_>) -> Result { + let snapshot = &inputs.snapshot.1; + let survival = &inputs.survival.1; + let native_files = &inputs.native_files.1; + let coverage = &inputs.coverage.1; + let ranking = &inputs.ranking.1; + let snapshot_identity = request["snapshot"]["identity"] + .as_str() + .ok_or_else(|| contract("project.orientation request snapshot identity is invalid"))?; + if snapshot["schema"] != "code-intel-repository-snapshot.v1" + || snapshot["snapshot"] != request["snapshot"] + || survival["schema"] != "code-intel-repository-survival-scan-result.v1" + || survival["snapshotIdentity"] != snapshot_identity + || survival["repository"]["sourceSha256"] != inputs.snapshot.0.sha256() + || survival["inventory"]["sourceSha256"] != inputs.inventory.0.sha256() + { + return Err(contract( + "project.orientation snapshot or survival evidence is incoherent", + )); + } + let native = native_files["files"] + .as_array() + .ok_or_else(|| contract("native files evidence is invalid"))?; + let native_paths = native + .iter() + .map(|file| { + file["path"] + .as_str() + .map(str::to_string) + .ok_or_else(|| contract("native file path is invalid")) + }) + .collect::, _>>()?; + let inventory_paths = inputs.inventory.1.iter().cloned().collect::>(); + if native_paths != inventory_paths + || survival["inventory"]["fileCount"].as_u64() != Some(inventory_paths.len() as u64) + { + return Err(contract( + "inventory, survival, and native file evidence disagree", + )); + } + if coverage["schema"] != "code-evidence-coverage.v1" + || ranking["schema"] != "agent-code-slice-ranking.v1" + { + return Err(contract("native evidence schema is invalid")); + } + + let snapshot_provenance = provenance(inputs.snapshot.0, "/snapshot"); + let dirty_provenance = provenance(inputs.snapshot.0, "/dirtyOverlay"); + let inventory_provenance = provenance(inputs.inventory.0, "/"); + let survival_provenance = provenance(inputs.survival.0, "/completeness"); + let coverage_provenance = provenance(inputs.coverage.0, "/"); + let native_files_provenance = provenance(inputs.native_files.0, "/files"); + let ranking_provenance = provenance(inputs.ranking.0, "/files"); + + let mut language_counts = BTreeMap::::new(); + for file in native { + let language = file["language"] + .as_str() + .ok_or_else(|| contract("native file language is invalid"))?; + if language != "text" { + *language_counts.entry(language.to_string()).or_default() += 1; + } + } + let mut languages = language_counts + .into_iter() + .map(|(name, file_count)| json!({"name":name,"fileCount":file_count,"provenance":[native_files_provenance.clone()]})) + .collect::>(); + languages.sort_by(|left, right| { + right["fileCount"] + .as_u64() + .cmp(&left["fileCount"].as_u64()) + .then_with(|| left["name"].as_str().cmp(&right["name"].as_str())) + }); + + let boundaries = inventory_paths + .iter() + .filter_map(|path| path.split_once('/').map(|(root, _)| root.to_string())) + .collect::>() + .into_iter() + .map(|path| json!({"path":path,"kind":"top_level_directory","provenance":[inventory_provenance.clone()]})) + .collect::>(); + let ranked = ranking["files"] + .as_array() + .ok_or_else(|| contract("native ranking files are invalid"))?; + let entry_points = ranked + .iter() + .filter(|item| { + item["reasons"] + .as_array() + .is_some_and(|reasons| reasons.iter().any(|reason| reason == "entrypoint")) + }) + .map(|item| { + let path = item["path"] + .as_str() + .filter(|path| native_paths.contains(*path)) + .ok_or_else(|| contract("native ranking references an unknown entry point"))?; + Ok(json!({"path":path,"classification":"heuristic","provenance":[ranking_provenance.clone()]})) + }) + .collect::, AdapterError>>()?; + let commands = inventory_paths + .iter() + .filter(|path| !path.contains('/') && command_script(path)) + .map(|path| json!({"path":path,"kind":"script_path","provenance":[inventory_provenance.clone()]})) + .collect::>(); + + let dirty = snapshot["dirtyOverlay"]["present"] + .as_bool() + .ok_or_else(|| contract("snapshot dirty overlay is invalid"))?; + let active_paths = snapshot["dirtyOverlay"]["paths"] + .as_array() + .ok_or_else(|| contract("snapshot dirty paths are invalid"))? + .clone(); + let mut unknowns = vec![json!({ + "field":"purpose", + "reason":"no admitted purpose evidence is present in the composed inputs", + "provenance":[inventory_provenance.clone(), native_files_provenance.clone()] + })]; + if commands.is_empty() { + unknowns.push(json!({ + "field":"commands", + "reason":"no root command script is present in the verified inventory", + "provenance":[inventory_provenance.clone()] + })); + } + unknowns.extend([ + json!({"field":"structural_relationships","reason":"survival evidence reports structural verdict unknown","provenance":[survival_provenance.clone()]}), + json!({"field":"call_graph","reason":"native coverage reports call graph precision unknown","provenance":[coverage_provenance.clone()]}), + ]); + + let provider_ready = coverage["producer"] == "benchmark-provider-ready"; + let mut risks = vec![ + json!({"code":"heuristic_native_precision","statement":"native symbols and imports are heuristic and call graph precision is unknown","provenance":[coverage_provenance.clone()]}), + ]; + if !provider_ready { + risks.insert(0, json!({"code":"structural_evidence_unavailable","statement":"deeper structural perception is unavailable","provenance":[survival_provenance.clone()]})); + } + + Ok(json!({ + "schema":"code-intel-project-orientation.v1", + "snapshotIdentity":snapshot_identity, + "identity":{ + "status":"known", + "repositoryIdentity":snapshot["snapshot"]["repoIdentity"], + "repositoryKind":snapshot["repository"]["kind"], + "revision":snapshot["snapshot"]["head"], + "provenance":[snapshot_provenance] + }, + "purpose":{ + "status":"unknown", + "evidence":[], + "reason":"no admitted purpose evidence is present in the composed inputs", + "provenance":[inventory_provenance.clone(), native_files_provenance.clone()] + }, + "languages":languages, + "boundaries":boundaries, + "entryPoints":entry_points, + "commands":commands, + "activeChange":{ + "status":if dirty {"dirty"} else {"clean"}, + "paths":active_paths, + "provenance":[dirty_provenance] + }, + "evidenceAvailability":[ + {"evidence":"survival_scan","status":"reduced","provenance":[survival_provenance.clone()]}, + {"evidence":"benchmark_provider","status":if provider_ready {"available"} else {"unavailable"},"provenance":[coverage_provenance.clone()]}, + {"evidence":"native_files","status":"available","provenance":[native_files_provenance.clone()]}, + {"evidence":"native_structure","status":"heuristic","provenance":[coverage_provenance.clone()]} + ], + "risks":risks, + "unknowns":unknowns, + "confidence":{ + "level":"low", + "basis":["survival completeness is reduced","structural verdict and call graph remain unknown"], + "provenance":[survival_provenance, coverage_provenance] + } + })) +} + +fn provenance(artifact: &VerifiedArtifact, pointer: &str) -> Value { + json!({ + "artifactType":artifact.artifact_type(), + "artifactSha256":artifact.sha256(), + "jsonPointer":pointer + }) +} + +fn parse_json(artifact: &VerifiedArtifact, label: &str) -> Result { + serde_json::from_slice(artifact.bytes()) + .map_err(|_| contract(format!("verified {label} is invalid JSON"))) +} + +fn require_schema(artifact: &VerifiedArtifact, schema: &str) -> Result<(), AdapterError> { + if artifact.artifact_schema() == schema { + Ok(()) + } else { + Err(contract(format!( + "{} input must declare artifact schema {schema}", + artifact.artifact_type() + ))) + } +} + +fn inventory_paths(bytes: &[u8]) -> Result, AdapterError> { + let text = + std::str::from_utf8(bytes).map_err(|_| contract("verified inventory is not UTF-8"))?; + let mut paths = text + .split(['\0', '\n']) + .map(|path| path.trim_end_matches('\r').replace('\\', "/")) + .filter(|path| !path.is_empty()) + .collect::>(); + if paths + .iter() + .any(|path| path.starts_with('/') || path.split('/').any(|part| part == "..")) + { + return Err(contract("verified inventory contains a non-portable path")); + } + paths.sort(); + paths.dedup(); + Ok(paths) +} + +fn command_script(path: &str) -> bool { + [".ps1", ".sh", ".cmd", ".bat"] + .iter() + .any(|suffix| path.to_ascii_lowercase().ends_with(suffix)) +} + +fn render_summary(orientation: &Value) -> String { + let item_lines = |field: &str, key: &str| { + orientation[field] + .as_array() + .into_iter() + .flatten() + .filter_map(|item| item[key].as_str()) + .map(|value| format!("- {value}")) + .collect::>() + .join("\n") + }; + format!( + "# Project Orientation\n\n## Identity\n- Repository: {}\n- Revision: {}\n\n## Purpose\n- Status: {}\n\n## Languages\n{}\n\n## Boundaries\n{}\n\n## Entry Points\n{}\n\n## Commands\n{}\n\n## Active Change\n- Status: {}\n\n## Risks\n{}\n\n## Unknowns\n{}\n\n## Confidence\n- Level: {}\n", + orientation["identity"]["repositoryIdentity"].as_str().unwrap_or("unknown"), + orientation["identity"]["revision"].as_str().unwrap_or("unknown"), + orientation["purpose"]["status"].as_str().unwrap_or("unknown"), + item_lines("languages", "name"), + item_lines("boundaries", "path"), + item_lines("entryPoints", "path"), + item_lines("commands", "path"), + orientation["activeChange"]["status"].as_str().unwrap_or("unknown"), + item_lines("risks", "statement"), + item_lines("unknowns", "field"), + orientation["confidence"]["level"].as_str().unwrap_or("unknown") + ) +} + +fn publish(out: &Path, relative: &str, bytes: &[u8]) -> Result<(), AdapterError> { + fs::create_dir_all(out) + .map_err(|error| AdapterError::Io(format!("create orientation output: {error}")))?; + let path = out.join(relative); + if path.exists() { + return Err(AdapterError::Io(format!( + "refusing to overwrite orientation artifact: {relative}" + ))); + } + fs::write(&path, bytes).map_err(|error| AdapterError::Io(format!("write {relative}: {error}"))) +} + +fn contract(message: impl Into) -> AdapterError { + AdapterError::Contract(message.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::capability::sha256_hex; + + #[test] + fn registry_toolchain_digest_is_the_orientation_source_sha256() { + let registry_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/integrations.json"); + let registry: Value = serde_json::from_slice(&fs::read(registry_path).unwrap()).unwrap(); + let integration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|item| item["id"] == "project.orientation") + .unwrap(); + assert_eq!( + integration["toolchainDigestEvidence"], + json!({ + "algorithm":"sha256", + "inputs":["crates/code-intel-cli/src/project_orientation.rs"] + }) + ); + assert_eq!( + integration["capabilityDeclaration"]["implementation"]["toolchainDigests"][0], + sha256_hex(include_bytes!("project_orientation.rs")) + ); + } +} diff --git a/crates/code-intel-cli/src/project_orientation_benchmark.rs b/crates/code-intel-cli/src/project_orientation_benchmark.rs new file mode 100644 index 0000000..2b9ea07 --- /dev/null +++ b/crates/code-intel-cli/src/project_orientation_benchmark.rs @@ -0,0 +1,759 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::Instant; + +use serde_json::{json, Value}; + +use crate::artifact_ref::VerifiedArtifact; +use crate::capability::sha256_hex; +use crate::capability_inventory::{AdapterArtifact, AdapterError, AdapterOutput}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const TARGET_MS: u64 = 60_000; + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + if request["options"] + .as_object() + .map_or(true, |options| !options.is_empty()) + { + return Err(AdapterError::InvalidOptions( + "project.orientation-benchmark accepts no options".into(), + )); + } + if verified_inputs.len() != 1 + || verified_inputs[0].artifact_schema() + != "code-intel-project-orientation-benchmark-observations.v1" + || verified_inputs[0].artifact_type() != "benchmark.orientation-observations" + { + return Err(AdapterError::Contract( + "project.orientation-benchmark requires one A03-verified observation corpus".into(), + )); + } + let observations: Value = + serde_json::from_slice(verified_inputs[0].bytes()).map_err(|error| { + AdapterError::Contract(format!("parse benchmark observations: {error}")) + })?; + if observations["snapshotIdentity"] != request["snapshot"]["identity"] { + return Err(AdapterError::Contract( + "benchmark observations do not match the request snapshot".into(), + )); + } + let report = evaluate(&observations)?; + let bytes = serde_json::to_vec(&report) + .map_err(|error| AdapterError::Internal(format!("serialize benchmark report: {error}")))?; + let markdown = render(&report).into_bytes(); + publish(out, "report.json", &bytes)?; + publish(out, "report.md", &markdown)?; + Ok(AdapterOutput { + artifacts: vec![ + AdapterArtifact { + artifact_schema: "code-intel-project-orientation-benchmark.v1".into(), + artifact_type: "benchmark.orientation-report".into(), + relative_path: "report.json".into(), + bytes, + }, + AdapterArtifact { + artifact_schema: "code-intel-project-orientation-benchmark-markdown.v1".into(), + artifact_type: "benchmark.orientation-report-view".into(), + relative_path: "report.md".into(), + bytes: markdown, + }, + ], + observed_effects: vec!["local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match run(raw) { + Ok(report) => { + println!("{}", serde_json::to_string(&report).unwrap()); + 0 + } + Err((code, message)) => { + eprintln!("{message}"); + code + } + } +} + +fn run(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("orientation") { + return Err((64, "usage: benchmark orientation --out [--repetitions <2..10>] [--manifest ]".into())); + } + let mut out = None; + let mut manifest = None; + let mut repetitions = 3usize; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!(flag, "--out" | "--repetitions" | "--manifest") { + return Err(( + 64, + format!("unknown orientation benchmark argument: {flag}"), + )); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| (64, format!("{flag} requires one value")))?; + match flag { + "--out" if out.replace(PathBuf::from(value)).is_some() => { + return Err((64, "duplicate --out".into())) + } + "--manifest" if manifest.replace(PathBuf::from(value)).is_some() => { + return Err((64, "duplicate --manifest".into())) + } + "--repetitions" => { + repetitions = value + .parse() + .map_err(|_| (64, "--repetitions must be an integer".into()))?; + } + _ => {} + } + index += 2; + } + if !(2..=10).contains(&repetitions) { + return Err((64, "--repetitions must be between 2 and 10".into())); + } + let out = out.ok_or_else(|| (64, "--out is required".into()))?; + fs::create_dir(&out) + .map_err(|error| (74, format!("exclusive benchmark output create: {error}")))?; + let manifest = manifest.unwrap_or_else(default_registry); + let binary = std::env::current_exe() + .map_err(|error| (74, format!("locate benchmark executable: {error}")))?; + let fixtures = build_fixtures(); + let mut observed = Vec::new(); + for fixture in fixtures { + let warm_root = out.join("work").join("warm").join(&fixture.id); + let materialize_start = Instant::now(); + let request_path = materialize(&warm_root, &fixture, &manifest)?; + let warm_materialization = elapsed_ms(materialize_start); + let mut warm = Vec::new(); + for repetition in 0..repetitions { + warm.push(run_sample( + &binary, + &manifest, + &warm_root, + &request_path, + &warm_root.join(format!("out-{repetition}")), + 0, + )?); + } + let mut cold = Vec::new(); + for repetition in 0..repetitions { + let root = out + .join("work") + .join("cold") + .join(format!("{}-{repetition}", fixture.id)); + let start = Instant::now(); + let request_path = materialize(&root, &fixture, &manifest)?; + let materialization = elapsed_ms(start); + cold.push(run_sample( + &binary, + &manifest, + &root, + &request_path, + &root.join("out"), + materialization, + )?); + } + observed.push(json!({ + "id":fixture.id, + "size":fixture.size, + "condition":fixture.condition, + "typical":fixture.typical, + "expected":{ + "activeChange":fixture.active_change, + "fileCount":fixture.file_count, + "providerStatus":if fixture.condition == "provider_missing" {"unavailable"} else {"available"}, + "unknownFields":["call_graph","purpose","structural_relationships"], + "unsupportedFiles":["Cargo.toml","README.md"] + }, + "warmPreparationMs":warm_materialization, + "samples":{"cold":cold,"warm":warm} + })); + } + let observations = json!({ + "schema":"code-intel-project-orientation-benchmark-observations.v1", + "snapshotIdentity":SNAPSHOT, + "method":{ + "clock":"std::time::Instant", + "execution":"sequential_child_process", + "concurrency":1, + "repetitionsPerTemperature":repetitions, + "coldDefinition":"fresh materialized immutable Artifact Ref corpus and fresh output directory", + "warmDefinition":"reused immutable Artifact Ref corpus with a fresh output directory", + "percentile":"nearest_rank", + "llm":"disabled" + }, + "environment":{ + "os":std::env::consts::OS, + "arch":std::env::consts::ARCH, + "processor":std::env::var("PROCESSOR_IDENTIFIER").unwrap_or_else(|_| "unknown".into()), + "cleanMachine":false + }, + "fixtures":observed + }); + fs::write( + out.join("observations.json"), + serde_json::to_vec(&observations).unwrap(), + ) + .map_err(|error| (74, format!("write benchmark observations: {error}")))?; + let report = evaluate(&observations).map_err(|error| (65, adapter_message(error)))?; + fs::write( + out.join("report.json"), + serde_json::to_vec(&report).unwrap(), + ) + .map_err(|error| (74, format!("write benchmark report: {error}")))?; + fs::write(out.join("report.md"), render(&report)) + .map_err(|error| (74, format!("write benchmark Markdown: {error}")))?; + Ok(report) +} + +struct Fixture { + id: String, + size: &'static str, + condition: &'static str, + typical: bool, + file_count: usize, + active_change: &'static str, +} + +fn build_fixtures() -> Vec { + let mut fixtures = Vec::new(); + for (size, file_count, typical) in [ + ("small", 5, true), + ("medium", 50, true), + ("large", 500, false), + ] { + for condition in ["clean", "dirty", "provider_missing"] { + fixtures.push(Fixture { + id: format!("{size}-{condition}"), + size, + condition, + typical, + file_count, + active_change: if condition == "dirty" { + "dirty" + } else { + "clean" + }, + }); + } + } + fixtures +} + +fn materialize(root: &Path, fixture: &Fixture, manifest: &Path) -> Result { + fs::create_dir_all(root) + .map_err(|error| (74, format!("create benchmark fixture root: {error}")))?; + let mut paths = vec![ + "Cargo.toml".to_string(), + "README.md".into(), + "run.ps1".into(), + "src/main.rs".into(), + "tests/orientation.rs".into(), + ]; + for index in paths.len()..fixture.file_count { + paths.push(format!("src/module-{index:04}.rs")); + } + paths.sort(); + let native = paths + .iter() + .map(|path| json!({ + "path":path, + "language":if path.ends_with(".rs") {"rust"} else if path.ends_with(".ps1") {"powershell"} else {"text"}, + "bytes":16,"lines":1,"textHash":"3".repeat(64),"source":"benchmark-fixture" + })) + .collect::>(); + let dirty = fixture.condition == "dirty"; + let snapshot = json!({ + "schema":"code-intel-repository-snapshot.v1", + "snapshot":{"identity":SNAPSHOT,"repoIdentity":format!("content-v1:{}", "c".repeat(64)),"head":"benchmark-corpus-v1","workingTreePolicy":"explicit_overlay","scope":["."],"inputDigest":"d".repeat(64)}, + "dirtyOverlay":{"present":dirty,"digest":if dirty {json!("e".repeat(64))} else {Value::Null},"paths":if dirty {json!(["src/main.rs"])} else {json!([])},"members":{"trackedModified":if dirty {json!(["src/main.rs"])} else {json!([])},"trackedDeleted":[],"untracked":[],"renamed":[],"typeChanged":[],"staged":[]},"ignoredPolicy":"excluded_by_git_ignore"}, + "repository":{"kind":"unversioned"} + }); + let inventory = format!("{}\n", paths.join("\n")); + let snapshot_ref = write_ref( + root, + "snapshot.json", + "code-intel-repository-snapshot.v1", + "repository.snapshot", + serde_json::to_vec(&snapshot).unwrap(), + )?; + let inventory_ref = write_ref( + root, + "files.txt", + "code-intel-file-inventory.v1", + "inventory.files", + inventory.into_bytes(), + )?; + let survival = json!({ + "schema":"code-intel-repository-survival-scan-result.v1","status":"completed","snapshotIdentity":SNAPSHOT, + "repository":{"kind":"unversioned","identity":format!("content-v1:{}", "c".repeat(64)),"revision":"benchmark-corpus-v1","dirty":dirty,"sourceSha256":snapshot_ref["sha256"]}, + "inventory":{"fileCount":fixture.file_count,"extensions":{"rs":fixture.file_count.saturating_sub(3),"md":1,"toml":1,"ps1":1},"sourceSha256":inventory_ref["sha256"]}, + "providerDiagnosis":{"providerId":"codenexus.full","status":"provider_unavailable","domainVerdict":"unknown"}, + "completeness":"reduced","structuralVerdict":"unknown", + "limitations":["only repository identity and basic file inventory are available","deeper structural perception requires an admitted provider result"], + "engineeringFacts":[ + {"kind":"repository_identity","value":format!("content-v1:{}", "c".repeat(64)),"sourceSha256":snapshot_ref["sha256"]}, + {"kind":"repository_revision","value":"benchmark-corpus-v1","sourceSha256":snapshot_ref["sha256"]}, + {"kind":"inventory_file_count","value":fixture.file_count,"sourceSha256":inventory_ref["sha256"]} + ] + }); + let inputs = vec![ + snapshot_ref, + inventory_ref, + write_json_ref( + root, + "survival.json", + "code-intel-repository-survival-scan-result.v1", + "repository.survival-scan", + &survival, + )?, + write_json_ref( + root, + "native-files.json", + "code-evidence-files.v1", + "code_evidence.files", + &json!({"schema":"code-evidence-files.v1","files":native}), + )?, + write_json_ref( + root, + "native-coverage.json", + "code-evidence-coverage.v1", + "code_evidence.coverage", + &json!({"schema":"code-evidence-coverage.v1","producer":if fixture.condition == "provider_missing" {"benchmark-provider-missing"} else {"benchmark-provider-ready"},"parserKind":"line-heuristic","supportedHeuristics":["rust","powershell"],"unsupportedFiles":["Cargo.toml","README.md"],"symbolPrecision":"heuristic","importPrecision":"heuristic","relationshipPrecision":"unknown","callGraph":"unknown","effects":["repo_read","local_write"]}), + )?, + write_json_ref( + root, + "native-ranking.json", + "agent-code-slice-ranking.v1", + "code_evidence.agent_slice", + &json!({"schema":"agent-code-slice-ranking.v1","strategy":"benchmark-fixture","files":[{"path":"src/main.rs","language":"rust","score":40,"reasons":["entrypoint"],"symbols":null,"imports":null}]}), + )?, + ]; + let registry: Value = serde_json::from_slice( + &fs::read(manifest).map_err(|error| (74, format!("read registry: {error}")))?, + ) + .map_err(|error| (65, format!("parse registry: {error}")))?; + let declaration = registry["integrations"] + .as_array() + .and_then(|items| { + items + .iter() + .find(|item| item["id"] == "project.orientation") + }) + .ok_or_else(|| (65, "project.orientation declaration is missing".into()))?; + let request = json!({ + "schema":"code-intel-capability-request.v1","capability":"project.orientation","contractVersion":1, + "implementation":declaration["capabilityDeclaration"]["implementation"],"snapshot":snapshot["snapshot"],"options":{},"inputs":inputs, + "effectPolicy":{"allowedEffects":["local_write"]} + }); + let path = root.join("request.json"); + fs::write(&path, serde_json::to_vec(&request).unwrap()) + .map_err(|error| (74, format!("write fixture request: {error}")))?; + Ok(path) +} + +fn write_json_ref( + root: &Path, + path: &str, + schema: &str, + kind: &str, + value: &Value, +) -> Result { + write_ref(root, path, schema, kind, serde_json::to_vec(value).unwrap()) +} + +fn write_ref( + root: &Path, + path: &str, + schema: &str, + kind: &str, + bytes: Vec, +) -> Result { + fs::write(root.join(path), &bytes) + .map_err(|error| (74, format!("write fixture artifact: {error}")))?; + Ok( + json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":schema,"type":kind,"path":path,"sha256":sha256_hex(&bytes),"consumedSnapshotIdentity":SNAPSHOT}), + ) +} + +fn run_sample( + binary: &Path, + manifest: &Path, + root: &Path, + request: &Path, + out: &Path, + materialization_ms: u64, +) -> Result { + let start = Instant::now(); + let output = Command::new(binary) + .args(["capability", "exec", "project.orientation", "--request"]) + .arg(request) + .arg("--out") + .arg(out) + .arg("--artifact-root") + .arg(root) + .arg("--manifest") + .arg(manifest) + .output() + .map_err(|error| (69, format!("launch project.orientation: {error}")))?; + let orientation_ms = elapsed_ms(start); + if !output.status.success() { + return Err(( + output.status.code().unwrap_or(70), + format!( + "project.orientation benchmark sample failed: {}", + String::from_utf8_lossy(&output.stderr) + ), + )); + } + let orientation_bytes = fs::read(out.join("project-orientation.json")) + .map_err(|error| (74, format!("read orientation sample: {error}")))?; + let orientation: Value = serde_json::from_slice(&orientation_bytes) + .map_err(|error| (65, format!("parse orientation sample: {error}")))?; + let coverage: Value = serde_json::from_slice( + &fs::read(root.join("native-coverage.json")) + .map_err(|error| (74, format!("read native coverage sample: {error}")))?, + ) + .map_err(|error| (65, format!("parse native coverage sample: {error}")))?; + Ok(json!({ + "wallTimeMs":materialization_ms.saturating_add(orientation_ms), + "cost":{"materializationMs":materialization_ms,"orientationProcessMs":orientation_ms}, + "artifact":{"bytes":orientation_bytes.len(),"sha256":sha256_hex(&orientation_bytes)}, + "coverage":{"unsupportedFiles":coverage["unsupportedFiles"]}, + "orientation":orientation + })) +} + +fn evaluate(observations: &Value) -> Result { + validate_observation_header(observations)?; + let fixtures = observations["fixtures"] + .as_array() + .ok_or_else(|| contract("benchmark fixtures must be an array"))?; + let expected_pairs = ["small", "medium", "large"] + .into_iter() + .flat_map(|size| { + ["clean", "dirty", "provider_missing"] + .into_iter() + .map(move |condition| format!("{size}:{condition}")) + }) + .collect::>(); + let actual_pairs = fixtures + .iter() + .filter_map(|fixture| { + Some(format!( + "{}:{}", + fixture["size"].as_str()?, + fixture["condition"].as_str()? + )) + }) + .collect::>(); + if fixtures.len() != 9 || actual_pairs != expected_pairs { + return Err(contract("benchmark corpus must contain the exact small/medium/large x clean/dirty/provider_missing matrix")); + } + let repetitions = observations["method"]["repetitionsPerTemperature"] + .as_u64() + .ok_or_else(|| contract("benchmark repetitions are invalid"))? + as usize; + let mut typical = Vec::new(); + let mut cold = Vec::new(); + let mut warm = Vec::new(); + let mut materialization = Vec::new(); + let mut process = Vec::new(); + let mut artifact_sizes = Vec::new(); + let mut typical_artifact_sizes = Vec::new(); + let mut field_total = 0u64; + let mut field_ok = 0u64; + let mut unknown_total = 0u64; + let mut unknown_ok = 0u64; + let mut unsupported_total = 0u64; + let mut unsupported_ok = 0u64; + let mut determinism_total = 0u64; + let mut determinism_ok = 0u64; + let mut provenance_total = 0u64; + let mut provenance_ok = 0u64; + for fixture in fixtures { + let mut replay_digests = BTreeSet::new(); + for temperature in ["cold", "warm"] { + let samples = fixture["samples"][temperature] + .as_array() + .ok_or_else(|| contract("benchmark samples are invalid"))?; + if samples.len() != repetitions { + return Err(contract( + "each fixture temperature must have the declared repetitions", + )); + } + for sample in samples { + let wall = sample["wallTimeMs"] + .as_u64() + .ok_or_else(|| contract("sample wall time is invalid"))?; + if fixture["typical"] == true { + typical.push(wall); + } + if temperature == "cold" { + cold.push(wall); + } else { + warm.push(wall); + } + materialization.push( + sample["cost"]["materializationMs"] + .as_u64() + .ok_or_else(|| contract("materialization cost is invalid"))?, + ); + process.push( + sample["cost"]["orientationProcessMs"] + .as_u64() + .ok_or_else(|| contract("orientation cost is invalid"))?, + ); + let orientation = &sample["orientation"]; + let canonical_orientation = serde_json::to_vec(orientation) + .map_err(|_| contract("orientation sample cannot be serialized"))?; + let artifact_bytes = sample["artifact"]["bytes"] + .as_u64() + .ok_or_else(|| contract("sample artifact byte size is invalid"))?; + let artifact_sha = sample["artifact"]["sha256"] + .as_str() + .ok_or_else(|| contract("sample artifact digest is invalid"))?; + if artifact_bytes != canonical_orientation.len() as u64 + || artifact_sha != sha256_hex(&canonical_orientation) + { + return Err(contract( + "sample artifact size or digest does not bind the orientation bytes", + )); + } + artifact_sizes.push(artifact_bytes); + if fixture["typical"] == true { + typical_artifact_sizes.push(artifact_bytes); + } + replay_digests.insert(artifact_sha.to_string()); + let observed_file_count = orientation["languages"] + .as_array() + .map(|items| { + items + .iter() + .filter_map(|item| item["fileCount"].as_u64()) + .sum::() + }) + .unwrap_or(0); + let provider_status = orientation["evidenceAvailability"] + .as_array() + .and_then(|items| { + items + .iter() + .find(|item| item["evidence"] == "benchmark_provider") + }) + .and_then(|item| item["status"].as_str()); + let checks = [ + orientation["schema"] == "code-intel-project-orientation.v1", + orientation["snapshotIdentity"] == observations["snapshotIdentity"], + orientation["purpose"]["status"] == "unknown", + orientation["purpose"]["evidence"] == json!([]), + orientation["activeChange"]["status"] == fixture["expected"]["activeChange"], + observed_file_count.saturating_add(2) + == fixture["expected"]["fileCount"] + .as_u64() + .unwrap_or(u64::MAX), + provider_status == fixture["expected"]["providerStatus"].as_str(), + orientation["languages"] + .as_array() + .is_some_and(|items| !items.is_empty()), + orientation["risks"].as_array().is_some_and(|items| { + items + .iter() + .any(|item| item["code"] == "structural_evidence_unavailable") + == (fixture["condition"] == "provider_missing") + }), + ]; + field_total += checks.len() as u64; + field_ok += checks.iter().filter(|value| **value).count() as u64; + let expected_unknowns = fixture["expected"]["unknownFields"] + .as_array() + .unwrap() + .iter() + .filter_map(Value::as_str) + .collect::>(); + let actual_unknowns = orientation["unknowns"] + .as_array() + .ok_or_else(|| contract("orientation unknowns are invalid"))? + .iter() + .filter_map(|item| item["field"].as_str()) + .collect::>(); + unknown_total += actual_unknowns.len().max(expected_unknowns.len()) as u64; + unknown_ok += actual_unknowns.intersection(&expected_unknowns).count() as u64; + let expected_unsupported = fixture["expected"]["unsupportedFiles"] + .as_array() + .ok_or_else(|| contract("expected unsupported files are invalid"))? + .iter() + .filter_map(Value::as_str) + .collect::>(); + let actual_unsupported = sample["coverage"]["unsupportedFiles"] + .as_array() + .ok_or_else(|| contract("sample unsupported files are invalid"))? + .iter() + .filter_map(Value::as_str) + .collect::>(); + unsupported_total += + actual_unsupported.len().max(expected_unsupported.len()) as u64; + unsupported_ok += actual_unsupported + .intersection(&expected_unsupported) + .count() as u64; + for claim in claim_nodes(orientation)? { + provenance_total += 1; + if claim["provenance"] + .as_array() + .is_some_and(|items| !items.is_empty()) + { + provenance_ok += 1; + } + } + } + } + determinism_total += 1; + if replay_digests.len() == 1 { + determinism_ok += 1; + } + } + if provenance_ok != provenance_total { + return Err(contract( + "fast orientation result without complete claim provenance is rejected", + )); + } + let field_correctness = ratio(field_ok, field_total); + let unresolved_coverage = ratio(unknown_ok, unknown_total); + let unsupported_coverage = ratio(unsupported_ok, unsupported_total); + let deterministic_replay_rate = ratio(determinism_ok, determinism_total); + let provenance_completeness = ratio(provenance_ok, provenance_total); + let typical_p95 = percentile(&mut typical, 95); + let verdict = if typical_p95 <= TARGET_MS + && field_correctness == 1.0 + && unresolved_coverage == 1.0 + && unsupported_coverage == 1.0 + && deterministic_replay_rate == 1.0 + && provenance_completeness == 1.0 + { + "pass" + } else { + "fail" + }; + Ok(json!({ + "schema":"code-intel-project-orientation-benchmark.v1","verdict":verdict,"target":{"typicalP95WallTimeMs":TARGET_MS,"llm":"disabled"}, + "corpus":{"fixtureCount":fixtures.len(),"sizes":["small","medium","large"],"conditions":["clean","dirty","provider_missing"],"typicalDefinition":"small_and_medium_all_conditions","stressDefinition":"large_all_conditions"}, + "method":observations["method"],"environment":observations["environment"], + "latency":{"typical":{"p50WallTimeMs":percentile(&mut typical,50),"p95WallTimeMs":typical_p95},"cold":{"p50WallTimeMs":percentile(&mut cold,50),"p95WallTimeMs":percentile(&mut cold,95)},"warm":{"p50WallTimeMs":percentile(&mut warm,50),"p95WallTimeMs":percentile(&mut warm,95)}}, + "artifactSize":{"typical":{"p50Bytes":percentile(&mut typical_artifact_sizes,50),"p95Bytes":percentile(&mut typical_artifact_sizes,95)},"all":{"p50Bytes":percentile(&mut artifact_sizes,50),"p95Bytes":percentile(&mut artifact_sizes,95),"maxBytes":artifact_sizes.iter().copied().max().unwrap_or(0)}}, + "quality":{"fieldCorrectness":field_correctness,"unknownPrecision":unresolved_coverage,"unresolvedCoverage":unresolved_coverage,"unsupportedCoverage":unsupported_coverage,"deterministicReplayRate":deterministic_replay_rate,"provenanceCompleteness":provenance_completeness}, + "costCenters":[{"name":"fixture_materialization","p50Ms":percentile(&mut materialization,50),"p95Ms":percentile(&mut materialization,95)},{"name":"a01_process_and_orientation","p50Ms":percentile(&mut process,50),"p95Ms":percentile(&mut process,95)}], + "limitations":["this run was not performed on a clean machine","cold means fresh materialization, not an operating-system page-cache flush","all measurements are sequential and local; hosted services and LLMs are excluded","provider conditions are deterministic committed benchmark evidence, not live hosted-provider probes"] + })) +} + +fn validate_observation_header(value: &Value) -> Result<(), AdapterError> { + if value["schema"] != "code-intel-project-orientation-benchmark-observations.v1" + || value["snapshotIdentity"] != SNAPSHOT + || value["method"]["clock"] != "std::time::Instant" + || value["method"]["execution"] != "sequential_child_process" + || value["method"]["concurrency"] != 1 + || value["method"]["llm"] != "disabled" + || value["method"]["percentile"] != "nearest_rank" + || value["environment"]["cleanMachine"] != false + { + return Err(contract( + "benchmark observation method is not reproducible or no-LLM", + )); + } + Ok(()) +} + +fn claim_nodes<'a>(orientation: &'a Value) -> Result, AdapterError> { + let mut claims = vec![ + &orientation["identity"], + &orientation["purpose"], + &orientation["activeChange"], + &orientation["confidence"], + ]; + for field in [ + "languages", + "boundaries", + "entryPoints", + "commands", + "evidenceAvailability", + "risks", + "unknowns", + ] { + claims.extend( + orientation[field] + .as_array() + .ok_or_else(|| contract(format!("orientation {field} is invalid")))?, + ); + } + Ok(claims) +} + +fn percentile(values: &mut [u64], percentile: usize) -> u64 { + if values.is_empty() { + return 0; + } + values.sort_unstable(); + let rank = (percentile * values.len()).div_ceil(100).max(1); + values[rank - 1] +} + +fn ratio(numerator: u64, denominator: u64) -> f64 { + if denominator == 0 { + 0.0 + } else { + numerator as f64 / denominator as f64 + } +} + +fn elapsed_ms(start: Instant) -> u64 { + start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64 +} + +fn publish(out: &Path, relative: &str, bytes: &[u8]) -> Result<(), AdapterError> { + fs::create_dir_all(out) + .map_err(|error| AdapterError::Io(format!("create benchmark output: {error}")))?; + let path = out.join(relative); + if path.exists() { + return Err(AdapterError::Io(format!( + "refusing to overwrite benchmark artifact: {relative}" + ))); + } + fs::write(path, bytes).map_err(|error| AdapterError::Io(format!("write {relative}: {error}"))) +} + +fn render(report: &Value) -> String { + format!("# Project Orientation Benchmark\n\n- Verdict: {}\n- Typical p50: {} ms\n- Typical p95: {} ms\n- Typical artifact p95: {} bytes\n- Field correctness: {}\n- Unresolved coverage: {}\n- Unsupported coverage: {}\n- Deterministic replay rate: {}\n- Provenance completeness: {}\n- Clean machine: false\n", + report["verdict"].as_str().unwrap_or("fail"), report["latency"]["typical"]["p50WallTimeMs"], report["latency"]["typical"]["p95WallTimeMs"], report["artifactSize"]["typical"]["p95Bytes"], report["quality"]["fieldCorrectness"], report["quality"]["unresolvedCoverage"], report["quality"]["unsupportedCoverage"], report["quality"]["deterministicReplayRate"], report["quality"]["provenanceCompleteness"]) +} + +fn default_registry() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/integrations.json") +} + +fn contract(message: impl Into) -> AdapterError { + AdapterError::Contract(message.into()) +} + +fn adapter_message(error: AdapterError) -> String { + match error { + AdapterError::InvalidOptions(message) + | AdapterError::Contract(message) + | AdapterError::Unavailable(message) + | AdapterError::Internal(message) + | AdapterError::Io(message) => message, + } +} diff --git a/crates/code-intel-cli/src/providers.rs b/crates/code-intel-cli/src/providers.rs index a7ff2c2..2e18c31 100644 --- a/crates/code-intel-cli/src/providers.rs +++ b/crates/code-intel-cli/src/providers.rs @@ -1,8 +1,16 @@ -use crate::{graph, Result}; +use crate::{codenexus_adapter, graph, Result}; use serde_json::{json, Value}; -use std::io::Write; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::{env, fs}; + +#[path = "graph_adapter.rs"] +mod graph_adapter; +#[path = "repowise_adapter.rs"] +mod repowise_adapter; +#[path = "sentrux_adapter.rs"] +mod sentrux_adapter; pub struct Options<'a> { pub action: &'a str, @@ -32,6 +40,76 @@ pub struct ProviderOperation { } pub const OPERATIONS: &[ProviderOperation] = &[ + ProviderOperation { + provider: "codenexus", + operation: "adapt", + stage: "localization", + protocol: "provider-port+cli", + method: "POST", + route: "/api/providers/codenexus/adapt", + command_template: "target/debug/code-intel.exe provider codenexus-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + artifact: "code-intel-codenexus-route-result.v1", + required: false, + status: "active", + source_spec: "Pipeline-owned B04 adapter over full CodeNexus or explicit lite compatibility output and A04 admissibility", + notes: "Canonical CodeNexus evidence route. Full is primary; lite is explicit fallback/legacy rollback only. Provider process, storage, retrieval, and impact semantics remain external.", + }, + ProviderOperation { + provider: "sentrux", + operation: "adapt", + stage: "structure_governance", + protocol: "provider-port+cli", + method: "POST", + route: "/api/providers/sentrux/adapt", + command_template: "target/debug/code-intel.exe provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + artifact: "code-intel-sentrux-route-result.v1", + required: true, + status: "active", + source_spec: "Pipeline-owned B03 translation over Sentrux/shim native output and A04 admissibility", + notes: "Canonical structural evidence route. The bundled shim and Invoke-SentruxAgentTool.ps1 remain replaceable provider implementations/rollback surfaces, never diagnosis authority.", + }, + ProviderOperation { + provider: "session", + operation: "adapt", + stage: "verification", + protocol: "provider-port+cli", + method: "POST", + route: "/api/providers/session/adapt", + command_template: "target/debug/code-intel.exe provider session-adapt --repo --trace [--hotspots ] [--out ]", + artifact: "code-intel-session-evidence.v1", + required: false, + status: "active", + source_spec: "Pipeline-owned privacy, snapshot, normalization, and structural-join logic over optional Mindwalk trace v1 input", + notes: "Optional session-review route. Mindwalk extraction remains replaceable; raw prompts, summaries, outside paths, and provider-specific policy never enter the normalized artifact.", + }, + ProviderOperation { + provider: "graph", + operation: "adapt", + stage: "architecture_graph", + protocol: "provider-port+cli", + method: "POST", + route: "/api/providers/graph/adapt", + command_template: "target/debug/code-intel.exe provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + artifact: "code-intel-graph-route-result.v1", + required: true, + status: "active", + source_spec: "Pipeline-owned B02 adapter over internal Rust or explicit Understand-compatible fallback output and A04 admissibility", + notes: "Canonical graph evidence route. Current snapshot binding is mandatory; external graph execution remains explicit fallback/legacy rollback only.", + }, + ProviderOperation { + provider: "repowise", + operation: "adapt", + stage: "semantic_memory", + protocol: "provider-port+cli", + method: "POST", + route: "/api/providers/repowise/adapt", + command_template: "target/debug/code-intel.exe provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + artifact: "code-intel-repowise-route-result.v1", + required: false, + status: "active", + source_spec: "Pipeline-owned B01 adapter over Repowise-native result and A04 admissibility", + notes: "Production evidence route; every emitted observation passes A04. Legacy provider probes and direct CLI commands remain optional diagnostics/rollback only.", + }, ProviderOperation { provider: "repowise", operation: "status", @@ -44,7 +122,7 @@ pub const OPERATIONS: &[ProviderOperation] = &[ required: true, status: "active", source_spec: "Repowise CLI: status [PATH], MCP/HTTP serve surfaces for agent callers", - notes: "No model required; reports wiki sync and page statistics.", + notes: "Health/readiness only; not evidence. No model required; reports wiki sync and page statistics.", }, ProviderOperation { provider: "repowise", @@ -59,7 +137,7 @@ pub const OPERATIONS: &[ProviderOperation] = &[ required: true, status: "active", source_spec: "Repowise CLI: init/update --index-only, MCP tools include semantic code retrieval", - notes: "No model required; refreshes index, dependency graph, git/dead-code artifacts.", + notes: "No model required; refreshes index artifacts. B01 translation must pass A04 before fact promotion.", }, ProviderOperation { provider: "repowise", @@ -73,21 +151,35 @@ pub const OPERATIONS: &[ProviderOperation] = &[ required: false, status: "compatibility", source_spec: "Repowise CLI: update --docs, provider/model options", - notes: "Model-backed; provider quota can disable this without disabling status/index.", + notes: "Model-backed and separately partial/freshness-scoped; provider quota cannot disable status/index. A04 admission is required before fact promotion.", + }, + ProviderOperation { + provider: "codenexus", + operation: "lite", + stage: "localization", + protocol: "artifact+command", + method: "POST", + route: "/api/providers/codenexus/lite", + command_template: r#"pwsh -NoProfile -File "$env:CODE_INTEL_HOME\Invoke-CodeNexusLite.ps1" -RepoPath ''"#, + artifact: "codenexus-context.json", + required: false, + status: "compatibility", + source_spec: "Repository-owned Invoke-CodeNexusLite.ps1 localization adapter", + notes: "Canonical compatibility route; non-blocking and replaceable, with Survival Scanner preserving localization when unavailable.", }, ProviderOperation { provider: "repowise", operation: "lite", stage: "localization", - protocol: "http+iii-worker", + protocol: "compatibility-alias", method: "POST", route: "/api/providers/repowise/lite", - command_template: "target/debug/code-nexus-lite.exe codenexus::lite", + command_template: r#"pwsh -NoProfile -File "$env:CODE_INTEL_HOME\Invoke-CodeNexusLite.ps1" -RepoPath ''"#, artifact: "codenexus-context.json", - required: true, - status: "active", - source_spec: "CodeNexus-lite worker reads Repowise wiki.db into compact agent context", - notes: "Agent localization view over the Repowise artifact contract.", + required: false, + status: "compatibility", + source_spec: "Legacy Repowise-namespaced alias for the repository-owned CodeNexus-lite adapter", + notes: "Deprecated compatibility alias for one release; migrate callers to codenexus/lite.", }, ProviderOperation { provider: "understand", @@ -135,6 +227,568 @@ pub const OPERATIONS: &[ProviderOperation] = &[ }, ]; +pub fn translate_repowise_native( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> std::result::Result { + repowise_adapter::translate(native, evaluated_at, max_age_seconds) +} + +pub fn translate_graph_native( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> std::result::Result { + graph_adapter::translate(native, evaluated_at, max_age_seconds) +} + +pub fn translate_sentrux_native( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> std::result::Result { + sentrux_adapter::translate(native, evaluated_at, max_age_seconds) +} + +pub fn translate_codenexus_native( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> std::result::Result { + codenexus_adapter::translate(native, evaluated_at, max_age_seconds) +} + +pub(crate) fn run_codenexus_adapt_raw(raw: &[String]) -> i32 { + let cli = match parse_codenexus_adapt_cli(raw) { + Ok(cli) => cli, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let native = match read_provider_native(&cli.request, "CodeNexus") { + Ok(value) => value, + Err(message) => return print_codenexus_route_rejection(&message), + }; + let mut adapter = + match translate_codenexus_native(&native, cli.evaluated_at, cli.max_age_seconds) { + Ok(value) => value, + Err(message) => return print_codenexus_route_rejection(&message), + }; + let admitted = match crate::admissibility::validate_for_consumer( + &adapter["evidence"]["request"], + &cli.artifact_root, + ) { + Ok(value) => value, + Err(message) => return print_codenexus_route_rejection(&message), + }; + if let Err(message) = codenexus_adapter::validate_admitted_payload(admitted.payload(), &adapter) + { + return print_codenexus_route_rejection(&message); + } + adapter["port"]["perceptionUsable"] = json!( + admitted.result()["domainVerdict"] == "observed" + && adapter["port"]["status"] == "current" + && adapter["port"]["freshness"] == "current" + ); + let result = json!({ + "schema":"code-intel-codenexus-route-result.v1", + "status":"completed", + "adapter":adapter, + "admission":admitted.result(), + "engineeringFacts":[], + "diagnostics":[] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + 0 +} + +struct CodeNexusAdaptCli { + request: String, + artifact_root: PathBuf, + evaluated_at: u64, + max_age_seconds: u64, +} + +fn parse_codenexus_adapt_cli(raw: &[String]) -> std::result::Result { + let mut request = None; + let mut artifact_root = None; + let mut evaluated_at = None; + let mut max_age_seconds = None; + let mut index = 0; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--request" | "--artifact-root" | "--evaluated-at" | "--max-age-seconds" + ) { + return Err(format!("unknown CodeNexus adapter argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + match flag { + "--request" if request.replace(value.clone()).is_some() => { + return Err("duplicate CodeNexus adapter argument: --request".to_string()) + } + "--artifact-root" if artifact_root.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate CodeNexus adapter argument: --artifact-root".to_string()) + } + "--evaluated-at" => set_u64(&mut evaluated_at, value, flag, "CodeNexus")?, + "--max-age-seconds" => set_u64(&mut max_age_seconds, value, flag, "CodeNexus")?, + _ => {} + } + index += 2; + } + Ok(CodeNexusAdaptCli { + request: request.ok_or("CodeNexus adapter requires --request")?, + artifact_root: artifact_root.ok_or("CodeNexus adapter requires --artifact-root")?, + evaluated_at: evaluated_at.ok_or("CodeNexus adapter requires --evaluated-at")?, + max_age_seconds: max_age_seconds + .filter(|value| *value > 0) + .ok_or("--max-age-seconds requires a positive integer")?, + }) +} + +fn print_codenexus_route_rejection(message: &str) -> i32 { + let result = json!({ + "schema":"code-intel-codenexus-route-result.v1", + "status":"rejected", + "adapter":null, + "admission":null, + "engineeringFacts":[], + "diagnostics":[message] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + eprintln!("{message}"); + 65 +} + +pub(crate) fn run_sentrux_adapt_raw(raw: &[String]) -> i32 { + let cli = match parse_sentrux_adapt_cli(raw) { + Ok(cli) => cli, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let native = match read_provider_native(&cli.request, "Sentrux") { + Ok(value) => value, + Err(message) => return print_sentrux_route_rejection(&message), + }; + let mut adapter = match translate_sentrux_native(&native, cli.evaluated_at, cli.max_age_seconds) + { + Ok(value) => value, + Err(message) => return print_sentrux_route_rejection(&message), + }; + let admitted = match crate::admissibility::validate_for_consumer( + &adapter["evidence"]["request"], + &cli.artifact_root, + ) { + Ok(value) => value, + Err(message) => return print_sentrux_route_rejection(&message), + }; + if let Err(message) = sentrux_adapter::validate_admitted_payload(admitted.payload(), &adapter) { + return print_sentrux_route_rejection(&message); + } + adapter["port"]["diagnosisEligible"] = json!( + admitted.result()["domainVerdict"] == "observed" + && adapter["port"]["completeness"] == "complete" + && adapter["port"]["freshness"] == "current" + ); + let result = json!({ + "schema":"code-intel-sentrux-route-result.v1", + "status":"completed", + "adapter":adapter, + "admission":admitted.result(), + "engineeringFacts":[], + "diagnostics":[] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + 0 +} + +struct SentruxAdaptCli { + request: String, + artifact_root: PathBuf, + evaluated_at: u64, + max_age_seconds: u64, +} + +fn parse_sentrux_adapt_cli(raw: &[String]) -> std::result::Result { + let mut request = None; + let mut artifact_root = None; + let mut evaluated_at = None; + let mut max_age_seconds = None; + let mut index = 0; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--request" | "--artifact-root" | "--evaluated-at" | "--max-age-seconds" + ) { + return Err(format!("unknown Sentrux adapter argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + match flag { + "--request" if request.replace(value.clone()).is_some() => { + return Err("duplicate Sentrux adapter argument: --request".to_string()) + } + "--artifact-root" if artifact_root.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate Sentrux adapter argument: --artifact-root".to_string()) + } + "--evaluated-at" => set_u64(&mut evaluated_at, value, flag, "Sentrux")?, + "--max-age-seconds" => set_u64(&mut max_age_seconds, value, flag, "Sentrux")?, + _ => {} + } + index += 2; + } + Ok(SentruxAdaptCli { + request: request.ok_or("Sentrux adapter requires --request")?, + artifact_root: artifact_root.ok_or("Sentrux adapter requires --artifact-root")?, + evaluated_at: evaluated_at.ok_or("Sentrux adapter requires --evaluated-at")?, + max_age_seconds: max_age_seconds + .filter(|value| *value > 0) + .ok_or("--max-age-seconds requires a positive integer")?, + }) +} + +fn print_sentrux_route_rejection(message: &str) -> i32 { + let result = json!({ + "schema":"code-intel-sentrux-route-result.v1", + "status":"rejected", + "adapter":null, + "admission":null, + "engineeringFacts":[], + "diagnostics":[message] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + eprintln!("{message}"); + 65 +} + +pub(crate) fn run_graph_adapt_raw(raw: &[String]) -> i32 { + let cli = match parse_graph_adapt_cli(raw) { + Ok(cli) => cli, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let native = match read_provider_native(&cli.request, "graph") { + Ok(value) => value, + Err(message) => return print_graph_route_rejection(&message), + }; + let mut adapter = match translate_graph_native(&native, cli.evaluated_at, cli.max_age_seconds) { + Ok(value) => value, + Err(message) => return print_graph_route_rejection(&message), + }; + let admitted = match crate::admissibility::validate_for_consumer( + &adapter["evidence"]["request"], + &cli.artifact_root, + ) { + Ok(value) => value, + Err(message) => return print_graph_route_rejection(&message), + }; + if let Err(message) = graph_adapter::validate_admitted_payload(admitted.payload(), &adapter) { + return print_graph_route_rejection(&message); + } + adapter["port"]["anatomyUsable"] = json!( + admitted.result()["domainVerdict"] == "observed" + && adapter["port"]["status"] == "current" + && adapter["port"]["freshness"] == "current" + ); + let result = json!({ + "schema":"code-intel-graph-route-result.v1", + "status":"completed", + "adapter":adapter, + "admission":admitted.result(), + "engineeringFacts":[], + "diagnostics":[] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + 0 +} + +struct GraphAdaptCli { + request: String, + artifact_root: PathBuf, + evaluated_at: u64, + max_age_seconds: u64, +} + +fn parse_graph_adapt_cli(raw: &[String]) -> std::result::Result { + let mut request = None; + let mut artifact_root = None; + let mut evaluated_at = None; + let mut max_age_seconds = None; + let mut index = 0; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--request" | "--artifact-root" | "--evaluated-at" | "--max-age-seconds" + ) { + return Err(format!("unknown graph adapter argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + match flag { + "--request" if request.replace(value.clone()).is_some() => { + return Err("duplicate graph adapter argument: --request".to_string()) + } + "--artifact-root" if artifact_root.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate graph adapter argument: --artifact-root".to_string()) + } + "--evaluated-at" => set_u64(&mut evaluated_at, value, flag, "graph")?, + "--max-age-seconds" => set_u64(&mut max_age_seconds, value, flag, "graph")?, + _ => {} + } + index += 2; + } + Ok(GraphAdaptCli { + request: request.ok_or("graph adapter requires --request")?, + artifact_root: artifact_root.ok_or("graph adapter requires --artifact-root")?, + evaluated_at: evaluated_at.ok_or("graph adapter requires --evaluated-at")?, + max_age_seconds: max_age_seconds + .filter(|value| *value > 0) + .ok_or("--max-age-seconds requires a positive integer")?, + }) +} + +fn print_graph_route_rejection(message: &str) -> i32 { + let result = json!({ + "schema":"code-intel-graph-route-result.v1", + "status":"rejected", + "adapter":null, + "admission":null, + "engineeringFacts":[], + "diagnostics":[message] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + eprintln!("{message}"); + 65 +} + +const MAX_REPOWISE_NATIVE_BYTES: u64 = 8 * 1024 * 1024; +const MAX_PROVIDER_NATIVE_BYTES: u64 = 8 * 1024 * 1024; + +pub(crate) fn run_repowise_adapt_raw(raw: &[String]) -> i32 { + let cli = match parse_repowise_adapt_cli(raw) { + Ok(cli) => cli, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let native = match read_repowise_native(&cli.request) { + Ok(value) => value, + Err(message) => return print_repowise_route_rejection(&message), + }; + let adapter = match translate_repowise_native(&native, cli.evaluated_at, cli.max_age_seconds) { + Ok(value) => value, + Err(message) => return print_repowise_route_rejection(&message), + }; + + let mut admissions = Vec::new(); + let mut diagnostics = Vec::new(); + for evidence in adapter["evidence"] + .as_array() + .expect("the adapter always emits an evidence array") + { + let channel = evidence["channel"] + .as_str() + .expect("the adapter always emits a channel"); + match crate::admissibility::validate_for_consumer(&evidence["request"], &cli.artifact_root) + { + Ok(admitted) => admissions.push(json!({ + "channel":channel, + "result":admitted.result() + })), + Err(message) => { + diagnostics.push(format!("{channel}: {message}")); + admissions.push(json!({ + "channel":channel, + "result":rejected_admission(&message) + })); + } + } + } + let rejected = !diagnostics.is_empty(); + let result = json!({ + "schema":"code-intel-repowise-route-result.v1", + "status":if rejected { "rejected" } else { "completed" }, + "adapter":adapter, + "admissions":admissions, + "engineeringFacts":[], + "diagnostics":diagnostics + }); + println!("{}", serde_json::to_string(&result).unwrap()); + if rejected { + 65 + } else { + 0 + } +} + +struct RepowiseAdaptCli { + request: String, + artifact_root: PathBuf, + evaluated_at: u64, + max_age_seconds: u64, +} + +fn parse_repowise_adapt_cli(raw: &[String]) -> std::result::Result { + let mut request = None; + let mut artifact_root = None; + let mut evaluated_at = None; + let mut max_age_seconds = None; + let mut index = 0; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--request" | "--artifact-root" | "--evaluated-at" | "--max-age-seconds" + ) { + return Err(format!("unknown Repowise adapter argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + match flag { + "--request" if request.replace(value.clone()).is_some() => { + return Err("duplicate Repowise adapter argument: --request".to_string()) + } + "--artifact-root" if artifact_root.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate Repowise adapter argument: --artifact-root".to_string()) + } + "--evaluated-at" => set_u64(&mut evaluated_at, value, flag, "Repowise")?, + "--max-age-seconds" => set_u64(&mut max_age_seconds, value, flag, "Repowise")?, + _ => {} + } + index += 2; + } + let max_age_seconds = max_age_seconds + .filter(|value| *value > 0) + .ok_or("--max-age-seconds requires a positive integer")?; + Ok(RepowiseAdaptCli { + request: request.ok_or("Repowise adapter requires --request")?, + artifact_root: artifact_root.ok_or("Repowise adapter requires --artifact-root")?, + evaluated_at: evaluated_at.ok_or("Repowise adapter requires --evaluated-at")?, + max_age_seconds, + }) +} + +fn set_u64( + slot: &mut Option, + value: &str, + flag: &str, + adapter: &str, +) -> std::result::Result<(), String> { + if slot.is_some() { + return Err(format!("duplicate {adapter} adapter argument: {flag}")); + } + *slot = Some( + value + .parse() + .map_err(|_| format!("{flag} requires an unsigned integer"))?, + ); + Ok(()) +} + +fn read_repowise_native(path: &str) -> std::result::Result { + let mut bytes = Vec::new(); + if path == "-" { + std::io::stdin() + .take(MAX_REPOWISE_NATIVE_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| format!("read Repowise native request: {error}"))?; + } else { + let path = Path::new(path); + let metadata = fs::metadata(path) + .map_err(|error| format!("read Repowise native request metadata: {error}"))?; + if !metadata.is_file() { + return Err("Repowise native request must be a regular file".to_string()); + } + if metadata.len() > MAX_REPOWISE_NATIVE_BYTES { + return Err("Repowise native request exceeds size limit".to_string()); + } + bytes = fs::read(path).map_err(|error| format!("read Repowise native request: {error}"))?; + } + if bytes.len() as u64 > MAX_REPOWISE_NATIVE_BYTES { + return Err("Repowise native request exceeds size limit".to_string()); + } + let text = std::str::from_utf8(&bytes) + .map_err(|error| format!("Repowise native request is not UTF-8: {error}"))?; + crate::capability::reject_duplicate_json_keys(text)?; + serde_json::from_str(text) + .map_err(|error| format!("invalid Repowise native request JSON: {error}")) +} + +fn read_provider_native(path: &str, provider: &str) -> std::result::Result { + let mut bytes = Vec::new(); + if path == "-" { + std::io::stdin() + .take(MAX_PROVIDER_NATIVE_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| format!("read {provider} native request: {error}"))?; + } else { + let path = Path::new(path); + let metadata = fs::metadata(path) + .map_err(|error| format!("read {provider} native request metadata: {error}"))?; + if !metadata.is_file() { + return Err(format!("{provider} native request must be a regular file")); + } + if metadata.len() > MAX_PROVIDER_NATIVE_BYTES { + return Err(format!("{provider} native request exceeds size limit")); + } + bytes = + fs::read(path).map_err(|error| format!("read {provider} native request: {error}"))?; + } + if bytes.len() as u64 > MAX_PROVIDER_NATIVE_BYTES { + return Err(format!("{provider} native request exceeds size limit")); + } + let text = std::str::from_utf8(&bytes) + .map_err(|error| format!("{provider} native request is not UTF-8: {error}"))?; + crate::capability::reject_duplicate_json_keys(text)?; + serde_json::from_str(text).map_err(|_| format!("invalid {provider} native request JSON")) +} + +fn print_repowise_route_rejection(message: &str) -> i32 { + let result = json!({ + "schema":"code-intel-repowise-route-result.v1", + "status":"rejected", + "adapter":null, + "admissions":[], + "engineeringFacts":[], + "diagnostics":[message] + }); + println!("{}", serde_json::to_string(&result).unwrap()); + eprintln!("{message}"); + 65 +} + +fn rejected_admission(message: &str) -> Value { + json!({ + "schema":"code-intel-evidence-admissibility-result.v1", + "status":"rejected", + "domainVerdict":"unknown", + "admissionIdentity":null, + "evidence":null, + "verifiedPayload":null, + "engineeringFacts":[], + "diagnostics":[message] + }) +} + pub fn run(options: &Options<'_>) -> Result<()> { let action = options.action.to_ascii_lowercase(); let value = match action.as_str() { @@ -206,8 +860,19 @@ pub fn validate() -> Value { if operation.artifact.trim().is_empty() { errors.push(format!("{key} missing artifact contract")); } + if operation.command_template.contains("code-nexus-lite.exe") + && (operation.required || operation.status == "active") + { + errors.push(format!( + "{key} references removed code-nexus-lite.exe but is active or required" + )); + } } + validate_codenexus_registry(&mut errors); + validate_graph_registry(&mut errors); + validate_sentrux_registry(&mut errors); + json!({ "ok": errors.is_empty(), "schema": "code-intel-provider-api.v1", @@ -216,6 +881,440 @@ pub fn validate() -> Value { }) } +fn validate_sentrux_registry(errors: &mut Vec) { + let Some(operation) = find("sentrux", "adapt") else { + errors.push("missing canonical provider operation: sentrux/adapt".to_string()); + return; + }; + if !operation.required || operation.status != "active" { + errors.push("sentrux/adapt must be an active required provider route".to_string()); + } + let (manifest_path, root) = match orchestration_manifest() { + Ok(value) => value, + Err(error) => { + errors.push(error); + return; + } + }; + let manifest: Value = match fs::read_to_string(&manifest_path) + .map_err(|e| e.to_string()) + .and_then(|text| { + serde_json::from_str(text.trim_start_matches('\u{feff}')).map_err(|e| e.to_string()) + }) { + Ok(value) => value, + Err(error) => { + errors.push(format!( + "cannot read Sentrux provider manifest {}: {error}", + manifest_path.display() + )); + return; + } + }; + let integration = manifest["integrations"].as_array().and_then(|items| { + items + .iter() + .find(|item| item["id"] == "provider.sentrux-adapt") + }); + let Some(integration) = integration else { + errors.push("provider binding missing integration: provider.sentrux-adapt".to_string()); + return; + }; + if integration["required"] != operation.required { + errors.push("provider.sentrux-adapt required flag drifts from sentrux/adapt".to_string()); + } + if integration["commands"]["adapt"] != operation.command_template { + errors.push("provider.sentrux-adapt command drifts from sentrux/adapt".to_string()); + } + if integration["entrypoint"] != "crates/code-intel-cli/src/providers.rs" { + errors.push("provider.sentrux-adapt entrypoint is invalid".to_string()); + } + for path in [ + "orchestration/schemas/code-intel-structural-evidence-port.v1.schema.json", + "orchestration/schemas/code-intel-sentrux-route-result.v1.schema.json", + "docs/sentrux-provider-adapter.md", + ] { + if !root.join(path).is_file() { + errors.push(format!( + "provider.sentrux-adapt contract is missing: {path}" + )); + } + } +} + +fn validate_graph_registry(errors: &mut Vec) { + let Some(operation) = find("graph", "adapt") else { + errors.push("missing canonical provider operation: graph/adapt".to_string()); + return; + }; + if !operation.required || operation.status != "active" { + errors.push("graph/adapt must be an active required provider route".to_string()); + } + + let (manifest_path, root) = match orchestration_manifest() { + Ok(value) => value, + Err(error) => { + errors.push(error); + return; + } + }; + let manifest: Value = match fs::read_to_string(&manifest_path) + .map_err(|error| error.to_string()) + .and_then(|text| { + serde_json::from_str(text.trim_start_matches('\u{feff}')) + .map_err(|error| error.to_string()) + }) { + Ok(value) => value, + Err(error) => { + errors.push(format!( + "cannot read graph provider manifest {}: {error}", + manifest_path.display() + )); + return; + } + }; + let integration = manifest + .get("integrations") + .and_then(Value::as_array) + .and_then(|items| { + items + .iter() + .find(|item| item["id"] == "provider.graph-adapt") + }); + let Some(integration) = integration else { + errors.push("provider binding missing integration: provider.graph-adapt".to_string()); + return; + }; + if integration["required"] != operation.required { + errors.push("provider.graph-adapt required flag drifts from graph/adapt".to_string()); + } + if integration["commands"]["adapt"] != operation.command_template { + errors.push("provider.graph-adapt command drifts from graph/adapt".to_string()); + } + if integration["entrypoint"] != "crates/code-intel-cli/src/providers.rs" + || !root + .join("crates/code-intel-cli/src/providers.rs") + .is_file() + { + errors.push("provider.graph-adapt entrypoint is missing or invalid".to_string()); + } + for schema in [ + "code-intel-architecture-graph-port.v1.schema.json", + "code-intel-graph-route-result.v1.schema.json", + ] { + if !root.join("orchestration/schemas").join(schema).is_file() { + errors.push(format!("provider.graph-adapt schema is missing: {schema}")); + } + } + if !root.join("docs/graph-provider-adapter.md").is_file() { + errors.push("provider.graph-adapt documentation is missing".to_string()); + } +} + +fn validate_codenexus_registry(errors: &mut Vec) { + let Some(canonical) = find("codenexus", "lite") else { + errors.push("missing canonical provider operation: codenexus/lite".to_string()); + return; + }; + if canonical.required || canonical.status != "compatibility" { + errors.push( + "codenexus/lite must reflect its non-blocking compatibility runtime policy".to_string(), + ); + } + + let Some(legacy) = find("repowise", "lite") else { + errors.push("missing legacy provider operation: repowise/lite".to_string()); + return; + }; + if legacy.required + || legacy.status != "compatibility" + || !legacy.notes.to_ascii_lowercase().contains("deprecated") + { + errors.push( + "repowise/lite must be optional and explicitly deprecated compatibility".to_string(), + ); + } + + let (manifest_path, root) = match orchestration_manifest() { + Ok(value) => value, + Err(error) => { + errors.push(error); + return; + } + }; + let manifest_text = match fs::read_to_string(&manifest_path) { + Ok(value) => value, + Err(error) => { + errors.push(format!( + "cannot read orchestration manifest {}: {error}", + manifest_path.display() + )); + return; + } + }; + let manifest: Value = match serde_json::from_str(manifest_text.trim_start_matches('\u{feff}')) { + Ok(value) => value, + Err(error) => { + errors.push(format!( + "cannot parse orchestration manifest {}: {error}", + manifest_path.display() + )); + return; + } + }; + + validate_codenexus_adapter_integration(&manifest, &root, errors); + + validate_codenexus_integration( + &manifest, + &root, + "localization.codenexus-lite", + canonical, + false, + errors, + ); + validate_codenexus_integration( + &manifest, + &root, + "runtime.code-nexus-lite", + legacy, + false, + errors, + ); +} + +fn validate_codenexus_adapter_integration(manifest: &Value, root: &Path, errors: &mut Vec) { + let Some(operation) = find("codenexus", "adapt") else { + errors.push("missing canonical provider operation: codenexus/adapt".to_string()); + return; + }; + if operation.required + || operation.status != "active" + || operation.route != "/api/providers/codenexus/adapt" + || operation.artifact != "code-intel-codenexus-route-result.v1" + { + errors.push("codenexus/adapt provider contract is invalid".to_string()); + } + + let integration = manifest + .get("integrations") + .and_then(Value::as_array) + .and_then(|items| { + items.iter().find(|item| { + item.get("id").and_then(Value::as_str) == Some("provider.codenexus-adapt") + }) + }); + let Some(integration) = integration else { + errors.push("provider binding missing integration: provider.codenexus-adapt".to_string()); + return; + }; + + if integration.get("required").and_then(Value::as_bool) != Some(operation.required) { + errors + .push("provider.codenexus-adapt required flag drifts from codenexus/adapt".to_string()); + } + if integration.get("kind").and_then(Value::as_str) != Some("internal-adapter") { + errors.push("provider.codenexus-adapt kind must be internal-adapter".to_string()); + } + if integration + .get("commands") + .and_then(|commands| commands.get("adapt")) + .and_then(Value::as_str) + != Some(operation.command_template) + { + errors.push("provider.codenexus-adapt command drifts from codenexus/adapt".to_string()); + } + let entrypoint = "crates/code-intel-cli/src/providers.rs"; + if integration.get("entrypoint").and_then(Value::as_str) != Some(entrypoint) + || !root.join(entrypoint).is_file() + { + errors.push("provider.codenexus-adapt entrypoint is missing or invalid".to_string()); + } + + let contracts = integration + .get("artifactContract") + .and_then(Value::as_array); + for path in [ + "orchestration/schemas/code-intel-codenexus-port.v1.schema.json", + "orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json", + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json", + ] { + let declared = + contracts.is_some_and(|items| items.iter().any(|item| item.as_str() == Some(path))); + if !declared || !root.join(path).is_file() { + errors.push(format!( + "provider.codenexus-adapt contract is missing or undeclared: {path}" + )); + } + } + if !root.join("docs/codenexus-provider-adapter.md").is_file() { + errors.push("provider.codenexus-adapt documentation is missing".to_string()); + } +} + +fn validate_codenexus_integration( + manifest: &Value, + root: &Path, + integration_id: &str, + operation: &ProviderOperation, + expected_required: bool, + errors: &mut Vec, +) { + let integration = manifest + .get("integrations") + .and_then(Value::as_array) + .and_then(|items| { + items + .iter() + .find(|item| item.get("id").and_then(Value::as_str) == Some(integration_id)) + }); + let Some(integration) = integration else { + errors.push(format!( + "provider binding missing integration: {integration_id}" + )); + return; + }; + + let entrypoint = integration + .get("entrypoint") + .and_then(Value::as_str) + .unwrap_or_default(); + let command = integration + .get("commands") + .and_then(|commands| commands.get("compat")) + .and_then(Value::as_str) + .unwrap_or_default(); + let required = integration + .get("required") + .and_then(Value::as_bool) + .unwrap_or(false); + let kind = integration + .get("kind") + .and_then(Value::as_str) + .unwrap_or_default(); + + if required != expected_required { + errors.push(format!( + "{integration_id} required={required} drifts from provider contract required={expected_required}" + )); + } + if operation.status == "compatibility" && !kind.starts_with("compatibility") { + errors.push(format!( + "{integration_id} kind={kind} drifts from compatibility provider status" + )); + } + if entrypoint.is_empty() || !root.join(entrypoint).is_file() { + errors.push(format!( + "{integration_id} entrypoint missing from repository: {entrypoint}" + )); + } + let entrypoint_reference = format!(r#"$env:CODE_INTEL_HOME\{entrypoint}"#); + if !command.contains(&entrypoint_reference) { + errors.push(format!( + "{integration_id} command does not invoke its entrypoint: {command}" + )); + } + if command != operation.command_template { + errors.push(format!( + "{integration_id} command drifts from {}/{} provider command: {command}", + operation.provider, operation.operation + )); + } +} + +fn orchestration_manifest() -> std::result::Result<(PathBuf, PathBuf), String> { + if let Ok(explicit) = env::var("CODE_INTEL_INTEGRATIONS_MANIFEST") { + let path = PathBuf::from(explicit); + let path = if path.is_absolute() { + path + } else { + env::current_dir() + .map_err(|error| format!("cannot resolve current directory: {error}"))? + .join(path) + }; + return manifest_candidate(path).ok_or_else(|| { + "CODE_INTEL_INTEGRATIONS_MANIFEST does not identify a readable integrations manifest" + .to_string() + }); + } + + if let Ok(home) = env::var("CODE_INTEL_HOME") { + return manifest_candidate( + PathBuf::from(home) + .join("orchestration") + .join("integrations.json"), + ) + .ok_or_else(|| { + "CODE_INTEL_HOME does not contain orchestration/integrations.json".to_string() + }); + } + + if let Ok(exe) = env::current_exe() { + if let Some(parent) = exe.parent() { + for ancestor in parent.ancestors() { + if let Some(found) = + manifest_candidate(ancestor.join("orchestration").join("integrations.json")) + { + return Ok(found); + } + } + } + } + + let dev_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + if let Some(found) = + manifest_candidate(dev_root.join("orchestration").join("integrations.json")) + { + return Ok(found); + } + + let cwd = + env::current_dir().map_err(|error| format!("cannot resolve current directory: {error}"))?; + for ancestor in cwd.ancestors() { + let candidate = ancestor.join("orchestration").join("integrations.json"); + if is_safe_cwd_manifest(&candidate) { + return manifest_candidate(candidate) + .ok_or_else(|| "validated cwd manifest became unavailable".to_string()); + } + } + Err("trusted orchestration manifest not found; set CODE_INTEL_HOME or CODE_INTEL_INTEGRATIONS_MANIFEST".to_string()) +} + +fn manifest_candidate(path: PathBuf) -> Option<(PathBuf, PathBuf)> { + if !path.is_file() { + return None; + } + let orchestration_dir = path.parent()?; + if !orchestration_dir + .file_name()? + .to_string_lossy() + .eq_ignore_ascii_case("orchestration") + { + return None; + } + let root = orchestration_dir.parent()?.to_path_buf(); + Some((path, root)) +} + +fn is_safe_cwd_manifest(path: &Path) -> bool { + let Ok(text) = fs::read_to_string(path) else { + return false; + }; + let Ok(value) = serde_json::from_str::(text.trim_start_matches('\u{feff}')) else { + return false; + }; + value.pointer("/policy/name").and_then(Value::as_str) + == Some("code-intel-integration-orchestration") + && value + .get("integrations") + .and_then(Value::as_array) + .is_some_and(|items| { + items.iter().any(|item| { + item.get("id").and_then(Value::as_str) == Some("runtime.code-intel") + }) + }) +} + pub fn invoke(options: &Options<'_>) -> Result { let operation = find_required(options)?; let repo_input = options @@ -314,6 +1413,9 @@ fn invoke_repowise(repo: &Path, subcommand: &str, args: &[&str]) -> Result Value { pub fn render_command(operation: &ProviderOperation, repo: Option<&Path>) -> String { match repo { + Some(repo) if operation.command_template.contains("''") => operation + .command_template + .replace("''", &powershell_literal(repo)), Some(repo) => operation .command_template .replace("", &repo.to_string_lossy()), @@ -372,6 +1477,10 @@ pub fn render_command(operation: &ProviderOperation, repo: Option<&Path>) -> Str } } +fn powershell_literal(path: &Path) -> String { + format!("'{}'", path.to_string_lossy().replace('\'', "''")) +} + fn print_human(value: &Value) { if let Some(operations) = value.get("operations").and_then(|value| value.as_array()) { for operation in operations { @@ -398,6 +1507,7 @@ fn tail(value: &str, max_lines: usize) -> String { #[cfg(test)] mod tests { use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn provider_registry_validates() { @@ -424,4 +1534,128 @@ mod tests { assert!(understand.get(key).is_some(), "understand missing {key}"); } } + + #[test] + fn codenexus_registry_uses_real_adapter_and_deprecates_legacy_alias() { + let canonical = find("codenexus", "lite").expect("canonical CodeNexus provider"); + assert_eq!( + canonical.command_template, + r#"pwsh -NoProfile -File "$env:CODE_INTEL_HOME\Invoke-CodeNexusLite.ps1" -RepoPath ''"# + ); + assert!(!canonical.required); + assert_eq!(canonical.status, "compatibility"); + assert!(canonical.notes.contains("non-blocking")); + + let legacy = find("repowise", "lite").expect("legacy Repowise alias"); + assert!(!legacy.required); + assert_eq!(legacy.status, "compatibility"); + assert!(legacy.notes.to_ascii_lowercase().contains("deprecated")); + } + + #[test] + fn codenexus_manifest_command_drift_is_rejected() { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after epoch") + .as_nanos(); + let root = env::temp_dir().join(format!("code-intel-provider-drift-{stamp}")); + fs::create_dir_all(&root).expect("fixture root"); + fs::write(root.join("Invoke-CodeNexusLite.ps1"), "# fixture\n") + .expect("fixture entrypoint"); + let manifest = json!({ + "integrations": [{ + "id": "localization.codenexus-lite", + "required": false, + "kind": "compatibility-adapter", + "entrypoint": "Invoke-CodeNexusLite.ps1", + "commands": {"compat": "target/debug/code-nexus-lite.exe codenexus::lite"} + }] + }); + let mut errors = Vec::new(); + validate_codenexus_integration( + &manifest, + &root, + "localization.codenexus-lite", + find("codenexus", "lite").unwrap(), + false, + &mut errors, + ); + fs::remove_dir_all(&root).expect("fixture cleanup"); + + assert!(errors + .iter() + .any(|error| error.contains("command does not invoke its entrypoint"))); + assert!(errors + .iter() + .any(|error| error.contains("command drifts from codenexus/lite"))); + } + + #[test] + fn codenexus_adapter_registry_rename_is_rejected() { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be after epoch") + .as_nanos(); + let root = env::temp_dir().join(format!("code-intel-codenexus-adapter-drift-{stamp}")); + fs::create_dir_all(root.join("crates/code-intel-cli/src")).expect("fixture source root"); + fs::create_dir_all(root.join("orchestration/schemas")).expect("fixture schema root"); + fs::create_dir_all(root.join("docs")).expect("fixture docs root"); + fs::write( + root.join("crates/code-intel-cli/src/providers.rs"), + "// fixture\n", + ) + .expect("fixture entrypoint"); + for path in [ + "orchestration/schemas/code-intel-codenexus-port.v1.schema.json", + "orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json", + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json", + ] { + fs::write(root.join(path), "{}\n").expect("fixture schema"); + } + fs::write( + root.join("docs/codenexus-provider-adapter.md"), + "# fixture\n", + ) + .expect("fixture docs"); + + let operation = find("codenexus", "adapt").expect("codenexus adapter operation"); + let manifest = json!({ + "integrations": [{ + "id": "provider.codenexus-adapt-drift", + "kind": "internal-adapter", + "required": operation.required, + "entrypoint": "crates/code-intel-cli/src/providers.rs", + "commands": {"adapt": operation.command_template}, + "artifactContract": [ + "orchestration/schemas/code-intel-codenexus-port.v1.schema.json", + "orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json", + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json" + ] + }] + }); + let mut errors = Vec::new(); + validate_codenexus_adapter_integration(&manifest, &root, &mut errors); + fs::remove_dir_all(&root).expect("fixture cleanup"); + + assert!( + errors + .iter() + .any(|error| error + == "provider binding missing integration: provider.codenexus-adapt") + ); + } + + #[test] + fn codenexus_plan_quotes_repo_paths_for_powershell() { + let command = render_command( + find("codenexus", "lite").unwrap(), + Some(Path::new(r"D:\work repo\O'Brien")), + ); + assert_eq!( + command, + r#"pwsh -NoProfile -File "$env:CODE_INTEL_HOME\Invoke-CodeNexusLite.ps1" -RepoPath 'D:\work repo\O''Brien'"# + ); + } } diff --git a/crates/code-intel-cli/src/repowise_adapter.rs b/crates/code-intel-cli/src/repowise_adapter.rs new file mode 100644 index 0000000..b74f0e9 --- /dev/null +++ b/crates/code-intel-cli/src/repowise_adapter.rs @@ -0,0 +1,328 @@ +use std::collections::BTreeSet; + +use serde_json::{json, Value}; + +const SNAPSHOT_DIGEST_LEN: usize = 64; + +pub(crate) fn translate( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> Result { + validate_native(native)?; + if max_age_seconds == 0 { + return Err("Repowise freshness policy max age must be positive".to_string()); + } + + let cli_available = native["cli"]["status"] == "available"; + let health_status = if cli_available { + native["health"]["status"].as_str().unwrap() + } else { + "unavailable" + }; + let health = json!({ + "kind":"health", + "status":health_status, + "evidence":false, + "effects":["process_probe"] + }); + + let mut evidence = Vec::new(); + let index = translate_channel( + native, + "index", + evaluated_at, + max_age_seconds, + cli_available, + &["read_repository", "write_repowise_index"], + &mut evidence, + )?; + let docs = translate_channel( + native, + "docs", + evaluated_at, + max_age_seconds, + cli_available, + &[ + "read_repowise_index", + "network_provider", + "model_inference", + "write_repowise_docs", + ], + &mut evidence, + )?; + + Ok(json!({ + "schema":"code-intel-repowise-adapter-result.v1", + "provider":"repowise", + "health":health, + "index":index, + "docs":docs, + "evidence":evidence, + "factPromotion":{ + "eligible":false, + "requires":"evidence.admissibility-validate", + "engineeringFacts":[] + } + })) +} + +fn translate_channel( + native: &Value, + channel: &str, + evaluated_at: u64, + max_age_seconds: u64, + cli_available: bool, + effects: &[&str], + evidence: &mut Vec, +) -> Result { + let input = &native[channel]; + let native_status = input["status"].as_str().unwrap(); + if !cli_available { + return Ok(channel_summary( + "unavailable", + "none", + "not_observed", + effects, + "local_tool_error", + )); + } + + let (completeness, failure_kind, failure_message) = match (channel, native_status) { + ("index", "current" | "stale") | ("docs", "complete") => ("complete", "none", None), + ("docs", "partial") => ( + "partial", + "domain_unknown", + Some("Repowise docs are incomplete"), + ), + ("docs", "quota") => ( + "partial", + "provider_unavailable", + Some("Repowise docs provider quota unavailable"), + ), + ("docs", "not_requested") => { + return Ok(channel_summary( + native_status, + "none", + "not_applicable", + effects, + "none", + )); + } + ("index", "missing" | "unavailable") | ("docs", "unavailable") => { + return Ok(channel_summary( + native_status, + "none", + "not_observed", + effects, + "provider_unavailable", + )); + } + _ => return Err(format!("Repowise {channel} status is invalid")), + }; + + let observed_at = input["observedAt"] + .as_u64() + .ok_or_else(|| format!("Repowise {channel} observedAt is invalid"))?; + let freshness = freshness_state(observed_at, evaluated_at, max_age_seconds); + let payload = input + .get("payload") + .filter(|value| value.is_object()) + .ok_or_else(|| format!("Repowise {channel} payload is required"))?; + let failure = match failure_message { + Some(message) => json!({"kind":failure_kind,"message":message}), + None => json!({"kind":"none"}), + }; + let request = json!({ + "schema":"code-intel-evidence-admissibility-request.v1", + "expectedSnapshotIdentity":native["snapshotIdentity"], + "policy":{"evaluatedAt":evaluated_at,"maxAgeSeconds":max_age_seconds}, + "observation":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{ + "id":format!("repowise.{channel}"), + "implementation":{ + "id":"provider.repowise-adapt", + "version":native["implementation"]["version"], + "digest":native["implementation"]["digest"] + } + }, + "source":{"revision":native["sourceRevision"]}, + "consumedSnapshotIdentity":native["snapshotIdentity"], + "observedAt":observed_at, + "completeness":completeness, + "claimedComplete":completeness == "complete", + "payload":payload, + "provenance":{ + "collectionId":format!("repowise-{channel}-{}-{observed_at}", native["sourceRevision"].as_str().unwrap()), + "command":format!("repowise adapter:{channel}"), + "startedAt":native["collectedAt"], + "completedAt":observed_at + }, + "failure":failure + } + }); + evidence.push(json!({"channel":channel,"request":request})); + + Ok(channel_summary( + native_status, + completeness, + freshness, + effects, + failure_kind, + )) +} + +fn channel_summary( + status: &str, + completeness: &str, + freshness: &str, + effects: &[&str], + failure_kind: &str, +) -> Value { + json!({ + "status":status, + "completeness":completeness, + "freshness":freshness, + "effects":effects, + "failureKind":failure_kind + }) +} + +fn freshness_state(observed_at: u64, evaluated_at: u64, max_age_seconds: u64) -> &'static str { + if observed_at <= evaluated_at && evaluated_at - observed_at <= max_age_seconds { + "current" + } else { + "stale" + } +} + +fn validate_native(native: &Value) -> Result<(), String> { + exact_optional( + native, + &[ + "schema", + "implementation", + "sourceRevision", + "snapshotIdentity", + "collectedAt", + "cli", + "health", + "index", + "docs", + ], + &["diagnostic"], + "Repowise native result", + )?; + if native["schema"] != "code-intel-repowise-native-result.v1" { + return Err("Repowise native result schema is invalid".to_string()); + } + exact( + &native["implementation"], + &["version", "digest"], + "Repowise implementation", + )?; + nonempty(&native["implementation"]["version"], "Repowise version")?; + digest( + &native["implementation"]["digest"], + "Repowise implementation digest", + )?; + nonempty(&native["sourceRevision"], "Repowise source revision")?; + digest(&native["snapshotIdentity"], "Repowise snapshot identity")?; + native["collectedAt"] + .as_u64() + .ok_or("Repowise collectedAt is invalid")?; + exact(&native["cli"], &["status"], "Repowise CLI status")?; + if !matches!( + native["cli"]["status"].as_str(), + Some("available" | "missing") + ) { + return Err("Repowise CLI status is invalid".to_string()); + } + exact(&native["health"], &["status"], "Repowise health")?; + if !matches!( + native["health"]["status"].as_str(), + Some("healthy" | "unavailable") + ) { + return Err("Repowise health status is invalid".to_string()); + } + validate_channel_shape(&native["index"], "index")?; + validate_channel_shape(&native["docs"], "docs")?; + Ok(()) +} + +fn validate_channel_shape(value: &Value, channel: &str) -> Result<(), String> { + exact_optional( + value, + &["status"], + &["observedAt", "payload"], + &format!("Repowise {channel}"), + )?; + let status = value["status"] + .as_str() + .ok_or_else(|| format!("Repowise {channel} status is invalid"))?; + let allowed = if channel == "index" { + ["current", "stale", "missing", "unavailable"].as_slice() + } else { + [ + "complete", + "partial", + "quota", + "not_requested", + "unavailable", + ] + .as_slice() + }; + if !allowed.contains(&status) { + return Err(format!("Repowise {channel} status is invalid")); + } + Ok(()) +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + exact_optional(value, fields, &[], label) +} + +fn exact_optional( + value: &Value, + required: &[&str], + optional: &[&str], + label: &str, +) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let required = required.iter().copied().collect::>(); + let allowed = required + .iter() + .copied() + .chain(optional.iter().copied()) + .collect::>(); + if required.is_subset(&actual) && actual.is_subset(&allowed) { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn nonempty(value: &Value, label: &str) -> Result<(), String> { + if value.as_str().is_some_and(|value| !value.is_empty()) { + Ok(()) + } else { + Err(format!("{label} is invalid")) + } +} + +fn digest(value: &Value, label: &str) -> Result<(), String> { + if value.as_str().is_some_and(|value| { + value.len() == SNAPSHOT_DIGEST_LEN + && value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + }) { + Ok(()) + } else { + Err(format!("{label} is invalid")) + } +} diff --git a/crates/code-intel-cli/src/run_commit.rs b/crates/code-intel-cli/src/run_commit.rs new file mode 100644 index 0000000..e03b19c --- /dev/null +++ b/crates/code-intel-cli/src/run_commit.rs @@ -0,0 +1,923 @@ +use std::collections::BTreeSet; +use std::fmt; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use serde_json::{json, Value}; + +use crate::artifact_ref; +use crate::capability::{reject_duplicate_json_keys, sha256_hex, validate_artifact_ref_shape}; +use crate::stable_artifact; +use crate::staged_artifact::{self, ArtifactWriteContract, StagedArtifactSet, StagedWriter}; + +const MAX_MANIFEST_BYTES: u64 = 8 * 1024 * 1024; +const MAX_MARKER_BYTES: u64 = 64 * 1024; +static MARKER_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PublicationPhase { + Prevalidate, + Rename, + DirectorySync, + MarkerTemp, + MarkerPublish, + PostMarkerVerify, + Rollback, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct CommitOptions { + pub(crate) interrupt_before: Option, + pub(crate) fail_marker_sync: bool, + pub(crate) fail_marker_read: bool, +} + +#[derive(Debug)] +pub(crate) enum CommitError { + Contract(String), + Collision(String), + Interrupted(PublicationPhase), + HostIo(String), +} + +impl fmt::Display for CommitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Contract(message) | Self::Collision(message) | Self::HostIo(message) => { + f.write_str(message) + } + Self::Interrupted(phase) => write!(f, "injected interruption before {phase:?}"), + } + } +} + +impl std::error::Error for CommitError {} + +#[derive(Clone, Debug)] +pub(crate) struct CommitResult { + pub(crate) final_path: PathBuf, + pub(crate) marker: Value, +} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + match parse_cli(raw).and_then(execute_cli) { + Ok(result) => { + println!("{}", serde_json::to_string(&result.marker).unwrap()); + 0 + } + Err(CommitError::Contract(message) | CommitError::Collision(message)) => { + eprintln!("{message}"); + 65 + } + Err(CommitError::HostIo(message)) => { + eprintln!("{message}"); + 74 + } + Err(CommitError::Interrupted(phase)) => { + eprintln!("injected interruption before {phase:?}"); + 75 + } + } +} + +struct CommitCli { + source_root: PathBuf, + authority_root: PathBuf, + manifest_ref: PathBuf, + final_name: String, +} + +fn parse_cli(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("commit") { + return Err(CommitError::Contract("usage: run commit --source-root --authority-root --manifest-ref --final-name ".to_string())); + } + let mut source_root = None; + let mut authority_root = None; + let mut manifest_ref = None; + let mut final_name = None; + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--source-root" | "--authority-root" | "--manifest-ref" | "--final-name" + ) { + return Err(CommitError::Contract(format!( + "unknown run commit argument: {flag}" + ))); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| CommitError::Contract(format!("{flag} requires one value")))?; + let target = match flag { + "--source-root" => &mut source_root, + "--authority-root" => &mut authority_root, + "--manifest-ref" => &mut manifest_ref, + "--final-name" => { + if final_name.replace(value.clone()).is_some() { + return Err(CommitError::Contract("duplicate --final-name".to_string())); + } + index += 2; + continue; + } + _ => unreachable!(), + }; + if target.replace(PathBuf::from(value)).is_some() { + return Err(CommitError::Contract(format!("duplicate {flag}"))); + } + index += 2; + } + let cli = CommitCli { + source_root: source_root + .ok_or_else(|| CommitError::Contract("--source-root is required".to_string()))?, + authority_root: authority_root + .ok_or_else(|| CommitError::Contract("--authority-root is required".to_string()))?, + manifest_ref: manifest_ref + .ok_or_else(|| CommitError::Contract("--manifest-ref is required".to_string()))?, + final_name: final_name + .ok_or_else(|| CommitError::Contract("--final-name is required".to_string()))?, + }; + if !cli.source_root.is_dir() || !cli.authority_root.is_dir() { + return Err(CommitError::Contract( + "source and authority roots must be existing directories".to_string(), + )); + } + validate_final_name(&cli.final_name)?; + Ok(cli) +} + +fn execute_cli(cli: CommitCli) -> Result { + let bytes = fs::read(&cli.manifest_ref) + .map_err(|error| CommitError::HostIo(format!("read manifest Artifact Ref: {error}")))?; + let text = std::str::from_utf8(&bytes) + .map_err(|_| CommitError::Contract("manifest Artifact Ref is not UTF-8".to_string()))?; + reject_duplicate_json_keys(text).map_err(CommitError::Contract)?; + let source_manifest_ref: Value = serde_json::from_str(text) + .map_err(|_| CommitError::Contract("manifest Artifact Ref is invalid JSON".to_string()))?; + let manifest = prevalidate_existing(&cli.source_root, &source_manifest_ref)?; + let snapshot = manifest["snapshotIdentity"].as_str().unwrap(); + let source_refs = manifest_artifact_refs(&manifest)?; + let verified = artifact_ref::verify_inputs( + &Value::Array(source_refs.clone()), + Some(&cli.source_root), + snapshot, + ) + .map_err(|error| CommitError::Contract(error.message().to_string()))?; + let mut writer = StagedWriter::begin(&cli.authority_root, snapshot).map_err(map_stage_error)?; + let mut published_refs = Vec::with_capacity(source_refs.len()); + for (source_ref, artifact) in source_refs.iter().zip(verified.iter()) { + let contract = artifact_ref::registered_contract(source_ref) + .map_err(|error| CommitError::Contract(error.message().to_string()))?; + let staged_ref = writer + .stage( + artifact.bytes(), + ArtifactWriteContract { + artifact_schema: contract.artifact_schema, + artifact_type: contract.artifact_type, + max_bytes: contract.max_bytes, + validate_payload: contract.validate_payload, + }, + ) + .map_err(map_stage_error)?; + published_refs.push(staged_ref.to_artifact_ref_value()); + } + let mut published_manifest = manifest; + replace_manifest_artifact_refs(&mut published_manifest, &published_refs)?; + let manifest_bytes = serde_json::to_vec(&published_manifest) + .map_err(|error| CommitError::Contract(format!("serialize published manifest: {error}")))?; + let staged_manifest_ref = writer + .stage( + &manifest_bytes, + ArtifactWriteContract { + artifact_schema: "code-intel-run-manifest.v1", + artifact_type: "run.manifest", + max_bytes: MAX_MANIFEST_BYTES, + validate_payload: validate_run_manifest_bytes, + }, + ) + .map_err(map_stage_error)? + .to_artifact_ref_value(); + let staged = writer.seal().map_err(map_stage_error)?; + commit( + staged, + &staged_manifest_ref, + &cli.final_name, + CommitOptions::default(), + ) +} + +fn replace_manifest_artifact_refs( + manifest: &mut Value, + replacements: &[Value], +) -> Result<(), CommitError> { + let nodes = manifest["nodes"] + .as_object_mut() + .ok_or_else(|| CommitError::Contract("run manifest nodes must be an object".to_string()))?; + let mut replacement = replacements.iter(); + for node in nodes.values_mut() { + if node["status"] != "succeeded" && node["status"] != "domain_failed" { + continue; + } + let artifacts = node["artifacts"].as_array_mut().ok_or_else(|| { + CommitError::Contract( + "artifact-producing run node artifacts must be an array".to_string(), + ) + })?; + for artifact in artifacts { + *artifact = replacement + .next() + .ok_or_else(|| { + CommitError::Contract( + "published Artifact Ref count is smaller than the run manifest".to_string(), + ) + })? + .clone(); + } + } + if replacement.next().is_some() { + return Err(CommitError::Contract( + "published Artifact Ref count is larger than the run manifest".to_string(), + )); + } + Ok(()) +} + +pub(crate) fn commit( + mut staged: StagedArtifactSet, + manifest_ref: &Value, + final_name: &str, + options: CommitOptions, +) -> Result { + interrupt(&options, PublicationPhase::Prevalidate)?; + validate_final_name(final_name)?; + let stage_path = staged.path().to_path_buf(); + let authority_root = staged.authority_root().to_path_buf(); + let manifest = prevalidate(staged.artifacts(), &stage_path, manifest_ref)?; + staged + .prepare_for_commit() + .map_err(|error| CommitError::HostIo(error.to_string()))?; + + interrupt(&options, PublicationPhase::Rename)?; + let final_path = authority_root.join(final_name); + rename_directory_no_replace(&stage_path, &final_path)?; + + interrupt_after_promotion(&options, PublicationPhase::DirectorySync, &final_path)?; + sync_directory(&authority_root)?; + + publish_marker(&final_path, manifest_ref, &manifest, &options) +} + +pub(crate) fn recover( + final_path: &Path, + manifest_ref: &Value, + options: CommitOptions, +) -> Result { + if final_path.join("run-complete.json").exists() { + return Err(CommitError::Collision( + "run is already committed or has a competing completion marker".to_string(), + )); + } + interrupt(&options, PublicationPhase::Prevalidate)?; + let manifest = prevalidate_existing(final_path, manifest_ref)?; + let authority_root = final_path.parent().ok_or_else(|| { + CommitError::Contract("recoverable run has no authority root".to_string()) + })?; + sync_directory(authority_root)?; + publish_marker(final_path, manifest_ref, &manifest, &options) +} + +pub(crate) fn classify(path: &Path) -> &'static str { + if path + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| { + name.starts_with(".staging") || name.starts_with("stage-") || name.contains(".staging-") + }) + { + return "staged"; + } + match validate_published_marker(path) { + Ok(_) => "committed", + Err(_) if path.is_dir() => "legacy-uncommitted", + Err(_) => "invalid", + } +} + +pub(crate) fn validate_run_manifest_bytes(bytes: &[u8]) -> Result<(), String> { + if bytes.len() as u64 > MAX_MANIFEST_BYTES { + return Err("run manifest exceeds size limit".to_string()); + } + let text = std::str::from_utf8(bytes).map_err(|_| "run manifest is not UTF-8".to_string())?; + reject_duplicate_json_keys(text)?; + let value: Value = + serde_json::from_str(text).map_err(|_| "run manifest is invalid JSON".to_string())?; + validate_run_manifest(&value) +} + +fn prevalidate( + staged_refs: &[crate::staged_artifact::StagedArtifactRef], + root: &Path, + manifest_ref: &Value, +) -> Result { + let declared = staged_refs + .iter() + .map(|item| item.to_artifact_ref_value()) + .collect::>(); + if !declared.iter().any(|item| item == manifest_ref) { + return Err(CommitError::Contract( + "run manifest Artifact Ref is not owned by the staged set".to_string(), + )); + } + prevalidate_refs(root, &declared, manifest_ref) +} + +fn prevalidate_existing(root: &Path, manifest_ref: &Value) -> Result { + let manifest = read_manifest(root, manifest_ref)?; + let refs = manifest_artifact_refs(&manifest)?; + let mut all = refs; + all.push(manifest_ref.clone()); + prevalidate_refs(root, &all, manifest_ref) +} + +fn prevalidate_refs( + root: &Path, + refs: &[Value], + manifest_ref: &Value, +) -> Result { + validate_artifact_ref_shape(manifest_ref).map_err(CommitError::Contract)?; + if manifest_ref["artifactSchema"] != "code-intel-run-manifest.v1" + || manifest_ref["type"] != "run.manifest" + { + return Err(CommitError::Contract( + "run manifest Artifact Ref contract is invalid".to_string(), + )); + } + let manifest = read_manifest(root, manifest_ref)?; + let snapshot = manifest["snapshotIdentity"].as_str().unwrap(); + if manifest_ref["consumedSnapshotIdentity"] != snapshot { + return Err(CommitError::Contract( + "run manifest Artifact Ref snapshot does not match the manifest".to_string(), + )); + } + let manifest_refs = manifest_artifact_refs(&manifest)?; + let declared_non_manifest = refs + .iter() + .filter(|item| *item != manifest_ref) + .map(ref_identity) + .collect::, _>>()?; + let manifest_identities = manifest_refs + .iter() + .map(ref_identity) + .collect::, _>>()?; + if declared_non_manifest.len() != refs.iter().filter(|item| *item != manifest_ref).count() + || manifest_identities.len() != manifest_refs.len() + { + return Err(CommitError::Contract( + "run publication contains duplicate Artifact Refs".to_string(), + )); + } + if declared_non_manifest != manifest_identities { + return Err(CommitError::Contract( + "staged Artifact Refs do not exactly match the complete run manifest".to_string(), + )); + } + artifact_ref::verify_inputs(&Value::Array(manifest_refs), Some(root), snapshot) + .map_err(|error| CommitError::Contract(error.message().to_string()))?; + Ok(manifest) +} + +fn read_manifest(root: &Path, manifest_ref: &Value) -> Result { + validate_artifact_ref_shape(manifest_ref).map_err(CommitError::Contract)?; + let path = manifest_ref["path"] + .as_str() + .ok_or_else(|| CommitError::Contract("run manifest path is invalid".to_string()))?; + let components = path.split('/').collect::>(); + let stable = stable_artifact::read_beneath(root, &components, MAX_MANIFEST_BYTES) + .map_err(map_stable_error)?; + let digest = sha256_hex(&stable.bytes); + if manifest_ref["sha256"] != digest { + return Err(CommitError::Contract( + "run manifest digest mismatch".to_string(), + )); + } + validate_run_manifest_bytes(&stable.bytes).map_err(CommitError::Contract)?; + serde_json::from_slice(&stable.bytes) + .map_err(|_| CommitError::Contract("run manifest is invalid JSON".to_string())) +} + +fn validate_run_manifest(value: &Value) -> Result<(), String> { + exact( + value, + &[ + "schema", + "runIdentity", + "snapshotIdentity", + "outcome", + "nodes", + ], + "run manifest", + )?; + if value["schema"] != "code-intel-run-manifest.v1" + || !value["runIdentity"] + .as_str() + .is_some_and(valid_run_identity) + || !value["snapshotIdentity"].as_str().is_some_and(valid_digest) + || !matches!( + value["outcome"].as_str(), + Some("completed" | "domain_failed" | "domain_unknown" | "process_failed") + ) + { + return Err("run manifest identity/outcome is incomplete or invalid".to_string()); + } + let nodes = value["nodes"] + .as_object() + .ok_or("run manifest nodes must be an object")?; + if nodes.is_empty() { + return Err("run manifest must contain at least one terminal node".to_string()); + } + for (id, node) in nodes { + if !id + .bytes() + .next() + .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()) + || !id.bytes().all(|b| { + b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-') + }) + { + return Err("run manifest node id is invalid".to_string()); + } + match node["status"].as_str() { + Some("succeeded") => { + exact( + node, + &["status", "verdict", "artifacts"], + "succeeded run node", + )?; + if !matches!( + node["verdict"].as_str(), + Some("pass" | "unknown" | "not_applicable") + ) || !node["artifacts"].is_array() + { + return Err("succeeded run node is invalid".to_string()); + } + } + Some("domain_failed") => { + exact( + node, + &["status", "verdict", "diagnostic", "artifacts"], + "domain-failed run node", + )?; + if node["verdict"] != "fail" + || !node["diagnostic"] + .as_str() + .is_some_and(|text| !text.is_empty()) + || !node["artifacts"].is_array() + { + return Err("domain-failed run node is invalid".to_string()); + } + } + Some("process_failed") => { + exact( + node, + &["status", "failure", "diagnostic"], + "process-failed run node", + )?; + if !matches!( + node["failure"].as_str(), + Some("contract" | "unavailable" | "internal" | "io") + ) || !node["diagnostic"] + .as_str() + .is_some_and(|text| !text.is_empty()) + { + return Err("process-failed run node is invalid".to_string()); + } + } + Some("dependency_blocked") => { + exact(node, &["status", "blockedBy"], "blocked run node")?; + let blocked = node["blockedBy"] + .as_array() + .ok_or("blocked run node is invalid")?; + let unique = blocked + .iter() + .filter_map(Value::as_str) + .collect::>(); + if blocked.is_empty() + || blocked + .iter() + .any(|item| !item.as_str().is_some_and(|text| !text.is_empty())) + || unique.len() != blocked.len() + { + return Err("blocked run node is invalid".to_string()); + } + } + _ => return Err("run manifest contains a non-terminal node".to_string()), + } + } + Ok(()) +} + +fn manifest_artifact_refs(manifest: &Value) -> Result, CommitError> { + let mut refs = Vec::new(); + for node in manifest["nodes"].as_object().unwrap().values() { + if node["status"] == "succeeded" || node["status"] == "domain_failed" { + for item in node["artifacts"].as_array().unwrap() { + validate_artifact_ref_shape(item).map_err(CommitError::Contract)?; + refs.push(item.clone()); + } + } + } + Ok(refs) +} + +fn publish_marker( + final_path: &Path, + manifest_ref: &Value, + manifest: &Value, + options: &CommitOptions, +) -> Result { + interrupt_after_promotion(options, PublicationPhase::MarkerTemp, final_path)?; + let marker = json!({ + "schema":"code-intel-run-commit.v1", + "runIdentity":manifest["runIdentity"], + "snapshotIdentity":manifest["snapshotIdentity"], + "manifest":{"path":manifest_ref["path"],"sha256":manifest_ref["sha256"]} + }); + let bytes = serde_json::to_vec(&marker).unwrap(); + let marker_path = final_path.join("run-complete.json"); + let (temp_path, temp_name, mut temp) = create_marker_temp(final_path)?; + if let Err(error) = temp.write_all(&bytes).and_then(|_| temp.sync_all()) { + drop(temp); + return Err(CommitError::HostIo(format!( + "write completion marker: {error}; owned temp retained for cleanup: {}", + temp_path.display() + ))); + } + drop(temp); + let temp_identity = + stable_artifact::read_beneath(final_path, &[temp_name.as_str()], MAX_MARKER_BYTES) + .map_err(map_stable_error)? + .id; + if let Err(error) = interrupt(options, PublicationPhase::MarkerPublish) { + remove_owned_marker(final_path, &temp_path, &bytes, temp_identity); + return Err(error); + } + if let Err(error) = rename_file_no_replace(&temp_path, &marker_path) { + remove_owned_marker(final_path, &temp_path, &bytes, temp_identity); + return Err(error); + } + let marker_sync = if options.fail_marker_sync { + Err(CommitError::HostIo( + "injected completion marker directory sync failure".to_string(), + )) + } else { + sync_directory(final_path) + }; + if let Err(error) = marker_sync { + remove_owned_marker(final_path, &marker_path, &bytes, temp_identity); + let _ = sync_directory(final_path); + return Err(error); + } + if let Err(error) = interrupt(options, PublicationPhase::PostMarkerVerify) { + remove_owned_marker(final_path, &marker_path, &bytes, temp_identity); + let _ = sync_directory(final_path); + return Err(error); + } + let verified = if options.fail_marker_read { + Err(CommitError::HostIo( + "injected completion marker read failure".to_string(), + )) + } else { + validate_published_marker(final_path) + }; + let verified = match verified { + Ok(marker) => marker, + Err(error) => { + remove_owned_marker(final_path, &marker_path, &bytes, temp_identity); + let _ = sync_directory(final_path); + return Err(error); + } + }; + if verified != marker { + remove_owned_marker(final_path, &marker_path, &bytes, temp_identity); + let _ = sync_directory(final_path); + return Err(CommitError::Contract( + "published completion marker verification failed".to_string(), + )); + } + if options.interrupt_before == Some(PublicationPhase::Rollback) { + remove_owned_marker(final_path, &marker_path, &bytes, temp_identity); + let _ = sync_directory(final_path); + return Err(CommitError::Interrupted(PublicationPhase::Rollback)); + } + Ok(CommitResult { + final_path: final_path.to_path_buf(), + marker, + }) +} + +fn create_marker_temp(final_path: &Path) -> Result<(PathBuf, String, fs::File), CommitError> { + for _ in 0..1024 { + let sequence = MARKER_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); + let name = format!(".run-complete.json.tmp.{}.{}", std::process::id(), sequence); + let path = final_path.join(&name); + match OpenOptions::new().write(true).create_new(true).open(&path) { + Ok(file) => return Ok((path, name, file)), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => { + return Err(CommitError::HostIo(format!( + "create completion marker temp: {error}" + ))) + } + } + } + Err(CommitError::Collision( + "completion marker temp namespace is exhausted".to_string(), + )) +} + +pub(crate) fn validate_committed_run(root: &Path) -> Result<(Value, Value), CommitError> { + let marker = validate_published_marker(root)?; + let manifest_ref = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-run-manifest.v1", + "type":"run.manifest", + "path":marker["manifest"]["path"], + "sha256":marker["manifest"]["sha256"], + "consumedSnapshotIdentity":marker["snapshotIdentity"] + }); + let manifest = prevalidate_existing(root, &manifest_ref)?; + Ok((marker, manifest)) +} + +fn validate_published_marker(root: &Path) -> Result { + let stable = stable_artifact::read_beneath(root, &["run-complete.json"], MAX_MARKER_BYTES) + .map_err(map_stable_error)?; + let text = std::str::from_utf8(&stable.bytes) + .map_err(|_| CommitError::Contract("completion marker is not UTF-8".to_string()))?; + reject_duplicate_json_keys(text).map_err(CommitError::Contract)?; + let marker: Value = serde_json::from_str(text) + .map_err(|_| CommitError::Contract("completion marker is invalid JSON".to_string()))?; + exact( + &marker, + &["schema", "runIdentity", "snapshotIdentity", "manifest"], + "completion marker", + ) + .map_err(CommitError::Contract)?; + exact( + &marker["manifest"], + &["path", "sha256"], + "completion marker manifest", + ) + .map_err(CommitError::Contract)?; + if marker["schema"] != "code-intel-run-commit.v1" + || !marker["runIdentity"] + .as_str() + .is_some_and(valid_run_identity) + || !marker["snapshotIdentity"] + .as_str() + .is_some_and(valid_digest) + || !marker["manifest"]["sha256"] + .as_str() + .is_some_and(valid_digest) + { + return Err(CommitError::Contract( + "completion marker fields are invalid".to_string(), + )); + } + let manifest_ref = json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-run-manifest.v1","type":"run.manifest","path":marker["manifest"]["path"],"sha256":marker["manifest"]["sha256"],"consumedSnapshotIdentity":marker["snapshotIdentity"]}); + let manifest = read_manifest(root, &manifest_ref)?; + if manifest["runIdentity"] != marker["runIdentity"] + || manifest["snapshotIdentity"] != marker["snapshotIdentity"] + { + return Err(CommitError::Contract( + "completion marker identity does not bind the run manifest".to_string(), + )); + } + Ok(marker) +} + +fn ref_identity(value: &Value) -> Result<(String, String, String, String), CommitError> { + validate_artifact_ref_shape(value).map_err(CommitError::Contract)?; + Ok(( + value["path"].as_str().unwrap().to_string(), + value["sha256"].as_str().unwrap().to_string(), + value["artifactSchema"].as_str().unwrap().to_string(), + value["type"].as_str().unwrap().to_string(), + )) +} + +fn validate_final_name(name: &str) -> Result<(), CommitError> { + if name.is_empty() + || name == "." + || name == ".." + || name.contains(['/', '\\']) + || name.starts_with('.') + || name.contains("staging") + { + Err(CommitError::Contract( + "final run name is invalid".to_string(), + )) + } else { + Ok(()) + } +} + +fn interrupt(options: &CommitOptions, phase: PublicationPhase) -> Result<(), CommitError> { + if options.interrupt_before == Some(phase) { + Err(CommitError::Interrupted(phase)) + } else { + Ok(()) + } +} + +fn interrupt_after_promotion( + options: &CommitOptions, + phase: PublicationPhase, + final_path: &Path, +) -> Result<(), CommitError> { + interrupt(options, phase).map_err(|error| { + debug_assert!(final_path.is_dir()); + error + }) +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn valid_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f')) +} +fn valid_run_identity(value: &str) -> bool { + value.strip_prefix("dag-v1:").is_some_and(|tail| { + !tail.is_empty() + && tail.len() % 2 == 0 + && tail + .bytes() + .all(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f')) + }) +} + +fn remove_owned_marker( + root: &Path, + path: &Path, + expected: &[u8], + expected_identity: stable_artifact::FileId, +) { + let Some(name) = path.file_name().and_then(|name| name.to_str()) else { + return; + }; + if stable_artifact::read_beneath(root, &[name], MAX_MARKER_BYTES) + .ok() + .is_some_and(|stable| stable.id == expected_identity && stable.bytes == expected) + { + let _ = fs::remove_file(path); + } +} + +fn map_stable_error(error: stable_artifact::StableReadError) -> CommitError { + match error { + stable_artifact::StableReadError::HostIo(message) => CommitError::HostIo(message), + stable_artifact::StableReadError::TooLarge(message) + | stable_artifact::StableReadError::Boundary(message) + | stable_artifact::StableReadError::Identity(message) => CommitError::Contract(message), + } +} + +fn map_stage_error(error: staged_artifact::StageWriteError) -> CommitError { + match error { + staged_artifact::StageWriteError::Contract(message) + | staged_artifact::StageWriteError::Boundary(message) => CommitError::Contract(message), + staged_artifact::StageWriteError::Collision(message) => CommitError::Collision(message), + staged_artifact::StageWriteError::Interrupted(phase) => { + CommitError::HostIo(format!("A06 staging interrupted after {phase:?}")) + } + staged_artifact::StageWriteError::HostIo(message) => CommitError::HostIo(message), + } +} + +fn sync_directory(path: &Path) -> Result<(), CommitError> { + staged_artifact::sync_directory_path(path) + .map_err(|error| CommitError::HostIo(error.to_string())) +} + +#[cfg(windows)] +fn rename_directory_no_replace(source: &Path, destination: &Path) -> Result<(), CommitError> { + rename_windows_no_replace(source, destination, "promote staged run") +} +#[cfg(windows)] +fn rename_file_no_replace(source: &Path, destination: &Path) -> Result<(), CommitError> { + rename_windows_no_replace(source, destination, "publish completion marker") +} + +#[cfg(windows)] +fn rename_windows_no_replace( + source: &Path, + destination: &Path, + action: &str, +) -> Result<(), CommitError> { + use std::os::windows::ffi::OsStrExt; + unsafe extern "system" { + fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32; + } + let source = source + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect::>(); + let destination = destination + .as_os_str() + .encode_wide() + .chain(Some(0)) + .collect::>(); + let result = unsafe { MoveFileExW(source.as_ptr(), destination.as_ptr(), 0) }; + if result != 0 { + Ok(()) + } else { + Err(CommitError::Collision(format!( + "{action} without replacement: {}", + std::io::Error::last_os_error() + ))) + } +} + +#[cfg(target_os = "linux")] +fn rename_directory_no_replace(source: &Path, destination: &Path) -> Result<(), CommitError> { + rename_linux_no_replace(source, destination, "promote staged run") +} +#[cfg(target_os = "linux")] +fn rename_file_no_replace(source: &Path, destination: &Path) -> Result<(), CommitError> { + rename_linux_no_replace(source, destination, "publish completion marker") +} + +#[cfg(target_os = "linux")] +fn rename_linux_no_replace( + source: &Path, + destination: &Path, + action: &str, +) -> Result<(), CommitError> { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + unsafe extern "C" { + fn renameat2( + olddirfd: i32, + oldpath: *const i8, + newdirfd: i32, + newpath: *const i8, + flags: u32, + ) -> i32; + } + const AT_FDCWD: i32 = -100; + const RENAME_NOREPLACE: u32 = 1; + let source = CString::new(source.as_os_str().as_bytes()) + .map_err(|_| CommitError::Contract("source path contains NUL".to_string()))?; + let destination = CString::new(destination.as_os_str().as_bytes()) + .map_err(|_| CommitError::Contract("destination path contains NUL".to_string()))?; + let result = unsafe { + renameat2( + AT_FDCWD, + source.as_ptr(), + AT_FDCWD, + destination.as_ptr(), + RENAME_NOREPLACE, + ) + }; + if result == 0 { + Ok(()) + } else { + Err(CommitError::Collision(format!( + "{action} without replacement: {}", + std::io::Error::last_os_error() + ))) + } +} + +#[cfg(not(any(windows, target_os = "linux")))] +fn rename_directory_no_replace(_: &Path, _: &Path) -> Result<(), CommitError> { + Err(CommitError::HostIo( + "atomic no-replace directory promotion is unsupported on this platform".to_string(), + )) +} +#[cfg(not(any(windows, target_os = "linux")))] +fn rename_file_no_replace(_: &Path, _: &Path) -> Result<(), CommitError> { + Err(CommitError::HostIo( + "atomic no-replace marker publication is unsupported on this platform".to_string(), + )) +} diff --git a/crates/code-intel-cli/src/runtime_ci_evidence.rs b/crates/code-intel-cli/src/runtime_ci_evidence.rs new file mode 100644 index 0000000..3b4f8bf --- /dev/null +++ b/crates/code-intel-cli/src/runtime_ci_evidence.rs @@ -0,0 +1,466 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Component, Path}; + +use serde_json::{json, Value}; + +const REQUEST_SCHEMA: &str = "code-intel-runtime-ci-ingest-request.v1"; +const SOURCE_SCHEMA: &str = "code-intel-runtime-ci-observation.v1"; +const SUMMARY_SCHEMA: &str = "code-intel-runtime-ci-summary.v1"; +const MAX_ARTIFACT_BYTES: u64 = 4 * 1024 * 1024; + +pub(crate) fn parse_request_bytes(bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes) + .map_err(|_| "runtime/CI request must be UTF-8 JSON".to_string())?; + reject_duplicate_json_keys(text)?; + serde_json::from_str(text) + .map_err(|error| format!("runtime/CI request is invalid JSON: {error}")) +} + +pub(crate) fn ingest_request(artifact_root: &Path, request: &Value) -> Result { + require_object_keys( + request, + &["schema", "expectedSnapshotIdentity", "artifact", "policy"], + "request", + )?; + require_const(request, "schema", REQUEST_SCHEMA, "request")?; + let expected_snapshot = digest_field(request, "expectedSnapshotIdentity", "request")?; + let artifact = object_field(request, "artifact", "request")?; + require_object_keys(artifact, &["path", "sha256"], "request.artifact")?; + let relative = string_field(artifact, "path", "request.artifact")?; + let expected_digest = digest_field(artifact, "sha256", "request.artifact")?; + let policy = object_field(request, "policy", "request")?; + require_object_keys(policy, &["evaluatedAt", "maxAgeSeconds"], "request.policy")?; + let evaluated_at = integer_field(policy, "evaluatedAt", "request.policy")?; + let max_age = positive_integer_field(policy, "maxAgeSeconds", "request.policy")?; + let path = safe_join(artifact_root, relative)?; + + if !path.is_file() { + return Ok(unknown_summary( + expected_snapshot, + "missing", + "missing", + "artifact_missing", + )); + } + let canonical_root = fs::canonicalize(artifact_root) + .map_err(|error| format!("resolve runtime/CI artifact root: {error}"))?; + let canonical_path = fs::canonicalize(&path) + .map_err(|error| format!("resolve runtime/CI artifact path: {error}"))?; + if !canonical_path.starts_with(&canonical_root) { + return Err("runtime/CI artifact resolves outside the artifact root".into()); + } + let length = fs::metadata(&canonical_path) + .map_err(|error| format!("inspect runtime/CI artifact: {error}"))? + .len(); + if length > MAX_ARTIFACT_BYTES { + return Err(format!( + "runtime/CI artifact exceeds {MAX_ARTIFACT_BYTES} bytes" + )); + } + let bytes = + fs::read(&canonical_path).map_err(|error| format!("read runtime/CI artifact: {error}"))?; + if sha256_hex(&bytes) != expected_digest { + return Err("runtime/CI artifact digest mismatch".into()); + } + let text = std::str::from_utf8(&bytes) + .map_err(|_| "runtime/CI artifact must be UTF-8 JSON".to_string())?; + reject_duplicate_json_keys(text)?; + let source: Value = serde_json::from_str(text) + .map_err(|error| format!("runtime/CI artifact is invalid JSON: {error}"))?; + normalize(&source, expected_snapshot, evaluated_at, max_age) +} + +pub(crate) fn normalize( + source: &Value, + expected_snapshot: &str, + evaluated_at: u64, + max_age_seconds: u64, +) -> Result { + validate_source(source)?; + if !is_digest(expected_snapshot) { + return Err("expected snapshot identity must be a lowercase SHA-256 digest".into()); + } + let source_snapshot = source["snapshotIdentity"].as_str().expect("validated"); + let completeness = source["completeness"].as_str().expect("validated"); + let observed_at = source["observedAt"].as_u64().expect("validated"); + let provider = source["provider"].clone(); + let provenance = source["provenance"].clone(); + let signals = source["signals"].clone(); + + let (freshness, failure_kind) = if source_snapshot != expected_snapshot { + ("snapshot_mismatch", "snapshot_mismatch") + } else if observed_at > evaluated_at || evaluated_at - observed_at > max_age_seconds { + ("stale", "stale") + } else { + ("current", "none") + }; + if freshness != "current" { + return Ok(json!({ + "schema":SUMMARY_SCHEMA, + "admission":"rejected", + "health":"unknown", + "freshness":freshness, + "completeness":completeness, + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":source_snapshot, + "provider":provider, + "provenance":provenance, + "observedAt":observed_at, + "signals":signals, + "facts":[], + "failureKind":failure_kind + })); + } + + let statuses = [ + signals["tests"]["status"].as_str().expect("validated"), + signals["build"]["status"].as_str().expect("validated"), + signals["runtime"]["status"].as_str().expect("validated"), + ]; + let observed_failure = statuses[0] == "failed" + || statuses[1] == "failed" + || matches!(statuses[2], "failed" | "degraded"); + let fully_positive = completeness == "complete" + && statuses[0] == "passed" + && statuses[1] == "passed" + && statuses[2] == "healthy"; + let health = if observed_failure { + "red" + } else if fully_positive { + "green" + } else { + "unknown" + }; + let failure_kind = if observed_failure { + "observed_failure" + } else if fully_positive { + "none" + } else { + "partial_coverage" + }; + let facts = if health == "green" { + vec![ + "tests_observed_passed", + "build_observed_passed", + "runtime_observed_healthy", + ] + } else if health == "red" { + vec!["runtime_ci_observed_failure"] + } else { + Vec::new() + }; + Ok(json!({ + "schema":SUMMARY_SCHEMA, + "admission":"accepted", + "health":health, + "freshness":"current", + "completeness":completeness, + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":source_snapshot, + "provider":provider, + "provenance":provenance, + "observedAt":observed_at, + "signals":signals, + "facts":facts, + "failureKind":failure_kind + })) +} + +fn validate_source(source: &Value) -> Result<(), String> { + require_object_keys( + source, + &[ + "schema", + "provider", + "provenance", + "snapshotIdentity", + "observedAt", + "completeness", + "signals", + ], + "observation", + )?; + require_const(source, "schema", SOURCE_SCHEMA, "observation")?; + digest_field(source, "snapshotIdentity", "observation")?; + integer_field(source, "observedAt", "observation")?; + enum_field( + source, + "completeness", + &["complete", "partial"], + "observation", + )?; + + let provider = object_field(source, "provider", "observation")?; + require_object_keys( + provider, + &["id", "runId", "sourceRevision"], + "observation.provider", + )?; + string_field(provider, "id", "observation.provider")?; + string_field(provider, "runId", "observation.provider")?; + string_field(provider, "sourceRevision", "observation.provider")?; + + let provenance = object_field(source, "provenance", "observation")?; + require_object_keys( + provenance, + &[ + "collectorId", + "collectorVersion", + "collectionId", + "collectedAt", + ], + "observation.provenance", + )?; + string_field(provenance, "collectorId", "observation.provenance")?; + string_field(provenance, "collectorVersion", "observation.provenance")?; + string_field(provenance, "collectionId", "observation.provenance")?; + integer_field(provenance, "collectedAt", "observation.provenance")?; + + let signals = object_field(source, "signals", "observation")?; + require_object_keys( + signals, + &["tests", "build", "runtime"], + "observation.signals", + )?; + validate_signal( + object_field(signals, "tests", "observation.signals")?, + "observation.signals.tests", + &["passed", "failed", "cancelled", "unknown"], + )?; + validate_signal( + object_field(signals, "build", "observation.signals")?, + "observation.signals.build", + &["passed", "failed", "cancelled", "unknown"], + )?; + validate_signal( + object_field(signals, "runtime", "observation.signals")?, + "observation.signals.runtime", + &["healthy", "degraded", "failed", "unknown"], + )?; + Ok(()) +} + +fn validate_signal(value: &Value, context: &str, statuses: &[&str]) -> Result<(), String> { + require_object_keys(value, &["status", "observed", "summary"], context)?; + enum_field(value, "status", statuses, context)?; + let observed = value["observed"] + .as_bool() + .ok_or_else(|| format!("{context}.observed must be a boolean"))?; + let status = value["status"].as_str().expect("validated"); + if !observed && status != "unknown" { + return Err(format!( + "{context} cannot claim {status} when observed is false" + )); + } + string_field(value, "summary", context)?; + Ok(()) +} + +fn unknown_summary( + expected_snapshot: &str, + freshness: &str, + completeness: &str, + failure: &str, +) -> Value { + json!({ + "schema":SUMMARY_SCHEMA, + "admission":"rejected", + "health":"unknown", + "freshness":freshness, + "completeness":completeness, + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":Value::Null, + "provider":Value::Null, + "provenance":Value::Null, + "observedAt":Value::Null, + "signals":{ + "tests":{"status":"unknown","observed":false,"summary":"not available"}, + "build":{"status":"unknown","observed":false,"summary":"not available"}, + "runtime":{"status":"unknown","observed":false,"summary":"not available"} + }, + "facts":[], + "failureKind":failure + }) +} + +fn safe_join(root: &Path, relative: &str) -> Result { + let path = Path::new(relative); + if relative.is_empty() + || relative.contains('\0') + || path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) + { + return Err("runtime/CI artifact path must be repository-relative without '..'".into()); + } + Ok(root.join(path)) +} + +fn require_object_keys(value: &Value, expected: &[&str], context: &str) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{context} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + if actual != expected { + return Err(format!("{context} fields are invalid")); + } + Ok(()) +} + +fn require_const(value: &Value, field: &str, expected: &str, context: &str) -> Result<(), String> { + if value[field].as_str() != Some(expected) { + return Err(format!("{context}.{field} must be {expected}")); + } + Ok(()) +} + +fn object_field<'a>(value: &'a Value, field: &str, context: &str) -> Result<&'a Value, String> { + value[field] + .as_object() + .map(|_| &value[field]) + .ok_or_else(|| format!("{context}.{field} must be an object")) +} + +fn string_field<'a>(value: &'a Value, field: &str, context: &str) -> Result<&'a str, String> { + value[field] + .as_str() + .filter(|text| !text.is_empty()) + .ok_or_else(|| format!("{context}.{field} must be a non-empty string")) +} + +fn digest_field<'a>(value: &'a Value, field: &str, context: &str) -> Result<&'a str, String> { + let digest = string_field(value, field, context)?; + if !is_digest(digest) { + return Err(format!( + "{context}.{field} must be a lowercase SHA-256 digest" + )); + } + Ok(digest) +} + +fn is_digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) +} + +fn integer_field(value: &Value, field: &str, context: &str) -> Result { + value[field] + .as_u64() + .ok_or_else(|| format!("{context}.{field} must be a non-negative integer")) +} + +fn positive_integer_field(value: &Value, field: &str, context: &str) -> Result { + integer_field(value, field, context).and_then(|number| { + if number == 0 { + Err(format!("{context}.{field} must be positive")) + } else { + Ok(number) + } + }) +} + +fn enum_field<'a>( + value: &'a Value, + field: &str, + allowed: &[&str], + context: &str, +) -> Result<&'a str, String> { + let actual = string_field(value, field, context)?; + if !allowed.contains(&actual) { + return Err(format!("{context}.{field} has an unsupported value")); + } + Ok(actual) +} + +#[cfg(not(test))] +pub(crate) fn sha256_hex(bytes: &[u8]) -> String { + crate::capability::sha256_hex(bytes) +} + +#[cfg(not(test))] +fn reject_duplicate_json_keys(text: &str) -> Result<(), String> { + crate::capability::reject_duplicate_json_keys(text) + .map_err(|error| format!("runtime/CI artifact {error}")) +} + +#[cfg(test)] +fn reject_duplicate_json_keys(_text: &str) -> Result<(), String> { + Ok(()) +} + +#[cfg(test)] +pub(crate) fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0); + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (i, word) in chunk.chunks_exact(4).enumerate() { + w[i] = u32::from_be_bytes(word.try_into().unwrap()); + } + for i in 16..64 { + let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3); + let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + .wrapping_add(s0) + .wrapping_add(w[i - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for i in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let t1 = hh + .wrapping_add(s1) + .wrapping_add((e & f) ^ (!e & g)) + .wrapping_add(K[i]) + .wrapping_add(w[i]); + let t2 = (a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22)) + .wrapping_add((a & b) ^ (a & c) ^ (b & c)); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value); + } + } + h.iter().map(|value| format!("{value:08x}")).collect() +} diff --git a/crates/code-intel-cli/src/sentrux_adapter.rs b/crates/code-intel-cli/src/sentrux_adapter.rs new file mode 100644 index 0000000..2a2b131 --- /dev/null +++ b/crates/code-intel-cli/src/sentrux_adapter.rs @@ -0,0 +1,351 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde_json::{json, Value}; + +pub(crate) const AUTHORITATIVE_RULE_KINDS: [&str; 6] = [ + "max_cc", + "max_cycles", + "max_coupling", + "no_god_files", + "layer_order", + "boundary_dependency", +]; +const COMMAND_RULE_KINDS: [&str; 2] = ["sentrux_gate", "sentrux_check"]; + +pub(crate) fn translate( + native: &Value, + evaluated_at: u64, + max_age_seconds: u64, +) -> Result { + validate_native(native)?; + if max_age_seconds == 0 { + return Err("Sentrux freshness policy max age must be positive".to_string()); + } + + let expected = native["expectedSnapshotIdentity"].as_str().unwrap(); + let consumed = native["sourceSnapshotIdentity"].as_str().unwrap(); + let observed_at = native["observedAt"].as_u64().unwrap(); + let status = native["status"].as_str().unwrap(); + let rules = normalize_rules(&native["authoritativeRules"])?; + let known = rules + .iter() + .filter_map(|rule| rule["kind"].as_str()) + .filter(|kind| known_rule(kind)) + .collect::>(); + let has_unknown = rules.iter().any(|rule| rule["status"] == "unsupported"); + let all_known_evaluated = rules.iter().all(|rule| { + !known_rule(rule["kind"].as_str().unwrap_or("")) || rule["status"] == "evaluated" + }); + let complete_legacy_rules = AUTHORITATIVE_RULE_KINDS + .iter() + .all(|kind| known.contains(kind)); + let complete_command_observation = COMMAND_RULE_KINDS.iter().all(|kind| known.contains(kind)); + let complete = status == "complete" + && !has_unknown + && all_known_evaluated + && (complete_legacy_rules || complete_command_observation); + let completeness = if complete { "complete" } else { "partial" }; + let failure = if status == "crashed" { + native["nativeFailure"].clone() + } else if complete { + json!({"kind":"none"}) + } else { + json!({ + "kind":"domain_unknown", + "message":"Sentrux authoritative rule normalization is incomplete" + }) + }; + let freshness = if expected != consumed { + "snapshot_mismatch" + } else if observed_at <= evaluated_at && evaluated_at - observed_at <= max_age_seconds { + "current" + } else { + "stale" + }; + let effects = sorted_effects(&native["declaredEffects"])?; + let request = json!({ + "schema":"code-intel-evidence-admissibility-request.v1", + "expectedSnapshotIdentity":native["expectedSnapshotIdentity"], + "policy":{"evaluatedAt":evaluated_at,"maxAgeSeconds":max_age_seconds}, + "observation":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{ + "id":"structural-evidence.sentrux", + "implementation":native["implementation"] + }, + "source":{"revision":native["sourceRevision"]}, + "consumedSnapshotIdentity":native["sourceSnapshotIdentity"], + "observedAt":observed_at, + "completeness":completeness, + "claimedComplete":complete, + "payload":native["payload"], + "provenance":{ + "collectionId":format!("sentrux-{}-{observed_at}", native["sourceRevision"].as_str().unwrap()), + "command":"provider sentrux-adapt", + "startedAt":native["collectedAt"], + "completedAt":observed_at + }, + "failure":failure + } + }); + + Ok(json!({ + "schema":"code-intel-sentrux-adapter-result.v1", + "port":{ + "schema":"code-intel-structural-evidence-port.v1", + "status":status, + "completeness":completeness, + "freshness":freshness, + "expectedSnapshotIdentity":expected, + "sourceSnapshotIdentity":consumed, + "provider":{ + "implementationId":native["implementation"]["id"], + "rollbackIdentity":native["rollbackIdentity"] + }, + "provenance":{ + "sourceRevision":native["sourceRevision"], + "observedAt":observed_at + }, + "effects":{ + "declared":effects, + "observed":effects, + "match":true + }, + "rules":rules, + "payload":native["payload"], + "diagnosisEligible":false + }, + "evidence":{"request":request}, + "factPromotion":{ + "eligible":false, + "requires":"evidence.admissibility-validate", + "engineeringFacts":[] + } + })) +} + +pub(crate) fn validate_admitted_payload(payload: &Value, adapter: &Value) -> Result<(), String> { + exact(payload, &["schema", "data"], "Sentrux evidence payload")?; + if payload["schema"] != "code-intel-evidence-payload.v1" { + return Err("Sentrux evidence payload schema is invalid".to_string()); + } + let evidence = &payload["data"]["structuralEvidence"]; + exact( + evidence, + &[ + "schema", + "snapshotIdentity", + "provider", + "provenance", + "effects", + "completeness", + "rules", + ], + "Sentrux structural evidence", + )?; + if evidence["schema"] != "code-intel-structural-evidence-payload.v1" + || evidence["snapshotIdentity"] != adapter["port"]["sourceSnapshotIdentity"] + || evidence["provider"] != adapter["port"]["provider"] + || evidence["provenance"] != adapter["port"]["provenance"] + || evidence["effects"] != adapter["port"]["effects"] + || evidence["completeness"] != adapter["port"]["completeness"] + || evidence["rules"] != adapter["port"]["rules"] + { + return Err( + "Sentrux admitted payload does not match the structural evidence port".to_string(), + ); + } + Ok(()) +} + +fn normalize_rules(value: &Value) -> Result, String> { + let rules = value + .as_array() + .ok_or("Sentrux authoritative rules must be an array")?; + let mut normalized = BTreeMap::new(); + for rule in rules { + exact( + rule, + &["kind", "status", "verdict", "failure"], + "Sentrux authoritative rule", + )?; + let kind = rule["kind"] + .as_str() + .filter(|kind| !kind.is_empty()) + .ok_or("Sentrux authoritative rule kind is invalid")?; + if normalized.contains_key(kind) { + return Err("Sentrux authoritative rule kinds must be unique".to_string()); + } + let normalized_rule = if known_rule(kind) { + validate_known_rule(rule)?; + rule.clone() + } else { + json!({ + "kind":kind, + "status":"unsupported", + "verdict":"unknown", + "failure":{ + "kind":"domain_unknown", + "message":"unrecognized authoritative Sentrux rule kind" + } + }) + }; + normalized.insert(kind.to_string(), normalized_rule); + } + Ok(normalized.into_values().collect()) +} + +fn known_rule(kind: &str) -> bool { + AUTHORITATIVE_RULE_KINDS.contains(&kind) || COMMAND_RULE_KINDS.contains(&kind) +} + +fn validate_known_rule(rule: &Value) -> Result<(), String> { + let status = rule["status"].as_str().unwrap_or(""); + let verdict = rule["verdict"].as_str().unwrap_or(""); + let failure = &rule["failure"]; + let failure_kind = failure["kind"].as_str().unwrap_or(""); + match status { + "evaluated" + if matches!(verdict, "pass" | "fail") + && failure_kind == "none" + && failure.as_object().is_some_and(|object| object.len() == 1) => + { + Ok(()) + } + "not_evaluated" + if verdict == "unknown" + && failure_kind == "domain_unknown" + && failure["message"] + .as_str() + .is_some_and(|message| !message.is_empty()) + && failure.as_object().is_some_and(|object| object.len() == 2) => + { + Ok(()) + } + _ => Err("Sentrux authoritative rule status/verdict/failure is inconsistent".to_string()), + } +} + +fn validate_native(native: &Value) -> Result<(), String> { + exact( + native, + &[ + "schema", + "status", + "implementation", + "rollbackIdentity", + "sourceRevision", + "expectedSnapshotIdentity", + "sourceSnapshotIdentity", + "collectedAt", + "observedAt", + "declaredEffects", + "observedEffects", + "authoritativeRules", + "nativeFailure", + "payload", + ], + "Sentrux provider native result", + )?; + if native["schema"] != "code-intel-sentrux-provider-native.v1" + || !matches!( + native["status"].as_str(), + Some("complete" | "partial" | "crashed") + ) + || !digest(&native["expectedSnapshotIdentity"]) + || !digest(&native["sourceSnapshotIdentity"]) + || native["collectedAt"].as_u64().is_none() + || native["observedAt"].as_u64().is_none() + || native["observedAt"].as_u64().unwrap() < native["collectedAt"].as_u64().unwrap() + { + return Err("Sentrux native identity/status/time is invalid".to_string()); + } + exact( + &native["implementation"], + &["id", "version", "digest"], + "Sentrux provider implementation", + )?; + if !nonempty(&native["implementation"]["id"]) + || !nonempty(&native["implementation"]["version"]) + || !digest(&native["implementation"]["digest"]) + || !nonempty(&native["rollbackIdentity"]) + || !nonempty(&native["sourceRevision"]) + { + return Err("Sentrux implementation/rollback/source identity is invalid".to_string()); + } + let declared = sorted_effects(&native["declaredEffects"])?; + let observed = sorted_effects(&native["observedEffects"])?; + if declared != observed { + return Err("Sentrux observed effects do not match declared effects".to_string()); + } + let failure = &native["nativeFailure"]; + if native["status"] == "crashed" { + if failure["kind"] != "provider_unavailable" + || !failure["message"] + .as_str() + .is_some_and(|message| !message.is_empty()) + || failure.as_object().is_none_or(|object| object.len() != 2) + || native["authoritativeRules"] + .as_array() + .is_none_or(|rules| !rules.is_empty()) + { + return Err("crashed Sentrux provider failure semantics are invalid".to_string()); + } + } else if failure != &json!({"kind":"none"}) { + return Err("non-crashed Sentrux provider cannot report native failure".to_string()); + } + crate::capability::validate_artifact_ref_shape(&native["payload"])?; + if native["payload"]["artifactSchema"] != "code-intel-evidence-payload.v1" + || native["payload"]["type"] != "observed.evidence.payload" + || native["payload"]["consumedSnapshotIdentity"] != native["sourceSnapshotIdentity"] + { + return Err("Sentrux payload contract/snapshot is invalid".to_string()); + } + Ok(()) +} + +fn sorted_effects(value: &Value) -> Result, String> { + let effects = value.as_array().ok_or("Sentrux effects must be an array")?; + let mut result = BTreeSet::new(); + for effect in effects { + let effect = effect.as_str().unwrap_or(""); + if !matches!(effect, "repo_read" | "local_write" | "process_spawn") { + return Err("Sentrux effect is invalid".to_string()); + } + if !result.insert(effect) { + return Err("Sentrux effects must be unique".to_string()); + } + } + if result.is_empty() { + return Err("Sentrux must declare at least one effect".to_string()); + } + Ok(result.into_iter().collect()) +} + +fn exact(value: &Value, fields: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = fields.iter().copied().collect::>(); + if actual == expected { + Ok(()) + } else { + Err(format!("{label} fields are invalid")) + } +} + +fn nonempty(value: &Value) -> bool { + value.as_str().is_some_and(|text| !text.is_empty()) +} + +fn digest(value: &Value) -> bool { + value.as_str().is_some_and(|text| { + text.len() == 64 + && text + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) + }) +} diff --git a/crates/code-intel-cli/src/sentrux_analysis.rs b/crates/code-intel-cli/src/sentrux_analysis.rs index e5d74a8..045c25e 100644 --- a/crates/code-intel-cli/src/sentrux_analysis.rs +++ b/crates/code-intel-cli/src/sentrux_analysis.rs @@ -113,6 +113,7 @@ pub fn analyze(target: &Path) -> Result { fn source_inventory(target: &Path) -> Result { let listed = inventory_files(target)?; + let governed_visible = governed_visible_files(target); let mut included = Vec::new(); let mut excluded: BTreeMap)> = BTreeMap::new(); @@ -130,6 +131,13 @@ fn source_inventory(target: &Path) -> Result { { reason = Some("oversized_generated_or_bundle".to_string()); } + if reason.is_none() + && governed_visible + .as_ref() + .is_some_and(|visible| !visible.contains(&relative)) + { + reason = Some("repository_ignored".to_string()); + } if let Some(reason) = reason { let entry = excluded.entry(reason).or_default(); entry.0 += 1; @@ -166,6 +174,41 @@ fn source_inventory(target: &Path) -> Result { }) } +fn governed_visible_files(target: &Path) -> Option> { + let output = Command::new("rg") + .arg("--files") + .args([ + "--hidden", + "--no-ignore-parent", + "--no-ignore-global", + "--no-ignore-exclude", + ]) + .current_dir(target) + .output() + .ok()?; + if !output.status.success() && output.status.code() != Some(1) { + return None; + } + let mut visible = String::from_utf8_lossy(&output.stdout) + .lines() + .map(normalize_path) + .collect::>(); + + if let Ok(output) = Command::new("git") + .args(["-C", &target.to_string_lossy(), "ls-files", "-z"]) + .output() + { + if output.status.success() { + for relative in String::from_utf8_lossy(&output.stdout).split('\0') { + if !relative.is_empty() { + visible.insert(normalize_path(relative)); + } + } + } + } + Some(visible) +} + fn inventory_files(root: &Path) -> Result, String> { fn visit(root: &Path, current: &Path, out: &mut Vec) -> Result<(), String> { let entries = fs::read_dir(current) diff --git a/crates/code-intel-cli/src/session_evidence.rs b/crates/code-intel-cli/src/session_evidence.rs new file mode 100644 index 0000000..abaef88 --- /dev/null +++ b/crates/code-intel-cli/src/session_evidence.rs @@ -0,0 +1,1021 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +use crate::capability::sha256_hex; + +const MAX_TRACE_BYTES: u64 = 128 * 1024 * 1024; +const MAX_HOTSPOT_BYTES: u64 = 64 * 1024 * 1024; +const MINDWALK_COMPATIBILITY_REVISION: &str = "e208b6b8504138843f671e031f28129b66003a67"; + +#[derive(Debug)] +enum AdapterError { + Usage(String), + Contract(String), + Io(String), +} + +impl AdapterError { + fn exit_code(&self) -> i32 { + match self { + Self::Usage(_) => 64, + Self::Contract(_) => 65, + Self::Io(_) => 74, + } + } + + fn message(&self) -> &str { + match self { + Self::Usage(message) | Self::Contract(message) | Self::Io(message) => message, + } + } +} + +struct Cli { + repo: PathBuf, + trace: PathBuf, + hotspots: Option, + out: Option, + working_tree_policy: String, +} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let cli = match parse_cli(raw) { + Ok(cli) => cli, + Err(error) => { + eprintln!("{}", error.message()); + return error.exit_code(); + } + }; + let artifact = match adapt(&cli) { + Ok(artifact) => artifact, + Err(error) => { + eprintln!("{}", error.message()); + return error.exit_code(); + } + }; + let bytes = serde_json::to_vec_pretty(&artifact).expect("session evidence serializes"); + if let Some(path) = cli.out.as_deref() { + if let Err(error) = write_new_artifact(path, &bytes) { + eprintln!("{}", error.message()); + return error.exit_code(); + } + let receipt = json!({ + "schema":"code-intel-session-adapter-result.v1", + "status":"completed", + "artifactSha256":sha256_hex(&bytes), + "summary":artifact["summary"] + }); + println!("{}", serde_json::to_string(&receipt).unwrap()); + } else { + println!("{}", String::from_utf8(bytes).expect("JSON is UTF-8")); + } + 0 +} + +fn parse_cli(raw: &[String]) -> Result { + let mut repo = None; + let mut trace = None; + let mut hotspots = None; + let mut out = None; + let mut working_tree_policy = "explicit_overlay".to_string(); + let mut index = 0; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--repo" | "--trace" | "--hotspots" | "--out" | "--working-tree-policy" + ) { + return Err(AdapterError::Usage(format!( + "unknown session adapter argument: {flag}" + ))); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| AdapterError::Usage(format!("{flag} requires exactly one value")))?; + match flag { + "--repo" if repo.replace(PathBuf::from(value)).is_some() => { + return Err(AdapterError::Usage("duplicate --repo".into())) + } + "--trace" if trace.replace(PathBuf::from(value)).is_some() => { + return Err(AdapterError::Usage("duplicate --trace".into())) + } + "--hotspots" if hotspots.replace(PathBuf::from(value)).is_some() => { + return Err(AdapterError::Usage("duplicate --hotspots".into())) + } + "--out" if out.replace(PathBuf::from(value)).is_some() => { + return Err(AdapterError::Usage("duplicate --out".into())) + } + "--working-tree-policy" => { + if !matches!(value.as_str(), "head_only" | "explicit_overlay") { + return Err(AdapterError::Usage( + "--working-tree-policy must be head_only or explicit_overlay".into(), + )); + } + working_tree_policy = value.clone(); + } + _ => {} + } + index += 2; + } + let repo = repo.ok_or_else(|| AdapterError::Usage("--repo is required".into()))?; + if !repo.is_dir() { + return Err(AdapterError::Usage(format!( + "repository path is not a directory: {}", + repo.display() + ))); + } + Ok(Cli { + repo, + trace: trace.ok_or_else(|| AdapterError::Usage("--trace is required".into()))?, + hotspots, + out, + working_tree_policy, + }) +} + +fn adapt(cli: &Cli) -> Result { + let trace_bytes = read_bounded_regular(&cli.trace, MAX_TRACE_BYTES, "session trace")?; + let trace: Value = serde_json::from_slice(&trace_bytes) + .map_err(|_| AdapterError::Contract("session trace is not valid JSON".into()))?; + validate_trace(&trace)?; + + let repo = fs::canonicalize(&cli.repo) + .map_err(|error| AdapterError::Io(format!("cannot resolve repository root: {error}")))?; + let snapshot = + crate::snapshot::build_for_dag(&repo, &cli.working_tree_policy, &[".".to_string()]) + .map_err(AdapterError::Contract)?; + let hotspot_index = match cli.hotspots.as_deref() { + Some(path) => { + let bytes = read_bounded_regular(path, MAX_HOTSPOT_BYTES, "Sentrux enrichment")?; + let value: Value = serde_json::from_slice(&bytes).map_err(|_| { + AdapterError::Contract("Sentrux enrichment is not valid JSON".into()) + })?; + build_hotspot_index(&value)? + } + None => BTreeMap::new(), + }; + + let session = &trace["session"]; + let session_cwd = session["cwd"] + .as_str() + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| repo.clone()); + let session_cwd = fs::canonicalize(&session_cwd).unwrap_or(session_cwd); + let observability = &trace["stats"]["observability"]; + let target_grade = grade(observability["reads"].as_str()); + let error_grade = grade(observability["errors"].as_str()); + let raw_events = trace["events"].as_array().unwrap(); + let last_verify = raw_events + .iter() + .filter(|event| event["action"] == "verify") + .filter_map(|event| event["seq"].as_u64()) + .max(); + + let mut events = Vec::with_capacity(raw_events.len()); + let mut signals = Vec::new(); + let mut seen_seq = BTreeSet::new(); + let mut target_count = 0_u64; + let mut matched_target_count = 0_u64; + let mut unmatched_target_count = 0_u64; + let mut unsafe_target_count = 0_u64; + let mut structural_attention_touches = 0_u64; + let mut edit_events = 0_u64; + let mut verify_events = 0_u64; + let mut error_events = 0_u64; + + for raw_event in raw_events { + let seq = raw_event["seq"].as_u64().unwrap(); + if !seen_seq.insert(seq) { + return Err(AdapterError::Contract( + "session trace event sequence values must be unique".into(), + )); + } + let action = normalized_action(raw_event["action"].as_str()); + let is_error = raw_event["isError"].as_bool().unwrap(); + let raw_targets = raw_event["targets"].as_array().unwrap(); + let mut targets = Vec::new(); + let event_has_edit = action == "edit" + || raw_targets + .iter() + .any(|target| normalized_touch(target["touch"].as_str()) == "edit"); + for raw_target in raw_targets { + target_count += 1; + let raw_path = raw_target["path"].as_str().unwrap(); + let Some(path) = normalize_target(&repo, &session_cwd, raw_path) else { + unsafe_target_count += 1; + continue; + }; + let touch = normalized_touch(raw_target["touch"].as_str()); + let structural = if let Some(hotspot) = hotspot_index.get(&path) { + matched_target_count += 1; + let attention = structural_attention(hotspot); + if attention { + structural_attention_touches += 1; + if event_has_edit { + signals.push(signal("structural_attention_edit", seq, &path, "review")); + } + if is_error { + signals.push(signal( + "error_on_structural_attention", + seq, + &path, + "review", + )); + } + if event_has_edit && last_verify.is_none_or(|verify| seq > verify) { + signals.push(signal( + "unverified_structural_attention_edit", + seq, + &path, + "review", + )); + } + } + json!({ + "status":"matched", + "attention":attention, + "maxComplexity":u64_any(hotspot, &["maxComplexity", "max_complexity"]), + "avgComplexity":f64_any(hotspot, &["avgComplexity", "avg_complexity"]), + "loc":hotspot["loc"].as_u64().unwrap_or(0), + "gitChurn":hotspot["git"]["churn"].as_u64().unwrap_or(0), + "dirty":hotspot["git"]["dirty"].as_bool().unwrap_or(false) + }) + } else { + unmatched_target_count += 1; + json!({"status":"unknown"}) + }; + targets.push(json!({ + "path":path, + "touch":touch, + "observability":target_grade, + "structural":structural + })); + } + targets.sort_by(|left, right| { + (left["path"].as_str(), left["touch"].as_str()) + .cmp(&(right["path"].as_str(), right["touch"].as_str())) + }); + targets.dedup_by(|left, right| { + left["path"] == right["path"] && left["touch"] == right["touch"] + }); + unsafe_target_count += raw_event["outside"] + .as_array() + .map(|outside| outside.len() as u64) + .unwrap_or(0); + edit_events += u64::from(event_has_edit); + verify_events += u64::from(action == "verify"); + error_events += u64::from(is_error); + events.push(json!({ + "seq":seq, + "action":action, + "toolFamily":tool_family(raw_event["tool"].as_str()), + "isError":is_error, + "targets":targets + })); + } + events.sort_by_key(|event| event["seq"].as_u64().unwrap()); + signals.sort_by(|left, right| { + ( + left["eventSeq"].as_u64(), + left["path"].as_str(), + left["kind"].as_str(), + ) + .cmp(&( + right["eventSeq"].as_u64(), + right["path"].as_str(), + right["kind"].as_str(), + )) + }); + signals.dedup(); + + // Mindwalk v1 exposes tool activity, but edit and verification intent remain + // heuristic. Keep the aggregate status honest even when every target joins. + let status = "partial"; + let source_session_id = session["id"].as_str().unwrap(); + let harness = coarse_harness_family(session["harness"].as_str().unwrap()); + let artifact = json!({ + "schema":"code-intel-session-evidence.v1", + "status":status, + "reviewAuthority":"advisory_only", + "snapshot":snapshot["snapshot"], + "source":{ + "provider":"mindwalk", + "traceSchema":"mindwalk-trace.v1", + "compatibilityRevision":MINDWALK_COMPATIBILITY_REVISION, + "license":"MIT", + "copiedSource":false, + "traceSha256":sha256_hex(&trace_bytes), + "sessionDigest":sha256_hex(source_session_id.as_bytes()), + "harness":harness + }, + "implementation":{ + "id":"code-intel.session-evidence.rust", + "version":"1" + }, + "privacy":{ + "rawTracePersisted":false, + "userMessageMarksConsumed":false, + "eventSummariesConsumed":false, + "absolutePathsEmitted":false + }, + "observability":{ + "events":"exact", + "targets":target_grade, + "errors":error_grade, + "edits":"estimated", + "verification":"estimated" + }, + "summary":{ + "events":events.len(), + "targetEvents":events.iter().filter(|event| event["targets"].as_array().is_some_and(|targets| !targets.is_empty())).count(), + "targets":target_count, + "matchedTargets":matched_target_count, + "unmatchedTargets":unmatched_target_count, + "unsafeOrOutsideTargets":unsafe_target_count, + "structuralAttentionTouches":structural_attention_touches, + "editEvents":edit_events, + "verifyEvents":verify_events, + "errorEvents":error_events, + "signals":signals.len() + }, + "events":events, + "signals":signals + }); + validate_artifact_value(&artifact).map_err(AdapterError::Contract)?; + Ok(artifact) +} + +pub(crate) fn validate_artifact_value(value: &Value) -> Result<(), String> { + exact_keys( + value, + &[ + "schema", + "status", + "reviewAuthority", + "snapshot", + "source", + "implementation", + "privacy", + "observability", + "summary", + "events", + "signals", + ], + "session evidence", + )?; + if value["schema"] != "code-intel-session-evidence.v1" + || !matches!(value["status"].as_str(), Some("complete" | "partial")) + || value["reviewAuthority"] != "advisory_only" + { + return Err("session evidence authority or top-level identity is invalid".into()); + } + validate_snapshot(&value["snapshot"])?; + validate_source(&value["source"])?; + exact_keys( + &value["implementation"], + &["id", "version"], + "implementation", + )?; + if value["implementation"]["id"] != "code-intel.session-evidence.rust" + || value["implementation"]["version"] != "1" + { + return Err("session evidence implementation identity is invalid".into()); + } + exact_keys( + &value["privacy"], + &[ + "rawTracePersisted", + "userMessageMarksConsumed", + "eventSummariesConsumed", + "absolutePathsEmitted", + ], + "privacy", + )?; + if [ + "rawTracePersisted", + "userMessageMarksConsumed", + "eventSummariesConsumed", + "absolutePathsEmitted", + ] + .iter() + .any(|field| value["privacy"][field] != false) + { + return Err("session evidence privacy boundary is invalid".into()); + } + exact_keys( + &value["observability"], + &["events", "targets", "errors", "edits", "verification"], + "observability", + )?; + if ["events", "targets", "errors", "edits", "verification"] + .iter() + .any(|field| !is_grade(value["observability"][field].as_str())) + { + return Err("session evidence observability grade is invalid".into()); + } + validate_session_body(value) +} + +fn validate_snapshot(value: &Value) -> Result<(), String> { + exact_keys( + value, + &[ + "identity", + "repoIdentity", + "head", + "workingTreePolicy", + "scope", + "inputDigest", + ], + "snapshot", + )?; + let scopes = value["scope"] + .as_array() + .filter(|items| !items.is_empty()) + .ok_or_else(|| "session evidence snapshot scope is invalid".to_string())?; + if !value["identity"].as_str().is_some_and(valid_digest) + || !value["inputDigest"].as_str().is_some_and(valid_digest) + || value["repoIdentity"].as_str().is_none_or(str::is_empty) + || value["head"].as_str().is_none_or(str::is_empty) + || !matches!( + value["workingTreePolicy"].as_str(), + Some("head_only" | "explicit_overlay") + ) + || scopes + .iter() + .any(|scope| scope.as_str().is_none_or(str::is_empty)) + { + return Err("session evidence snapshot contract is invalid".into()); + } + Ok(()) +} + +fn validate_source(value: &Value) -> Result<(), String> { + exact_keys( + value, + &[ + "provider", + "traceSchema", + "compatibilityRevision", + "license", + "copiedSource", + "traceSha256", + "sessionDigest", + "harness", + ], + "source", + )?; + if value["provider"] != "mindwalk" + || value["traceSchema"] != "mindwalk-trace.v1" + || value["license"] != "MIT" + || value["copiedSource"] != false + || !value["compatibilityRevision"] + .as_str() + .is_some_and(|revision| revision.len() == 40 && revision.bytes().all(is_lower_hex)) + || !value["traceSha256"].as_str().is_some_and(valid_digest) + || !value["sessionDigest"].as_str().is_some_and(valid_digest) + || value["harness"].as_str().is_none_or(str::is_empty) + { + return Err("session evidence source contract is invalid".into()); + } + Ok(()) +} + +fn validate_session_body(value: &Value) -> Result<(), String> { + const SUMMARY_FIELDS: [&str; 11] = [ + "events", + "targetEvents", + "targets", + "matchedTargets", + "unmatchedTargets", + "unsafeOrOutsideTargets", + "structuralAttentionTouches", + "editEvents", + "verifyEvents", + "errorEvents", + "signals", + ]; + exact_keys(&value["summary"], &SUMMARY_FIELDS, "summary")?; + if SUMMARY_FIELDS + .iter() + .any(|field| value["summary"][field].as_u64().is_none()) + { + return Err("session evidence summary contains a non-counter".into()); + } + let events = value["events"] + .as_array() + .filter(|events| !events.is_empty()) + .ok_or_else(|| "session evidence requires events".to_string())?; + let signals = value["signals"] + .as_array() + .ok_or_else(|| "session evidence signals must be an array".to_string())?; + let mut sequences = BTreeSet::new(); + let mut target_events = 0_u64; + let mut targets = 0_u64; + let mut matched = 0_u64; + let mut attention = 0_u64; + let mut edits = 0_u64; + let mut verifies = 0_u64; + let mut errors = 0_u64; + for event in events { + validate_event(event, &mut sequences)?; + let event_targets = event["targets"].as_array().expect("validated targets"); + target_events += u64::from(!event_targets.is_empty()); + targets += event_targets.len() as u64; + for target in event_targets { + if target["structural"]["status"] == "matched" { + matched += 1; + attention += u64::from(target["structural"]["attention"] == true); + } + } + edits += u64::from( + event["action"] == "edit" + || event_targets.iter().any(|target| target["touch"] == "edit"), + ); + verifies += u64::from(event["action"] == "verify"); + errors += u64::from(event["isError"] == true); + } + for signal in signals { + validate_signal(signal, &sequences)?; + } + let summary = &value["summary"]; + let exact = [ + ("events", events.len() as u64), + ("targetEvents", target_events), + ("matchedTargets", matched), + ("structuralAttentionTouches", attention), + ("editEvents", edits), + ("verifyEvents", verifies), + ("errorEvents", errors), + ("signals", signals.len() as u64), + ]; + if exact + .iter() + .any(|(field, expected)| summary[field].as_u64() != Some(*expected)) + || summary["matchedTargets"].as_u64().unwrap() + + summary["unmatchedTargets"].as_u64().unwrap() + > summary["targets"].as_u64().unwrap() + || summary["targets"].as_u64().unwrap() < targets + { + return Err("session evidence summary does not match normalized events".into()); + } + Ok(()) +} + +fn validate_event(event: &Value, sequences: &mut BTreeSet) -> Result<(), String> { + exact_keys( + event, + &["seq", "action", "toolFamily", "isError", "targets"], + "event", + )?; + let seq = event["seq"] + .as_u64() + .filter(|seq| sequences.insert(*seq)) + .ok_or_else(|| "session evidence event sequence is invalid or duplicated".to_string())?; + let _ = seq; + if !matches!( + event["action"].as_str(), + Some("search" | "read" | "edit" | "exec" | "verify" | "other") + ) || !matches!( + event["toolFamily"].as_str(), + Some("edit" | "shell" | "read" | "search" | "orchestration" | "other") + ) || !event["isError"].is_boolean() + { + return Err("session evidence event classification is invalid".into()); + } + for target in event["targets"] + .as_array() + .ok_or_else(|| "session evidence event targets must be an array".to_string())? + { + validate_target(target)?; + } + Ok(()) +} + +fn validate_target(target: &Value) -> Result<(), String> { + exact_keys( + target, + &["path", "touch", "observability", "structural"], + "target", + )?; + let path = target["path"] + .as_str() + .filter(|path| normalize_artifact_path(path).as_deref() == Some(*path)) + .ok_or_else(|| { + "session evidence target path is not normalized repository-relative syntax".to_string() + })?; + let _ = path; + if !matches!(target["touch"].as_str(), Some("hit" | "read" | "edit")) + || !is_grade(target["observability"].as_str()) + { + return Err("session evidence target classification is invalid".into()); + } + let structural = &target["structural"]; + match structural["status"].as_str() { + Some("unknown") => exact_keys(structural, &["status"], "unknown structural target"), + Some("matched") => { + exact_keys( + structural, + &[ + "status", + "attention", + "maxComplexity", + "avgComplexity", + "loc", + "gitChurn", + "dirty", + ], + "matched structural target", + )?; + if !structural["attention"].is_boolean() + || structural["maxComplexity"].as_u64().is_none() + || structural["avgComplexity"].as_f64().is_none() + || structural["loc"].as_u64().is_none() + || structural["gitChurn"].as_u64().is_none() + || !structural["dirty"].is_boolean() + { + return Err("session evidence structural measurement is invalid".into()); + } + Ok(()) + } + _ => Err("session evidence structural status is invalid".into()), + } +} + +fn validate_signal(signal: &Value, sequences: &BTreeSet) -> Result<(), String> { + exact_keys( + signal, + &[ + "kind", + "status", + "severity", + "eventSeq", + "path", + "authority", + ], + "signal", + )?; + if !matches!( + signal["kind"].as_str(), + Some( + "structural_attention_edit" + | "error_on_structural_attention" + | "unverified_structural_attention_edit" + ) + ) || signal["status"] != "observed" + || signal["severity"] != "review" + || signal["authority"] != "advisory_only" + || !signal["eventSeq"] + .as_u64() + .is_some_and(|seq| sequences.contains(&seq)) + || signal["path"] + .as_str() + .is_none_or(|path| normalize_artifact_path(path).as_deref() != Some(path)) + { + return Err("session evidence advisory signal is invalid".into()); + } + Ok(()) +} + +fn exact_keys(value: &Value, expected: &[&str], label: &str) -> Result<(), String> { + let actual = value + .as_object() + .ok_or_else(|| format!("session evidence {label} must be an object"))? + .keys() + .map(String::as_str) + .collect::>(); + let expected = expected.iter().copied().collect::>(); + if actual != expected { + return Err(format!("session evidence {label} fields are not closed")); + } + Ok(()) +} + +fn is_grade(value: Option<&str>) -> bool { + matches!(value, Some("exact" | "estimated" | "unavailable")) +} + +fn valid_digest(value: &str) -> bool { + value.len() == 64 && value.bytes().all(is_lower_hex) +} + +fn is_lower_hex(byte: u8) -> bool { + byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte) +} + +fn coarse_harness_family(value: &str) -> &'static str { + let normalized = value.to_ascii_lowercase(); + if normalized.contains("codex") { + "codex" + } else if normalized.contains("claude") { + "claude" + } else { + "other" + } +} + +fn validate_trace(trace: &Value) -> Result<(), AdapterError> { + if trace["version"] != 1 { + return Err(AdapterError::Contract( + "unsupported session trace schema; expected Mindwalk trace version 1".into(), + )); + } + let session = trace["session"].as_object().ok_or_else(|| { + AdapterError::Contract("session trace is missing session metadata".into()) + })?; + if session + .get("id") + .and_then(Value::as_str) + .is_none_or(str::is_empty) + || session + .get("harness") + .and_then(Value::as_str) + .is_none_or(str::is_empty) + { + return Err(AdapterError::Contract( + "session trace identity or harness is invalid".into(), + )); + } + let events = trace["events"] + .as_array() + .ok_or_else(|| AdapterError::Contract("session trace is missing events".into()))?; + if events.is_empty() || events.len() > 1_000_000 { + return Err(AdapterError::Contract( + "session trace event count is outside the supported range".into(), + )); + } + for event in events { + if event["seq"].as_u64().is_none() + || event["tool"].as_str().is_none() + || event["action"].as_str().is_none() + || event["isError"].as_bool().is_none() + || event["targets"].as_array().is_none() + || event["targets"] + .as_array() + .is_some_and(|targets| targets.len() > 10_000) + { + return Err(AdapterError::Contract( + "session trace contains an invalid event".into(), + )); + } + for target in event["targets"].as_array().unwrap() { + if target["path"].as_str().is_none_or(str::is_empty) + || target["touch"].as_str().is_none() + { + return Err(AdapterError::Contract( + "session trace contains an invalid target".into(), + )); + } + } + } + Ok(()) +} + +fn build_hotspot_index(value: &Value) -> Result, AdapterError> { + let files = value + .get("files") + .or_else(|| value.get("file_details")) + .and_then(Value::as_array) + .ok_or_else(|| { + AdapterError::Contract("Sentrux enrichment is missing files or file_details".into()) + })?; + let mut result = BTreeMap::new(); + for file in files { + let path = file["path"].as_str().ok_or_else(|| { + AdapterError::Contract("Sentrux enrichment contains an invalid file path".into()) + })?; + let normalized = normalize_artifact_path(path).ok_or_else(|| { + AdapterError::Contract("Sentrux enrichment path is not repository-relative".into()) + })?; + if result.insert(normalized, file.clone()).is_some() { + return Err(AdapterError::Contract( + "Sentrux enrichment contains duplicate normalized paths".into(), + )); + } + } + Ok(result) +} + +fn normalize_target(repo: &Path, session_cwd: &Path, raw: &str) -> Option { + if raw.contains('\0') { + return None; + } + let raw_path = PathBuf::from(raw); + let candidate = if raw_path.is_absolute() { + raw_path + } else { + session_cwd.join(raw_path) + }; + let cleaned = clean_path(&candidate)?; + let boundary_checked = nearest_existing_ancestor(&cleaned) + .and_then(|ancestor| fs::canonicalize(ancestor).ok()) + .is_some_and(|ancestor| strip_prefix_portable(&ancestor, repo).is_some()); + if !boundary_checked { + return None; + } + let relative = strip_prefix_portable(&cleaned, repo)?; + normalize_artifact_path(&relative.to_string_lossy()) +} + +fn clean_path(path: &Path) -> Option { + let mut result = PathBuf::new(); + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir | Component::Normal(_) => { + result.push(component.as_os_str()) + } + Component::CurDir => {} + Component::ParentDir if !result.pop() => return None, + Component::ParentDir => {} + } + } + Some(result) +} + +fn nearest_existing_ancestor(path: &Path) -> Option<&Path> { + path.ancestors().find(|ancestor| ancestor.exists()) +} + +fn strip_prefix_portable(path: &Path, root: &Path) -> Option { + let path_components = path.components().collect::>(); + let root_components = root.components().collect::>(); + if root_components.len() > path_components.len() { + return None; + } + for (actual, expected) in path_components.iter().zip(&root_components) { + let actual = actual.as_os_str().to_string_lossy(); + let expected = expected.as_os_str().to_string_lossy(); + if cfg!(windows) { + if !actual.eq_ignore_ascii_case(&expected) { + return None; + } + } else if actual != expected { + return None; + } + } + let mut relative = PathBuf::new(); + for component in &path_components[root_components.len()..] { + relative.push(component.as_os_str()); + } + Some(relative) +} + +fn normalize_artifact_path(path: &str) -> Option { + let replaced = path.replace('\\', "/"); + let trimmed = replaced.trim().trim_start_matches("./"); + if trimmed.is_empty() || Path::new(trimmed).is_absolute() { + return None; + } + let mut components = Vec::new(); + for component in trimmed.split('/') { + match component { + "" | "." => {} + ".." => return None, + value => components.push(value), + } + } + (!components.is_empty()).then(|| { + let normalized = components.join("/"); + if cfg!(windows) { + normalized.to_lowercase() + } else { + normalized + } + }) +} + +fn normalized_action(value: Option<&str>) -> &'static str { + match value { + Some("search") => "search", + Some("read") => "read", + Some("edit") => "edit", + Some("exec") => "exec", + Some("verify") => "verify", + _ => "other", + } +} + +fn normalized_touch(value: Option<&str>) -> &'static str { + match value { + Some("read") => "read", + Some("edit") => "edit", + _ => "hit", + } +} + +fn tool_family(value: Option<&str>) -> &'static str { + let value = value.unwrap_or("").to_ascii_lowercase(); + if value.contains("apply_patch") || value.contains("write") || value.contains("edit") { + "edit" + } else if value.contains("exec") || value.contains("command") || value.contains("shell") { + "shell" + } else if value.contains("read") || value.contains("view") || value.contains("open") { + "read" + } else if value.contains("search") || value.contains("find") || value == "rg" { + "search" + } else if value.contains("agent") || value.contains("wait") || value.contains("message") { + "orchestration" + } else { + "other" + } +} + +fn grade(value: Option<&str>) -> &'static str { + match value { + Some("exact") => "exact", + Some("estimated") => "estimated", + _ => "unavailable", + } +} + +fn u64_any(value: &Value, fields: &[&str]) -> u64 { + fields + .iter() + .find_map(|field| value.get(field).and_then(Value::as_u64)) + .unwrap_or(0) +} + +fn f64_any(value: &Value, fields: &[&str]) -> f64 { + fields + .iter() + .find_map(|field| value.get(field).and_then(Value::as_f64)) + .unwrap_or(0.0) +} + +fn structural_attention(hotspot: &Value) -> bool { + u64_any(hotspot, &["maxComplexity", "max_complexity"]) >= 20 + || hotspot["git"]["churn"].as_u64().unwrap_or(0) >= 5 + || hotspot["git"]["dirty"].as_bool().unwrap_or(false) +} + +fn signal(kind: &str, event_seq: u64, path: &str, severity: &str) -> Value { + json!({ + "kind":kind, + "status":"observed", + "severity":severity, + "eventSeq":event_seq, + "path":path, + "authority":"advisory_only" + }) +} + +fn read_bounded_regular(path: &Path, max_bytes: u64, label: &str) -> Result, AdapterError> { + let metadata = fs::symlink_metadata(path) + .map_err(|error| AdapterError::Io(format!("cannot inspect {label}: {error}")))?; + if !metadata.is_file() || metadata.file_type().is_symlink() { + return Err(AdapterError::Contract(format!( + "{label} must be a regular file" + ))); + } + if metadata.len() > max_bytes { + return Err(AdapterError::Contract(format!( + "{label} exceeds the {max_bytes}-byte limit" + ))); + } + fs::read(path).map_err(|error| AdapterError::Io(format!("cannot read {label}: {error}"))) +} + +fn write_new_artifact(path: &Path, bytes: &[u8]) -> Result<(), AdapterError> { + if path.exists() { + return Err(AdapterError::Usage(format!( + "output already exists: {}", + path.display() + ))); + } + let parent = path + .parent() + .filter(|parent| parent.is_dir()) + .ok_or_else(|| AdapterError::Usage("output parent directory does not exist".into()))?; + let name = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| AdapterError::Usage("output file name is invalid".into()))?; + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| AdapterError::Io(error.to_string()))? + .as_nanos(); + let temporary = parent.join(format!(".{name}.tmp-{}-{nonce}", std::process::id())); + let result = (|| { + let mut file = OpenOptions::new() + .write(true) + .create_new(true) + .open(&temporary) + .map_err(|error| AdapterError::Io(format!("cannot create staged output: {error}")))?; + file.write_all(bytes) + .and_then(|_| file.sync_all()) + .map_err(|error| AdapterError::Io(format!("cannot write staged output: {error}")))?; + fs::rename(&temporary, path) + .map_err(|error| AdapterError::Io(format!("cannot publish output: {error}"))) + })(); + if result.is_err() { + let _ = fs::remove_file(&temporary); + } + result +} diff --git a/crates/code-intel-cli/src/snapshot.rs b/crates/code-intel-cli/src/snapshot.rs new file mode 100644 index 0000000..9142d25 --- /dev/null +++ b/crates/code-intel-cli/src/snapshot.rs @@ -0,0 +1,1464 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::io::Write; +use std::path::{Component, Path, PathBuf}; +use std::process::{Command, Stdio}; + +use serde_json::{json, Value}; + +use crate::capability::sha256_hex; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Policy { + HeadOnly, + ExplicitOverlay, +} + +impl Policy { + fn parse(value: &str) -> Result { + match value { + "head_only" => Ok(Self::HeadOnly), + "explicit_overlay" => Ok(Self::ExplicitOverlay), + _ => Err(SnapshotError::Usage( + "working-tree policy must be head_only or explicit_overlay".into(), + )), + } + } + + fn as_str(self) -> &'static str { + match self { + Self::HeadOnly => "head_only", + Self::ExplicitOverlay => "explicit_overlay", + } + } +} + +#[derive(Debug)] +enum SnapshotError { + Usage(String), + Contract(String), + Unavailable(String), + Io(String), +} + +impl SnapshotError { + fn exit_code(&self) -> i32 { + match self { + Self::Usage(_) => 64, + Self::Contract(_) => 65, + Self::Unavailable(_) => 69, + Self::Io(_) => 74, + } + } + + fn message(&self) -> &str { + match self { + Self::Usage(message) + | Self::Contract(message) + | Self::Unavailable(message) + | Self::Io(message) => message, + } + } +} + +struct Cli { + repo: PathBuf, + policy: Policy, + scopes: Vec, + alternate_vcs_command: Option, + alternate_vcs_args: Vec, +} + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let cli = match parse_cli(raw) { + Ok(cli) => cli, + Err(error) => { + eprintln!("{}", error.message()); + return error.exit_code(); + } + }; + match build(&cli.repo, cli.policy, &cli.scopes) + .and_then(|value| verify_alternate_vcs(&cli, value)) + { + Ok(value) => { + println!( + "{}", + serde_json::to_string(&value).expect("snapshot serializes") + ); + 0 + } + Err(error) => { + eprintln!("{}", error.message()); + error.exit_code() + } + } +} + +pub(crate) fn build_for_capability(repo: &Path, expected: &Value) -> Result { + let policy = expected + .get("workingTreePolicy") + .and_then(Value::as_str) + .ok_or_else(|| "expected snapshot omits workingTreePolicy".to_string()) + .and_then(|value| Policy::parse(value).map_err(|error| error.message().to_string()))?; + let scopes = expected + .get("scope") + .and_then(Value::as_array) + .ok_or("expected snapshot omits scope")? + .iter() + .map(|value| { + value + .as_str() + .map(str::to_string) + .ok_or_else(|| "expected snapshot scope contains a non-string".to_string()) + }) + .collect::, _>>()?; + let scopes = canonical_scopes(&scopes).map_err(|error| error.message().to_string())?; + let actual = build(repo, policy, &scopes).map_err(|error| error.message().to_string())?; + if actual["snapshot"] != *expected { + return Err("repository inputs do not match the expected snapshot identity".into()); + } + Ok(actual) +} + +pub(crate) fn build_for_dag( + repo: &Path, + working_tree_policy: &str, + scopes: &[String], +) -> Result { + let policy = Policy::parse(working_tree_policy).map_err(|error| error.message().to_string())?; + build(repo, policy, scopes).map_err(|error| error.message().to_string()) +} + +fn parse_cli(raw: &[String]) -> Result { + if raw.first().map(String::as_str) != Some("identity") { + return Err(SnapshotError::Usage( + "usage: snapshot identity --repo --working-tree-policy [--scope ]...".into(), + )); + } + let mut repo = None; + let mut policy = None; + let mut scopes = Vec::new(); + let mut alternate_vcs_command = None; + let mut alternate_vcs_args = Vec::new(); + let mut index = 1; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!( + flag, + "--repo" + | "--working-tree-policy" + | "--scope" + | "--alternate-vcs-command" + | "--alternate-vcs-arg" + ) { + return Err(SnapshotError::Usage(format!( + "unknown snapshot argument: {flag}" + ))); + } + let value = raw + .get(index + 1) + .filter(|value| !value.is_empty() && !value.starts_with("--")) + .ok_or_else(|| SnapshotError::Usage(format!("{flag} requires one value")))?; + match flag { + "--repo" if repo.replace(PathBuf::from(value)).is_some() => { + return Err(SnapshotError::Usage("duplicate --repo".into())) + } + "--repo" => {} + "--working-tree-policy" if policy.replace(Policy::parse(value)?).is_some() => { + return Err(SnapshotError::Usage( + "duplicate --working-tree-policy".into(), + )) + } + "--working-tree-policy" => {} + "--scope" => scopes.push(value.clone()), + "--alternate-vcs-command" if alternate_vcs_command.replace(value.clone()).is_some() => { + return Err(SnapshotError::Usage( + "duplicate --alternate-vcs-command".into(), + )) + } + "--alternate-vcs-command" => {} + "--alternate-vcs-arg" => alternate_vcs_args.push(value.clone()), + _ => unreachable!(), + } + index += 2; + } + let repo = repo.ok_or_else(|| SnapshotError::Usage("--repo is required".into()))?; + if !repo.is_dir() { + return Err(SnapshotError::Usage(format!( + "repository path is not a directory: {}", + repo.display() + ))); + } + let policy = + policy.ok_or_else(|| SnapshotError::Usage("--working-tree-policy is required".into()))?; + let scopes = canonical_scopes(&scopes)?; + if alternate_vcs_command.is_none() && !alternate_vcs_args.is_empty() { + return Err(SnapshotError::Usage( + "--alternate-vcs-arg requires --alternate-vcs-command".into(), + )); + } + Ok(Cli { + repo, + policy, + scopes, + alternate_vcs_command, + alternate_vcs_args, + }) +} + +fn verify_alternate_vcs(cli: &Cli, authoritative: Value) -> Result { + let Some(command) = cli.alternate_vcs_command.as_ref() else { + return Ok(authoritative); + }; + let request = json!({ + "schema": "code-intel-alternate-vcs-snapshot-request.v1", + "repo": cli.repo, + "workingTreePolicy": cli.policy.as_str(), + "scope": cli.scopes, + "authoritativeSnapshot": authoritative["snapshot"] + }); + let mut child = Command::new(command) + .args(&cli.alternate_vcs_args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| { + SnapshotError::Contract(format!("alternate VCS adapter could not start: {error}")) + })?; + child + .stdin + .take() + .ok_or_else(|| { + SnapshotError::Contract("alternate VCS adapter stdin is unavailable".into()) + })? + .write_all( + serde_json::to_string(&request) + .expect("alternate VCS request serializes") + .as_bytes(), + ) + .map_err(|error| { + SnapshotError::Contract(format!("alternate VCS adapter request failed: {error}")) + })?; + let output = child.wait_with_output().map_err(|error| { + SnapshotError::Contract(format!("alternate VCS adapter did not complete: {error}")) + })?; + if !output.status.success() { + return Err(SnapshotError::Contract(format!( + "alternate VCS adapter exited with {}: {}", + output.status.code().unwrap_or(65), + String::from_utf8_lossy(&output.stderr).trim() + ))); + } + let alternate: Value = serde_json::from_slice(&output.stdout).map_err(|error| { + SnapshotError::Contract(format!( + "alternate VCS adapter returned invalid JSON: {error}" + )) + })?; + if alternate.get("snapshot") != authoritative.get("snapshot") { + return Err(SnapshotError::Contract( + "alternate VCS adapter snapshot does not match the authoritative snapshot".into(), + )); + } + Ok(authoritative) +} + +fn canonical_scopes(values: &[String]) -> Result, SnapshotError> { + let values = if values.is_empty() { + vec![".".to_string()] + } else { + values.to_vec() + }; + let mut result = BTreeSet::new(); + for value in values { + let path = Path::new(&value); + if value.contains('\0') + || path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) + { + return Err(SnapshotError::Usage(format!( + "scope must be a repository-relative path without '..': {value}" + ))); + } + let normalized = path + .components() + .filter_map(|component| match component { + Component::Normal(value) => Some(value.to_string_lossy().into_owned()), + Component::CurDir => None, + _ => None, + }) + .collect::>() + .join("/"); + result.insert(if normalized.is_empty() { + ".".to_string() + } else { + normalized + }); + } + let sorted = result.into_iter().collect::>(); + #[cfg(windows)] + for (index, left) in sorted.iter().enumerate() { + for right in &sorted[index + 1..] { + let left_folded = left.to_ascii_lowercase(); + let right_folded = right.to_ascii_lowercase(); + let folded_overlap = left_folded == right_folded + || right_folded + .strip_prefix(&left_folded) + .is_some_and(|suffix| suffix.starts_with('/')) + || left_folded + .strip_prefix(&right_folded) + .is_some_and(|suffix| suffix.starts_with('/')); + let exact_overlap = left == right + || right + .strip_prefix(left) + .is_some_and(|suffix| suffix.starts_with('/')) + || left + .strip_prefix(right) + .is_some_and(|suffix| suffix.starts_with('/')); + if folded_overlap && !exact_overlap { + return Err(SnapshotError::Usage(format!( + "scope case collision is ambiguous on Windows: {left} vs {right}" + ))); + } + } + } + let mut minimal = Vec::::new(); + for scope in sorted { + if minimal.iter().any(|parent| { + parent == "." + || scope == *parent + || scope + .strip_prefix(parent) + .is_some_and(|suffix| suffix.starts_with('/')) + }) { + continue; + } + minimal.push(scope); + } + Ok(minimal) +} + +pub(crate) struct SnapshotLease { + expected: Value, + policy: Policy, + scopes: Vec, + manifest: InputManifest, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ManifestEntry { + path: String, + kind: String, + mode: String, + digest: String, + control_bytes: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct InputManifest { + policy: Policy, + scopes: Vec, + entries: Vec, +} + +fn is_ignore_control_path(path: &str) -> bool { + Path::new(path) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| matches!(name, ".gitignore" | ".ignore" | ".rgignore")) +} + +fn path_in_scopes(path: &str, scopes: &[String]) -> bool { + scopes.iter().any(|scope| { + scope == "." + || path == scope + || path + .strip_prefix(scope) + .is_some_and(|suffix| suffix.starts_with('/')) + }) +} + +fn ignore_control_relevant(path: &str, scopes: &[String]) -> bool { + if !is_ignore_control_path(path) { + return false; + } + let parent = path + .rsplit_once('/') + .map(|(parent, _)| parent) + .unwrap_or("."); + scopes.iter().any(|scope| { + scope == "." + || parent == scope + || parent + .strip_prefix(scope) + .is_some_and(|suffix| suffix.starts_with('/')) + }) +} + +fn filesystem_ignore_controls( + repo: &Path, + scopes: &[String], +) -> Result, SnapshotError> { + let rg = if cfg!(windows) { "rg.exe" } else { "rg" }; + let output = Command::new(rg) + .args([ + "--files", + "--hidden", + "--null", + "--no-ignore-parent", + "--no-ignore-global", + "--no-ignore-exclude", + "-g", + "**/.gitignore", + "-g", + "**/.ignore", + "-g", + "**/.rgignore", + ]) + .env_remove("RIPGREP_CONFIG_PATH") + .current_dir(repo) + .output() + .map_err(|error| SnapshotError::Unavailable(format!("cannot launch {rg}: {error}")))?; + if !output.status.success() && output.status.code() != Some(1) { + return Err(SnapshotError::Unavailable(format!( + "ignore-control inventory failed: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + let mut controls = decode_paths(&output.stdout)? + .into_iter() + .filter(|path| ignore_control_relevant(path, scopes)) + .collect::>(); + controls.sort(); + controls.dedup(); + Ok(controls) +} + +pub(crate) fn begin_consumption(repo: &Path, expected: &Value) -> Result { + let policy = expected + .get("workingTreePolicy") + .and_then(Value::as_str) + .ok_or_else(|| "expected snapshot omits workingTreePolicy".to_string()) + .and_then(|value| Policy::parse(value).map_err(|error| error.message().to_string()))?; + let scopes = expected + .get("scope") + .and_then(Value::as_array) + .ok_or("expected snapshot omits scope")? + .iter() + .map(|value| { + value + .as_str() + .map(str::to_string) + .ok_or_else(|| "expected snapshot scope contains a non-string".to_string()) + }) + .collect::, _>>()?; + let scopes = canonical_scopes(&scopes).map_err(|error| error.message().to_string())?; + let actual = build(repo, policy, &scopes).map_err(|error| error.message().to_string())?; + if actual["snapshot"] != *expected { + return Err("repository inputs do not match the expected snapshot identity".into()); + } + let manifest = + input_manifest(repo, policy, &scopes).map_err(|error| error.message().to_string())?; + Ok(SnapshotLease { + expected: expected.clone(), + policy, + scopes, + manifest, + }) +} + +impl SnapshotLease { + pub(crate) fn scopes(&self) -> &[String] { + &self.scopes + } + + pub(crate) fn inventory_mirror_files(&self) -> BTreeMap>> { + let mut paths = BTreeMap::new(); + for entry in &self.manifest.entries { + if matches!(entry.kind.as_str(), "tombstone" | "gitlink" | "symlink") { + continue; + } + paths.insert(entry.path.clone(), entry.control_bytes.clone()); + } + paths + } + + pub(crate) fn inventory_gitlink_paths(&self) -> Vec { + self.manifest + .entries + .iter() + .filter(|entry| entry.kind == "gitlink") + .map(|entry| entry.path.clone()) + .collect() + } + + pub(crate) fn verify_after(&self, repo: &Path) -> Result<(), String> { + let actual = + build(repo, self.policy, &self.scopes).map_err(|error| error.message().to_string())?; + if actual["snapshot"] != self.expected { + return Err("repository inputs changed while the capability consumed them".into()); + } + let manifest = input_manifest(repo, self.policy, &self.scopes) + .map_err(|error| error.message().to_string())?; + if manifest != self.manifest { + return Err( + "repository input manifest changed while the capability consumed it".into(), + ); + } + Ok(()) + } +} + +fn build(repo: &Path, policy: Policy, scopes: &[String]) -> Result { + let git = git_context(repo)?; + if git.is_none() && policy == Policy::HeadOnly { + return Err(SnapshotError::Unavailable( + "head_only snapshot identity requires Git with a resolvable HEAD".into(), + )); + } + if git.is_none() + && scopes + .iter() + .filter(|scope| scope.as_str() != ".") + .any(|scope| !repo.join(scope).exists()) + { + return Err(SnapshotError::Usage( + "non-root scope has no manifest entries and does not exist".into(), + )); + } + + let (repo_identity, head, input_digest, overlay, repository_kind) = match git { + Some(git) => { + let (input_digest, overlay) = match policy { + Policy::HeadOnly => (digest_head(repo, &git.head, scopes)?, Overlay::default()), + Policy::ExplicitOverlay => stable_overlay_snapshot(repo, scopes)?, + }; + (git.repo_identity, git.head, input_digest, overlay, "git") + } + None => { + let (input_digest, paths) = stable_unversioned_snapshot(repo, scopes)?; + let overlay = Overlay::unversioned(paths); + let unborn = + git_output(repo, &["rev-parse", "--is-inside-work-tree"]).is_ok_and(|output| { + output.status.success() && trim_ascii(&output.stdout) == b"true" + }); + ( + format!("content-v1:{input_digest}"), + if unborn { "unborn" } else { "unversioned" }.to_string(), + input_digest, + overlay, + if unborn { "git_unborn" } else { "unversioned" }, + ) + } + }; + + let dirty_paths = overlay.paths(); + let overlay_digest = if dirty_paths.is_empty() { + None + } else { + Some(hash_records( + dirty_paths + .iter() + .map(|path| path.as_bytes().to_vec()) + .collect::>() + .as_slice(), + )) + }; + let identity = hash_records(&[ + b"code-intel-snapshot.v1".to_vec(), + repo_identity.as_bytes().to_vec(), + head.as_bytes().to_vec(), + policy.as_str().as_bytes().to_vec(), + scopes.join("\0").into_bytes(), + input_digest.as_bytes().to_vec(), + ]); + let document = json!({ + "schema": "code-intel-repository-snapshot.v1", + "snapshot": { + "identity": identity, + "repoIdentity": repo_identity, + "head": head, + "workingTreePolicy": policy.as_str(), + "scope": scopes, + "inputDigest": input_digest + }, + "dirtyOverlay": { + "present": !dirty_paths.is_empty(), + "digest": overlay_digest, + "paths": dirty_paths, + "members": overlay.members_json(), + "ignoredPolicy": "excluded_by_git_ignore" + }, + "repository": { "kind": repository_kind } + }); + let _ = input_manifest(repo, policy, scopes)?; + Ok(document) +} + +fn input_manifest( + repo: &Path, + policy: Policy, + scopes: &[String], +) -> Result { + let git = git_context(repo)?; + let mut entries = match (git, policy) { + (Some(git), Policy::HeadOnly) => head_manifest(repo, &git.head, scopes)?, + (Some(_), Policy::ExplicitOverlay) => worktree_manifest(repo, scopes)?, + (None, Policy::ExplicitOverlay) => { + if scopes + .iter() + .filter(|scope| scope.as_str() != ".") + .any(|scope| !repo.join(scope).exists()) + { + return Err(SnapshotError::Usage( + "non-root scope has no manifest entries and does not exist".into(), + )); + } + unversioned_manifest(repo, scopes)? + } + (None, Policy::HeadOnly) => { + return Err(SnapshotError::Unavailable( + "head_only snapshot identity requires Git with a resolvable HEAD".into(), + )) + } + }; + entries.sort_by(|left, right| left.path.cmp(&right.path)); + if entries.is_empty() + && scopes + .iter() + .filter(|scope| scope.as_str() != ".") + .any(|scope| !repo.join(scope).exists()) + { + return Err(SnapshotError::Usage( + "non-root scope has no manifest entries and does not exist".into(), + )); + } + Ok(InputManifest { + policy, + scopes: scopes.to_vec(), + entries, + }) +} + +fn head_manifest( + repo: &Path, + head: &str, + scopes: &[String], +) -> Result, SnapshotError> { + let args = ["ls-tree", "-r", "-z", "--full-tree", head, "--"]; + let bytes = git_required(repo, &args, "build HEAD input manifest")?; + let mut entries = Vec::new(); + for raw in bytes + .split(|byte| *byte == 0) + .filter(|entry| !entry.is_empty()) + { + let tab = raw.iter().position(|byte| *byte == b'\t').ok_or_else(|| { + SnapshotError::Unavailable("Git tree entry omitted path separator".into()) + })?; + let header = String::from_utf8(raw[..tab].to_vec()).map_err(|error| { + SnapshotError::Unavailable(format!("Git tree header is not UTF-8: {error}")) + })?; + let fields = header.split(' ').collect::>(); + if fields.len() != 3 { + return Err(SnapshotError::Unavailable( + "malformed Git tree entry".into(), + )); + } + let path = String::from_utf8(raw[tab + 1..].to_vec()) + .map_err(|error| SnapshotError::Unavailable(format!("Git path is not UTF-8: {error}")))? + .replace('\\', "/"); + if !path_in_scopes(&path, scopes) && !ignore_control_relevant(&path, scopes) { + continue; + } + let content = if fields[1] == "blob" { + git_required(repo, &["cat-file", "blob", fields[2]], "read Git tree blob")? + } else { + fields[2].as_bytes().to_vec() + }; + entries.push(ManifestEntry { + path, + kind: if fields[1] == "commit" { + "gitlink".into() + } else if fields[0] == "120000" { + "symlink".into() + } else { + "file".into() + }, + mode: fields[0].into(), + digest: sha256_hex(&content), + control_bytes: is_ignore_control_path( + std::str::from_utf8(&raw[tab + 1..]).unwrap_or_default(), + ) + .then_some(content), + }); + } + Ok(entries) +} + +fn worktree_manifest(repo: &Path, scopes: &[String]) -> Result, SnapshotError> { + let mut index = index_entries(repo, scopes)?; + for (path, entry) in index_entries(repo, &[".".to_string()])? { + if ignore_control_relevant(&path, scopes) { + index.insert(path, entry); + } + } + let mut paths = index.keys().cloned().collect::>(); + paths.extend(untracked_paths(repo, scopes)?); + paths.extend(filesystem_ignore_controls(repo, scopes)?); + paths + .into_iter() + .map(|path| manifest_entry_from_worktree(repo, &path, index.get(&path))) + .collect() +} + +fn unversioned_manifest( + repo: &Path, + scopes: &[String], +) -> Result, SnapshotError> { + inventory_unversioned(repo, scopes)? + .into_iter() + .map(|path| manifest_entry_from_worktree(repo, &path, None)) + .collect() +} + +fn manifest_entry_from_worktree( + repo: &Path, + path: &str, + indexed: Option<&IndexEntry>, +) -> Result { + let mut mode = indexed + .map(|entry| entry.mode.clone()) + .unwrap_or_else(|| "100644".into()); + if mode == "160000" { + let content = indexed.expect("gitlink is indexed").oid.as_bytes(); + return Ok(ManifestEntry { + path: path.into(), + kind: "gitlink".into(), + mode, + digest: sha256_hex(content), + control_bytes: None, + }); + } + let full_path = repo.join(path); + match fs::symlink_metadata(&full_path) { + Ok(metadata) if metadata.file_type().is_symlink() => { + mode = "120000".into(); + let target = fs::read_link(&full_path) + .map_err(|error| SnapshotError::Io(format!("read symlink {path}: {error}")))?; + let bytes = target + .to_str() + .ok_or_else(|| SnapshotError::Io(format!("non-UTF-8 symlink: {path}")))? + .replace('\\', "/") + .into_bytes(); + Ok(ManifestEntry { + path: path.into(), + kind: "symlink".into(), + mode, + digest: sha256_hex(&bytes), + control_bytes: is_ignore_control_path(path).then_some(bytes), + }) + } + Ok(metadata) if metadata.is_file() => { + mode = effective_file_mode(&mode, &metadata); + let bytes = fs::read(&full_path) + .map_err(|error| SnapshotError::Io(format!("read {path}: {error}")))?; + Ok(ManifestEntry { + path: path.into(), + kind: "file".into(), + mode, + digest: sha256_hex(&bytes), + control_bytes: is_ignore_control_path(path).then_some(bytes), + }) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(ManifestEntry { + path: path.into(), + kind: "tombstone".into(), + mode, + digest: sha256_hex(b""), + control_bytes: None, + }), + Ok(_) => Err(SnapshotError::Io(format!( + "manifest input has unsupported filesystem kind: {path}" + ))), + Err(error) => Err(SnapshotError::Io(format!("inspect {path}: {error}"))), + } +} + +struct GitContext { + repo_identity: String, + head: String, +} + +fn git_context(repo: &Path) -> Result, SnapshotError> { + let inside = git_output(repo, &["rev-parse", "--is-inside-work-tree"])?; + if !inside.status.success() || trim_ascii(&inside.stdout) != b"true" { + return Ok(None); + } + let top = git_required(repo, &["rev-parse", "--show-toplevel"], "resolve Git root")?; + let top = String::from_utf8(top) + .map_err(|error| SnapshotError::Unavailable(format!("Git root is not UTF-8: {error}")))?; + let actual = fs::canonicalize(repo) + .map_err(|error| SnapshotError::Io(format!("canonicalize repository: {error}")))?; + let expected = fs::canonicalize(top.trim()) + .map_err(|error| SnapshotError::Io(format!("canonicalize Git root: {error}")))?; + if actual != expected { + return Err(SnapshotError::Usage( + "--repo must name the Git worktree root; express subdirectories with --scope".into(), + )); + } + let shallow = git_required( + repo, + &["rev-parse", "--is-shallow-repository"], + "inspect shallow repository state", + )?; + if trim_ascii(&shallow) == b"true" { + return Err(SnapshotError::Unavailable( + "shallow Git repositories are unsupported because lineage identity is incomplete" + .into(), + )); + } + let head_output = git_output(repo, &["rev-parse", "--verify", "HEAD"])?; + if !head_output.status.success() { + return Ok(None); + } + let head = String::from_utf8(head_output.stdout) + .map_err(|error| SnapshotError::Unavailable(format!("Git HEAD is not UTF-8: {error}")))? + .trim() + .to_string(); + let roots = git_required( + repo, + &["rev-list", "--max-parents=0", &head], + "resolve Git lineage", + )?; + let mut roots = String::from_utf8(roots) + .map_err(|error| SnapshotError::Unavailable(format!("Git lineage is not UTF-8: {error}")))? + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(str::to_string) + .collect::>(); + roots.sort(); + roots.dedup(); + if roots.is_empty() { + return Err(SnapshotError::Unavailable( + "Git repository has no resolvable root commit".into(), + )); + } + let repo_identity = format!( + "git-lineage-v1:{}", + hash_records( + &roots + .iter() + .map(|root| root.as_bytes().to_vec()) + .collect::>() + ) + ); + Ok(Some(GitContext { + repo_identity, + head, + })) +} + +fn digest_head(repo: &Path, head: &str, scopes: &[String]) -> Result { + let args = ["ls-tree", "-r", "-z", "--full-tree", head, "--"]; + let output = git_required(repo, &args, "enumerate HEAD snapshot")?; + let mut records = output + .split(|byte| *byte == 0) + .filter(|record| !record.is_empty()) + .filter(|record| { + let Some(tab) = record.iter().position(|byte| *byte == b'\t') else { + return true; + }; + let Ok(path) = std::str::from_utf8(&record[tab + 1..]) else { + return true; + }; + path_in_scopes(path, scopes) || ignore_control_relevant(path, scopes) + }) + .map(Vec::from) + .collect::>(); + records.sort(); + let mut framed = vec![b"code-intel-head-input.v1".to_vec()]; + framed.append(&mut records); + Ok(hash_records(&framed)) +} + +fn stable_overlay_snapshot( + repo: &Path, + scopes: &[String], +) -> Result<(String, Overlay), SnapshotError> { + stable_overlay_snapshot_with(repo, scopes, || {}) +} + +fn stable_overlay_snapshot_with( + repo: &Path, + scopes: &[String], + between_reads: F, +) -> Result<(String, Overlay), SnapshotError> { + let before = overlay_status(repo, scopes)?; + let first = digest_worktree(repo, scopes)?; + between_reads(); + let second = digest_worktree(repo, scopes)?; + let after = overlay_status(repo, scopes)?; + if first != second || before != after { + return Err(SnapshotError::Io( + "working tree changed while snapshot identity was being computed; retry".into(), + )); + } + Ok((first, after)) +} + +#[derive(Clone)] +struct IndexEntry { + mode: String, + oid: String, +} + +fn digest_worktree(repo: &Path, scopes: &[String]) -> Result { + let mut index = index_entries(repo, scopes)?; + for (path, entry) in index_entries(repo, &[".".to_string()])? { + if ignore_control_relevant(&path, scopes) { + index.insert(path, entry); + } + } + let mut paths = index.keys().cloned().collect::>(); + paths.extend(untracked_paths(repo, scopes)?); + paths.extend(filesystem_ignore_controls(repo, scopes)?); + let mut records = vec![b"code-intel-overlay-input.v1".to_vec()]; + for relative in paths { + let indexed = index.get(&relative); + let mut mode = indexed + .map(|entry| entry.mode.clone()) + .unwrap_or_else(|| "100644".into()); + let mut kind = match mode.as_str() { + "160000" => "gitlink", + "120000" => "symlink", + _ => "file", + }; + let path = repo.join(Path::new(&relative)); + let content = if kind == "gitlink" { + indexed.expect("gitlink is tracked").oid.as_bytes().to_vec() + } else { + match fs::symlink_metadata(&path) { + Ok(metadata) if metadata.file_type().is_symlink() => { + kind = "symlink"; + mode = "120000".into(); + fs::read_link(&path) + .map_err(|error| { + SnapshotError::Io(format!("read symlink target {relative}: {error}")) + })? + .to_str() + .ok_or_else(|| { + SnapshotError::Io(format!( + "symlink target is not portable UTF-8: {relative}" + )) + })? + .replace('\\', "/") + .into_bytes() + } + Ok(metadata) if metadata.is_file() => { + mode = effective_file_mode(&mode, &metadata); + fs::read(&path).map_err(|error| { + SnapshotError::Io(format!("read scoped input {relative}: {error}")) + })? + } + Ok(_) => { + return Err(SnapshotError::Io(format!( + "scoped input has unsupported filesystem kind: {relative}" + ))) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let mut record = b"tombstone".to_vec(); + record.push(0); + record.extend_from_slice(mode.as_bytes()); + record.push(0); + record.extend_from_slice(relative.as_bytes()); + records.push(record); + continue; + } + Err(error) => { + return Err(SnapshotError::Io(format!( + "inspect scoped input {relative}: {error}" + ))) + } + } + }; + let mut record = kind.as_bytes().to_vec(); + record.push(0); + record.extend_from_slice(mode.as_bytes()); + record.push(0); + record.extend_from_slice(relative.as_bytes()); + record.push(0); + record.extend_from_slice(&(content.len() as u64).to_be_bytes()); + record.extend_from_slice(&content); + records.push(record); + } + Ok(hash_records(&records)) +} + +fn index_entries( + repo: &Path, + scopes: &[String], +) -> Result, SnapshotError> { + let mut args = vec!["ls-files", "-s", "-z", "--"]; + args.extend(scopes.iter().map(String::as_str)); + let bytes = git_required(repo, &args, "read Git index")?; + let mut entries = BTreeMap::new(); + for raw in bytes + .split(|byte| *byte == 0) + .filter(|entry| !entry.is_empty()) + { + let tab = raw.iter().position(|byte| *byte == b'\t').ok_or_else(|| { + SnapshotError::Unavailable("Git index entry omitted path separator".into()) + })?; + let header = String::from_utf8(raw[..tab].to_vec()).map_err(|error| { + SnapshotError::Unavailable(format!("Git index header is not UTF-8: {error}")) + })?; + let fields = header.split(' ').collect::>(); + if fields.len() != 3 || fields[2] != "0" { + return Err(SnapshotError::Unavailable( + "unmerged or malformed Git index cannot produce a snapshot identity".into(), + )); + } + let path = String::from_utf8(raw[tab + 1..].to_vec()) + .map_err(|error| SnapshotError::Unavailable(format!("Git path is not UTF-8: {error}")))? + .replace('\\', "/"); + entries.insert( + path, + IndexEntry { + mode: fields[0].to_string(), + oid: fields[1].to_string(), + }, + ); + } + Ok(entries) +} + +fn effective_file_mode(index_mode: &str, metadata: &fs::Metadata) -> String { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if metadata.permissions().mode() & 0o111 != 0 { + "100755".into() + } else { + "100644".into() + } + } + #[cfg(not(unix))] + { + let _ = metadata; + index_mode.to_string() + } +} + +fn untracked_paths(repo: &Path, scopes: &[String]) -> Result, SnapshotError> { + let mut args = vec![ + "ls-files", + "--others", + "--exclude-per-directory=.gitignore", + "-z", + "--", + ]; + args.extend(scopes.iter().map(String::as_str)); + decode_paths(&git_required( + repo, + &args, + "enumerate untracked snapshot inputs", + )?) +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct Overlay { + tracked_modified: BTreeSet, + tracked_deleted: BTreeSet, + untracked: BTreeSet, + renamed: BTreeSet, + type_changed: BTreeSet, + staged: BTreeSet, +} + +impl Overlay { + fn unversioned(paths: Vec) -> Self { + Self { + untracked: paths.into_iter().collect(), + ..Self::default() + } + } + + fn paths(&self) -> Vec { + let mut paths = BTreeSet::new(); + paths.extend(self.tracked_modified.iter().cloned()); + paths.extend(self.tracked_deleted.iter().cloned()); + paths.extend(self.untracked.iter().cloned()); + paths.extend(self.renamed.iter().cloned()); + paths.extend(self.type_changed.iter().cloned()); + paths.extend(self.staged.iter().cloned()); + paths.into_iter().collect() + } + + fn members_json(&self) -> Value { + json!({ + "trackedModified": self.tracked_modified, + "trackedDeleted": self.tracked_deleted, + "untracked": self.untracked, + "renamed": self.renamed, + "typeChanged": self.type_changed, + "staged": self.staged + }) + } +} + +fn overlay_status(repo: &Path, scopes: &[String]) -> Result { + let mut args = vec![ + "status", + "--porcelain=v1", + "-z", + "--untracked-files=all", + "--", + ]; + let mut pathspecs = scopes.to_vec(); + pathspecs.extend(filesystem_ignore_controls(repo, scopes)?); + pathspecs.extend( + index_entries(repo, &[".".to_string()])? + .into_keys() + .filter(|path| ignore_control_relevant(path, scopes)), + ); + pathspecs.sort(); + pathspecs.dedup(); + args.extend(pathspecs.iter().map(String::as_str)); + let output = git_required(repo, &args, "inspect working-tree overlay")?; + let entries = output + .split(|byte| *byte == 0) + .filter(|entry| !entry.is_empty()) + .collect::>(); + let mut overlay = Overlay::default(); + let mut index = 0; + while index < entries.len() { + let entry = entries[index]; + if entry.len() < 4 { + return Err(SnapshotError::Unavailable( + "Git returned malformed porcelain status".into(), + )); + } + let status = &entry[..2]; + let path = String::from_utf8(entry[3..].to_vec()) + .map_err(|error| SnapshotError::Unavailable(format!("Git path is not UTF-8: {error}")))? + .replace('\\', "/"); + classify_status(&mut overlay, status, &path)?; + if status.contains(&b'R') || status.contains(&b'C') { + index += 1; + let source = entries.get(index).ok_or_else(|| { + SnapshotError::Unavailable("Git rename status omitted source path".into()) + })?; + overlay.renamed.insert( + String::from_utf8(source.to_vec()) + .map_err(|error| { + SnapshotError::Unavailable(format!("Git path is not UTF-8: {error}")) + })? + .replace('\\', "/"), + ); + } + index += 1; + } + overlay.untracked.extend(untracked_paths(repo, scopes)?); + let indexed = index_entries(repo, &[".".to_string()])?; + overlay.untracked.extend( + filesystem_ignore_controls(repo, scopes)? + .into_iter() + .filter(|path| !indexed.contains_key(path)), + ); + Ok(overlay) +} + +fn classify_status(overlay: &mut Overlay, status: &[u8], path: &str) -> Result<(), SnapshotError> { + if matches!( + status, + b"DD" | b"AU" | b"UD" | b"UA" | b"DU" | b"AA" | b"UU" + ) { + return Err(SnapshotError::Unavailable(format!( + "unmerged Git status cannot produce a snapshot identity: {}", + String::from_utf8_lossy(status) + ))); + } + if status == b"!!" { + return Ok(()); + } + let x = status[0]; + let y = status[1]; + if status == b"??" { + overlay.untracked.insert(path.into()); + return Ok(()); + } + if x != b' ' { + overlay.staged.insert(path.into()); + } + if x == b'D' || y == b'D' { + overlay.tracked_deleted.insert(path.into()); + } + if x == b'R' || y == b'R' || x == b'C' || y == b'C' { + overlay.renamed.insert(path.into()); + } + if x == b'T' || y == b'T' { + overlay.type_changed.insert(path.into()); + } + if matches!(x, b'M' | b'A') || matches!(y, b'M' | b'A') { + overlay.tracked_modified.insert(path.into()); + } + Ok(()) +} + +fn decode_paths(bytes: &[u8]) -> Result, SnapshotError> { + let mut paths = bytes + .split(|byte| *byte == 0) + .filter(|path| !path.is_empty()) + .map(|path| { + String::from_utf8(path.to_vec()).map(|path| { + let normalized = path.replace('\\', "/"); + normalized + .strip_prefix("./") + .unwrap_or(&normalized) + .to_string() + }) + }) + .collect::, _>>() + .map_err(|error| SnapshotError::Unavailable(format!("Git path is not UTF-8: {error}")))?; + paths.sort(); + paths.dedup(); + Ok(paths) +} + +fn inventory_unversioned(repo: &Path, scopes: &[String]) -> Result, SnapshotError> { + let rg = if cfg!(windows) { "rg.exe" } else { "rg" }; + let output = Command::new(rg) + .args([ + "--files", + "--hidden", + "--null", + "--no-require-git", + "--no-ignore-parent", + "--no-ignore-global", + "--no-ignore-exclude", + "-g", + "!**/.git/**", + ]) + .args(scopes) + .env_remove("RIPGREP_CONFIG_PATH") + .current_dir(repo) + .output() + .map_err(|error| SnapshotError::Unavailable(format!("cannot launch {rg}: {error}")))?; + if !output.status.success() && output.status.code() != Some(1) { + return Err(SnapshotError::Unavailable(format!( + "unversioned inventory failed: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + let mut paths = decode_paths(&output.stdout)?; + paths.extend(filesystem_ignore_controls(repo, scopes)?); + paths.sort(); + paths.dedup(); + Ok(paths) +} + +fn digest_unversioned(repo: &Path, scopes: &[String]) -> Result { + let paths = inventory_unversioned(repo, scopes)?; + let mut records = vec![b"code-intel-unversioned-input.v1".to_vec()]; + for relative in paths { + let path = repo.join(&relative); + let metadata = fs::symlink_metadata(&path) + .map_err(|error| SnapshotError::Io(format!("inspect {relative}: {error}")))?; + let (kind, bytes) = if metadata.file_type().is_symlink() { + let target = fs::read_link(&path) + .map_err(|error| SnapshotError::Io(format!("read symlink {relative}: {error}")))?; + ( + "symlink", + target + .to_str() + .ok_or_else(|| SnapshotError::Io(format!("non-UTF-8 symlink: {relative}")))? + .replace('\\', "/") + .into_bytes(), + ) + } else if metadata.is_file() { + ( + "file", + fs::read(&path) + .map_err(|error| SnapshotError::Io(format!("read {relative}: {error}")))?, + ) + } else { + continue; + }; + let mut record = kind.as_bytes().to_vec(); + record.push(0); + record.extend_from_slice(relative.as_bytes()); + record.push(0); + record.extend_from_slice(&(bytes.len() as u64).to_be_bytes()); + record.extend_from_slice(&bytes); + records.push(record); + } + Ok(hash_records(&records)) +} + +fn stable_unversioned_snapshot( + repo: &Path, + scopes: &[String], +) -> Result<(String, Vec), SnapshotError> { + let before_paths = inventory_unversioned(repo, scopes)?; + let first = digest_unversioned(repo, scopes)?; + let second = digest_unversioned(repo, scopes)?; + let after_paths = inventory_unversioned(repo, scopes)?; + if first != second || before_paths != after_paths { + return Err(SnapshotError::Io( + "unversioned tree changed while snapshot identity was being computed; retry".into(), + )); + } + Ok((first, after_paths)) +} + +fn git_output(repo: &Path, args: &[&str]) -> Result { + Command::new("git") + .args(args) + .current_dir(repo) + .output() + .map_err(|error| SnapshotError::Unavailable(format!("cannot launch Git: {error}"))) +} + +fn git_required(repo: &Path, args: &[&str], action: &str) -> Result, SnapshotError> { + let output = git_output(repo, args)?; + if !output.status.success() { + return Err(SnapshotError::Unavailable(format!( + "cannot {action}: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + Ok(output.stdout) +} + +fn trim_ascii(bytes: &[u8]) -> &[u8] { + let start = bytes + .iter() + .position(|byte| !byte.is_ascii_whitespace()) + .unwrap_or(bytes.len()); + let end = bytes + .iter() + .rposition(|byte| !byte.is_ascii_whitespace()) + .map(|index| index + 1) + .unwrap_or(start); + &bytes[start..end] +} + +fn hash_records(records: &[Vec]) -> String { + let mut canonical = Vec::new(); + for record in records { + canonical.extend_from_slice(&(record.len() as u64).to_be_bytes()); + canonical.extend_from_slice(record); + } + sha256_hex(&canonical) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn overlay_rejects_a_change_between_complete_reads() { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let repo = std::env::temp_dir().join(format!("code-intel-snapshot-race-{nonce}")); + fs::create_dir_all(&repo).unwrap(); + for args in [ + vec!["init", "--quiet"], + vec!["config", "user.name", "Snapshot Test"], + vec!["config", "user.email", "snapshot@example.invalid"], + ] { + assert!(Command::new("git") + .args(args) + .current_dir(&repo) + .status() + .unwrap() + .success()); + } + fs::write(repo.join("file.txt"), "one").unwrap(); + assert!(Command::new("git") + .args(["add", "."]) + .current_dir(&repo) + .status() + .unwrap() + .success()); + assert!(Command::new("git") + .args(["commit", "--quiet", "-m", "fixture"]) + .current_dir(&repo) + .status() + .unwrap() + .success()); + let result = stable_overlay_snapshot_with(&repo, &[".".into()], || { + fs::write(repo.join("file.txt"), "two").unwrap(); + }); + assert!(matches!(result, Err(SnapshotError::Io(message)) if message.contains("changed"))); + fs::remove_dir_all(repo).unwrap(); + } + + #[test] + fn porcelain_xy_table_classifies_every_supported_state() { + let cases: &[(&[u8], &str)] = &[ + (b"??", "untracked"), + (b"!!", "ignored"), + (b" M", "modified"), + (b" A", "modified"), + (b" D", "deleted"), + (b"M ", "staged_modified"), + (b"A ", "staged_modified"), + (b"D ", "staged_deleted"), + (b"R ", "renamed"), + (b"C ", "renamed"), + (b" T", "type"), + (b"T ", "staged_type"), + (b"MM", "staged_modified"), + (b"AM", "staged_modified"), + (b"RM", "renamed"), + (b"MD", "staged_deleted"), + (b"AD", "staged_deleted"), + (b"RD", "renamed_deleted"), + (b"CD", "renamed_deleted"), + ]; + for (status, expected) in cases { + let mut overlay = Overlay::default(); + classify_status(&mut overlay, status, "file").unwrap(); + let present = |set: &BTreeSet| set.contains("file"); + match *expected { + "untracked" => assert!(present(&overlay.untracked)), + "ignored" => assert!(overlay.paths().is_empty()), + "modified" => assert!(present(&overlay.tracked_modified)), + "deleted" => assert!(present(&overlay.tracked_deleted)), + "renamed" => assert!(present(&overlay.renamed)), + "type" => assert!(present(&overlay.type_changed)), + "staged_modified" => { + assert!(present(&overlay.staged)); + assert!(present(&overlay.tracked_modified)); + } + "staged_deleted" => { + assert!(present(&overlay.staged)); + assert!(present(&overlay.tracked_deleted)); + } + "staged_type" => { + assert!(present(&overlay.staged)); + assert!(present(&overlay.type_changed)); + } + "renamed_deleted" => { + assert!(present(&overlay.staged)); + assert!(present(&overlay.renamed)); + assert!(present(&overlay.tracked_deleted)); + } + _ => unreachable!(), + } + } + for status in [b"DD", b"AU", b"UD", b"UA", b"DU", b"AA", b"UU"] { + let mut overlay = Overlay::default(); + assert!(classify_status(&mut overlay, status, "file").is_err()); + } + } +} diff --git a/crates/code-intel-cli/src/stable_artifact.rs b/crates/code-intel-cli/src/stable_artifact.rs new file mode 100644 index 0000000..c960c5d --- /dev/null +++ b/crates/code-intel-cli/src/stable_artifact.rs @@ -0,0 +1,539 @@ +#[cfg(windows)] +use std::fs::OpenOptions; +use std::fs::{self, File}; +use std::io::Read; +use std::path::Path; +#[cfg(windows)] +use std::path::PathBuf; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct FileId { + volume: u64, + file: u128, +} + +#[derive(Debug)] +pub(crate) struct StableRead { + pub(crate) bytes: Vec, + pub(crate) id: FileId, +} + +#[derive(Debug)] +pub(crate) enum StableReadError { + TooLarge(String), + Boundary(String), + Identity(String), + HostIo(String), +} + +pub(crate) fn read_beneath( + root: &Path, + components: &[&str], + max_bytes: u64, +) -> Result { + read_beneath_with_hook(root, components, max_bytes, |_, _| Ok(())) +} + +fn read_beneath_with_hook( + root: &Path, + components: &[&str], + max_bytes: u64, + mut hook: F, +) -> Result +where + F: FnMut(usize, &Path) -> Result<(), StableReadError>, +{ + if components.is_empty() { + return Err(StableReadError::Boundary( + "stable relative read requires at least one component".into(), + )); + } + platform_read(root, components, max_bytes, &mut hook) +} + +#[cfg(windows)] +fn platform_read( + root: &Path, + components: &[&str], + max_bytes: u64, + hook: &mut F, +) -> Result +where + F: FnMut(usize, &Path) -> Result<(), StableReadError>, +{ + let mut held = Vec::new(); + let root_handle = open_windows(root, true).map_err(|error| open_error(root, error, "root"))?; + reject_kind(&root_handle, true, root)?; + held.push((root.to_path_buf(), file_id(&root_handle)?, root_handle)); + let mut path = root.to_path_buf(); + for (index, component) in components.iter().enumerate() { + hook(index, &path)?; + verify_held_windows(&held)?; + path.push(component); + let final_component = index + 1 == components.len(); + let opened = open_windows(&path, !final_component) + .map_err(|error| component_open_error(&path, error))?; + reject_kind(&opened, !final_component, &path)?; + verify_held_windows(&held)?; + if final_component { + let id = file_id(&opened)?; + let bytes = read_bounded(opened, max_bytes, &path)?; + drop(held); + return Ok(StableRead { bytes, id }); + } + held.push((path.clone(), file_id(&opened)?, opened)); + } + unreachable!() +} + +#[cfg(windows)] +fn verify_held_windows(held: &[(PathBuf, FileId, File)]) -> Result<(), StableReadError> { + for (path, expected, _) in held { + let current = + open_windows(path, true).map_err(|error| component_open_error(path, error))?; + reject_kind(¤t, true, path)?; + if file_id(¤t)? != *expected { + return Err(StableReadError::Identity(format!( + "artifact directory identity changed: {}", + path.display() + ))); + } + } + Ok(()) +} + +#[cfg(windows)] +fn open_windows(path: &Path, directory: bool) -> std::io::Result { + use std::os::windows::fs::OpenOptionsExt; + const GENERIC_READ: u32 = 0x8000_0000; + const FILE_READ_ATTRIBUTES: u32 = 0x80; + const OPEN_REPARSE_POINT: u32 = 0x0020_0000; + const BACKUP_SEMANTICS: u32 = 0x0200_0000; + OpenOptions::new() + .access_mode(if directory { + FILE_READ_ATTRIBUTES + } else { + GENERIC_READ + }) + .share_mode(1 | 2) + .custom_flags(OPEN_REPARSE_POINT | if directory { BACKUP_SEMANTICS } else { 0 }) + .open(path) +} + +#[cfg(target_os = "linux")] +fn platform_read( + root: &Path, + components: &[&str], + max_bytes: u64, + hook: &mut F, +) -> Result +where + F: FnMut(usize, &Path) -> Result<(), StableReadError>, +{ + use std::ffi::CString; + use std::os::fd::{AsRawFd, FromRawFd}; + use std::os::unix::ffi::OsStrExt; + const O_CLOEXEC: i32 = 0x80000; + const O_DIRECTORY: i32 = 0x10000; + const O_NOFOLLOW: i32 = 0x20000; + unsafe extern "C" { + fn open(pathname: *const i8, flags: i32, ...) -> i32; + fn openat(dirfd: i32, pathname: *const i8, flags: i32, ...) -> i32; + } + let root_c = CString::new(root.as_os_str().as_bytes()) + .map_err(|_| StableReadError::Boundary("artifact root contains NUL".to_string()))?; + let root_fd = unsafe { open(root_c.as_ptr(), O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW) }; + if root_fd < 0 { + return Err(open_error(root, std::io::Error::last_os_error(), "root")); + } + let root_file = unsafe { File::from_raw_fd(root_fd) }; + let mut held_paths = vec![(root.to_path_buf(), file_id(&root_file)?)]; + let mut held = vec![root_file]; + let mut path = root.to_path_buf(); + for (index, component) in components.iter().enumerate() { + hook(index, &path)?; + verify_held_unix(&held_paths)?; + let name = CString::new(component.as_bytes()).map_err(|_| { + StableReadError::Boundary("artifact component contains NUL".to_string()) + })?; + let final_component = index + 1 == components.len(); + let flags = O_CLOEXEC | O_NOFOLLOW | if final_component { 0 } else { O_DIRECTORY }; + let fd = unsafe { openat(held.last().unwrap().as_raw_fd(), name.as_ptr(), flags) }; + path.push(component); + if fd < 0 { + return Err(component_open_error(&path, std::io::Error::last_os_error())); + } + let opened = unsafe { File::from_raw_fd(fd) }; + reject_kind(&opened, !final_component, &path)?; + verify_held_unix(&held_paths)?; + if final_component { + let id = file_id(&opened)?; + let bytes = read_bounded(opened, max_bytes, &path)?; + return Ok(StableRead { bytes, id }); + } + held_paths.push((path.clone(), file_id(&opened)?)); + held.push(opened); + } + unreachable!() +} + +#[cfg(target_os = "linux")] +fn verify_held_unix(held: &[(std::path::PathBuf, FileId)]) -> Result<(), StableReadError> { + use std::os::unix::fs::MetadataExt; + for (path, expected) in held { + let metadata = + fs::symlink_metadata(path).map_err(|error| component_open_error(path, error))?; + if metadata.file_type().is_symlink() + || (FileId { + volume: metadata.dev(), + file: metadata.ino() as u128, + }) != *expected + { + return Err(StableReadError::Identity(format!( + "artifact directory identity changed: {}", + path.display() + ))); + } + } + Ok(()) +} + +#[cfg(not(any(windows, target_os = "linux")))] +fn platform_read( + root: &Path, + components: &[&str], + max_bytes: u64, + hook: &mut F, +) -> Result +where + F: FnMut(usize, &Path) -> Result<(), StableReadError>, +{ + let mut held = Vec::new(); + let root_handle = File::open(root).map_err(|error| open_error(root, error, "root"))?; + reject_kind(&root_handle, true, root)?; + held.push(root_handle); + let mut path = root.to_path_buf(); + for (index, component) in components.iter().enumerate() { + hook(index, &path)?; + path.push(component); + let final_component = index + 1 == components.len(); + let metadata = + fs::symlink_metadata(&path).map_err(|error| component_open_error(&path, error))?; + if metadata.file_type().is_symlink() { + return Err(StableReadError::Boundary(format!( + "artifact component is a symlink: {}", + path.display() + ))); + } + let opened = File::open(&path).map_err(|error| component_open_error(&path, error))?; + reject_kind(&opened, !final_component, &path)?; + if final_component { + let id = file_id(&opened)?; + return Ok(StableRead { + bytes: read_bounded(opened, max_bytes, &path)?, + id, + }); + } + held.push(opened); + } + unreachable!() +} + +fn reject_kind(handle: &File, directory: bool, path: &Path) -> Result<(), StableReadError> { + let metadata = handle + .metadata() + .map_err(|error| StableReadError::HostIo(error.to_string()))?; + if (directory && !metadata.is_dir()) + || (!directory && !metadata.is_file()) + || reparse(&metadata) + { + return Err(StableReadError::Boundary(format!( + "artifact component is not a plain {}: {}", + if directory { + "directory" + } else { + "regular file" + }, + path.display() + ))); + } + Ok(()) +} + +fn open_error(path: &Path, error: std::io::Error, role: &str) -> StableReadError { + if is_boundary_open_error(&error) { + StableReadError::Boundary(format!( + "stable artifact {role} is missing or not traversable without links: {}: {error}", + path.display() + )) + } else { + StableReadError::HostIo(format!( + "open stable artifact {role} {}: {error}", + path.display() + )) + } +} + +fn component_open_error(path: &Path, error: std::io::Error) -> StableReadError { + open_error(path, error, "component") +} + +fn is_boundary_open_error(error: &std::io::Error) -> bool { + if matches!( + error.kind(), + std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory + ) { + return true; + } + #[cfg(target_os = "linux")] + if matches!(error.raw_os_error(), Some(20 | 40)) { + return true; + } + false +} + +fn read_bounded(handle: File, max_bytes: u64, path: &Path) -> Result, StableReadError> { + if handle + .metadata() + .map_err(|error| StableReadError::HostIo(error.to_string()))? + .len() + > max_bytes + { + return Err(StableReadError::TooLarge(format!( + "stable file exceeds {max_bytes} bytes" + ))); + } + let mut bytes = Vec::new(); + handle + .take(max_bytes + 1) + .read_to_end(&mut bytes) + .map_err(|error| { + StableReadError::HostIo(format!("read stable artifact {}: {error}", path.display())) + })?; + if bytes.len() as u64 > max_bytes { + return Err(StableReadError::TooLarge(format!( + "stable file exceeds {max_bytes} bytes" + ))); + } + Ok(bytes) +} + +#[cfg(unix)] +fn file_id(file: &File) -> Result { + use std::os::unix::fs::MetadataExt; + let metadata = file + .metadata() + .map_err(|error| StableReadError::HostIo(error.to_string()))?; + Ok(FileId { + volume: metadata.dev(), + file: metadata.ino() as u128, + }) +} + +#[cfg(windows)] +fn file_id(file: &File) -> Result { + use std::ffi::c_void; + use std::mem::MaybeUninit; + use std::os::windows::io::AsRawHandle; + #[repr(C)] + struct Info { + volume: u64, + file_id: [u8; 16], + } + unsafe extern "system" { + fn GetFileInformationByHandleEx( + h: *mut c_void, + class: i32, + info: *mut c_void, + size: u32, + ) -> i32; + } + let mut info = MaybeUninit::::uninit(); + let ok = unsafe { + GetFileInformationByHandleEx( + file.as_raw_handle(), + 18, + info.as_mut_ptr().cast(), + std::mem::size_of::() as u32, + ) + }; + if ok == 0 { + return Err(StableReadError::HostIo( + std::io::Error::last_os_error().to_string(), + )); + } + let info = unsafe { info.assume_init() }; + Ok(FileId { + volume: info.volume, + file: u128::from_le_bytes(info.file_id), + }) +} + +#[cfg(not(any(unix, windows)))] +fn file_id(file: &File) -> Result { + let metadata = file + .metadata() + .map_err(|error| StableReadError::HostIo(error.to_string()))?; + Ok(FileId { + volume: 0, + file: metadata.len() as u128, + }) +} + +#[cfg(unix)] +fn reparse(metadata: &fs::Metadata) -> bool { + use std::os::unix::fs::FileTypeExt; + let kind = metadata.file_type(); + kind.is_symlink() + || kind.is_block_device() + || kind.is_char_device() + || kind.is_fifo() + || kind.is_socket() +} + +#[cfg(windows)] +fn reparse(metadata: &fs::Metadata) -> bool { + use std::os::windows::fs::MetadataExt; + metadata.file_attributes() & 0x400 != 0 +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + #[test] + fn held_parent_chain_rejects_intermediate_link_swap() { + let root = std::env::temp_dir().join(format!( + "code-intel-stable-chain-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let parent = root.join("parent"); + fs::create_dir_all(&parent).unwrap(); + fs::write(parent.join("payload"), b"owned").unwrap(); + let result = read_beneath_with_hook(&root, &["parent", "payload"], 32, |index, _| { + if index == 1 { + let renamed = root.join("renamed"); + if fs::rename(&parent, &renamed).is_ok() { + #[cfg(unix)] + std::os::unix::fs::symlink(&renamed, &parent).unwrap(); + #[cfg(windows)] + std::os::windows::fs::symlink_dir(&renamed, &parent).unwrap(); + } else { + return Err(StableReadError::Identity( + "held parent blocked directory swap".to_string(), + )); + } + } + Ok(()) + }); + assert!(result.is_err()); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn bounded_read_accepts_max_minus_one_and_max_but_rejects_max_plus_one() { + let root = std::env::temp_dir().join(format!( + "code-intel-stable-size-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir(&root).unwrap(); + for (name, size, accepted) in [("low", 7, true), ("max", 8, true), ("high", 9, false)] { + fs::write(root.join(name), vec![b'x'; size]).unwrap(); + let result = read_beneath(&root, &[name], 8); + assert_eq!(result.is_ok(), accepted, "{name}: {result:?}"); + if !accepted { + assert!(matches!(result, Err(StableReadError::TooLarge(_)))); + } + } + let _ = fs::remove_dir_all(root); + } + + #[test] + fn root_link_is_a_boundary_not_a_traversal_authority() { + let base = std::env::temp_dir().join(format!( + "code-intel-stable-root-link-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let real = base.join("real"); + let link = base.join("link"); + fs::create_dir_all(&real).unwrap(); + fs::write(real.join("payload"), b"x").unwrap(); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(&real, &link).is_ok(); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_dir(&real, &link).is_ok(); + if linked { + assert!(matches!( + read_beneath(&link, &["payload"], 8), + Err(StableReadError::Boundary(_)) + )); + } + let _ = fs::remove_dir_all(base); + } + + #[cfg(target_os = "linux")] + #[test] + fn linux_root_intermediate_and_leaf_links_are_typed_boundaries() { + use std::os::unix::fs::symlink; + let base = std::env::temp_dir().join(format!( + "code-intel-linux-link-types-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let real = base.join("real"); + let nested = real.join("nested"); + fs::create_dir_all(&nested).unwrap(); + fs::write(nested.join("payload"), b"x").unwrap(); + symlink(&real, base.join("root-link")).unwrap(); + symlink(&nested, real.join("intermediate-link")).unwrap(); + symlink(nested.join("payload"), real.join("leaf-link")).unwrap(); + + for result in [ + read_beneath(&base.join("root-link"), &["nested", "payload"], 8), + read_beneath(&real, &["intermediate-link", "payload"], 8), + read_beneath(&real, &["leaf-link"], 8), + ] { + assert!(matches!(result, Err(StableReadError::Boundary(_)))); + } + let _ = fs::remove_dir_all(base); + } + + #[cfg(windows)] + #[test] + fn deny_share_lock_is_host_io() { + use std::os::windows::fs::OpenOptionsExt; + let root = std::env::temp_dir().join(format!( + "code-intel-stable-lock-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::create_dir(&root).unwrap(); + let path = root.join("payload"); + fs::write(&path, b"x").unwrap(); + let lock = OpenOptions::new() + .read(true) + .share_mode(0) + .open(&path) + .unwrap(); + assert!(matches!( + read_beneath(&root, &["payload"], 8), + Err(StableReadError::HostIo(_)) + )); + drop(lock); + let _ = fs::remove_dir_all(root); + } +} diff --git a/crates/code-intel-cli/src/staged_artifact.rs b/crates/code-intel-cli/src/staged_artifact.rs new file mode 100644 index 0000000..1dce3fc --- /dev/null +++ b/crates/code-intel-cli/src/staged_artifact.rs @@ -0,0 +1,1240 @@ +use std::collections::BTreeMap; +use std::fmt; +use std::fs::{self, File, OpenOptions}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +use crate::stable_artifact::{self, StableReadError}; + +static NONCE_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +#[derive(Clone, Copy)] +pub(crate) struct ArtifactWriteContract { + pub(crate) artifact_schema: &'static str, + pub(crate) artifact_type: &'static str, + pub(crate) max_bytes: u64, + pub(crate) validate_payload: fn(&[u8]) -> Result<(), String>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum InterruptAfter { + StageCreated, + TempCreated, + FileSynced, + ObjectPublished, + DirectorySynced, +} + +#[derive(Clone, Debug)] +pub(crate) struct WriterOptions { + pub(crate) nonce: String, + pub(crate) interrupt_after: Option, + pub(crate) before_publish: Option Result<(), String>>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct StagedArtifactRef { + pub(crate) artifact_schema: String, + pub(crate) artifact_type: String, + pub(crate) path: String, + pub(crate) sha256: String, + pub(crate) consumed_snapshot_identity: String, + pub(crate) size: u64, + pub(crate) owned_by_stage: bool, +} + +impl StagedArtifactRef { + pub(crate) fn to_artifact_ref_value(&self) -> Value { + json!({ + "schema": "code-intel-artifact-ref.v1", + "artifactSchema": self.artifact_schema, + "type": self.artifact_type, + "path": self.path, + "sha256": self.sha256, + "consumedSnapshotIdentity": self.consumed_snapshot_identity, + }) + } +} + +#[derive(Debug)] +pub(crate) enum StageWriteError { + Contract(String), + Boundary(String), + Collision(String), + Interrupted(InterruptAfter), + HostIo(String), +} + +impl fmt::Display for StageWriteError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Contract(message) + | Self::Boundary(message) + | Self::Collision(message) + | Self::HostIo(message) => formatter.write_str(message), + Self::Interrupted(phase) => write!(formatter, "injected interruption after {phase:?}"), + } + } +} + +impl std::error::Error for StageWriteError {} + +impl StageWriteError { + pub(crate) fn kind(&self) -> &'static str { + match self { + Self::Contract(_) => "contract", + Self::Boundary(_) => "boundary", + Self::Collision(_) => "collision", + Self::Interrupted(_) => "interrupted", + Self::HostIo(_) => "host_io", + } + } +} + +#[derive(Debug)] +struct Layout { + _root: HeldDir, + _staging: HeldDir, + stage: HeldDir, + _objects: HeldDir, + sha256: HeldDir, +} + +#[derive(Debug)] +struct Ownership { + files: BTreeMap, + directories: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct OwnedFileIdentity { + volume: u64, + file: u128, +} + +#[derive(Debug, Default)] +struct CleanupReport { + residuals: Vec, + failures: Vec, +} + +impl CleanupReport { + fn is_clean(&self) -> bool { + self.residuals.is_empty() && self.failures.is_empty() + } + + fn describe(&self) -> String { + let mut parts = Vec::new(); + if !self.residuals.is_empty() { + parts.push(format!( + "residual preserved: {}", + self.residuals + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(", ") + )); + } + if !self.failures.is_empty() { + parts.push(format!("cleanup failures: {}", self.failures.join("; "))); + } + parts.join("; ") + } +} + +#[derive(Debug)] +pub(crate) struct StagedWriter { + authority_root: PathBuf, + stage_path: PathBuf, + snapshot_identity: String, + artifacts: Vec, + interrupt_after: Option, + before_publish: Option Result<(), String>>, + layout: Option, + ownership: Ownership, +} + +#[derive(Debug)] +pub(crate) struct StagedArtifactSet { + authority_root: PathBuf, + stage_path: PathBuf, + snapshot_identity: String, + artifacts: Vec, + layout: Option, + ownership: Ownership, +} + +impl StagedWriter { + pub(crate) fn begin( + authority_root: &Path, + snapshot_identity: &str, + ) -> Result { + let sequence = NONCE_SEQUENCE.fetch_add(1, Ordering::Relaxed); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| StageWriteError::HostIo(error.to_string()))? + .as_nanos(); + Self::begin_with_options( + authority_root, + snapshot_identity, + WriterOptions { + nonce: format!("{}-{nanos}-{sequence}", std::process::id()), + interrupt_after: None, + before_publish: None, + }, + ) + } + + pub(crate) fn begin_with_options( + authority_root: &Path, + snapshot_identity: &str, + options: WriterOptions, + ) -> Result { + validate_digest(snapshot_identity, "snapshot identity")?; + validate_nonce(&options.nonce)?; + let root = HeldDir::open_root(authority_root)?; + let authority_volume = root.volume_identity()?; + let staging = root.ensure_child(".staging")?; + require_same_volume(authority_volume, &staging)?; + let stage_name = format!("stage-{}", options.nonce); + let stage = staging.create_unique_child(&stage_name)?; + let stage_path = stage.path.clone(); + let mut owned_directories = vec![stage_path.clone()]; + let setup = (|| { + require_same_volume(authority_volume, &stage)?; + let objects = stage.create_unique_child("objects")?; + owned_directories.push(objects.path.clone()); + require_same_volume(authority_volume, &objects)?; + let sha256 = objects.create_unique_child("sha256")?; + owned_directories.push(sha256.path.clone()); + require_same_volume(authority_volume, &sha256)?; + Ok((objects, sha256)) + })(); + let (objects, sha256) = match setup { + Ok(layout) => layout, + Err(cause) => { + drop(stage); + drop(staging); + drop(root); + let report = cleanup_owned_entries(&mut Ownership { + files: BTreeMap::new(), + directories: owned_directories, + }); + return Err(append_cleanup_report(cause, &report)); + } + }; + let mut writer = Self { + authority_root: authority_root.to_path_buf(), + stage_path: stage_path.clone(), + snapshot_identity: snapshot_identity.to_string(), + artifacts: Vec::new(), + interrupt_after: options.interrupt_after, + before_publish: options.before_publish, + layout: Some(Layout { + _root: root, + _staging: staging, + stage, + _objects: objects, + sha256, + }), + ownership: Ownership { + files: BTreeMap::new(), + directories: owned_directories, + }, + }; + if let Err(cause) = writer.interrupt(InterruptAfter::StageCreated) { + let report = writer.cleanup_owned(); + return Err(append_cleanup_report(cause, &report)); + } + Ok(writer) + } + + pub(crate) fn stage( + &mut self, + bytes: &[u8], + contract: ArtifactWriteContract, + ) -> Result { + let result = self.stage_once(bytes, contract); + if let Err(cause) = result { + let report = self.cleanup_owned(); + return Err(append_cleanup_report(cause, &report)); + } + result + } + + fn stage_once( + &mut self, + bytes: &[u8], + contract: ArtifactWriteContract, + ) -> Result { + validate_contract(contract, bytes)?; + let digest = sha256_hex(bytes); + let temp_name = format!( + ".tmp-{}-{}", + digest, + NONCE_SEQUENCE.fetch_add(1, Ordering::Relaxed) + ); + let layout = self + .layout + .as_ref() + .ok_or_else(|| StageWriteError::HostIo("staged writer is not active".to_string()))?; + + let mut temporary = layout.sha256.create_new_file(&temp_name)?; + let temp_path = layout.sha256.path.join(&temp_name); + let temp_identity = platform::file_identity(&temporary) + .map_err(|error| io_error("identify owned temporary", &temp_path, error))?; + self.ownership + .files + .insert(temp_path.clone(), temp_identity); + self.interrupt(InterruptAfter::TempCreated)?; + let write_result = (|| { + temporary + .write_all(bytes) + .map_err(|error| io_error("write staged artifact", &temp_path, error))?; + temporary + .sync_all() + .map_err(|error| io_error("flush staged artifact", &temp_path, error))?; + Ok::<_, StageWriteError>(()) + })(); + drop(temporary); + if let Err(error) = write_result { + if layout.sha256.remove_owned_file(&temp_name).is_ok() { + self.ownership.files.remove(&temp_path); + } + return Err(error); + } + self.interrupt(InterruptAfter::FileSynced)?; + + if let Some(hook) = self.before_publish { + hook(&layout.sha256.path, &digest).map_err(|error| { + StageWriteError::HostIo(format!("before-publish proving hook failed: {error}")) + })?; + } + + let owned_by_stage = match layout.sha256.publish_no_replace(&temp_name, &digest) { + Ok(()) => { + self.ownership.files.remove(&temp_path); + self.ownership + .files + .insert(layout.sha256.path.join(&digest), temp_identity); + true + } + Err(PublishError::Exists) => { + layout.sha256.remove_owned_file(&temp_name)?; + self.ownership.files.remove(&temp_path); + self.ownership + .files + .contains_key(&layout.sha256.path.join(&digest)) + } + Err(PublishError::Io(error)) => { + if layout.sha256.remove_owned_file(&temp_name).is_ok() { + self.ownership.files.remove(&temp_path); + } + return Err(error); + } + }; + self.interrupt(InterruptAfter::ObjectPublished)?; + layout.sha256.sync_metadata()?; + self.interrupt(InterruptAfter::DirectorySynced)?; + + let verified = stable_artifact::read_beneath( + &self.stage_path, + &["objects", "sha256", &digest], + contract.max_bytes, + ) + .map_err(map_stable_error)?; + if verified.bytes != bytes || sha256_hex(&verified.bytes) != digest { + return Err(StageWriteError::Collision(format!( + "content-addressed object collision for sha256:{digest}" + ))); + } + (contract.validate_payload)(&verified.bytes).map_err(|error| { + StageWriteError::Contract(format!("staged payload validation failed: {error}")) + })?; + + let artifact = StagedArtifactRef { + artifact_schema: contract.artifact_schema.to_string(), + artifact_type: contract.artifact_type.to_string(), + path: format!("objects/sha256/{digest}"), + sha256: digest, + consumed_snapshot_identity: self.snapshot_identity.clone(), + size: bytes.len() as u64, + owned_by_stage, + }; + validate_ref_coherence(&artifact)?; + self.artifacts.push(artifact.clone()); + Ok(artifact) + } + + pub(crate) fn observed_effects(&self) -> &'static [&'static str] { + &["local_write"] + } + + pub(crate) fn seal(mut self) -> Result { + if self + .artifacts + .iter() + .any(|artifact| !artifact.owned_by_stage) + { + let report = self.cleanup_owned(); + return Err(append_cleanup_report( + StageWriteError::Collision( + "cannot seal a staging set containing an unowned deduplicated object" + .to_string(), + ), + &report, + )); + } + let layout = self + .layout + .as_ref() + .ok_or_else(|| StageWriteError::HostIo("staged writer is not active".to_string()))?; + layout.sha256.sync_metadata()?; + layout.stage.sync_metadata()?; + let layout = self.layout.take(); + Ok(StagedArtifactSet { + authority_root: self.authority_root.clone(), + stage_path: self.stage_path.clone(), + snapshot_identity: self.snapshot_identity.clone(), + artifacts: std::mem::take(&mut self.artifacts), + layout, + ownership: std::mem::replace( + &mut self.ownership, + Ownership { + files: BTreeMap::new(), + directories: Vec::new(), + }, + ), + }) + } + + fn interrupt(&self, phase: InterruptAfter) -> Result<(), StageWriteError> { + if self.interrupt_after == Some(phase) { + Err(StageWriteError::Interrupted(phase)) + } else { + Ok(()) + } + } + + fn cleanup_owned(&mut self) -> CleanupReport { + let mut report = cleanup_owned_files(&mut self.ownership); + self.layout.take(); + cleanup_owned_directories(&mut self.ownership, &mut report); + report + } +} + +impl Drop for StagedWriter { + fn drop(&mut self) { + let _ = self.cleanup_owned(); + } +} + +impl StagedArtifactSet { + pub(crate) fn path(&self) -> &Path { + &self.stage_path + } + + pub(crate) fn authority_root(&self) -> &Path { + &self.authority_root + } + + pub(crate) fn artifacts(&self) -> &[StagedArtifactRef] { + &self.artifacts + } + + pub(crate) fn to_manifest_value(&self) -> Value { + json!({ + "schema": "code-intel-staged-artifact-set.v1", + "snapshotIdentity": self.snapshot_identity, + "artifacts": self.artifacts.iter().map(StagedArtifactRef::to_artifact_ref_value).collect::>(), + }) + } + + pub(crate) fn prepare_for_commit(&mut self) -> Result<(), StageWriteError> { + if let Some(layout) = self.layout.as_ref() { + layout.sha256.sync_metadata()?; + layout.stage.sync_metadata()?; + } + self.layout.take(); + Ok(()) + } +} + +impl Drop for StagedArtifactSet { + fn drop(&mut self) { + let mut report = cleanup_owned_files(&mut self.ownership); + self.layout.take(); + cleanup_owned_directories(&mut self.ownership, &mut report); + } +} + +pub(crate) fn sync_directory_path(path: &Path) -> Result<(), StageWriteError> { + let directory = platform::open_directory(path) + .map_err(|error| classify_open_error("open directory for sync", path, error))?; + platform::sync_directory(&directory).map_err(|error| io_error("sync directory", path, error)) +} + +fn cleanup_owned_entries(ownership: &mut Ownership) -> CleanupReport { + let mut report = cleanup_owned_files(ownership); + cleanup_owned_directories(ownership, &mut report); + report +} + +fn cleanup_owned_files(ownership: &mut Ownership) -> CleanupReport { + let mut report = CleanupReport::default(); + let files = ownership + .files + .iter() + .map(|(path, identity)| (path.clone(), *identity)) + .collect::>(); + for (path, expected) in files { + match platform::open_file_identity(&path) { + Ok(actual) if actual == expected => match fs::remove_file(&path) { + Ok(()) => { + ownership.files.remove(&path); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + ownership.files.remove(&path); + } + Err(error) => report.failures.push(format!( + "remove identity-owned file {}: {error}", + path.display() + )), + }, + Ok(_) => { + ownership.files.remove(&path); + report.residuals.push(path); + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + ownership.files.remove(&path); + } + Err(error) => report.failures.push(format!( + "verify identity-owned file {}: {error}", + path.display() + )), + } + } + report +} + +fn cleanup_owned_directories(ownership: &mut Ownership, report: &mut CleanupReport) { + for path in ownership.directories.iter().rev() { + match fs::remove_dir(path) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::DirectoryNotEmpty | std::io::ErrorKind::PermissionDenied + ) => + { + report.residuals.push(path.clone()); + } + Err(error) => report.failures.push(format!( + "remove owned directory {}: {error}", + path.display() + )), + } + } + ownership.directories.clear(); +} + +fn append_cleanup_report(cause: StageWriteError, report: &CleanupReport) -> StageWriteError { + if report.is_clean() { + return cause; + } + let detail = format!("{cause}; {}", report.describe()); + match cause { + StageWriteError::Contract(_) => StageWriteError::Contract(detail), + StageWriteError::Boundary(_) => StageWriteError::Boundary(detail), + StageWriteError::Collision(_) => StageWriteError::Collision(detail), + StageWriteError::Interrupted(phase) => StageWriteError::HostIo(format!( + "interrupted after {phase:?}; {}", + report.describe() + )), + StageWriteError::HostIo(_) => StageWriteError::HostIo(detail), + } +} + +fn validate_contract(contract: ArtifactWriteContract, bytes: &[u8]) -> Result<(), StageWriteError> { + for (label, value) in [ + ("artifact schema", contract.artifact_schema), + ("artifact type", contract.artifact_type), + ] { + if value.is_empty() + || value.len() > 128 + || !value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_')) + { + return Err(StageWriteError::Contract(format!( + "{label} is not a portable identifier" + ))); + } + } + if contract.max_bytes == 0 || bytes.len() as u64 > contract.max_bytes { + return Err(StageWriteError::Contract(format!( + "artifact payload exceeds the registered {} byte limit", + contract.max_bytes + ))); + } + (contract.validate_payload)(bytes).map_err(|error| { + StageWriteError::Contract(format!( + "artifact payload failed schema validation: {error}" + )) + }) +} + +fn validate_ref_coherence(artifact: &StagedArtifactRef) -> Result<(), StageWriteError> { + validate_digest(&artifact.sha256, "artifact digest")?; + validate_digest( + &artifact.consumed_snapshot_identity, + "consumed snapshot identity", + )?; + if artifact.path != format!("objects/sha256/{}", artifact.sha256) { + return Err(StageWriteError::Contract( + "artifact path and digest are incoherent".to_string(), + )); + } + Ok(()) +} + +fn validate_digest(value: &str, label: &str) -> Result<(), StageWriteError> { + if value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + Ok(()) + } else { + Err(StageWriteError::Contract(format!( + "{label} must be a lowercase sha256 digest" + ))) + } +} + +fn validate_nonce(nonce: &str) -> Result<(), StageWriteError> { + if !nonce.is_empty() + && nonce.len() <= 96 + && nonce + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + { + Ok(()) + } else { + Err(StageWriteError::Boundary( + "staging nonce is not a portable path component".to_string(), + )) + } +} + +fn map_stable_error(error: StableReadError) -> StageWriteError { + match error { + StableReadError::TooLarge(message) => StageWriteError::Contract(message), + StableReadError::Boundary(message) | StableReadError::Identity(message) => { + StageWriteError::Boundary(message) + } + StableReadError::HostIo(message) => StageWriteError::HostIo(message), + } +} + +fn io_error(action: &str, path: &Path, error: std::io::Error) -> StageWriteError { + StageWriteError::HostIo(format!("{action} {}: {error}", path.display())) +} + +#[derive(Debug)] +struct HeldDir { + path: PathBuf, + handle: File, +} + +enum PublishError { + Exists, + Io(StageWriteError), +} + +impl HeldDir { + fn open_root(path: &Path) -> Result { + let handle = platform::open_directory(path) + .map_err(|error| classify_open_error("open staging authority", path, error))?; + Ok(Self { + path: path.to_path_buf(), + handle, + }) + } + + fn ensure_child(&self, name: &str) -> Result { + match platform::create_directory(&self.handle, &self.path, name) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(io_error( + "create staging directory", + &self.path.join(name), + error, + )) + } + } + self.open_child(name) + } + + fn create_unique_child(&self, name: &str) -> Result { + match platform::create_directory(&self.handle, &self.path, name) { + Ok(()) => self.open_child(name), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + Err(StageWriteError::Collision(format!( + "staging nonce collision: {}", + self.path.join(name).display() + ))) + } + Err(error) => Err(io_error( + "create owned staging directory", + &self.path.join(name), + error, + )), + } + } + + fn open_child(&self, name: &str) -> Result { + let path = self.path.join(name); + let handle = platform::open_child_directory(&self.handle, &path, name) + .map_err(|error| classify_open_error("open staging directory", &path, error))?; + Ok(Self { path, handle }) + } + + fn create_new_file(&self, name: &str) -> Result { + platform::create_new_file(&self.handle, &self.path, name).map_err(|error| { + io_error( + "create staged artifact temporary", + &self.path.join(name), + error, + ) + }) + } + + fn publish_no_replace(&self, source: &str, target: &str) -> Result<(), PublishError> { + platform::publish_no_replace(&self.handle, &self.path, source, target).map_err(|error| { + if error.kind() == std::io::ErrorKind::AlreadyExists { + PublishError::Exists + } else { + PublishError::Io(io_error( + "publish staged content-addressed object", + &self.path.join(target), + error, + )) + } + }) + } + + fn remove_owned_file(&self, name: &str) -> Result<(), StageWriteError> { + platform::remove_file(&self.handle, &self.path, name) + .map_err(|error| io_error("remove owned temporary", &self.path.join(name), error)) + } + + fn sync_metadata(&self) -> Result<(), StageWriteError> { + platform::sync_directory(&self.handle) + .map_err(|error| io_error("sync staging directory", &self.path, error)) + } + + fn volume_identity(&self) -> Result { + platform::volume_identity(&self.handle) + .map_err(|error| io_error("read staging volume identity", &self.path, error)) + } +} + +fn require_same_volume(expected: u64, directory: &HeldDir) -> Result<(), StageWriteError> { + if directory.volume_identity()? == expected { + Ok(()) + } else { + Err(StageWriteError::Boundary(format!( + "staging directory crosses the authority volume: {}", + directory.path.display() + ))) + } +} + +fn classify_open_error(action: &str, path: &Path, error: std::io::Error) -> StageWriteError { + if matches!( + error.kind(), + std::io::ErrorKind::NotFound + | std::io::ErrorKind::NotADirectory + | std::io::ErrorKind::InvalidInput + ) { + StageWriteError::Boundary(format!("{action} {}: {error}", path.display())) + } else { + StageWriteError::HostIo(format!("{action} {}: {error}", path.display())) + } +} + +#[cfg(windows)] +mod platform { + use super::*; + use std::ffi::c_void; + use std::os::windows::ffi::OsStrExt; + use std::os::windows::fs::{MetadataExt, OpenOptionsExt}; + use std::os::windows::io::AsRawHandle; + + const FILE_READ_ATTRIBUTES: u32 = 0x80; + const OPEN_REPARSE_POINT: u32 = 0x0020_0000; + const BACKUP_SEMANTICS: u32 = 0x0200_0000; + const WRITE_THROUGH: u32 = 0x0000_0008; + + pub(super) fn open_directory(path: &Path) -> std::io::Result { + let file = OpenOptions::new() + .access_mode(FILE_READ_ATTRIBUTES) + .share_mode(1 | 2) + .custom_flags(OPEN_REPARSE_POINT | BACKUP_SEMANTICS) + .open(path)?; + let metadata = file.metadata()?; + if !metadata.is_dir() || metadata.file_attributes() & 0x400 != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "directory authority is a reparse point or not a directory", + )); + } + Ok(file) + } + + pub(super) fn open_child_directory( + _parent: &File, + path: &Path, + _name: &str, + ) -> std::io::Result { + open_directory(path) + } + + pub(super) fn create_directory( + _parent: &File, + parent_path: &Path, + name: &str, + ) -> std::io::Result<()> { + fs::create_dir(parent_path.join(name)) + } + + pub(super) fn create_new_file( + _parent: &File, + parent_path: &Path, + name: &str, + ) -> std::io::Result { + OpenOptions::new() + .write(true) + .create_new(true) + .share_mode(1 | 2) + .custom_flags(OPEN_REPARSE_POINT) + .open(parent_path.join(name)) + } + + pub(super) fn publish_no_replace( + _parent: &File, + parent_path: &Path, + source: &str, + target: &str, + ) -> std::io::Result<()> { + unsafe extern "system" { + fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32; + fn GetLastError() -> u32; + } + let wide = |path: &Path| { + path.as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>() + }; + // Rust's standard filesystem operations opt into extended-length paths, but this + // direct Win32 call does not. Canonicalizing the existing authority directory gives + // MoveFileExW a verbatim (\\?\-prefixed) absolute path and keeps publication working + // when the content-addressed target crosses the legacy MAX_PATH boundary. + let authority = fs::canonicalize(parent_path)?; + let source = wide(&authority.join(source)); + let target = wide(&authority.join(target)); + let ok = unsafe { MoveFileExW(source.as_ptr(), target.as_ptr(), WRITE_THROUGH) }; + if ok != 0 { + return Ok(()); + } + let code = unsafe { GetLastError() }; + if matches!(code, 80 | 183) { + Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "content-addressed target already exists", + )) + } else { + Err(std::io::Error::from_raw_os_error(code as i32)) + } + } + + pub(super) fn remove_file( + _parent: &File, + parent_path: &Path, + name: &str, + ) -> std::io::Result<()> { + fs::remove_file(parent_path.join(name)) + } + + pub(super) fn sync_directory(_directory: &File) -> std::io::Result<()> { + // MoveFileExW(MOVEFILE_WRITE_THROUGH) is the Windows metadata durability barrier. + Ok(()) + } + + pub(super) fn volume_identity(directory: &File) -> std::io::Result { + Ok(file_identity(directory)?.volume) + } + + pub(super) fn file_identity(file: &File) -> std::io::Result { + #[repr(C)] + struct FileIdInfo { + volume_serial_number: u64, + file_id: [u8; 16], + } + unsafe extern "system" { + fn GetFileInformationByHandleEx( + file: *mut c_void, + class: i32, + information: *mut c_void, + size: u32, + ) -> i32; + } + let mut info = std::mem::MaybeUninit::::uninit(); + let ok = unsafe { + GetFileInformationByHandleEx( + file.as_raw_handle(), + 18, + info.as_mut_ptr().cast(), + std::mem::size_of::() as u32, + ) + }; + if ok == 0 { + Err(std::io::Error::last_os_error()) + } else { + let info = unsafe { info.assume_init() }; + Ok(OwnedFileIdentity { + volume: info.volume_serial_number, + file: u128::from_le_bytes(info.file_id), + }) + } + } + + pub(super) fn open_file_identity(path: &Path) -> std::io::Result { + let file = OpenOptions::new() + .read(true) + .share_mode(1 | 2) + .custom_flags(OPEN_REPARSE_POINT) + .open(path)?; + let metadata = file.metadata()?; + if !metadata.is_file() || metadata.file_attributes() & 0x400 != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "owned file path is a reparse point or not a regular file", + )); + } + file_identity(&file) + } +} + +#[cfg(target_os = "linux")] +mod platform { + use super::*; + use std::ffi::CString; + use std::os::fd::{AsRawFd, FromRawFd}; + use std::os::unix::ffi::OsStrExt; + use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; + + const O_WRONLY: i32 = 1; + const O_CREAT: i32 = 0x40; + const O_EXCL: i32 = 0x80; + const O_CLOEXEC: i32 = 0x80000; + const O_DIRECTORY: i32 = 0x10000; + const O_NOFOLLOW: i32 = 0x20000; + + unsafe extern "C" { + fn open(pathname: *const i8, flags: i32, ...) -> i32; + fn openat(dirfd: i32, pathname: *const i8, flags: i32, ...) -> i32; + fn mkdirat(dirfd: i32, pathname: *const i8, mode: u32) -> i32; + fn linkat( + olddirfd: i32, + oldpath: *const i8, + newdirfd: i32, + newpath: *const i8, + flags: i32, + ) -> i32; + fn unlinkat(dirfd: i32, pathname: *const i8, flags: i32) -> i32; + } + + fn name(value: &str) -> std::io::Result { + CString::new(value) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "NUL in name")) + } + + pub(super) fn open_directory(path: &Path) -> std::io::Result { + let path = CString::new(path.as_os_str().as_bytes()) + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "NUL in path"))?; + let fd = unsafe { open(path.as_ptr(), O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW) }; + if fd < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(unsafe { File::from_raw_fd(fd) }) + } + } + + pub(super) fn open_child_directory( + parent: &File, + _path: &Path, + child: &str, + ) -> std::io::Result { + let child = name(child)?; + let fd = unsafe { + openat( + parent.as_raw_fd(), + child.as_ptr(), + O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW, + ) + }; + if fd < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(unsafe { File::from_raw_fd(fd) }) + } + } + + pub(super) fn create_directory( + parent: &File, + _parent_path: &Path, + child: &str, + ) -> std::io::Result<()> { + let child = name(child)?; + if unsafe { mkdirat(parent.as_raw_fd(), child.as_ptr(), 0o700) } == 0 { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + } + } + + pub(super) fn create_new_file( + parent: &File, + _parent_path: &Path, + child: &str, + ) -> std::io::Result { + let child = name(child)?; + let fd = unsafe { + openat( + parent.as_raw_fd(), + child.as_ptr(), + O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, + 0o600u32, + ) + }; + if fd < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(unsafe { File::from_raw_fd(fd) }) + } + } + + pub(super) fn publish_no_replace( + parent: &File, + _parent_path: &Path, + source: &str, + target: &str, + ) -> std::io::Result<()> { + let source = name(source)?; + let target = name(target)?; + if unsafe { + linkat( + parent.as_raw_fd(), + source.as_ptr(), + parent.as_raw_fd(), + target.as_ptr(), + 0, + ) + } != 0 + { + return Err(std::io::Error::last_os_error()); + } + if unsafe { unlinkat(parent.as_raw_fd(), source.as_ptr(), 0) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + } + + pub(super) fn remove_file( + parent: &File, + _parent_path: &Path, + child: &str, + ) -> std::io::Result<()> { + let child = name(child)?; + if unsafe { unlinkat(parent.as_raw_fd(), child.as_ptr(), 0) } == 0 { + Ok(()) + } else { + Err(std::io::Error::last_os_error()) + } + } + + pub(super) fn sync_directory(directory: &File) -> std::io::Result<()> { + directory.sync_all() + } + + pub(super) fn volume_identity(directory: &File) -> std::io::Result { + Ok(directory.metadata()?.dev()) + } + + pub(super) fn file_identity(file: &File) -> std::io::Result { + let metadata = file.metadata()?; + Ok(OwnedFileIdentity { + volume: metadata.dev(), + file: metadata.ino() as u128, + }) + } + + pub(super) fn open_file_identity(path: &Path) -> std::io::Result { + let file = OpenOptions::new() + .read(true) + .custom_flags(O_CLOEXEC | O_NOFOLLOW) + .open(path)?; + if !file.metadata()?.is_file() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "owned file path is not a regular file", + )); + } + file_identity(&file) + } +} + +#[cfg(not(any(windows, target_os = "linux")))] +mod platform { + use super::*; + + fn unsupported() -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::Unsupported, + "secure staged writes are supported only on Windows and Linux", + ) + } + + pub(super) fn open_directory(_path: &Path) -> std::io::Result { + Err(unsupported()) + } + pub(super) fn open_child_directory( + _parent: &File, + _path: &Path, + _child: &str, + ) -> std::io::Result { + Err(unsupported()) + } + pub(super) fn create_directory( + _parent: &File, + _path: &Path, + _child: &str, + ) -> std::io::Result<()> { + Err(unsupported()) + } + pub(super) fn create_new_file( + _parent: &File, + _path: &Path, + _child: &str, + ) -> std::io::Result { + Err(unsupported()) + } + pub(super) fn publish_no_replace( + _parent: &File, + _path: &Path, + _source: &str, + _target: &str, + ) -> std::io::Result<()> { + Err(unsupported()) + } + pub(super) fn remove_file(_parent: &File, _path: &Path, _child: &str) -> std::io::Result<()> { + Err(unsupported()) + } + pub(super) fn sync_directory(_directory: &File) -> std::io::Result<()> { + Err(unsupported()) + } + pub(super) fn volume_identity(_directory: &File) -> std::io::Result { + Err(unsupported()) + } + pub(super) fn file_identity(_file: &File) -> std::io::Result { + Err(unsupported()) + } + pub(super) fn open_file_identity(_path: &Path) -> std::io::Result { + Err(unsupported()) + } +} + +fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0); + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut hash = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut words = [0u32; 64]; + for (index, word) in chunk.chunks_exact(4).enumerate() { + words[index] = u32::from_be_bytes(word.try_into().unwrap()); + } + for index in 16..64 { + let s0 = words[index - 15].rotate_right(7) + ^ words[index - 15].rotate_right(18) + ^ (words[index - 15] >> 3); + let s1 = words[index - 2].rotate_right(17) + ^ words[index - 2].rotate_right(19) + ^ (words[index - 2] >> 10); + words[index] = words[index - 16] + .wrapping_add(s0) + .wrapping_add(words[index - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = hash; + for index in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let choice = (e & f) ^ (!e & g); + let first = h + .wrapping_add(s1) + .wrapping_add(choice) + .wrapping_add(K[index]) + .wrapping_add(words[index]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let majority = (a & b) ^ (a & c) ^ (b & c); + let second = s0.wrapping_add(majority); + h = g; + g = f; + f = e; + e = d.wrapping_add(first); + d = c; + c = b; + b = a; + a = first.wrapping_add(second); + } + for (state, value) in hash.iter_mut().zip([a, b, c, d, e, f, g, h]) { + *state = state.wrapping_add(value); + } + } + hash.iter().map(|value| format!("{value:08x}")).collect() +} diff --git a/crates/code-intel-cli/src/survival_scan.rs b/crates/code-intel-cli/src/survival_scan.rs new file mode 100644 index 0000000..45c3716 --- /dev/null +++ b/crates/code-intel-cli/src/survival_scan.rs @@ -0,0 +1,248 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use serde_json::{json, Value}; + +const MAX_REQUEST_BYTES: u64 = 8 * 1024 * 1024; + +pub(crate) fn run_raw(raw: &[String]) -> i32 { + let (request_path, artifact_root) = match parse_cli(raw) { + Ok(value) => value, + Err(message) => { + eprintln!("{message}"); + return 64; + } + }; + let request = match read_request(&request_path) { + Ok(value) => value, + Err(message) => return reject(&message), + }; + match scan(&request, &artifact_root) { + Ok(result) => { + println!("{}", serde_json::to_string(&result).unwrap()); + 0 + } + Err(message) => reject(&message), + } +} + +fn reject(message: &str) -> i32 { + eprintln!("{message}"); + println!( + "{}", + serde_json::to_string(&json!({ + "schema":"code-intel-repository-survival-scan-rejection.v1", + "status":"rejected", + "diagnostics":[message] + })) + .unwrap() + ); + 65 +} + +fn parse_cli(raw: &[String]) -> Result<(String, PathBuf), String> { + let mut request = None; + let mut artifact_root = None; + let mut index = 0; + while index < raw.len() { + let flag = raw[index].as_str(); + if !matches!(flag, "--request" | "--artifact-root") { + return Err(format!("unknown survival scan argument: {flag}")); + } + let value = raw + .get(index + 1) + .filter(|value| !value.starts_with("--")) + .ok_or_else(|| format!("{flag} requires exactly one value"))?; + match flag { + "--request" if request.replace(value.clone()).is_some() => { + return Err("duplicate survival scan argument: --request".into()) + } + "--artifact-root" if artifact_root.replace(PathBuf::from(value)).is_some() => { + return Err("duplicate survival scan argument: --artifact-root".into()) + } + _ => {} + } + index += 2; + } + Ok(( + request.ok_or("survival scan requires --request")?, + artifact_root.ok_or("survival scan requires --artifact-root")?, + )) +} + +fn read_request(path: &str) -> Result { + let mut bytes = Vec::new(); + if path == "-" { + io::stdin() + .take(MAX_REQUEST_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| format!("read survival scan request: {error}"))?; + } else { + let metadata = fs::metadata(path) + .map_err(|error| format!("read survival scan request metadata: {error}"))?; + if !metadata.is_file() || metadata.len() > MAX_REQUEST_BYTES { + return Err("survival scan request must be a bounded regular file".into()); + } + bytes = fs::read(path).map_err(|error| format!("read survival scan request: {error}"))?; + } + if bytes.len() as u64 > MAX_REQUEST_BYTES { + return Err("survival scan request exceeds size limit".into()); + } + let text = std::str::from_utf8(&bytes) + .map_err(|error| format!("survival scan request is not UTF-8: {error}"))?; + crate::capability::reject_duplicate_json_keys(text)?; + serde_json::from_str(text).map_err(|_| "invalid survival scan request JSON".into()) +} + +pub(crate) fn scan(request: &Value, artifact_root: &Path) -> Result { + exact( + request, + &["schema", "snapshotIdentity", "inputs", "codenexusAdapter"], + "survival scan request", + )?; + if request["schema"] != "code-intel-repository-survival-scan-request.v1" { + return Err("survival scan request schema is invalid".into()); + } + let snapshot_identity = request["snapshotIdentity"] + .as_str() + .filter(|value| digest(value)) + .ok_or("survival scan snapshot identity is invalid")?; + let verified = crate::artifact_ref::verify_inputs( + &request["inputs"], + Some(artifact_root), + snapshot_identity, + ) + .map_err(|error| error.message().to_string())?; + if verified.len() != 2 { + return Err("survival scan requires exactly snapshot and inventory Artifact Refs".into()); + } + let snapshot = verified + .iter() + .find(|item| item.artifact_type() == "repository.snapshot") + .ok_or("survival scan repository snapshot input is missing")?; + let inventory = verified + .iter() + .find(|item| item.artifact_type() == "inventory.files") + .ok_or("survival scan inventory input is missing")?; + let snapshot_value: Value = serde_json::from_slice(snapshot.bytes()) + .map_err(|_| "verified repository snapshot is invalid JSON")?; + if snapshot_value["snapshot"]["identity"] != snapshot_identity { + return Err("repository snapshot payload identity mismatch".into()); + } + + let adapter = &request["codenexusAdapter"]; + crate::codenexus_adapter::validate_adapter_result(adapter)?; + if adapter["schema"] != "code-intel-codenexus-adapter-result.v1" + || adapter["port"]["status"] != "unavailable" + || adapter["port"]["failureKind"] != "provider_unavailable" + || adapter["port"]["perceptionUsable"] != false + || adapter["factPromotion"]["engineeringFacts"] != json!([]) + || adapter["evidence"]["request"]["observation"]["failure"]["kind"] + != "provider_unavailable" + { + return Err("survival scan is only valid for unavailable CodeNexus evidence".into()); + } + let admitted = crate::admissibility::validate_for_consumer( + &adapter["evidence"]["request"], + artifact_root, + )?; + crate::codenexus_adapter::validate_admitted_payload(admitted.payload(), adapter)?; + if admitted.result()["domainVerdict"] != "unknown" + || admitted.result()["engineeringFacts"] != json!([]) + { + return Err("unavailable CodeNexus evidence must remain unknown without facts".into()); + } + + let paths = inventory_paths(inventory.bytes())?; + let mut extensions = BTreeMap::::new(); + for path in &paths { + if let Some(extension) = Path::new(path) + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .filter(|value| !value.is_empty()) + { + *extensions.entry(extension).or_default() += 1; + } + } + let repository_kind = snapshot_value["repository"]["kind"].clone(); + let repo_identity = snapshot_value["snapshot"]["repoIdentity"].clone(); + let revision = snapshot_value["snapshot"]["head"].clone(); + let inventory_count = paths.len() as u64; + Ok(json!({ + "schema":"code-intel-repository-survival-scan-result.v1", + "status":"completed", + "snapshotIdentity":snapshot_identity, + "repository":{ + "kind":repository_kind, + "identity":repo_identity, + "revision":revision, + "dirty":snapshot_value["dirtyOverlay"]["present"], + "sourceSha256":snapshot.sha256() + }, + "inventory":{ + "fileCount":inventory_count, + "extensions":extensions, + "sourceSha256":inventory.sha256() + }, + "providerDiagnosis":{ + "providerId":adapter["port"]["provider"]["providerId"], + "status":"provider_unavailable", + "domainVerdict":"unknown" + }, + "completeness":"reduced", + "structuralVerdict":"unknown", + "limitations":[ + "only repository identity and basic file inventory are available", + "deeper structural perception requires an admitted provider result" + ], + "engineeringFacts":[ + {"kind":"repository_identity","value":repo_identity,"sourceSha256":snapshot.sha256()}, + {"kind":"repository_revision","value":revision,"sourceSha256":snapshot.sha256()}, + {"kind":"inventory_file_count","value":inventory_count,"sourceSha256":inventory.sha256()} + ] + })) +} + +fn inventory_paths(bytes: &[u8]) -> Result, String> { + let delimiter = if bytes.contains(&0) { 0 } else { b'\n' }; + let mut seen = BTreeSet::new(); + let mut paths = Vec::new(); + for raw in bytes.split(|byte| *byte == delimiter) { + if raw.is_empty() { + continue; + } + let path = std::str::from_utf8(raw) + .map_err(|_| "verified inventory contains non-UTF-8 path")? + .trim_end_matches('\r'); + if path.is_empty() || path.starts_with('/') || path.contains("..") || path.contains('\\') { + return Err("verified inventory contains a non-portable path".into()); + } + if !seen.insert(path.to_ascii_lowercase()) { + return Err("verified inventory contains duplicate paths".into()); + } + paths.push(path.to_string()); + } + Ok(paths) +} + +fn exact(value: &Value, keys: &[&str], label: &str) -> Result<(), String> { + let object = value + .as_object() + .ok_or_else(|| format!("{label} must be an object"))?; + let actual = object.keys().map(String::as_str).collect::>(); + let expected = keys.iter().copied().collect::>(); + if actual != expected { + return Err(format!("{label} fields are invalid")); + } + Ok(()) +} + +fn digest(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) +} diff --git a/crates/code-intel-cli/src/understanding_quadrant.rs b/crates/code-intel-cli/src/understanding_quadrant.rs new file mode 100644 index 0000000..908c91a --- /dev/null +++ b/crates/code-intel-cli/src/understanding_quadrant.rs @@ -0,0 +1,440 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +use super::{AdapterArtifact, AdapterError, AdapterOutput}; +use crate::artifact_ref::VerifiedArtifact; + +const CRITICALITY_THRESHOLD: u64 = 50; +const CONFIDENCE_THRESHOLD: u64 = 50; + +pub(crate) fn execute( + request: &Value, + verified_inputs: &[VerifiedArtifact], + out: &Path, +) -> Result { + if request["options"] + .as_object() + .map_or(true, |options| !options.is_empty()) + { + return Err(AdapterError::InvalidOptions( + "understanding.quadrant accepts no options; method selection is a read-only consumer" + .into(), + )); + } + let [input] = verified_inputs else { + return Err(AdapterError::Contract( + "understanding.quadrant requires exactly one A03-verified project.orientation Artifact Ref" + .into(), + )); + }; + if input.artifact_schema() != "code-intel-project-orientation.v1" + || input.artifact_type() != "project.orientation" + { + return Err(AdapterError::Contract( + "understanding.quadrant consumes only the D01 project.orientation artifact".into(), + )); + } + let orientation: Value = serde_json::from_slice(input.bytes()) + .map_err(|error| AdapterError::Contract(format!("parse D01 orientation: {error}")))?; + if orientation["snapshotIdentity"] != request["snapshot"]["identity"] { + return Err(AdapterError::Contract( + "D01 orientation payload snapshot differs from the A01 request".into(), + )); + } + + let document = project(&orientation, input.sha256())?; + let bytes = serde_json::to_vec(&document).map_err(|error| { + AdapterError::Internal(format!("serialize understanding quadrant: {error}")) + })?; + fs::create_dir(out).map_err(|error| { + AdapterError::Io(format!( + "exclusive understanding quadrant staging create {}: {error}", + out.display() + )) + })?; + fs::write(out.join("understanding-quadrant.json"), &bytes) + .map_err(|error| AdapterError::Io(format!("write understanding-quadrant.json: {error}")))?; + + Ok(AdapterOutput { + artifacts: vec![AdapterArtifact { + artifact_schema: "code-intel-understanding-quadrant.v1".into(), + artifact_type: "understanding.quadrant".into(), + relative_path: "understanding-quadrant.json".into(), + bytes, + }], + observed_effects: vec!["local_write".into()], + domain_verdict: crate::capability_inventory::AdapterDomainVerdict::Pass, + domain_failure: None, + }) +} + +fn project(orientation: &Value, source_sha256: &str) -> Result { + let level = orientation["confidence"]["level"] + .as_str() + .ok_or_else(|| AdapterError::Contract("D01 confidence level is missing".into()))?; + let known_confidence = match level { + "high" => 100, + "medium" => 70, + "low" => 40, + _ => { + return Err(AdapterError::Contract( + "D01 confidence level is outside low/medium/high".into(), + )); + } + }; + let mut items = BTreeMap::::new(); + + insert_item( + &mut items, + "identity".into(), + "repository identity".into(), + 100, + known_confidence, + "known", + "Repository identity and revision are established by D01.".into(), + provenance(&orientation["identity"], "identity")?, + )?; + let purpose_known = orientation["purpose"]["status"] == "known"; + insert_item( + &mut items, + "purpose".into(), + "project purpose".into(), + 90, + if purpose_known { known_confidence } else { 0 }, + if purpose_known { "known" } else { "unknown" }, + orientation["purpose"]["reason"] + .as_str() + .unwrap_or("Project purpose remains unknown.") + .to_string(), + provenance(&orientation["purpose"], "purpose")?, + )?; + for language in array(&orientation["languages"], "languages")? { + let name = string(language, "name", "language")?; + insert_item( + &mut items, + format!("language:{name}"), + format!("language {name}"), + 30, + known_confidence, + "known", + format!("D01 observed source files for {name}."), + provenance(language, "language")?, + )?; + } + for boundary in array(&orientation["boundaries"], "boundaries")? { + let path = string(boundary, "path", "boundary")?; + insert_item( + &mut items, + format!("boundary:{path}"), + format!("system boundary {path}"), + 90, + known_confidence, + "known", + format!("D01 identifies {path} as a top-level system boundary."), + provenance(boundary, "boundary")?, + )?; + } + for entry in array(&orientation["entryPoints"], "entryPoints")? { + let path = string(entry, "path", "entry point")?; + insert_item( + &mut items, + format!("entry-point:{path}"), + format!("entry point {path}"), + 80, + known_confidence, + "known", + format!("D01 classifies {path} as an entry point."), + provenance(entry, "entry point")?, + )?; + } + for command in array(&orientation["commands"], "commands")? { + let path = string(command, "path", "command")?; + insert_item( + &mut items, + format!("command:{path}"), + format!("command {path}"), + 75, + known_confidence, + "known", + format!("D01 identifies {path} as an executable project command."), + provenance(command, "command")?, + )?; + } + insert_item( + &mut items, + "active-change".into(), + "active change".into(), + 70, + known_confidence, + "known", + format!( + "The D01 working tree status is {}.", + orientation["activeChange"]["status"] + .as_str() + .unwrap_or("unknown") + ), + provenance(&orientation["activeChange"], "activeChange")?, + )?; + for risk in array(&orientation["risks"], "risks")? { + let code = string(risk, "code", "risk")?; + insert_item( + &mut items, + format!("risk:{code}"), + format!("risk {code}"), + 85, + known_confidence, + "known", + string(risk, "statement", "risk")?.to_string(), + provenance(risk, "risk")?, + )?; + } + for availability in array(&orientation["evidenceAvailability"], "evidenceAvailability")? { + let evidence = string(availability, "evidence", "evidence availability")?; + let status = string(availability, "status", "evidence availability")?; + insert_item( + &mut items, + format!("evidence:{evidence}"), + format!("evidence availability {evidence}"), + 35, + if status == "unknown" { + 0 + } else { + known_confidence + }, + if status == "unknown" { + "unknown" + } else { + "known" + }, + format!("D01 reports {evidence} evidence as {status}."), + provenance(availability, "evidence availability")?, + )?; + } + for unknown in array(&orientation["unknowns"], "unknowns")? { + let field = string(unknown, "field", "unknown")?; + insert_item( + &mut items, + format!("unknown:{field}"), + field.to_string(), + unknown_criticality(field), + 0, + "unknown", + string(unknown, "reason", "unknown")?.to_string(), + provenance(unknown, "unknown")?, + )?; + } + + let items = items.into_values().collect::>(); + let visible_unknowns = items + .iter() + .filter(|item| item["sourceState"] == "unknown") + .map(|item| item["id"].clone()) + .collect::>(); + let mut counts = BTreeMap::::new(); + for item in &items { + *counts + .entry(item["quadrant"].as_str().unwrap().to_string()) + .or_default() += 1; + } + + Ok(json!({ + "schema":"code-intel-understanding-quadrant.v1", + "snapshotIdentity":orientation["snapshotIdentity"], + "sourceOrientation":{ + "artifactSchema":"code-intel-project-orientation.v1", + "artifactType":"project.orientation", + "sha256":source_sha256 + }, + "classificationPolicy":{ + "schema":"code-intel-understanding-quadrant-policy.v1", + "scoreRange":{"minimum":0,"maximum":100}, + "systemCriticalityThreshold":CRITICALITY_THRESHOLD, + "evidenceConfidenceThreshold":CONFIDENCE_THRESHOLD, + "thresholdRule":"greater_than_or_equal_is_upper_band", + "unknownCriticalityRule":"critical_by_default_except_declared_supporting_context", + "methodConsumerPolicy":"C01_cards_and_C02_selection_may_consume_but_cannot_rewrite" + }, + "items":items, + "visibleUnknowns":visible_unknowns, + "counts":{ + "Known Core":counts.get("Known Core").copied().unwrap_or(0), + "Critical Unknown":counts.get("Critical Unknown").copied().unwrap_or(0), + "Supporting Context":counts.get("Supporting Context").copied().unwrap_or(0), + "Deferred Unknown":counts.get("Deferred Unknown").copied().unwrap_or(0) + } + })) +} + +#[allow(clippy::too_many_arguments)] +fn insert_item( + items: &mut BTreeMap, + id: String, + subject: String, + criticality: u64, + confidence: u64, + source_state: &str, + statement: String, + provenance: Value, +) -> Result<(), AdapterError> { + let (criticality_band, confidence_band, quadrant) = classify(criticality, confidence); + let prior = items.insert( + id.clone(), + json!({ + "id":id, + "subject":subject, + "sourceState":source_state, + "systemCriticality":{"score":criticality,"band":criticality_band}, + "evidenceConfidence":{"score":confidence,"band":confidence_band}, + "quadrant":quadrant, + "statement":statement, + "provenance":provenance + }), + ); + if prior.is_some() { + return Err(AdapterError::Contract(format!( + "D01 projects duplicate understanding item id {id}" + ))); + } + Ok(()) +} + +fn classify(criticality: u64, confidence: u64) -> (&'static str, &'static str, &'static str) { + let critical = criticality >= CRITICALITY_THRESHOLD; + let confident = confidence >= CONFIDENCE_THRESHOLD; + match (critical, confident) { + (true, true) => ("critical", "high", "Known Core"), + (true, false) => ("critical", "low", "Critical Unknown"), + (false, true) => ("supporting", "high", "Supporting Context"), + (false, false) => ("supporting", "low", "Deferred Unknown"), + } +} + +fn unknown_criticality(field: &str) -> u64 { + let field = field.to_ascii_lowercase(); + if [ + "language", + "documentation", + "example", + "context", + "metadata", + "style", + ] + .iter() + .any(|token| field.contains(token)) + { + 25 + } else { + 90 + } +} + +fn provenance(value: &Value, label: &str) -> Result { + let provenance = value["provenance"] + .as_array() + .filter(|values| !values.is_empty()) + .ok_or_else(|| AdapterError::Contract(format!("D01 {label} provenance is missing")))?; + for entry in provenance { + let object = entry.as_object().ok_or_else(|| { + AdapterError::Contract(format!("D01 {label} provenance entry must be an object")) + })?; + let exact = object.len() == 3 + && object.contains_key("artifactType") + && object.contains_key("artifactSha256") + && object.contains_key("jsonPointer"); + let valid = entry["artifactType"] + .as_str() + .is_some_and(|value| !value.is_empty()) + && entry["artifactSha256"].as_str().is_some_and(|value| { + value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) + }) + && entry["jsonPointer"] + .as_str() + .is_some_and(|value| value.starts_with('/')); + if !exact || !valid { + return Err(AdapterError::Contract(format!( + "D01 {label} provenance entry is invalid" + ))); + } + } + Ok(Value::Array(provenance.clone())) +} + +fn array<'a>(value: &'a Value, label: &str) -> Result<&'a Vec, AdapterError> { + value + .as_array() + .ok_or_else(|| AdapterError::Contract(format!("D01 {label} must be an array"))) +} + +fn string<'a>(value: &'a Value, field: &str, label: &str) -> Result<&'a str, AdapterError> { + value[field] + .as_str() + .filter(|value| !value.is_empty()) + .ok_or_else(|| AdapterError::Contract(format!("D01 {label}.{field} is missing"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn thresholds_are_inclusive_and_cover_all_four_quadrants() { + assert_eq!(classify(50, 50).2, "Known Core"); + assert_eq!(classify(50, 49).2, "Critical Unknown"); + assert_eq!(classify(49, 50).2, "Supporting Context"); + assert_eq!(classify(49, 49).2, "Deferred Unknown"); + } + + #[test] + fn unknown_criticality_is_conservative_with_explicit_supporting_exceptions() { + assert_eq!(unknown_criticality("dependencies.runtime"), 90); + assert_eq!(unknown_criticality("documentation.examples"), 25); + assert_eq!(unknown_criticality("unrecognized.future.field"), 90); + } + + #[test] + fn duplicate_projected_ids_fail_closed() { + let mut items = BTreeMap::new(); + let provenance = json!([{"artifactType":"project.orientation","sha256":"a".repeat(64),"jsonPointer":"/languages/0"}]); + insert_item( + &mut items, + "language:Rust".into(), + "language Rust".into(), + 30, + 100, + "known", + "first".into(), + provenance.clone(), + ) + .unwrap(); + let error = insert_item( + &mut items, + "language:Rust".into(), + "language Rust".into(), + 30, + 100, + "known", + "second".into(), + provenance, + ) + .unwrap_err(); + assert!( + matches!(error, AdapterError::Contract(message) if message.contains("duplicate understanding item id")) + ); + } + + #[test] + fn provenance_entries_must_be_closed_nonempty_claim_objects() { + for invalid in [ + json!([null]), + json!([{}]), + json!([{"artifactType":"","artifactSha256":"a".repeat(64),"jsonPointer":"/x"}]), + json!([{"artifactType":"project.orientation","artifactSha256":"a".repeat(64),"jsonPointer":"x"}]), + json!([{"artifactType":"project.orientation","artifactSha256":"a".repeat(64),"jsonPointer":"/x","extra":true}]), + ] { + assert!(provenance(&json!({"provenance":invalid}), "fixture").is_err()); + } + } +} diff --git a/crates/code-intel-cli/tests/artifact_index.rs b/crates/code-intel-cli/tests/artifact_index.rs new file mode 100644 index 0000000..c61488c --- /dev/null +++ b/crates/code-intel-cli/tests/artifact_index.rs @@ -0,0 +1,396 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/artifact_index.rs"] +mod artifact_index; +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/run_commit.rs"] +mod run_commit; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; +#[path = "../src/staged_artifact.rs"] +mod staged_artifact; + +use run_commit::CommitOptions; +use staged_artifact::{ArtifactWriteContract, StagedWriter}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +static SEQ: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-a08-{label}-{}-{nonce}-{}", + std::process::id(), + SEQ.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn inventory_contract() -> ArtifactWriteContract { + ArtifactWriteContract { + artifact_schema: "code-intel-file-inventory.v1", + artifact_type: "inventory.files", + max_bytes: 1024, + validate_payload: |bytes| { + if bytes == b"portable evidence\n" { + Ok(()) + } else { + Err("invalid inventory".into()) + } + }, + } +} + +fn manifest_contract() -> ArtifactWriteContract { + ArtifactWriteContract { + artifact_schema: "code-intel-run-manifest.v1", + artifact_type: "run.manifest", + max_bytes: 1024 * 1024, + validate_payload: run_commit::validate_run_manifest_bytes, + } +} + +fn staged_with_outcome( + root: &Path, + run_identity: &str, + outcome: &str, +) -> (staged_artifact::StagedArtifactSet, Value) { + let mut writer = StagedWriter::begin(root, SNAPSHOT).unwrap(); + let nodes = match outcome { + "completed" => { + let inventory = writer + .stage(b"portable evidence\n", inventory_contract()) + .unwrap() + .to_artifact_ref_value(); + json!({ + "inventory":{"status":"succeeded","verdict":"pass","artifacts":[inventory]} + }) + } + "domain_failed" => json!({ + "doctor":{"status":"domain_failed","verdict":"fail","diagnostic":"fixture domain failure","artifacts":[]} + }), + other => panic!("unsupported fixture outcome: {other}"), + }; + let manifest = json!({ + "schema":"code-intel-run-manifest.v1", + "runIdentity":run_identity, + "snapshotIdentity":SNAPSHOT, + "outcome":outcome, + "nodes":nodes + }); + let manifest_ref = writer + .stage(&serde_json::to_vec(&manifest).unwrap(), manifest_contract()) + .unwrap() + .to_artifact_ref_value(); + (writer.seal().unwrap(), manifest_ref) +} + +fn staged(root: &Path, run_identity: &str) -> (staged_artifact::StagedArtifactSet, Value) { + staged_with_outcome(root, run_identity, "completed") +} + +#[test] +fn complete_and_staged_side_by_side_indexes_only_complete_run() { + let tree = Temp::new("smallest"); + let repo = tree.0.join("repo-a"); + fs::create_dir(&repo).unwrap(); + let (set, manifest_ref) = staged(&repo, "dag-v1:aabb"); + run_commit::commit(set, &manifest_ref, "run-001", CommitOptions::default()).unwrap(); + let (staged_set, _) = staged(&repo, "dag-v1:ccdd"); + assert!(staged_set.path().is_dir()); + + let result = artifact_index::rebuild(&tree.0).unwrap(); + + assert_eq!(result["schema"], "code-intel-artifact-index.v1"); + assert_eq!(result["entries"].as_array().unwrap().len(), 1); + assert_eq!(result["entries"][0]["repo"], "repo-a"); + assert_eq!(result["entries"][0]["run"], "run-001"); + assert_eq!(result["entries"][0]["runIdentity"], "dag-v1:aabb"); + assert!(result["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|item| item["classification"] == "staging")); +} + +fn committed_repo(root: &Path, repo_name: &str, run_name: &str, identity: &str) -> PathBuf { + let repo = root.join(repo_name); + fs::create_dir_all(&repo).unwrap(); + let (set, manifest_ref) = staged(&repo, identity); + run_commit::commit(set, &manifest_ref, run_name, CommitOptions::default()) + .unwrap() + .final_path +} + +#[test] +fn forged_marker_manifest_and_artifact_bindings_are_diagnosed_and_rejected() { + let tree = Temp::new("forged"); + let bad_digest = committed_repo(&tree.0, "bad-digest", "run", "dag-v1:aabb"); + let mut marker: Value = + serde_json::from_slice(&fs::read(bad_digest.join("run-complete.json")).unwrap()).unwrap(); + marker["manifest"]["sha256"] = json!("0".repeat(64)); + fs::write( + bad_digest.join("run-complete.json"), + serde_json::to_vec(&marker).unwrap(), + ) + .unwrap(); + + let bad_identity = committed_repo(&tree.0, "bad-identity", "run", "dag-v1:aabb"); + let mut marker: Value = + serde_json::from_slice(&fs::read(bad_identity.join("run-complete.json")).unwrap()).unwrap(); + marker["runIdentity"] = json!("dag-v1:ccdd"); + fs::write( + bad_identity.join("run-complete.json"), + serde_json::to_vec(&marker).unwrap(), + ) + .unwrap(); + + let bad_snapshot = committed_repo(&tree.0, "bad-snapshot", "run", "dag-v1:aabb"); + let mut marker: Value = + serde_json::from_slice(&fs::read(bad_snapshot.join("run-complete.json")).unwrap()).unwrap(); + marker["snapshotIdentity"] = json!("b".repeat(64)); + fs::write( + bad_snapshot.join("run-complete.json"), + serde_json::to_vec(&marker).unwrap(), + ) + .unwrap(); + + let bad_artifact = committed_repo(&tree.0, "bad-artifact", "run", "dag-v1:aabb"); + let (_, manifest) = run_commit::validate_committed_run(&bad_artifact).unwrap(); + let artifact_path = manifest["nodes"]["inventory"]["artifacts"][0]["path"] + .as_str() + .unwrap(); + fs::write(bad_artifact.join(artifact_path), b"tampered evidence\n").unwrap(); + + let result = artifact_index::rebuild(&tree.0).unwrap(); + assert_eq!(result["entries"], json!([])); + let forged = result["diagnostics"] + .as_array() + .unwrap() + .iter() + .filter(|item| item["classification"] == "forged") + .count(); + assert_eq!(forged, 4); +} + +#[test] +fn incomplete_and_legacy_runs_are_diagnostic_only_and_newer_invalid_does_not_win() { + let tree = Temp::new("diagnostics"); + let valid = committed_repo(&tree.0, "repo-a", "run-001", "dag-v1:aabb"); + assert!(valid.is_dir()); + let incomplete = committed_repo(&tree.0, "repo-a", "run-999", "dag-v1:ccdd"); + fs::remove_file(incomplete.join("run-complete.json")).unwrap(); + let legacy = tree.0.join("legacy-repo/20260713-120000"); + fs::create_dir_all(&legacy).unwrap(); + fs::write(legacy.join("report.json"), b"{}\n").unwrap(); + + let result = artifact_index::rebuild(&tree.0).unwrap(); + assert_eq!(result["entries"].as_array().unwrap().len(), 1); + assert_eq!(result["entries"][0]["run"], "run-001"); + assert!(result["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|item| item["run"] == "run-999" && item["classification"] == "incomplete")); + assert!(result["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|item| item["repo"] == "legacy-repo" && item["classification"] == "legacy")); +} + +#[test] +fn newer_committed_non_completed_run_is_diagnostic_only_and_never_becomes_latest() { + let tree = Temp::new("non-completed"); + committed_repo(&tree.0, "repo-a", "run-001", "dag-v1:aabb"); + let repo = tree.0.join("repo-a"); + let (set, manifest_ref) = staged_with_outcome(&repo, "dag-v1:ccdd", "domain_failed"); + run_commit::commit(set, &manifest_ref, "run-999", CommitOptions::default()).unwrap(); + + let result = artifact_index::rebuild(&tree.0).unwrap(); + + assert_eq!(result["entries"].as_array().unwrap().len(), 1); + assert_eq!(result["entries"][0]["run"], "run-001"); + assert_eq!(result["entries"][0]["outcome"], "completed"); + assert!(result["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|item| { + item["run"] == "run-999" + && item["classification"] == "non_completed" + && item["reason"] + .as_str() + .is_some_and(|reason| reason.contains("domain_failed")) + })); +} + +#[test] +fn rebuild_and_incremental_are_byte_equivalent_and_stably_sorted() { + let tree = Temp::new("equivalence"); + committed_repo(&tree.0, "repo-z", "run-001", "dag-v1:aabb"); + committed_repo(&tree.0, "repo-a", "run-001", "dag-v1:ccdd"); + let first = artifact_index::rebuild(&tree.0).unwrap(); + let second = artifact_index::incremental(&tree.0, &first).unwrap(); + + assert_eq!( + serde_json::to_vec(&first).unwrap(), + serde_json::to_vec(&second).unwrap() + ); + assert_eq!(first["entries"][0]["repo"], "repo-a"); + assert_eq!(first["entries"][1]["repo"], "repo-z"); + assert!(artifact_index::incremental(&tree.0, &json!({})).is_err()); +} + +#[test] +fn incremental_rejects_every_invalid_nested_existing_index_shape() { + let tree = Temp::new("nested-existing"); + committed_repo(&tree.0, "repo-a", "run-001", "dag-v1:aabb"); + let valid = artifact_index::rebuild(&tree.0).unwrap(); + + let mut cases = Vec::new(); + let mut value = valid.clone(); + value["entries"][0]["unexpected"] = json!(true); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["repo"] = json!("../escape"); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["run"] = json!(""); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["runIdentity"] = json!("dag-v1:not-hex"); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["snapshotIdentity"] = json!(7); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["outcome"] = json!("invented"); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["manifest"]["path"] = json!("../manifest.json"); + cases.push(value); + let mut value = valid.clone(); + value["entries"][0]["artifactRefs"][0]["path"] = json!("../outside.bin"); + cases.push(value); + + let valid_diagnostic = json!({ + "repo": "repo-a", + "run": "run-002", + "classification": "incomplete", + "reason": "A07 completion marker is absent" + }); + let mut value = valid.clone(); + value["diagnostics"] = json!([valid_diagnostic.clone()]); + value["diagnostics"][0]["unexpected"] = json!(true); + cases.push(value); + let mut value = valid.clone(); + value["diagnostics"] = json!([valid_diagnostic.clone()]); + value["diagnostics"][0]["classification"] = json!("accepted-anyway"); + cases.push(value); + let mut value = valid.clone(); + value["diagnostics"] = json!([valid_diagnostic]); + value["diagnostics"][0]["run"] = json!("../escape"); + cases.push(value); + + for (index, invalid) in cases.into_iter().enumerate() { + assert!( + artifact_index::incremental(&tree.0, &invalid).is_err(), + "invalid nested existing-index case {index} was accepted" + ); + } +} + +#[test] +fn production_cli_writes_the_registered_committed_only_schema() { + let tree = Temp::new("cli"); + committed_repo(&tree.0, "repo-a", "run-001", "dag-v1:aabb"); + let output = tree.0.join("index.json"); + let command = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "artifact", + "index", + "--artifact-root", + tree.0.to_str().unwrap(), + "--output", + output.to_str().unwrap(), + "--operation", + "rebuild", + ]) + .output() + .unwrap(); + + assert!( + command.status.success(), + "{}", + String::from_utf8_lossy(&command.stderr) + ); + let stdout: Value = serde_json::from_slice(&command.stdout).unwrap(); + let written: Value = serde_json::from_slice(&fs::read(output).unwrap()).unwrap(); + assert_eq!(stdout, written); + assert_eq!(written["schema"], "code-intel-artifact-index.v1"); + + let registry: Value = serde_json::from_slice( + &fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../orchestration/integrations.json"), + ) + .unwrap(), + ) + .unwrap(); + let route = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|item| item["id"] == "artifact.index-committed-only") + .unwrap(); + assert_eq!( + route["entrypoint"], + "crates/code-intel-cli/src/artifact_index.rs" + ); + assert!(route["commands"]["facade"] + .as_str() + .unwrap() + .contains("update-code-intel-index.ps1")); + assert!(route["artifactContract"].as_array().unwrap().iter().any( + |contract| contract == "orchestration/schemas/code-intel-artifact-index.v1.schema.json" + )); + + let schema: Value = serde_json::from_slice( + &fs::read( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../orchestration/schemas/code-intel-artifact-index.v1.schema.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["properties"]["schema"]["const"], + "code-intel-artifact-index.v1" + ); +} diff --git a/crates/code-intel-cli/tests/artifact_ref.rs b/crates/code-intel-cli/tests/artifact_ref.rs new file mode 100644 index 0000000..b687dd8 --- /dev/null +++ b/crates/code-intel-cli/tests/artifact_ref.rs @@ -0,0 +1,540 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const IMPLEMENTATION_DIGEST: &str = + "43ced9ef578e6484423468e059c93ef0bc5eeeb35d23271451b2d8f1a16f9bb6"; +static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct TempTree(PathBuf); + +impl TempTree { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "code-intel-a03-{label}-{}-{nonce}-{sequence}", + std::process::id() + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for TempTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn snapshot(repo: &Path) -> Value { + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "snapshot", + "identity", + "--repo", + repo.to_str().unwrap(), + "--working-tree-policy", + "explicit_overlay", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice::(&output.stdout).unwrap()["snapshot"].clone() +} + +fn artifact_ref(path: &str, bytes: &[u8], snapshot_identity: &Value) -> Value { + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-file-inventory.v1", + "type":"inventory.files", + "path":path, + "sha256":sha256(bytes), + "consumedSnapshotIdentity":snapshot_identity + }) +} + +fn sha256(bytes: &[u8]) -> String { + assert_eq!(bytes, b"portable evidence\n"); + "924278019c18519b69088648b6d5b4f58fc96afa66204bab1274a5a4ee2bd2c2".to_string() +} + +fn request(repo: &Path, input: Value) -> Value { + let snapshot = snapshot(repo); + json!({ + "schema":"code-intel-capability-request.v1", + "capability":"inventory.rg", + "contractVersion":1, + "implementation":{ + "id":"inventory.rg.compat", + "version":"1.0.0", + "toolchainDigests":[IMPLEMENTATION_DIGEST] + }, + "snapshot":snapshot, + "options":{"repoPath":repo}, + "inputs":[input], + "effectPolicy":{"allowedEffects":["repo_read","local_write"]} + }) +} + +fn request_with_inputs(repo: &Path, inputs: Vec) -> Value { + let mut value = request(repo, inputs[0].clone()); + value["inputs"] = Value::Array(inputs); + value +} + +fn run( + root: &Path, + request: &Value, + artifact_root: Option<&Path>, + out: &Path, +) -> std::process::Output { + let request_path = root.join(format!( + "request-{}.json", + out.file_name().unwrap().to_string_lossy() + )); + fs::write(&request_path, serde_json::to_vec(request).unwrap()).unwrap(); + let mut command = Command::new(env!("CARGO_BIN_EXE_code-intel")); + command + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(out); + if let Some(artifact_root) = artifact_root { + command.arg("--artifact-root").arg(artifact_root); + } + command.output().unwrap() +} + +fn fixture() -> (TempTree, PathBuf, Value, Vec) { + let root = TempTree::new("fixture"); + let repo = root.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("source.rs"), "fn main() {}\n").unwrap(); + let snapshot = snapshot(&repo); + let bytes = b"portable evidence\n".to_vec(); + (root, repo, snapshot, bytes) +} + +#[test] +fn artifact_ref_is_content_identified_and_relocation_safe() { + let (root, repo, snapshot, bytes) = fixture(); + let first = root.0.join("artifacts-one"); + let second = root.0.join("artifacts-two"); + fs::create_dir_all(first.join("nested")).unwrap(); + fs::create_dir_all(second.join("nested")).unwrap(); + fs::write(first.join("nested/payload.bin"), &bytes).unwrap(); + fs::write(second.join("nested/payload.bin"), &bytes).unwrap(); + let reference = artifact_ref("nested/payload.bin", &bytes, &snapshot["identity"]); + let request = request(&repo, reference); + + for (index, artifact_root) in [&first, &second].into_iter().enumerate() { + let out = root.0.join(format!("out-{index}")); + let output = run(&root.0, &request, Some(artifact_root), &out); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(out.join("files.txt").is_file()); + } +} + +#[test] +fn artifact_ref_fails_closed_for_missing_tamper_snapshot_and_root_omission() { + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + let reference = artifact_ref("payload.bin", &bytes, &snapshot["identity"]); + + let missing = run( + &root.0, + &request(&repo, reference.clone()), + Some(&artifacts), + &root.0.join("out-missing"), + ); + assert_eq!(missing.status.code(), Some(65)); + + fs::write(artifacts.join("payload.bin"), b"tampered\n").unwrap(); + let tampered = run( + &root.0, + &request(&repo, reference.clone()), + Some(&artifacts), + &root.0.join("out-tampered"), + ); + assert_eq!(tampered.status.code(), Some(65)); + + fs::write(artifacts.join("payload.bin"), &bytes).unwrap(); + let mut wrong_snapshot = reference.clone(); + wrong_snapshot["consumedSnapshotIdentity"] = json!("f".repeat(64)); + let wrong = run( + &root.0, + &request(&repo, wrong_snapshot), + Some(&artifacts), + &root.0.join("out-wrong"), + ); + assert_eq!(wrong.status.code(), Some(65)); + + let no_root = run( + &root.0, + &request(&repo, reference), + None, + &root.0.join("out-no-root"), + ); + assert_eq!(no_root.status.code(), Some(65)); +} + +#[cfg(windows)] +#[test] +fn artifact_ref_maps_a_deny_share_lock_to_host_io_exit_74() { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + let payload = artifacts.join("payload.bin"); + fs::write(&payload, &bytes).unwrap(); + let reference = artifact_ref("payload.bin", &bytes, &snapshot["identity"]); + let lock = OpenOptions::new() + .read(true) + .share_mode(0) + .open(&payload) + .unwrap(); + + let output = run( + &root.0, + &request(&repo, reference), + Some(&artifacts), + &root.0.join("out-locked"), + ); + assert_eq!( + output.status.code(), + Some(74), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + drop(lock); +} + +#[test] +fn artifact_ref_preflights_all_collisions_before_missing_or_tampered_files() { + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + + for (label, first_path, second_path) in [ + ("exact", "dup.bin", "dup.bin"), + ("casefold", "Ä.bin", "ä.bin"), + ("nfc", "é.bin", "e\u{301}.bin"), + ] { + let request = request_with_inputs( + &repo, + vec![ + artifact_ref(first_path, &bytes, &snapshot["identity"]), + artifact_ref(second_path, &bytes, &snapshot["identity"]), + ], + ); + let output = run( + &root.0, + &request, + Some(&artifacts), + &root.0.join(format!("out-preflight-{label}")), + ); + assert_eq!( + output.status.code(), + Some(65), + "{label}: stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let first = artifacts.join("Ä.bin"); + fs::write(&first, &bytes).unwrap(); + let collision = request_with_inputs( + &repo, + vec![ + artifact_ref("Ä.bin", &bytes, &snapshot["identity"]), + artifact_ref("ä.bin", &bytes, &snapshot["identity"]), + ], + ); + let output = run( + &root.0, + &collision, + Some(&artifacts), + &root.0.join("out-preflight-present"), + ); + assert_eq!(output.status.code(), Some(65)); + + fs::write(&first, b"tampered\n").unwrap(); + let output = run( + &root.0, + &collision, + Some(&artifacts), + &root.0.join("out-preflight-tampered"), + ); + assert_eq!(output.status.code(), Some(65)); +} + +#[cfg(windows)] +#[test] +fn artifact_ref_preflights_casefold_collision_before_a_first_file_lock() { + use std::fs::OpenOptions; + use std::os::windows::fs::OpenOptionsExt; + + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + let first = artifacts.join("Ä.bin"); + fs::write(&first, &bytes).unwrap(); + let lock = OpenOptions::new() + .read(true) + .share_mode(0) + .open(&first) + .unwrap(); + let collision = request_with_inputs( + &repo, + vec![ + artifact_ref("Ä.bin", &bytes, &snapshot["identity"]), + artifact_ref("ä.bin", &bytes, &snapshot["identity"]), + ], + ); + let output = run( + &root.0, + &collision, + Some(&artifacts), + &root.0.join("out-preflight-locked"), + ); + assert_eq!( + output.status.code(), + Some(65), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + drop(lock); +} + +#[test] +fn artifact_ref_rejects_absolute_parent_directory_and_link_payloads() { + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + fs::write(artifacts.join("payload.bin"), &bytes).unwrap(); + + for (index, path) in [ + "../payload.bin", + artifacts.join("payload.bin").to_str().unwrap(), + ".", + ] + .into_iter() + .enumerate() + { + let reference = artifact_ref(path, &bytes, &snapshot["identity"]); + let output = run( + &root.0, + &request(&repo, reference), + Some(&artifacts), + &root.0.join(format!("out-path-{index}")), + ); + assert_eq!( + output.status.code(), + Some(65), + "path={path} stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let outside = root.0.join("outside.bin"); + fs::write(&outside, &bytes).unwrap(); + let link = artifacts.join("link.bin"); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(&outside, &link).is_ok(); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_file(&outside, &link).is_ok(); + if linked { + let reference = artifact_ref("link.bin", &bytes, &snapshot["identity"]); + let output = run( + &root.0, + &request(&repo, reference), + Some(&artifacts), + &root.0.join("out-link"), + ); + assert_eq!(output.status.code(), Some(65)); + } +} + +#[cfg(target_os = "linux")] +#[test] +fn artifact_ref_maps_root_intermediate_and_leaf_links_to_contract_exit_65() { + use std::os::unix::fs::symlink; + let (root, repo, snapshot, bytes) = fixture(); + let real = root.0.join("artifacts-real"); + let nested = real.join("nested"); + fs::create_dir_all(&nested).unwrap(); + fs::write(nested.join("payload.bin"), &bytes).unwrap(); + let root_link = root.0.join("artifacts-root-link"); + symlink(&real, &root_link).unwrap(); + symlink(&nested, real.join("intermediate-link")).unwrap(); + symlink(nested.join("payload.bin"), real.join("leaf-link.bin")).unwrap(); + + for (label, artifact_root, path) in [ + ("root", root_link.as_path(), "nested/payload.bin"), + ( + "intermediate", + real.as_path(), + "intermediate-link/payload.bin", + ), + ("leaf", real.as_path(), "leaf-link.bin"), + ] { + let reference = artifact_ref(path, &bytes, &snapshot["identity"]); + let output = run( + &root.0, + &request(&repo, reference), + Some(artifact_root), + &root.0.join(format!("out-linux-{label}")), + ); + assert_eq!( + output.status.code(), + Some(65), + "{label}: stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } +} + +#[test] +fn artifact_ref_rejects_unregistered_contract_invalid_payload_and_duplicate_aliases() { + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + fs::write(artifacts.join("payload.bin"), &bytes).unwrap(); + + let mut unknown = artifact_ref("payload.bin", &bytes, &snapshot["identity"]); + unknown["artifactSchema"] = json!("unknown.v1"); + let output = run( + &root.0, + &request(&repo, unknown), + Some(&artifacts), + &root.0.join("out-contract"), + ); + assert_eq!(output.status.code(), Some(65)); + + let invalid_bytes = b"z.rs\na.rs\n"; + fs::write(artifacts.join("invalid.bin"), invalid_bytes).unwrap(); + let invalid = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-file-inventory.v1", + "type":"inventory.files", + "path":"invalid.bin", + "sha256":"e2e00e70528e4f58b153ed72c56e13fae6943bdde078ba7cec457cb425b0d8a4", + "consumedSnapshotIdentity":snapshot["identity"] + }); + let output = run( + &root.0, + &request(&repo, invalid), + Some(&artifacts), + &root.0.join("out-payload"), + ); + assert_eq!(output.status.code(), Some(65)); + + fs::write(artifacts.join("Payload.bin"), &bytes).unwrap(); + let first = artifact_ref("payload.bin", &bytes, &snapshot["identity"]); + let second = artifact_ref("Payload.bin", &bytes, &snapshot["identity"]); + let mut duplicate_request = request(&repo, first); + duplicate_request["inputs"] + .as_array_mut() + .unwrap() + .push(second); + let output = run( + &root.0, + &duplicate_request, + Some(&artifacts), + &root.0.join("out-duplicate"), + ); + assert_eq!(output.status.code(), Some(65)); +} + +#[test] +fn artifact_ref_rejects_payload_over_the_registered_size_limit_before_reading() { + let (root, repo, snapshot, _) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + let oversized = artifacts.join("large.bin"); + fs::File::create(&oversized) + .unwrap() + .set_len(64 * 1024 * 1024 + 1) + .unwrap(); + let reference = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-file-inventory.v1", + "type":"inventory.files", + "path":"large.bin", + "sha256":"0".repeat(64), + "consumedSnapshotIdentity":snapshot["identity"] + }); + let output = run( + &root.0, + &request(&repo, reference), + Some(&artifacts), + &root.0.join("out-large"), + ); + assert_eq!(output.status.code(), Some(65)); +} + +#[test] +fn artifact_ref_rejects_hardlink_aliases_and_duplicate_root_authority() { + let (root, repo, snapshot, bytes) = fixture(); + let artifacts = root.0.join("artifacts"); + fs::create_dir(&artifacts).unwrap(); + fs::write(artifacts.join("one.bin"), &bytes).unwrap(); + fs::hard_link(artifacts.join("one.bin"), artifacts.join("two.bin")).unwrap(); + let first = artifact_ref("one.bin", &bytes, &snapshot["identity"]); + let second = artifact_ref("two.bin", &bytes, &snapshot["identity"]); + let mut request_value = request(&repo, first); + request_value["inputs"].as_array_mut().unwrap().push(second); + let output = run( + &root.0, + &request_value, + Some(&artifacts), + &root.0.join("out-alias"), + ); + assert_eq!(output.status.code(), Some(65)); + + let request_path = root.0.join("duplicate-root-request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request( + &repo, + artifact_ref("one.bin", &bytes, &snapshot["identity"]), + )) + .unwrap(), + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(root.0.join("out-duplicate-root")) + .arg("--artifact-root") + .arg(&artifacts) + .arg("--artifact-root") + .arg(&artifacts) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); +} diff --git a/crates/code-intel-cli/tests/assistance_discovery.rs b/crates/code-intel-cli/tests/assistance_discovery.rs new file mode 100644 index 0000000..1469bd1 --- /dev/null +++ b/crates/code-intel-cli/tests/assistance_discovery.rs @@ -0,0 +1,169 @@ +#[path = "../src/assistance_discovery.rs"] +mod assistance_discovery; + +use serde_json::{json, Value}; +use std::fs; +use std::path::PathBuf; + +fn root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn request() -> Value { + json!({ + "schema": "code-intel-assistance-discovery-request.v1", + "gap": { + "schema": "code-intel-engineering-capability-gap.v1", + "id": "gap-schema-validation", + "capability": "validate versioned JSON contracts offline", + "description": "The runtime needs deterministic offline contract checks.", + "constraints": ["no network", "no new runtime service"], + "evidenceRefs": ["artifact:gap-analysis"] + }, + "candidates": [ + { + "id": "internal-contract-atom", + "kind": "internal_atom", + "name": "contract.validate", + "fit": {"status": "strong", "basis": "already validates adjacent envelopes"}, + "license": {"status": "not_applicable", "basis": "repository-owned code"}, + "security": {"status": "acceptable", "basis": "offline and bounded input"}, + "integration": {"effort": "low", "basis": "shared Rust module"}, + "reversibility": {"status": "high", "basis": "adapter can be removed"}, + "evidenceRefs": ["artifact:internal-inventory"] + }, + { + "id": "method-contract-testing", + "kind": "established_method", + "name": "contract testing", + "fit": {"status": "strong", "basis": "matches the boundary problem"}, + "license": {"status": "not_applicable", "basis": "engineering method"}, + "security": {"status": "acceptable", "basis": "no executable dependency"}, + "integration": {"effort": "low", "basis": "method card already exists"}, + "reversibility": {"status": "high", "basis": "advisory method selection"}, + "evidenceRefs": ["method:contract-testing"] + }, + { + "id": "external-validator", + "kind": "external_tool", + "name": "candidate validator", + "fit": {"status": "unknown", "basis": "not yet proven against fixtures"}, + "license": {"status": "review_required", "basis": "license evidence absent"}, + "security": {"status": "review_required", "basis": "supply-chain review absent"}, + "integration": {"effort": "unknown", "basis": "adapter not designed"}, + "reversibility": {"status": "unknown", "basis": "migration surface unknown"}, + "evidenceRefs": ["artifact:external-candidate-note"] + }, + { + "id": "docs-json-schema", + "kind": "documentation", + "name": "JSON Schema reference", + "fit": {"status": "partial", "basis": "documents semantics but does not execute"}, + "license": {"status": "acceptable", "basis": "reference use only"}, + "security": {"status": "acceptable", "basis": "non-executable reference"}, + "integration": {"effort": "low", "basis": "link from implementation notes"}, + "reversibility": {"status": "high", "basis": "documentation reference"}, + "evidenceRefs": ["doc:json-schema"] + } + ] + }) +} + +#[test] +fn named_gap_produces_comparable_proposal_dossiers_without_authority() { + let result = assistance_discovery::discover(&request()).unwrap(); + assert_eq!( + result["schema"], + "code-intel-assistance-discovery-result.v1" + ); + assert_eq!(result["gapId"], "gap-schema-validation"); + assert_eq!(result["status"], "completed"); + assert_eq!(result["dossiers"].as_array().unwrap().len(), 4); + assert_eq!(result["dossiers"][0]["kind"], "internal_atom"); + assert_eq!(result["dossiers"][1]["kind"], "established_method"); + assert_eq!(result["dossiers"][2]["kind"], "external_tool"); + assert_eq!(result["dossiers"][3]["kind"], "documentation"); + assert_eq!(result["proposalOnly"], true); + assert_eq!(result["effects"], json!([])); + assert_eq!(result["adoptionDecisions"], json!([])); + assert_eq!(result["committedEngineeringPlans"], json!([])); +} + +#[test] +fn unnamed_gap_and_popularity_only_candidates_are_rejected() { + let mut unnamed = request(); + unnamed["gap"]["id"] = json!(""); + assert!(assistance_discovery::discover(&unnamed).is_err()); + + let mut popularity = request(); + popularity["candidates"][2]["fit"]["basis"] = json!("popular on GitHub"); + popularity["candidates"][2]["evidenceRefs"] = json!(["metric:stars"]); + assert!(assistance_discovery::discover(&popularity) + .unwrap_err() + .to_string() + .contains("popularity")); +} + +#[test] +fn duplicate_candidates_and_unknown_fields_fail_closed() { + let mut duplicate = request(); + duplicate["candidates"][1]["id"] = json!("internal-contract-atom"); + assert!(assistance_discovery::discover(&duplicate).is_err()); + + let mut extra = request(); + extra["install"] = json!(true); + assert!(assistance_discovery::discover(&extra).is_err()); +} + +#[test] +fn core_has_no_install_network_or_authority_write_surface() { + let source = + fs::read_to_string(root().join("crates/code-intel-cli/src/assistance_discovery.rs")) + .unwrap(); + for forbidden in [ + "std::process::Command", + "std::net::", + "fs::write", + "authority::evaluate", + ] { + assert!( + !source.contains(forbidden), + "found forbidden surface: {forbidden}" + ); + } +} + +#[test] +fn schema_rating_enums_and_semantic_identity_match_runtime_policy() { + let dossier: Value = serde_json::from_slice( + &fs::read( + root().join("orchestration/schemas/code-intel-assistance-dossier.v1.schema.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!( + dossier["$defs"]["fit"]["properties"]["status"]["enum"], + json!(["strong", "partial", "weak", "unknown"]) + ); + assert_eq!( + dossier["$defs"]["security"]["properties"]["status"]["enum"], + json!(["acceptable", "review_required", "unacceptable", "unknown"]) + ); + let request_schema: Value = + serde_json::from_slice( + &fs::read(root().join( + "orchestration/schemas/code-intel-assistance-discovery-request.v1.schema.json", + )) + .unwrap(), + ) + .unwrap(); + assert_eq!( + request_schema["properties"]["candidates"]["uniqueItems"], + true + ); + assert_eq!( + request_schema["properties"]["candidates"]["x-code-intel-uniqueBy"], + "id" + ); +} diff --git a/crates/code-intel-cli/tests/authority_transition.rs b/crates/code-intel-cli/tests/authority_transition.rs new file mode 100644 index 0000000..553bbd1 --- /dev/null +++ b/crates/code-intel-cli/tests/authority_transition.rs @@ -0,0 +1,467 @@ +#[path = "../src/authority.rs"] +mod authority; + +use std::collections::BTreeSet; +use std::fs; +use std::path::PathBuf; + +use serde_json::{json, Value}; + +fn branch(id: &str, actor: &str, from: &str, to: &str, event: Option) -> Value { + let mut transition = json!({ + "to":to, + "outputId":format!("output-{id}"), + "evidenceIds":["evidence-1"] + }); + if let Some(event) = event { + transition["authorityEvent"] = event; + } + json!({ + "branchId":id, + "source":{"kind":actor,"id":format!("source-{id}")}, + "current":{"kind":from,"id":format!("current-{id}")}, + "transition":transition + }) +} + +fn approved_event(id: &str) -> Value { + json!({ + "schema":"code-intel-authority-event.v1", + "id":id, + "decision":"approved", + "approver":{"id":"eng-manager-1","role":"engineering_manager"}, + "evidenceIds":["evidence-1"], + "issuedAt":1_700_000_000u64, + "expiresAt":1_700_000_300u64 + }) +} + +fn repository_signed_event(id: &str) -> Value { + let mut event = approved_event(id); + event["approver"] = json!({"id":"code-intel-maintainers","role":"repository_governance"}); + let digest = authority::authority_event_digest(&event).unwrap(); + event["attestation"] = json!({ + "scheme":"repository-governed-sha256-v1", + "digest":digest + }); + event +} + +fn request(branches: Vec) -> Value { + json!({ + "schema":"code-intel-authority-transition-batch.v1", + "evaluatedAt":1_700_000_100u64, + "knownEvidenceIds":["evidence-1","evidence-2"], + "consumedAuthorityEventIds":[], + "branches":branches + }) +} + +fn evaluate(branches: Vec) -> Value { + authority::evaluate_batch(&request(branches)).unwrap() +} + +#[test] +fn every_declared_edge_is_enforced_and_approved_authority_edges_pass() { + let cases = [ + ("observed_evidence", "engineering_fact", None), + ("engineering_fact", "derived_engineering_model", None), + ("derived_engineering_model", "proposal", None), + ( + "proposal", + "adoption_decision", + Some(approved_event("event-adopt")), + ), + ( + "adoption_decision", + "committed_engineering_plan", + Some(approved_event("event-commit")), + ), + ( + "proposal", + "committed_engineering_plan", + Some(approved_event("event-direct-commit")), + ), + ]; + for (index, (from, to, event)) in cases.into_iter().enumerate() { + let actor = if event.is_some() { + "human" + } else { + "deterministic_pipeline" + }; + let result = evaluate(vec![branch( + &format!("edge-{index}"), + actor, + from, + to, + event, + )]); + assert_eq!( + result["branches"][0]["status"], "accepted", + "{from}->{to}: {result}" + ); + if result["branches"][0]["authorityEventId"].is_null() { + assert_eq!( + result["branches"][0]["effectiveAuthority"], + "deterministic_policy" + ); + } else { + assert_eq!( + result["branches"][0]["effectiveAuthority"], + "authority_event" + ); + } + } +} + +#[test] +fn all_kind_pairs_match_the_checked_policy_edge_table() { + let policy = authority::policy_document(); + let kinds = policy["artifactKinds"].as_array().unwrap(); + let edges = policy["edges"].as_array().unwrap(); + for from in kinds { + for to in kinds { + let declared = edges + .iter() + .find(|edge| edge["from"] == *from && edge["to"] == *to); + let event = declared + .and_then(|edge| edge["authorityEventRequired"].as_bool()) + .filter(|required| *required) + .map(|_| { + approved_event(&format!( + "event-{}-{}", + from.as_str().unwrap(), + to.as_str().unwrap() + )) + }); + let result = evaluate(vec![branch( + "matrix", + "deterministic_pipeline", + from.as_str().unwrap(), + to.as_str().unwrap(), + event, + )]); + assert_eq!( + result["branches"][0]["status"] == "accepted", + declared.is_some(), + "{} -> {}: {result}", + from, + to + ); + } + } +} + +#[test] +fn recommender_direct_commit_requires_explicit_approved_authority_event() { + let rejected = evaluate(vec![branch( + "reject", + "recommender", + "proposal", + "committed_engineering_plan", + None, + )]); + assert_eq!(rejected["branches"][0]["status"], "rejected"); + let accepted = evaluate(vec![branch( + "accept", + "recommender", + "proposal", + "committed_engineering_plan", + Some(approved_event("event-approved")), + )]); + assert_eq!(accepted["branches"][0]["status"], "accepted"); + assert_eq!( + accepted["branches"][0]["effectiveAuthority"], + "authority_event" + ); + assert_eq!( + accepted["branches"][0]["authorityEvent"]["approver"]["id"], + "eng-manager-1" + ); + assert_eq!( + accepted["consumedAuthorityEventIds"], + json!(["event-approved"]) + ); +} + +#[test] +fn llm_provider_and_recommender_cannot_create_facts_models_or_unapproved_commitments() { + for actor in ["llm", "provider", "recommender"] { + for to in ["engineering_fact", "derived_engineering_model"] { + let result = evaluate(vec![branch(actor, actor, "observed_evidence", to, None)]); + assert_eq!(result["branches"][0]["status"], "rejected", "{actor}->{to}"); + } + for (from, to) in [ + ("proposal", "adoption_decision"), + ("proposal", "committed_engineering_plan"), + ] { + let result = evaluate(vec![branch(actor, actor, from, to, None)]); + assert_eq!(result["branches"][0]["status"], "rejected", "{actor}->{to}"); + } + } +} + +#[test] +fn replay_missing_approver_unknown_evidence_expired_and_duplicate_event_fail_closed() { + let mut replay = request(vec![branch( + "replay", + "human", + "proposal", + "adoption_decision", + Some(approved_event("event-used")), + )]); + replay["consumedAuthorityEventIds"] = json!(["event-used"]); + assert_eq!( + authority::evaluate_batch(&replay).unwrap()["branches"][0]["status"], + "rejected" + ); + + let mut missing = approved_event("event-missing"); + missing["approver"]["id"] = json!(""); + assert_eq!( + evaluate(vec![branch( + "missing", + "human", + "proposal", + "adoption_decision", + Some(missing) + )])["branches"][0]["status"], + "rejected" + ); + + let mut unknown = branch( + "unknown", + "human", + "proposal", + "adoption_decision", + Some(approved_event("event-unknown")), + ); + unknown["transition"]["evidenceIds"] = json!(["not-known"]); + assert_eq!(evaluate(vec![unknown])["branches"][0]["status"], "rejected"); + + let mut expired = approved_event("event-expired"); + expired["expiresAt"] = json!(1_700_000_099u64); + assert_eq!( + evaluate(vec![branch( + "expired", + "human", + "proposal", + "adoption_decision", + Some(expired) + )])["branches"][0]["status"], + "rejected" + ); + + let duplicate = approved_event("event-duplicate"); + let result = evaluate(vec![ + branch( + "duplicate-a", + "human", + "proposal", + "adoption_decision", + Some(duplicate.clone()), + ), + branch( + "duplicate-b", + "human", + "proposal", + "adoption_decision", + Some(duplicate), + ), + ]); + assert!(result["branches"] + .as_array() + .unwrap() + .iter() + .all(|v| v["status"] == "rejected")); +} + +#[test] +fn v1_events_remain_compatible_and_repository_attestation_rejects_every_bound_tamper() { + let known = BTreeSet::from(["evidence-1".to_string(), "evidence-2".to_string()]); + let required = BTreeSet::from(["evidence-1".to_string()]); + let consumed = BTreeSet::new(); + + let legacy = approved_event("legacy-v1"); + assert!(authority::validate_authority_event( + &legacy, + 1_700_000_100, + &known, + &required, + &consumed + ) + .is_ok()); + assert!(authority::validate_signed_authority_event( + &legacy, + 1_700_000_100, + &known, + &required, + &consumed + ) + .unwrap_err() + .contains("attestation is required")); + + let signed = repository_signed_event("signed-v1"); + assert!(authority::validate_signed_authority_event( + &signed, + 1_700_000_100, + &known, + &required, + &consumed + ) + .is_ok()); + + for (label, mut tampered) in [ + ("actor", signed.clone()), + ("evidence", signed.clone()), + ("expiry", signed.clone()), + ("digest", signed.clone()), + ] { + match label { + "actor" => tampered["approver"]["id"] = json!("untrusted-maintainer"), + "evidence" => tampered["evidenceIds"] = json!(["evidence-1", "evidence-2"]), + "expiry" => tampered["expiresAt"] = json!(1_700_000_301u64), + "digest" => tampered["attestation"]["digest"] = json!("0".repeat(64)), + _ => unreachable!(), + } + assert!( + authority::validate_signed_authority_event( + &tampered, + 1_700_000_100, + &known, + &required, + &consumed, + ) + .is_err(), + "{label} tamper must fail" + ); + } +} + +#[test] +fn consumed_authority_events_survive_unprotected_batches_and_block_later_replay() { + let first = evaluate(vec![branch( + "protected", + "human", + "proposal", + "adoption_decision", + Some(approved_event("event-sequence")), + )]); + assert_eq!( + first["consumedAuthorityEventIds"], + json!(["event-sequence"]) + ); + + let mut unprotected = request(vec![branch( + "unprotected", + "deterministic_pipeline", + "observed_evidence", + "engineering_fact", + None, + )]); + unprotected["consumedAuthorityEventIds"] = first["consumedAuthorityEventIds"].clone(); + let second = authority::evaluate_batch(&unprotected).unwrap(); + assert_eq!(second["branches"][0]["status"], "accepted"); + assert_eq!( + second["consumedAuthorityEventIds"], + json!(["event-sequence"]), + "an unprotected batch must not erase replay history" + ); + + let mut replay = request(vec![branch( + "replay-after-gap", + "human", + "proposal", + "adoption_decision", + Some(approved_event("event-sequence")), + )]); + replay["consumedAuthorityEventIds"] = second["consumedAuthorityEventIds"].clone(); + let third = authority::evaluate_batch(&replay).unwrap(); + assert_eq!(third["branches"][0]["status"], "rejected"); + assert_eq!( + third["consumedAuthorityEventIds"], + json!(["event-sequence"]) + ); +} + +#[test] +fn rejected_transition_preserves_unrelated_analysis_branch() { + let result = evaluate(vec![ + branch( + "bad", + "recommender", + "proposal", + "committed_engineering_plan", + None, + ), + branch( + "good", + "deterministic_pipeline", + "observed_evidence", + "engineering_fact", + None, + ), + ]); + assert_eq!(result["branches"][0]["status"], "rejected"); + assert!(result["branches"][0]["outputId"].is_null()); + assert_eq!(result["branches"][1]["status"], "accepted"); + assert_eq!(result["branches"][1]["outputId"], "output-good"); +} + +#[test] +fn policy_has_no_product_priority_or_tool_specific_actor() { + let policy = authority::policy_document(); + let text = serde_json::to_string(&policy).unwrap().to_ascii_lowercase(); + for forbidden in [ + "priority", + "roadmap", + "repowise", + "codenexus", + "sentrux", + "openspec", + ] { + assert!( + !text.contains(forbidden), + "forbidden policy authority: {forbidden}" + ); + } + let actors = policy["actorKinds"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap()) + .collect::>(); + assert_eq!( + actors, + BTreeSet::from([ + "deterministic_pipeline", + "human", + "llm", + "provider", + "recommender" + ]) + ); +} + +#[test] +fn checked_policy_and_closed_schema_match_runtime_contract() { + let root = option_env!("CODE_INTEL_REPO_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")); + let checked: Value = serde_json::from_slice( + &fs::read(root.join("orchestration/authority-transition-policy.v1.json")).unwrap(), + ) + .unwrap(); + assert_eq!(checked, authority::policy_document()); + let schema: Value = serde_json::from_slice( + &fs::read( + root.join("orchestration/schemas/code-intel-authority-transition.v1.schema.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["$id"], "code-intel-authority-transition.v1"); + assert_eq!(schema["$defs"]["batch"]["additionalProperties"], false); + assert_eq!(schema["$defs"]["event"]["additionalProperties"], false); + assert_eq!(schema["$defs"]["result"]["additionalProperties"], false); +} diff --git a/crates/code-intel-cli/tests/capability_exec.rs b/crates/code-intel-cli/tests/capability_exec.rs new file mode 100644 index 0000000..48e2200 --- /dev/null +++ b/crates/code-intel-cli/tests/capability_exec.rs @@ -0,0 +1,1746 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const IMPLEMENTATION_DIGEST: &str = + "43ced9ef578e6484423468e059c93ef0bc5eeeb35d23271451b2d8f1a16f9bb6"; + +fn temp_dir(name: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + std::env::temp_dir().join(format!( + "code-intel-a01-{name}-{}-{nonce}", + std::process::id() + )) +} + +fn request(repo: &Path, capability: &str) -> Value { + request_with_policy_scopes(repo, capability, "explicit_overlay", &["."]) +} + +fn request_with_policy_scopes( + repo: &Path, + capability: &str, + policy: &str, + scopes: &[&str], +) -> Value { + let mut command = Command::new(env!("CARGO_BIN_EXE_code-intel")); + command + .args(["snapshot", "identity", "--repo"]) + .arg(repo) + .args(["--working-tree-policy", policy]); + for scope in scopes { + command.args(["--scope", scope]); + } + let snapshot_output = command.output().expect("compute A02 request snapshot"); + assert!( + snapshot_output.status.success(), + "snapshot stderr={}", + String::from_utf8_lossy(&snapshot_output.stderr) + ); + let snapshot_document: Value = + serde_json::from_slice(&snapshot_output.stdout).expect("snapshot JSON"); + json!({ + "schema": "code-intel-capability-request.v1", + "capability": capability, + "contractVersion": 1, + "implementation": { + "id": "inventory.rg.compat", + "version": "1.0.0", + "toolchainDigests": [IMPLEMENTATION_DIGEST] + }, + "snapshot": snapshot_document["snapshot"], + "options": { + "repoPath": repo + }, + "inputs": [], + "effectPolicy": { "allowedEffects": ["repo_read", "local_write"] } + }) +} + +fn run_with_request_file( + request: &Value, + request_path: &Path, + out: &Path, + cli_capability: &str, +) -> std::process::Output { + fs::write( + request_path, + serde_json::to_vec(request).expect("serialize request"), + ) + .expect("write request"); + Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", cli_capability, "--request"]) + .arg(request_path) + .arg("--out") + .arg(out) + .output() + .expect("run capability executor") +} + +fn base_command(out: &Path, cli_capability: &str) -> Command { + let mut command = Command::new(env!("CARGO_BIN_EXE_code-intel")); + command + .args(["capability", "exec", cli_capability, "--request"]) + .arg("-") + .arg("--out") + .arg(out); + command +} + +fn request_with_scopes(repo: &Path, capability: &str, scopes: &[&str]) -> Value { + request_with_policy_scopes(repo, capability, "explicit_overlay", scopes) +} + +fn git(repo: &Path, args: &[&str]) { + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output() + .expect("run git fixture command"); + assert!( + output.status.success(), + "git {args:?}: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +fn git_stdout(repo: &Path, args: &[&str]) -> String { + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output() + .expect("run git fixture command"); + assert!( + output.status.success(), + "git {args:?}: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout) + .expect("git fixture output is UTF-8") + .trim() + .to_string() +} + +fn create_file_symlink(target: &Path, link: &Path) -> bool { + #[cfg(windows)] + { + std::os::windows::fs::symlink_file(target, link).is_ok() + } + #[cfg(unix)] + { + std::os::unix::fs::symlink(target, link).is_ok() + } +} + +fn legacy_inventory(repo: &Path) -> Vec { + let excludes = [ + "!**/.git/**", + "!**/node_modules/**", + "!**/.repowise/**", + "!**/.understand-anything/**", + "!**/.sentrux/**", + "!**/target/**", + "!**/dist/**", + "!**/build/**", + "!**/.venv/**", + "!**/__pycache__/**", + ]; + let mut command = Command::new(if cfg!(windows) { "rg.exe" } else { "rg" }); + command.args(["--files", "--hidden"]); + for pattern in excludes { + command.args(["-g", pattern]); + } + let output = command + .arg(".") + .current_dir(repo) + .output() + .expect("run legacy rg inventory"); + assert!(output.status.success()); + let text = String::from_utf8(output.stdout).expect("legacy rg UTF-8"); + let mut files = text + .lines() + .map(str::trim_end) + .filter(|line| !line.is_empty()) + .map(|line| { + let normalized = line.replace('\\', "/"); + normalized + .strip_prefix("./") + .unwrap_or(&normalized) + .to_string() + }) + .collect::>(); + files.sort_unstable(); + files.dedup(); + let normalized = files.join("\n"); + if normalized.is_empty() { + Vec::new() + } else { + format!("{normalized}\n").into_bytes() + } +} + +#[test] +fn inventory_rg_exec_emits_one_result_and_stable_real_rg_artifact() { + let root = temp_dir("success"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("src")).expect("create fixture repo"); + fs::create_dir_all(repo.join("target")).expect("create excluded target"); + fs::create_dir_all(repo.join("node_modules")).expect("create excluded dependency"); + fs::write(repo.join("README.md"), "fixture\n").expect("write README"); + fs::write(repo.join("src").join("lib.rs"), "pub fn fixture() {}\n").expect("write source"); + fs::write(repo.join(".hidden"), "hidden\n").expect("write hidden file"); + fs::write(repo.join("space & quote' 文.rs"), "unicode\n").expect("write special path"); + fs::write(repo.join("target").join("ignored.rs"), "ignored\n").expect("write excluded target"); + fs::write(repo.join("node_modules").join("ignored.js"), "ignored\n") + .expect("write excluded dependency"); + let out = root.join("out"); + let artifact = out.join("files.txt"); + let request_path = root.join("request.json"); + + let request_value = request(&repo, "inventory.rg"); + let expected_snapshot = request_value["snapshot"]["identity"].clone(); + let output = run_with_request_file(&request_value, &request_path, &out, "inventory.rg"); + + assert_eq!( + output.status.code(), + Some(0), + "stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); + let mut stream = serde_json::Deserializer::from_str(&stdout).into_iter::(); + let result = stream + .next() + .expect("one result") + .expect("valid result JSON"); + assert!( + stream.next().is_none(), + "stdout must contain exactly one JSON document" + ); + assert_eq!(result["schema"], "code-intel-capability-result.v1"); + assert_eq!(result["capability"], "inventory.rg"); + assert_eq!(result["status"], "completed"); + assert_eq!(result["verdict"], "pass"); + assert_eq!(result["exitCode"], 0); + assert_eq!( + result["declaredEffects"], + json!(["repo_read", "local_write"]) + ); + assert_eq!( + result["observedEffects"], + json!(["repo_read", "local_write"]) + ); + assert_eq!( + result["artifacts"].as_array().expect("artifact refs").len(), + 1 + ); + assert_eq!( + result["artifacts"][0]["consumedSnapshotIdentity"], + expected_snapshot + ); + assert_eq!(result["artifacts"][0]["path"], "files.txt"); + assert!(result["artifacts"][0]["sha256"] + .as_str() + .is_some_and(|value| value.len() == 64)); + assert!( + output.stderr.is_empty(), + "success diagnostics belong in result, stderr={}", + String::from_utf8_lossy(&output.stderr) + ); + + let files = fs::read(&artifact).expect("inventory artifact"); + assert_eq!( + files, + legacy_inventory(&repo), + "new executor must preserve legacy rg inventory bytes" + ); + let files_text = String::from_utf8(files).expect("inventory UTF-8"); + assert!(files_text.contains(".hidden")); + assert!(files_text.contains("space & quote' 文.rs")); + assert!(!files_text.contains("target")); + assert!(!files_text.contains("node_modules")); + + let second_out = root.join("out-2"); + let second_artifact = second_out.join("files.txt"); + let second = run_with_request_file( + &request(&repo, "inventory.rg"), + &root.join("request-2.json"), + &second_out, + "inventory.rg", + ); + assert_eq!(second.status.code(), Some(0)); + assert_eq!( + fs::read(&artifact).unwrap(), + fs::read(&second_artifact).unwrap() + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn inventory_rg_ignores_repository_ignored_workspace_churn() { + let root = temp_dir("ignored-workspace-churn"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("work").join("nested")).unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Inventory Test"]); + git( + &repo, + &["config", "user.email", "inventory@example.invalid"], + ); + fs::write(repo.join(".gitignore"), "work/\n").unwrap(); + fs::write(repo.join("kept.txt"), "kept\n").unwrap(); + git(&repo, &["add", ".gitignore", "kept.txt"]); + git(&repo, &["commit", "--quiet", "-m", "baseline"]); + for index in 0..128 { + fs::write( + repo.join("work") + .join("nested") + .join(format!("generated-{index}.o")), + "generated\n", + ) + .unwrap(); + } + + let out = root.join("out"); + let output = run_with_request_file( + &request(&repo, "inventory.rg"), + &root.join("request.json"), + &out, + "inventory.rg", + ); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(files.lines().any(|path| path == ".gitignore")); + assert!(files.lines().any(|path| path == "kept.txt")); + assert!(!files.contains("generated-")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn inventory_rejects_repository_changes_after_snapshot_and_honors_snapshot_scope() { + let root = temp_dir("snapshot-lease"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::create_dir_all(repo.join("docs")).unwrap(); + fs::write(repo.join("src/lib.rs"), "one\n").unwrap(); + fs::write(repo.join("docs/readme.md"), "docs\n").unwrap(); + + let stale = request_with_scopes(&repo, "inventory.rg", &["src"]); + fs::write(repo.join("src/lib.rs"), "two\n").unwrap(); + let stale_out = root.join("stale-out"); + let rejected = run_with_request_file( + &stale, + &root.join("stale-request.json"), + &stale_out, + "inventory.rg", + ); + assert_eq!(rejected.status.code(), Some(65)); + assert!( + !stale_out.exists(), + "mismatched snapshot must publish nothing" + ); + let failure: Value = serde_json::from_slice(&rejected.stdout).unwrap(); + assert_eq!(failure["status"], "failed"); + assert_eq!(failure["exitCode"], 65); + + let current = request_with_scopes(&repo, "inventory.rg", &["src", "src/nested"]); + let current_out = root.join("current-out"); + let accepted = run_with_request_file( + ¤t, + &root.join("current-request.json"), + ¤t_out, + "inventory.rg", + ); + assert_eq!( + accepted.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&accepted.stderr) + ); + let inventory = String::from_utf8(fs::read(current_out.join("files.txt")).unwrap()).unwrap(); + assert!(inventory.contains("src")); + assert!(!inventory.contains("docs")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn head_only_inventory_rejects_untracked_paths_outside_the_head_manifest() { + let root = temp_dir("head-only-untracked"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Inventory Test"]); + git( + &repo, + &["config", "user.email", "inventory@example.invalid"], + ); + fs::write(repo.join("tracked.txt"), "tracked\n").unwrap(); + git(&repo, &["add", "tracked.txt"]); + git(&repo, &["commit", "--quiet", "-m", "fixture"]); + fs::write(repo.join("untracked.txt"), "not in HEAD\n").unwrap(); + + let value = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + let out = root.join("out"); + let output = run_with_request_file(&value, &root.join("request.json"), &out, "inventory.rg"); + + assert_eq!(output.status.code(), Some(65)); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["status"], "failed"); + assert_eq!(result["exitCode"], 65); + assert!(!out.exists(), "manifest mismatch must publish nothing"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn relocated_repositories_emit_identical_inventory_bytes_and_digest() { + let root = temp_dir("relocated-inventory"); + let left = root.join("left repo 空格"); + let right = root.join("另一个 path"); + for repo in [&left, &right] { + fs::create_dir_all(repo.join("src/子 目录")).unwrap(); + fs::write(repo.join("README.md"), "same\n").unwrap(); + fs::write(repo.join("src/子 目录/lib.rs"), "pub fn same() {}\n").unwrap(); + } + + let left_out = root.join("left-out"); + let right_out = root.join("right-out"); + let mut left_request = request(&left, "inventory.rg"); + left_request["options"]["inventoryExclude"] = json!(["!*.md"]); + let mut right_request = request(&right, "inventory.rg"); + right_request["options"]["inventoryExclude"] = json!(["!*.md"]); + let left_result = run_with_request_file( + &left_request, + &root.join("left-request.json"), + &left_out, + "inventory.rg", + ); + let right_result = run_with_request_file( + &right_request, + &root.join("right-request.json"), + &right_out, + "inventory.rg", + ); + assert_eq!(left_result.status.code(), Some(0)); + assert_eq!(right_result.status.code(), Some(0)); + assert_eq!( + fs::read(left_out.join("files.txt")).unwrap(), + fs::read(right_out.join("files.txt")).unwrap() + ); + assert!(!fs::read_to_string(left_out.join("files.txt")) + .unwrap() + .contains("README.md")); + let left_json: Value = serde_json::from_slice(&left_result.stdout).unwrap(); + let right_json: Value = serde_json::from_slice(&right_result.stdout).unwrap(); + assert_eq!( + left_json["artifacts"][0]["sha256"], + right_json["artifacts"][0]["sha256"] + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn inventory_rejects_simulated_rg_extra_path_without_publication() { + let root = temp_dir("rg-extra-path"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("kept.txt"), "kept\n").unwrap(); + let request_path = root.join("request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request(&repo, "inventory.rg")).unwrap(), + ) + .unwrap(); + let out = root.join("out"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .env("CODE_INTEL_TEST_RG_EXTRA_PATH", "transient/renamed.rs") + .output() + .unwrap(); + + assert_eq!(output.status.code(), Some(65)); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["exitCode"], 65); + assert!(!out.exists(), "set mismatch must publish nothing"); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn coherence_failure_exits_64_without_partial_artifact() { + let root = temp_dir("coherence"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).expect("create fixture repo"); + fs::write(repo.join("README.md"), "fixture\n").expect("write README"); + let out = root.join("out"); + let artifact = out.join("files.txt"); + + let output = run_with_request_file( + &request(&repo, "inventory.changed"), + &root.join("request.json"), + &out, + "inventory.rg", + ); + + assert_eq!(output.status.code(), Some(64)); + let result: Value = serde_json::from_slice(&output.stdout).expect("failure result JSON"); + assert_eq!(result["status"], "failed"); + assert_eq!(result["verdict"], "unknown"); + assert_eq!(result["exitCode"], 64); + assert!(result["artifacts"] + .as_array() + .expect("artifacts") + .is_empty()); + assert!( + !artifact.exists(), + "coherence failure must not leave a partial result artifact" + ); + assert!( + !output.stderr.is_empty(), + "human-readable diagnostic must use stderr" + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn stdin_rejects_more_than_one_request_with_one_failure_result() { + let root = temp_dir("stream"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).expect("create fixture repo"); + let out = root.join("out"); + let artifact = out.join("files.txt"); + let value = request(&repo, "inventory.rg"); + let mut child = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "inventory.rg", + "--request", + "-", + "--out", + ]) + .arg(&out) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn executor"); + { + use std::io::Write; + let stdin = child.stdin.as_mut().expect("stdin"); + write!(stdin, "{}\n{}", value, value).expect("write two requests"); + } + let output = child.wait_with_output().expect("wait executor"); + + assert_eq!(output.status.code(), Some(64)); + assert!( + output.stdout.is_empty(), + "multi-document input is a pre-envelope failure" + ); + assert!(!output.stderr.is_empty()); + assert!(!artifact.exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn pre_envelope_failures_have_typed_exit_stderr_and_empty_stdout() { + let root = temp_dir("pre-envelope"); + let missing = root.join("missing.json"); + let out = root.join("out"); + let unreadable = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&missing) + .arg("--out") + .arg(&out) + .output() + .expect("run unreadable request"); + assert_eq!(unreadable.status.code(), Some(74)); + assert!(unreadable.stdout.is_empty()); + assert!(!unreadable.stderr.is_empty()); + + let mut child = base_command(&out, "inventory.rg") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn malformed request"); + { + use std::io::Write; + child.stdin.as_mut().unwrap().write_all(b"{").unwrap(); + } + let malformed = child.wait_with_output().unwrap(); + assert_eq!(malformed.status.code(), Some(64)); + assert!(malformed.stdout.is_empty()); + assert!(!malformed.stderr.is_empty()); + assert!(!out.exists()); +} + +#[test] +fn stdin_accepts_exactly_one_request_and_keeps_stdout_pure() { + let root = temp_dir("stdin-one"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("文 & 'file.txt"), "fixture\n").unwrap(); + let out = root.join("out"); + let mut child = base_command(&out, "inventory.rg") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + { + use std::io::Write; + write!( + child.stdin.as_mut().unwrap(), + "{}", + request(&repo, "inventory.rg") + ) + .unwrap(); + } + let output = child.wait_with_output().unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.stderr.is_empty()); + let mut values = serde_json::Deserializer::from_slice(&output.stdout).into_iter::(); + assert_eq!(values.next().unwrap().unwrap()["exitCode"], 0); + assert!(values.next().is_none()); + assert_eq!( + fs::read(out.join("files.txt")).unwrap(), + legacy_inventory(&repo) + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn registry_drift_fails_closed_with_schema_shaped_result_and_no_output_tree() { + let root = temp_dir("registry-drift"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + let out = root.join("out"); + let request_path = root.join("request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request(&repo, "inventory.rg")).unwrap(), + ) + .unwrap(); + let registry_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json"); + let mut registry: Value = serde_json::from_slice(&fs::read(registry_path).unwrap()).unwrap(); + let declaration = registry["integrations"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry["id"] == "inventory.rg") + .unwrap(); + declaration["capabilityDeclaration"]["implementation"]["version"] = json!("9.9.9"); + let drift_path = root.join("integrations.json"); + fs::write(&drift_path, serde_json::to_vec(®istry).unwrap()).unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .arg("--manifest") + .arg(&drift_path) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["schema"], "code-intel-capability-result.v1"); + assert_eq!(result["status"], "failed"); + assert_eq!(result["verdict"], "unknown"); + assert_eq!(result["exitCode"], 64); + assert!(result["artifacts"].as_array().unwrap().is_empty()); + assert!(!out.exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn unavailable_rg_maps_to_69_without_partial_output() { + let root = temp_dir("rg-unavailable"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + let out = root.join("out"); + let request_path = root.join("request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request(&repo, "inventory.rg")).unwrap(), + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .env("PATH", "") + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(69)); + assert_eq!( + serde_json::from_slice::(&output.stdout).unwrap()["exitCode"], + 69 + ); + assert!(!out.exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn request_cannot_select_artifact_escape_and_out_must_be_unique() { + let root = temp_dir("write-boundary"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + let out = root.join("out"); + let mut escaping = request(&repo, "inventory.rg"); + escaping["options"]["artifactPath"] = json!("../escape.txt"); + let escaped = run_with_request_file( + &escaping, + &root.join("escape-request.json"), + &out, + "inventory.rg", + ); + assert_eq!(escaped.status.code(), Some(64)); + assert!(!out.exists()); + assert!(!root.join("escape.txt").exists()); + + fs::create_dir(&out).unwrap(); + fs::write(out.join("sentinel.txt"), "owned elsewhere").unwrap(); + let existing = run_with_request_file( + &request(&repo, "inventory.rg"), + &root.join("existing-request.json"), + &out, + "inventory.rg", + ); + assert_eq!(existing.status.code(), Some(74)); + assert_eq!( + fs::read_to_string(out.join("sentinel.txt")).unwrap(), + "owned elsewhere" + ); + assert!(!out.join("files.txt").exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn capability_parser_rejects_missing_unknown_duplicate_and_conflicting_arguments() { + let cases: Vec> = vec![ + vec!["capability", "exec", "inventory.rg", "--request"], + vec![ + "capability", + "exec", + "inventory.rg", + "--bogus", + "x", + "--request", + "x", + "--out", + "y", + ], + vec![ + "capability", + "exec", + "inventory.rg", + "--request", + "a", + "--request", + "b", + "--out", + "y", + ], + vec![ + "capability", + "exec", + "inventory.rg", + "--request", + "a", + "--out", + "x", + "--out", + "y", + ], + vec![ + "capability", + "exec", + "inventory.rg", + "--request", + "a", + "--out", + "y", + "--manifest", + "a", + "--manifest", + "b", + ], + vec![ + "capability", + "exec", + "inventory.rg", + "--request-file", + "a", + "--out", + "y", + ], + vec![ + "capability", + "exec", + "inventory.rg", + "--capability", + "other", + "--request", + "a", + "--out", + "y", + ], + ]; + for args in cases { + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(&args) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64), "args={args:?}"); + assert!(output.stdout.is_empty(), "args={args:?}"); + assert!(!output.stderr.is_empty(), "args={args:?}"); + } + let other_command = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["doctor", "--request", "x"]) + .output() + .unwrap(); + assert_eq!(other_command.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&other_command.stderr).contains("unknown argument for doctor")); +} + +#[test] +fn duplicate_json_keys_are_rejected_for_request_and_registry() { + let root = temp_dir("duplicate-json"); + fs::create_dir_all(&root).unwrap(); + let out = root.join("out"); + let request_path = root.join("request.json"); + fs::write(&request_path, "\u{feff}{\"schema\":\"code-intel-capability-request.v1\",\"schema\":\"code-intel-capability-request.v1\"}").unwrap(); + let request_output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .output() + .unwrap(); + assert_eq!(request_output.status.code(), Some(64)); + assert!(request_output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&request_output.stderr).contains("duplicate JSON object key")); + + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write( + &request_path, + serde_json::to_vec(&request(&repo, "inventory.rg")).unwrap(), + ) + .unwrap(); + let registry_path = root.join("registry.json"); + fs::write( + ®istry_path, + "\u{feff}{\"integrations\":[],\"integrations\":[]}", + ) + .unwrap(); + let registry_output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .arg("--manifest") + .arg(®istry_path) + .output() + .unwrap(); + assert_eq!(registry_output.status.code(), Some(65)); + let result: Value = serde_json::from_slice(®istry_output.stdout).unwrap(); + assert_eq!(result["exitCode"], 65); + assert!(String::from_utf8_lossy(®istry_output.stderr).contains("duplicate JSON object key")); + assert!(!out.exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn duplicate_registered_declarations_fail_closed() { + let root = temp_dir("duplicate-declaration"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + let request_path = root.join("request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request(&repo, "inventory.rg")).unwrap(), + ) + .unwrap(); + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json"); + let mut registry: Value = serde_json::from_slice(&fs::read(source).unwrap()).unwrap(); + let duplicate = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "inventory.rg") + .unwrap() + .clone(); + registry["integrations"] + .as_array_mut() + .unwrap() + .push(duplicate); + let registry_path = root.join("registry.json"); + fs::write(®istry_path, serde_json::to_vec(®istry).unwrap()).unwrap(); + let out = root.join("out"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .arg("--manifest") + .arg(®istry_path) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(65)); + assert!(String::from_utf8_lossy(&output.stderr) + .contains("duplicate registered capability declaration")); + assert!(!out.exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn empty_repository_is_a_successful_empty_inventory() { + let root = temp_dir("empty"); + let repo = root.join("empty repo & 文"); + fs::create_dir_all(&repo).unwrap(); + let out = root.join("out"); + let output = run_with_request_file( + &request(&repo, "inventory.rg"), + &root.join("request.json"), + &out, + "inventory.rg", + ); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(fs::read(out.join("files.txt")).unwrap(), Vec::::new()); + assert_eq!( + serde_json::from_slice::(&output.stdout).unwrap()["exitCode"], + 0 + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn inventory_exclude_is_applied_without_shell_interpretation() { + let root = temp_dir("custom-exclude"); + let repo = root.join("repo & 文"); + fs::create_dir_all(repo.join("custom excluded")).unwrap(); + fs::write(repo.join("keep.txt"), "keep").unwrap(); + fs::write(repo.join("custom excluded").join("drop.txt"), "drop").unwrap(); + let mut value = request(&repo, "inventory.rg"); + value["options"]["inventoryExclude"] = json!(["!**/custom excluded/**"]); + let out = root.join("out"); + let output = run_with_request_file(&value, &root.join("request.json"), &out, "inventory.rg"); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(files.contains("keep.txt")); + assert!(!files.contains("drop.txt")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn tracked_symlink_is_snapshot_bound_but_omitted_from_inventory_for_both_policies() { + let root = temp_dir("tracked-symlink"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Symlink Test"]); + git(&repo, &["config", "user.email", "symlink@example.invalid"]); + git(&repo, &["config", "core.symlinks", "true"]); + fs::write(repo.join("target-one.txt"), "one\n").unwrap(); + fs::write(repo.join("target-two.txt"), "two\n").unwrap(); + let link = repo.join("link.txt"); + if !create_file_symlink(Path::new("target-one.txt"), &link) { + let _ = fs::remove_dir_all(root); + return; + } + git(&repo, &["add", "."]); + git(&repo, &["commit", "--quiet", "-m", "tracked symlink"]); + + let real = Command::new(if cfg!(windows) { "rg.exe" } else { "rg" }) + .args(["--files", "--hidden", "--no-ignore"]) + .current_dir(&repo) + .output() + .expect("run real rg symlink fixture"); + assert!(real.status.success()); + assert!( + !String::from_utf8_lossy(&real.stdout) + .lines() + .any(|path| path == "link.txt"), + "ripgrep without -L must not enumerate a symlink" + ); + + let head_request = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + let explicit_before = + request_with_policy_scopes(&repo, "inventory.rg", "explicit_overlay", &["."]); + for (name, request) in [ + ("head", &head_request), + ("explicit-before", &explicit_before), + ] { + let out = root.join(format!("{name}-out")); + let output = run_with_request_file( + request, + &root.join(format!("{name}-request.json")), + &out, + "inventory.rg", + ); + assert_eq!( + output.status.code(), + Some(0), + "{name}: {}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(!files.lines().any(|path| path == "link.txt")); + assert!(files.lines().any(|path| path == "target-one.txt")); + assert!(files.lines().any(|path| path == "target-two.txt")); + } + + fs::remove_file(&link).unwrap(); + assert!(create_file_symlink(Path::new("target-two.txt"), &link)); + let explicit_after = + request_with_policy_scopes(&repo, "inventory.rg", "explicit_overlay", &["."]); + assert_ne!( + explicit_before["snapshot"]["identity"], explicit_after["snapshot"]["identity"], + "explicit snapshot identity must bind the symlink target bytes" + ); + let head_after = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + assert_eq!( + head_request["snapshot"]["identity"], head_after["snapshot"]["identity"], + "head-only identity must remain bound to the committed symlink target" + ); + let after_out = root.join("explicit-after-out"); + let after = run_with_request_file( + &explicit_after, + &root.join("explicit-after-request.json"), + &after_out, + "inventory.rg", + ); + assert_eq!( + after.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&after.stderr) + ); + let after_files = fs::read_to_string(after_out.join("files.txt")).unwrap(); + assert!(!after_files.lines().any(|path| path == "link.txt")); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn populated_gitlink_is_literal_excluded_and_oid_bound_for_both_policies() { + let root = temp_dir("populated-gitlink"); + let child = root.join("child"); + let repo = root.join("repo"); + fs::create_dir_all(&child).unwrap(); + fs::create_dir_all(&repo).unwrap(); + git(&child, &["init", "--quiet"]); + git(&child, &["config", "user.name", "Submodule Test"]); + git( + &child, + &["config", "user.email", "submodule@example.invalid"], + ); + fs::write(child.join("inside.txt"), "one\n").unwrap(); + git(&child, &["add", "."]); + git(&child, &["commit", "--quiet", "-m", "child one"]); + + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Superproject Test"]); + git(&repo, &["config", "user.email", "super@example.invalid"]); + fs::write(repo.join("root.txt"), "root\n").unwrap(); + let gitlink = "vendor/sub[glob]{x}!"; + git( + &repo, + &[ + "-c", + "protocol.file.allow=always", + "submodule", + "add", + child.to_str().unwrap(), + gitlink, + ], + ); + git(&repo, &["add", "."]); + git( + &repo, + &["commit", "--quiet", "-m", "add populated submodule"], + ); + + let real = Command::new(if cfg!(windows) { "rg.exe" } else { "rg" }) + .args(["--files", "--hidden", "--no-ignore"]) + .current_dir(&repo) + .output() + .expect("run real rg populated submodule fixture"); + assert!(real.status.success()); + assert!( + String::from_utf8_lossy(&real.stdout) + .lines() + .any(|path| path.replace('\\', "/").starts_with(gitlink)), + "fixture must prove that unfiltered real rg descends into the populated gitlink" + ); + + let head_before = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + let explicit_before = + request_with_policy_scopes(&repo, "inventory.rg", "explicit_overlay", &["."]); + for (name, request) in [ + ("gitlink-head-before", &head_before), + ("gitlink-explicit-before", &explicit_before), + ] { + let out = root.join(format!("{name}-out")); + let output = run_with_request_file( + request, + &root.join(format!("{name}-request.json")), + &out, + "inventory.rg", + ); + assert_eq!( + output.status.code(), + Some(0), + "{name}: {}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(files.lines().any(|path| path == ".gitmodules")); + assert!(files.lines().any(|path| path == "root.txt")); + assert!(!files.lines().any(|path| path.starts_with(gitlink))); + } + + fs::write(child.join("inside.txt"), "two\n").unwrap(); + git(&child, &["add", "inside.txt"]); + git(&child, &["commit", "--quiet", "-m", "child two"]); + let second_oid = git_stdout(&child, &["rev-parse", "HEAD"]); + let populated = repo.join(gitlink); + git(&populated, &["fetch", "--quiet", "origin"]); + git(&populated, &["checkout", "--quiet", &second_oid]); + git(&repo, &["add", gitlink]); + + let explicit_after = + request_with_policy_scopes(&repo, "inventory.rg", "explicit_overlay", &["."]); + assert_ne!( + explicit_before["snapshot"]["identity"], explicit_after["snapshot"]["identity"], + "staging a different gitlink OID must change explicit snapshot identity" + ); + let staged_head = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + assert_eq!( + head_before["snapshot"]["identity"], staged_head["snapshot"]["identity"], + "head-only identity must ignore an uncommitted gitlink update" + ); + let explicit_out = root.join("gitlink-explicit-after-out"); + let explicit_output = run_with_request_file( + &explicit_after, + &root.join("gitlink-explicit-after-request.json"), + &explicit_out, + "inventory.rg", + ); + assert_eq!( + explicit_output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&explicit_output.stderr) + ); + assert!(!fs::read_to_string(explicit_out.join("files.txt")) + .unwrap() + .lines() + .any(|path| path.starts_with(gitlink))); + + git(&repo, &["commit", "--quiet", "-m", "advance gitlink"]); + let head_after = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + assert_ne!( + head_before["snapshot"]["identity"], head_after["snapshot"]["identity"], + "committing a different gitlink OID must change head-only snapshot identity" + ); + let head_out = root.join("gitlink-head-after-out"); + let head_output = run_with_request_file( + &head_after, + &root.join("gitlink-head-after-request.json"), + &head_out, + "inventory.rg", + ); + assert_eq!( + head_output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&head_output.stderr) + ); + assert!(!fs::read_to_string(head_out.join("files.txt")) + .unwrap() + .lines() + .any(|path| path.starts_with(gitlink))); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn inventory_exclude_uses_ripgrep_glob_semantics_for_basename_brace_class_and_segment() { + let root = temp_dir("ripgrep-glob-semantics"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("nested/generated")).unwrap(); + fs::create_dir_all(repo.join("nested/kept")).unwrap(); + for (path, content) in [ + ("README.md", "markdown"), + ("nested/guide.md", "nested markdown"), + ("main.rs", "rust"), + ("notes.txt", "text"), + ("alpha.classcase", "class a"), + ("beta.classcase", "class b"), + ("charlie.classcase", "not class"), + ("nested/generated/data.json", "segment"), + ("nested/kept/data.json", "kept"), + ] { + fs::write(repo.join(path), content).unwrap(); + } + let mut value = request(&repo, "inventory.rg"); + value["options"]["inventoryExclude"] = json!([ + "!*.md", + "!*.{rs,txt}", + "![ab]*.classcase", + "!**/generated/**" + ]); + let out = root.join("out"); + let output = run_with_request_file(&value, &root.join("request.json"), &out, "inventory.rg"); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + normalized_lines(&fs::read_to_string(out.join("files.txt")).unwrap()), + vec![ + "charlie.classcase".to_string(), + "nested/kept/data.json".to_string() + ] + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn snapshot_ignore_control_bytes_drive_root_and_nested_ripgrep_semantics() { + let root = temp_dir("snapshot-ignore-controls"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("git-rule")).unwrap(); + fs::create_dir_all(repo.join("ignore-rule")).unwrap(); + fs::create_dir_all(repo.join("rgignore-rule")).unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Ignore Test"]); + git(&repo, &["config", "user.email", "ignore@example.invalid"]); + fs::write(repo.join("tracked.foo"), "tracked first\n").unwrap(); + fs::write(repo.join("kept.foo"), "kept\n").unwrap(); + fs::write(repo.join(".gitignore"), "tracked.foo\n").unwrap(); + fs::write(repo.join("git-rule/.gitignore"), "drop.foo\n").unwrap(); + fs::write(repo.join("git-rule/drop.foo"), "drop\n").unwrap(); + fs::write(repo.join("git-rule/keep.foo"), "keep\n").unwrap(); + fs::write(repo.join("ignore-rule/.ignore"), "drop.foo\n").unwrap(); + fs::write(repo.join("ignore-rule/drop.foo"), "drop\n").unwrap(); + fs::write(repo.join("ignore-rule/keep.foo"), "keep\n").unwrap(); + fs::write(repo.join("rgignore-rule/.rgignore"), "drop.foo\n").unwrap(); + fs::write(repo.join("rgignore-rule/drop.foo"), "drop\n").unwrap(); + fs::write(repo.join("rgignore-rule/keep.foo"), "keep\n").unwrap(); + git(&repo, &["add", "-f", "."]); + git(&repo, &["commit", "--quiet", "-m", "ignore controls"]); + + let out = root.join("out"); + let output = run_with_request_file( + &request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]), + &root.join("request.json"), + &out, + "inventory.rg", + ); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(files.contains("kept.foo")); + assert!(files.contains("git-rule/keep.foo")); + assert!(files.contains("ignore-rule/keep.foo")); + assert!(files.contains("rgignore-rule/keep.foo")); + assert!(!files.contains("tracked.foo")); + assert!(!files.contains("git-rule/drop.foo")); + assert!(!files.contains("ignore-rule/drop.foo")); + assert!(!files.contains("rgignore-rule/drop.foo")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn head_only_inventory_uses_frozen_ignore_bytes_for_same_and_different_worktree_semantics() { + let root = temp_dir("frozen-head-ignore"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Ignore Test"]); + git(&repo, &["config", "user.email", "ignore@example.invalid"]); + fs::write(repo.join("hidden.txt"), "hidden\n").unwrap(); + fs::write(repo.join("other.txt"), "other\n").unwrap(); + fs::write(repo.join(".gitignore"), "hidden.txt\n").unwrap(); + git(&repo, &["add", "-f", "."]); + git(&repo, &["commit", "--quiet", "-m", "ignore control"]); + let value = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["."]); + fs::write(repo.join(".gitignore"), "hidden.txt\n# same semantics\n").unwrap(); + let same_out = root.join("same-out"); + let same = run_with_request_file( + &value, + &root.join("same-request.json"), + &same_out, + "inventory.rg", + ); + assert_eq!( + same.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&same.stderr) + ); + let same_bytes = fs::read(same_out.join("files.txt")).unwrap(); + let same_files = String::from_utf8(same_bytes.clone()).unwrap(); + assert!(!same_files.contains("hidden.txt")); + assert!(same_files.contains("other.txt")); + + fs::write(repo.join(".gitignore"), "other.txt\n").unwrap(); + let different_out = root.join("different-out"); + let different = run_with_request_file( + &value, + &root.join("different-request.json"), + &different_out, + "inventory.rg", + ); + assert_eq!( + different.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&different.stderr) + ); + assert_eq!( + same_bytes, + fs::read(different_out.join("files.txt")).unwrap(), + "head_only artifact must depend on HEAD ignore bytes, not live worktree bytes" + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn explicit_overlay_rejects_same_semantics_ignore_byte_change_as_stale() { + let root = temp_dir("stale-overlay-ignore"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("hidden.txt"), "hidden\n").unwrap(); + fs::write(repo.join(".gitignore"), "hidden.txt\n").unwrap(); + let value = request(&repo, "inventory.rg"); + fs::write(repo.join(".gitignore"), "hidden.txt\n# same semantics\n").unwrap(); + let out = root.join("out"); + let output = run_with_request_file(&value, &root.join("request.json"), &out, "inventory.rg"); + assert_eq!(output.status.code(), Some(65)); + assert!(!out.exists()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn scoped_inventory_disables_ancestor_ignore_and_binds_nested_controls() { + let root = temp_dir("scoped-ancestor-ignore"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("src/nested")).unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Ignore Test"]); + git(&repo, &["config", "user.email", "ignore@example.invalid"]); + fs::write(repo.join(".gitignore"), "src/drop.foo\n").unwrap(); + fs::write(repo.join("src/drop.foo"), "drop\n").unwrap(); + fs::write(repo.join("src/keep.foo"), "keep\n").unwrap(); + fs::write(repo.join("src/nested/.ignore"), "nested-drop.foo\n").unwrap(); + fs::write(repo.join("src/nested/nested-drop.foo"), "drop\n").unwrap(); + fs::write(repo.join("src/nested/nested-keep.foo"), "keep\n").unwrap(); + git(&repo, &["add", "-f", "."]); + git( + &repo, + &["commit", "--quiet", "-m", "scoped ignore controls"], + ); + let value = request_with_policy_scopes(&repo, "inventory.rg", "head_only", &["src"]); + let out = root.join("out"); + let output = run_with_request_file(&value, &root.join("request.json"), &out, "inventory.rg"); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(files.contains("src/keep.foo")); + assert!(files.contains("src/nested/nested-keep.foo")); + assert!( + files.contains("src/drop.foo"), + "--no-ignore-parent excludes the root control from a src-scoped traversal" + ); + assert!(!files.contains("src/nested/nested-drop.foo")); + assert!( + !files.contains(".gitignore"), + "ancestor control is input, not scoped output" + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn inventory_disables_info_parent_and_ripgrep_config_ignore_sources() { + let root = temp_dir("external-ignore-sources"); + let repo = root.join("parent").join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(root.join("parent/.ignore"), "parent-hidden.txt\n").unwrap(); + git(&repo, &["init", "--quiet"]); + git(&repo, &["config", "user.name", "Ignore Test"]); + git(&repo, &["config", "user.email", "ignore@example.invalid"]); + fs::write(repo.join("tracked.txt"), "tracked\n").unwrap(); + git(&repo, &["add", "tracked.txt"]); + git(&repo, &["commit", "--quiet", "-m", "fixture"]); + fs::write(repo.join("info-hidden.txt"), "info\n").unwrap(); + fs::write(repo.join("parent-hidden.txt"), "parent\n").unwrap(); + fs::write(repo.join("config-hidden.txt"), "config\n").unwrap(); + fs::write(repo.join(".git/info/exclude"), "info-hidden.txt\n").unwrap(); + let config = root.join("ripgrep.conf"); + fs::write(&config, "--glob=!config-hidden.txt\n").unwrap(); + let value = request(&repo, "inventory.rg"); + let request_path = root.join("request.json"); + fs::write(&request_path, serde_json::to_vec(&value).unwrap()).unwrap(); + let out = root.join("out"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .env("RIPGREP_CONFIG_PATH", &config) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let files = fs::read_to_string(out.join("files.txt")).unwrap(); + assert!(files.contains("info-hidden.txt")); + assert!(files.contains("parent-hidden.txt")); + assert!(files.contains("config-hidden.txt")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn normalized_inventory_matches_real_legacy_runner_with_custom_exclude() { + let root = temp_dir("real-legacy"); + let repo = root.join("repo & 文"); + let artifacts = root.join("legacy artifacts"); + fs::create_dir_all(repo.join("custom excluded")).unwrap(); + fs::create_dir_all(repo.join("target")).unwrap(); + fs::write(repo.join("keep.txt"), "keep").unwrap(); + fs::write(repo.join(".hidden"), "hidden").unwrap(); + fs::write(repo.join("custom excluded").join("drop.txt"), "drop").unwrap(); + fs::write(repo.join("target").join("built.txt"), "built").unwrap(); + let runner = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("run-code-intel.ps1"); + let legacy = Command::new("pwsh") + .args(["-NoProfile", "-File"]) + .arg(&runner) + .arg("-RepoPath") + .arg(&repo) + .arg("-Mode") + .arg("lite") + .arg("-ArtifactRoot") + .arg(&artifacts) + .arg("-InventoryExclude") + .arg("!**/custom excluded/**") + .output() + .unwrap(); + assert!( + legacy.status.success(), + "legacy stderr={} stdout={}", + String::from_utf8_lossy(&legacy.stderr), + String::from_utf8_lossy(&legacy.stdout) + ); + let legacy_files = find_named_file(&artifacts, "files.txt").expect("legacy runner files.txt"); + + let mut value = request(&repo, "inventory.rg"); + value["options"]["inventoryExclude"] = json!(["!**/custom excluded/**"]); + let out = root.join("new staging"); + let new = run_with_request_file(&value, &root.join("request.json"), &out, "inventory.rg"); + assert_eq!( + new.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&new.stderr) + ); + assert_eq!( + normalized_repo_lines(&fs::read_to_string(legacy_files).unwrap(), &repo), + normalized_lines(&fs::read_to_string(out.join("files.txt")).unwrap()) + ); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn advisory_workflow_recommend_runs_through_a01_with_zero_effects_and_facade_parity() { + let root = temp_dir("workflow-recommend"); + let repo = root.join("repo"); + fs::create_dir_all(repo.join("openspec")).unwrap(); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::write(repo.join("src/main.ps1"), "'ok'\n").unwrap(); + + let mut value = request(&repo, "advisory.workflow-recommend"); + value["implementation"] = json!({ + "id":"advisory.workflow-recommend.compat", + "version":"1.0.0", + "toolchainDigests":[ + "03d9cbed70d83c59f7d9540fccc606ce0b2723135efd2c5e32943d367008a199", + "748c8b087c9d1a68f9aa5711cda200204ac0d05845058a1ee50058b161582de9" + ] + }); + value["options"] = json!({"repoPath":repo,"auto":true}); + value["effectPolicy"]["allowedEffects"] = json!([]); + let out = root.join("out"); + let output = run_with_request_file( + &value, + &root.join("request.json"), + &out, + "advisory.workflow-recommend", + ); + assert_eq!( + output.status.code(), + Some(0), + "stderr={} stdout={}", + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + ); + assert!( + output.stderr.is_empty(), + "successful envelope stderr must be empty" + ); + let envelope: Value = serde_json::from_slice(&output.stdout) + .expect("stdout must contain exactly one capability result JSON document"); + assert_eq!(envelope["capability"], "advisory.workflow-recommend"); + assert_eq!(envelope["declaredEffects"], json!([])); + assert_eq!(envelope["observedEffects"], json!([])); + assert_eq!(envelope["artifacts"].as_array().unwrap().len(), 1); + let envelope_proposal: Value = + serde_json::from_slice(&fs::read(out.join("workflow-recommendation.json")).unwrap()) + .unwrap(); + assert_eq!(envelope_proposal["effects"], json!([])); + assert_eq!(envelope_proposal["kind"], "proposal"); + + let facade = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("Invoke-WorkflowRecommendation.ps1"); + let direct = Command::new("pwsh") + .args(["-NoLogo", "-NoProfile", "-File"]) + .arg(facade) + .arg("-RepoPath") + .arg(&repo) + .args(["-Auto", "-Quiet", "-Json"]) + .output() + .unwrap(); + assert!( + direct.status.success(), + "{}", + String::from_utf8_lossy(&direct.stderr) + ); + let direct_proposal: Value = + serde_json::from_slice(&direct.stdout).expect("facade JSON mode must keep stdout pure"); + assert_eq!(envelope_proposal, direct_proposal); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn excessive_json_nesting_is_a_pre_envelope_usage_failure() { + let root = temp_dir("deep-json"); + fs::create_dir_all(&root).unwrap(); + let request_path = root.join("request.json"); + fs::write( + &request_path, + format!("{}0{}", "[".repeat(512), "]".repeat(512)), + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(root.join("out")) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("nesting")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn oversized_request_is_bounded_and_rejected_before_envelope() { + let root = temp_dir("oversized-json"); + fs::create_dir_all(&root).unwrap(); + let request_path = root.join("request.json"); + fs::write(&request_path, vec![b' '; 8 * 1024 * 1024 + 1]).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(root.join("out")) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("exceeds")); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn cwd_manifest_shadow_is_ignored() { + let root = temp_dir("manifest-shadow"); + let repo = root.join("repo"); + let cwd = root.join("untrusted"); + fs::create_dir_all(cwd.join("orchestration")).unwrap(); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("kept.txt"), "kept").unwrap(); + fs::write( + cwd.join("orchestration").join("integrations.json"), + r#"{"integrations":[]}"#, + ) + .unwrap(); + let request_path = root.join("request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request(&repo, "inventory.rg")).unwrap(), + ) + .unwrap(); + let out = root.join("out"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .current_dir(&cwd) + .env_remove("CODE_INTEL_HOME") + .env_remove("CODE_INTEL_INTEGRATIONS_MANIFEST") + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(out.join("files.txt").is_file()); + let _ = fs::remove_dir_all(root); +} + +#[test] +fn declaration_determinism_is_used_for_post_declaration_failures() { + let root = temp_dir("declaration-determinism"); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + let mut value = request(&repo, "inventory.rg"); + value["options"]["unsupported"] = json!(true); + let request_path = root.join("request.json"); + fs::write(&request_path, serde_json::to_vec(&value).unwrap()).unwrap(); + + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json"); + let mut registry: Value = serde_json::from_slice(&fs::read(source).unwrap()).unwrap(); + let declaration = registry["integrations"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry["id"] == "inventory.rg") + .unwrap() + .get_mut("capabilityDeclaration") + .unwrap(); + declaration["determinism"] = json!("external_nondeterministic"); + let registry_path = root.join("registry.json"); + fs::write(®istry_path, serde_json::to_vec(®istry).unwrap()).unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "inventory.rg", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(root.join("out")) + .arg("--manifest") + .arg(®istry_path) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["determinism"], "external_nondeterministic"); + assert_eq!(result["exitCode"], 64); + let _ = fs::remove_dir_all(root); +} + +fn normalized_lines(text: &str) -> Vec { + let mut lines = text + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect::>(); + lines.sort(); + lines.dedup(); + lines +} + +fn normalized_repo_lines(text: &str, repo: &Path) -> Vec { + let prefix = format!("{}/", repo.to_string_lossy().replace('\\', "/")); + let relative = text + .lines() + .map(|line| line.trim().replace('\\', "/")) + .map(|line| line.strip_prefix(&prefix).unwrap_or(&line).to_string()) + .collect::>() + .join("\n"); + normalized_lines(&relative) +} + +fn find_named_file(root: &Path, name: &str) -> Option { + let mut stack = vec![root.to_path_buf()]; + while let Some(dir) = stack.pop() { + for entry in fs::read_dir(dir).ok()? { + let entry = entry.ok()?; + let path = entry.path(); + if path.is_dir() { + stack.push(path) + } else if path.file_name().and_then(|v| v.to_str()) == Some(name) { + return Some(path); + } + } + } + None +} diff --git a/crates/code-intel-cli/tests/codenexus_adapter.rs b/crates/code-intel-cli/tests/codenexus_adapter.rs new file mode 100644 index 0000000..f457ab5 --- /dev/null +++ b/crates/code-intel-cli/tests/codenexus_adapter.rs @@ -0,0 +1,493 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/admissibility.rs"] +mod admissibility; +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/codenexus_adapter.rs"] +mod codenexus_adapter; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +const CURRENT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const WRONG: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const IMPLEMENTATION_DIGEST: &str = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +static SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-b04-{}-{nonce}-{}", + std::process::id(), + SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn descriptor(name: &str) -> Value { + let text = match name { + "full-current" => include_str!("fixtures/codenexus-adapter/full-current.json"), + "lite-current" => include_str!("fixtures/codenexus-adapter/lite-current.json"), + "unavailable" => include_str!("fixtures/codenexus-adapter/unavailable.json"), + _ => panic!("unknown fixture {name}"), + }; + serde_json::from_str(text).unwrap() +} + +fn build_case( + root: &Path, + fixture: &Value, + expected_snapshot: &str, + source_snapshot: &str, + observed_at: u64, +) -> Value { + let mode = fixture["providerMode"].as_str().unwrap(); + let implementation_id = fixture["implementationId"].as_str().unwrap(); + let completeness = if fixture["status"] == "current" { + "complete" + } else { + "partial" + }; + let availability = if fixture["status"] == "unavailable" { + "provider_unavailable" + } else { + "available" + }; + let provider_data = if fixture["status"] == "unavailable" { + Value::Null + } else if mode == "full" { + json!({"opaqueFullResult":{"queryId":"q-7","impactRelationships":[{"providerEdge":"opaque"}]}}) + } else { + json!({"opaqueLiteContext":{"files":[{"path":"src/lib.rs","reason":"hotspot"}]}}) + }; + let payload = json!({ + "schema":"code-intel-evidence-payload.v1", + "data":{ + "codenexus":{ + "schema":"code-intel-codenexus-evidence.v1", + "snapshotIdentity":source_snapshot, + "provider":{ + "mode":mode, + "providerId":fixture["providerId"], + "implementationId":implementation_id, + "activation":fixture["activation"] + }, + "provenance":{ + "sourceRevision":fixture["sourceRevision"], + "observedAt":observed_at + }, + "completeness":completeness, + "availability":availability, + "providerData":provider_data + } + } + }); + let bytes = serde_json::to_vec(&payload).unwrap(); + fs::write(root.join("payload.json"), &bytes).unwrap(); + json!({ + "schema":"code-intel-codenexus-native-result.v1", + "providerMode":mode, + "status":fixture["status"], + "providerId":fixture["providerId"], + "implementation":{ + "id":implementation_id, + "version":"1.0.0", + "digest":IMPLEMENTATION_DIGEST + }, + "sourceRevision":fixture["sourceRevision"], + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":source_snapshot, + "collectedAt":observed_at - 1, + "observedAt":observed_at, + "payload":{ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-evidence-payload.v1", + "type":"observed.evidence.payload", + "path":"payload.json", + "sha256":capability::sha256_hex(&bytes), + "consumedSnapshotIdentity":source_snapshot + }, + "activation":fixture["activation"], + "effects":fixture["effects"] + }) +} + +fn keys(value: &Value) -> BTreeSet<&str> { + value + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect() +} + +fn route(root: &Path, native: &Value) -> (i32, Vec, String) { + let request = root.join("native.json"); + fs::write(&request, serde_json::to_vec(native).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "provider", + "codenexus-adapt", + "--request", + request.to_str().unwrap(), + "--artifact-root", + root.to_str().unwrap(), + "--evaluated-at", + "2000", + "--max-age-seconds", + "100", + ]) + .output() + .unwrap(); + ( + output.status.code().unwrap(), + output.stdout, + String::from_utf8_lossy(&output.stderr).into_owned(), + ) +} + +#[test] +fn full_and_lite_share_one_port_shape_but_keep_distinct_provenance() { + let full_root = Temp::new(); + let lite_root = Temp::new(); + let full_native = build_case( + &full_root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_950, + ); + let lite_native = build_case( + &lite_root.0, + &descriptor("lite-current"), + CURRENT, + CURRENT, + 1_950, + ); + let full = codenexus_adapter::translate(&full_native, 2_000, 100).unwrap(); + let lite = codenexus_adapter::translate(&lite_native, 2_000, 100).unwrap(); + + assert_eq!(keys(&full["port"]), keys(&lite["port"])); + assert_eq!( + keys(&full["port"]["provider"]), + keys(&lite["port"]["provider"]) + ); + assert_eq!(full["port"]["schema"], "code-intel-codenexus-port.v1"); + assert_ne!( + full["port"]["provider"]["providerId"], + lite["port"]["provider"]["providerId"] + ); + assert_ne!( + full["port"]["provider"]["implementationId"], + lite["port"]["provider"]["implementationId"] + ); + assert_eq!(full["port"]["boundary"]["storageOwnership"], "provider"); + assert_eq!(lite["port"]["boundary"]["storageOwnership"], "provider"); + + for (root, adapter) in [(&full_root.0, &full), (&lite_root.0, &lite)] { + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], root).unwrap(); + assert_eq!(admitted.result()["domainVerdict"], "observed"); + codenexus_adapter::validate_admitted_payload(admitted.payload(), adapter).unwrap(); + } +} + +#[test] +fn unavailable_is_partial_provider_unavailable_and_never_fabricates_facts() { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor("unavailable"), CURRENT, CURRENT, 1_950); + let adapter = codenexus_adapter::translate(&native, 2_000, 100).unwrap(); + assert_eq!(adapter["port"]["status"], "unavailable"); + assert_eq!(adapter["port"]["completeness"], "partial"); + assert_eq!(adapter["port"]["failureKind"], "provider_unavailable"); + assert_eq!(adapter["port"]["perceptionUsable"], false); + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + assert_eq!(admitted.result()["domainVerdict"], "unknown"); + assert_eq!(admitted.result()["engineeringFacts"], json!([])); + assert_eq!( + admitted.payload()["data"]["codenexus"]["providerData"], + Value::Null + ); +} + +#[test] +fn snapshot_mismatch_and_stale_observation_fail_closed_in_a04() { + let wrong_root = Temp::new(); + let wrong_native = build_case( + &wrong_root.0, + &descriptor("full-current"), + CURRENT, + WRONG, + 1_950, + ); + let wrong = codenexus_adapter::translate(&wrong_native, 2_000, 100).unwrap(); + assert_eq!(wrong["port"]["freshness"], "snapshot_mismatch"); + let wrong_error = + match admissibility::validate_for_consumer(&wrong["evidence"]["request"], &wrong_root.0) { + Ok(_) => panic!("wrong-snapshot evidence was admitted"), + Err(error) => error, + }; + assert!(wrong_error.contains("consumed snapshot mismatch")); + + let stale_root = Temp::new(); + let stale_native = build_case( + &stale_root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_800, + ); + let stale = codenexus_adapter::translate(&stale_native, 2_000, 100).unwrap(); + assert_eq!(stale["port"]["freshness"], "stale"); + let stale_error = + match admissibility::validate_for_consumer(&stale["evidence"]["request"], &stale_root.0) { + Ok(_) => panic!("stale evidence was admitted"), + Err(error) => error, + }; + assert!(stale_error.contains("stale")); +} + +#[test] +fn lite_is_only_explicit_fallback_or_rollback_and_full_is_primary() { + let root = Temp::new(); + let mut lite = build_case( + &root.0, + &descriptor("lite-current"), + CURRENT, + CURRENT, + 1_950, + ); + lite["activation"] = json!("automatic_primary"); + assert!(codenexus_adapter::translate(&lite, 2_000, 100) + .unwrap_err() + .contains("explicit fallback or legacy rollback")); + + let mut full = build_case( + &root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_950, + ); + full["activation"] = json!("explicit_fallback"); + assert!(codenexus_adapter::translate(&full, 2_000, 100) + .unwrap_err() + .contains("full provider must be primary")); + + let mut relabelled_lite = build_case( + &root.0, + &descriptor("lite-current"), + CURRENT, + CURRENT, + 1_950, + ); + relabelled_lite["providerId"] = json!("codenexus.full"); + assert!(codenexus_adapter::translate(&relabelled_lite, 2_000, 100) + .unwrap_err() + .contains("lite compatibility identity")); +} + +#[test] +fn adapter_declares_effects_rejects_storage_coupling_and_drops_unknown_secrets() { + let root = Temp::new(); + let mut full = build_case( + &root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_950, + ); + full["databasePath"] = json!("C:/provider/private.db"); + full["apiToken"] = json!("B04-SECRET-SENTINEL"); + let error = codenexus_adapter::translate(&full, 2_000, 100).unwrap_err(); + assert!(error.contains("fields are invalid")); + assert!(!error.contains("B04-SECRET-SENTINEL")); + + let native = build_case( + &root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_950, + ); + let adapter = codenexus_adapter::translate(&native, 2_000, 100).unwrap(); + assert_eq!(adapter["port"]["boundary"]["transport"], "artifact_ref"); + assert!(adapter["port"]["effects"] + .as_array() + .unwrap() + .iter() + .any(|effect| effect == "network_provider")); + let rendered = serde_json::to_string(&adapter).unwrap(); + assert!(!rendered.contains("private.db")); + assert!(!rendered.contains("token")); +} + +#[test] +fn admitted_payload_identity_cannot_be_relabelled() { + let root = Temp::new(); + let native = build_case( + &root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_950, + ); + let adapter = codenexus_adapter::translate(&native, 2_000, 100).unwrap(); + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + let mut relabelled = admitted.payload().clone(); + relabelled["data"]["codenexus"]["provider"]["implementationId"] = + json!("pipeline.fake-impact-engine"); + assert!( + codenexus_adapter::validate_admitted_payload(&relabelled, &adapter) + .unwrap_err() + .contains("provider identity mismatch") + ); +} + +#[test] +fn port_schema_and_boundary_document_are_closed_and_real() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let schema: Value = serde_json::from_slice( + &fs::read(root.join("orchestration/schemas/code-intel-codenexus-port.v1.schema.json")) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["properties"]["boundary"]["properties"]["storageOwnership"]["const"], + "provider" + ); + assert_eq!( + schema["properties"]["boundary"]["properties"]["impactSemanticsOwnership"]["const"], + "provider" + ); + + let docs = fs::read_to_string(root.join("docs/codenexus-provider-adapter.md")).unwrap(); + assert!(docs.contains("does not import CodeNexus libraries")); + assert!(docs.contains("explicit_fallback")); + assert!(docs.contains("provider_unavailable")); +} + +#[test] +fn production_route_runs_full_lite_and_unavailable_through_a04() { + for (fixture_name, verdict, usable) in [ + ("full-current", "observed", true), + ("lite-current", "observed", true), + ("unavailable", "unknown", false), + ] { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor(fixture_name), CURRENT, CURRENT, 1_950); + let (exit, stdout, stderr) = route(&root.0, &native); + assert_eq!(exit, 0, "{fixture_name}: {stderr}"); + let result: Value = serde_json::from_slice(&stdout).unwrap(); + assert_eq!(result["schema"], "code-intel-codenexus-route-result.v1"); + assert_eq!(result["status"], "completed"); + assert_eq!(result["admission"]["domainVerdict"], verdict); + assert_eq!(result["adapter"]["port"]["perceptionUsable"], usable); + assert_eq!(result["engineeringFacts"], json!([])); + } +} + +#[test] +fn production_route_rejects_secret_fields_wrong_snapshot_and_bad_usage() { + let secret_root = Temp::new(); + let mut secret = build_case( + &secret_root.0, + &descriptor("full-current"), + CURRENT, + CURRENT, + 1_950, + ); + secret["apiToken"] = json!("B04-ROUTE-SECRET"); + let (exit, stdout, stderr) = route(&secret_root.0, &secret); + assert_eq!(exit, 65); + let rendered = format!("{}{}", String::from_utf8_lossy(&stdout), stderr); + assert!(!rendered.contains("B04-ROUTE-SECRET")); + + let wrong_root = Temp::new(); + let wrong = build_case( + &wrong_root.0, + &descriptor("full-current"), + CURRENT, + WRONG, + 1_950, + ); + let (exit, stdout, _) = route(&wrong_root.0, &wrong); + assert_eq!(exit, 65); + let result: Value = serde_json::from_slice(&stdout).unwrap(); + assert_eq!(result["status"], "rejected"); + assert_eq!(result["engineeringFacts"], json!([])); + + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["provider", "codenexus-adapt"]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); + assert!(output.stdout.is_empty()); +} + +#[test] +fn production_registry_facade_and_route_schema_are_declared() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest: Value = + serde_json::from_slice(&fs::read(root.join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let integration = manifest["integrations"] + .as_array() + .unwrap() + .iter() + .find(|item| item["id"] == "provider.codenexus-adapt") + .expect("provider.codenexus-adapt registry entry"); + assert_eq!(integration["required"], false); + assert!(integration["commands"]["adapt"] + .as_str() + .unwrap() + .contains("provider codenexus-adapt")); + assert!(integration["commands"]["facade"] + .as_str() + .unwrap() + .contains("-CodeNexusAdapterRequest")); + + let facade = fs::read_to_string(root.join("run-code-intel.ps1")).unwrap(); + assert!(facade.contains("provider codenexus-adapt")); + assert!(facade.contains("CodeNexusAdapterMaxAgeSeconds")); + + let schema: Value = serde_json::from_slice( + &fs::read( + root.join("orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["additionalProperties"], false); +} diff --git a/crates/code-intel-cli/tests/compatibility_retirement_gate.rs b/crates/code-intel-cli/tests/compatibility_retirement_gate.rs new file mode 100644 index 0000000..0b18706 --- /dev/null +++ b/crates/code-intel-cli/tests/compatibility_retirement_gate.rs @@ -0,0 +1,671 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const EVIDENCE_SCHEMA: &str = "code-intel-compatibility-retirement-evidence.v1"; +const NOW: u64 = 3_000_000; +static NONCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + let clock = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-e00-{}-{clock}-{}", + std::process::id(), + NONCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} +fn sha256(path: &Path) -> String { + let output = Command::new("certutil") + .arg("-hashfile") + .arg(path) + .arg("SHA256") + .output() + .unwrap(); + assert!(output.status.success()); + String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .find(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())) + .unwrap() + .to_ascii_lowercase() +} +fn declaration() -> Value { + declaration_for("compatibility.retirement-gate") +} +fn declaration_for(id: &str) -> Value { + let value: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + value["integrations"] + .as_array() + .unwrap() + .iter() + .find(|v| v["id"] == id) + .unwrap()["capabilityDeclaration"] + .clone() +} +fn write_artifact(temp: &Path, name: &str, schema: &str, kind: &str, value: &Value) -> Value { + let path = temp.join(name); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":schema,"type":kind,"path":name,"sha256":sha256(&path),"consumedSnapshotIdentity":SNAPSHOT}) +} +fn evidence(temp: &Path, class: &str, details: Value) -> Value { + let value = json!({"schema":EVIDENCE_SCHEMA,"snapshotIdentity":SNAPSHOT,"id":format!("ev-{class}"),"evidenceClass":class,"retirementId":"ret-1","legacyBranchId":"legacy.branch","replacementCapabilityId":"replacement.atom","details":details}); + write_artifact( + temp, + &format!("{class}.json"), + EVIDENCE_SCHEMA, + "compatibility.retirement-evidence", + &value, + ) +} +fn value_sha256(temp: &Path, name: &str, value: &Value) -> String { + let path = temp.join(name); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + sha256(&path) +} +fn bytes_sha256(temp: &Path, name: &str, bytes: &[u8]) -> String { + let path = temp.join(name); + fs::write(&path, bytes).unwrap(); + sha256(&path) +} +fn deletion_diff(temp: &Path, affected_files: &[&str]) -> Value { + let files = affected_files + .iter() + .enumerate() + .map(|(index, path)| { + let base = format!("legacy-{index}\nkeep-{index}\n"); + let result = format!("keep-{index}\n"); + json!({ + "path":path, + "baseBlobSha256":bytes_sha256(temp,&format!("base-{index}.txt"),base.as_bytes()), + "resultBlobSha256":bytes_sha256(temp,&format!("result-{index}.txt"),result.as_bytes()), + "baseText":base, + "resultText":result, + "hunks":[{"oldStart":1,"oldLines":1,"newStart":1,"newLines":0,"deletedLines":[format!("legacy-{index}")],"addedLines":[]}] + }) + }) + .collect::>(); + let files_value = Value::Array(files); + let patch_sha = value_sha256(temp, "deletion-patch-files.json", &files_value); + json!({ + "schema":"code-intel-compatibility-retirement-deletion-diff.v1", + "snapshotIdentity":SNAPSHOT, + "retirementId":"ret-1", + "legacyBranchId":"legacy.branch", + "affectedFiles":affected_files, + "deletionsOnly":true, + "summary":"summary is descriptive only; replayable hunks are authoritative", + "patch":{"algorithm":"replayable-delete-only-v1","sha256":patch_sha,"files":files_value} + }) +} +fn signed_event(temp: &Path, subject_sha: &str) -> Value { + let mut event = json!({ + "schema":"code-intel-authority-event.v1", + "id":"authority.retirement.ret-1", + "decision":"approved", + "approver":{"id":"code-intel-maintainers","role":"repository_governance"}, + "evidenceIds":[subject_sha], + "issuedAt":NOW-10, + "expiresAt":NOW+10 + }); + let payload = json!({ + "schema":event["schema"],"id":event["id"],"decision":event["decision"], + "approver":event["approver"],"evidenceIds":event["evidenceIds"], + "issuedAt":event["issuedAt"],"expiresAt":event["expiresAt"] + }); + let digest = value_sha256(temp, "authority-event-payload.json", &payload); + event["attestation"] = json!({"scheme":"repository-governed-sha256-v1","digest":digest}); + event +} +fn fixture(temp: &Path) -> (Value, Vec, String) { + let atom = evidence( + temp, + "replacement_atom", + json!({"status":"production_ready","outcome":"passed"}), + ); + let golden = evidence( + temp, + "golden_parity", + json!({"outcome":"passed","assertionCount":4}), + ); + let contract = evidence( + temp, + "contract_parity", + json!({"outcome":"passed","assertionCount":3}), + ); + let effects = evidence( + temp, + "effect_parity", + json!({"outcome":"passed","assertionCount":2}), + ); + let registry = evidence( + temp, + "registry_reconciliation", + json!({"outcome":"passed","registryParticipantId":"legacy.registry","replacementCapabilityId":"replacement.atom","status":"declared"}), + ); + let window = evidence( + temp, + "compatibility_window", + json!({"outcome":"passed","startedAt":1000,"observedThrough":1000+30*86400,"minimumDays":30,"checkedAt":2_600_000,"expiresAt":NOW+100}), + ); + let rollback = evidence( + temp, + "rollback_execution", + json!({"outcome":"passed","command":"restore legacy.branch","executedAt":9000,"exitCode":0}), + ); + let usage = evidence( + temp, + "usage_observation", + json!({"outcome":"passed","startedAt":1000,"endedAt":1000+30*86400,"totalInvocations":20,"legacyInvocations":0,"replacementInvocations":20}), + ); + let trace = json!({"retirementId":"ret-1","legacyBranchId":"legacy.branch","replacementCapabilityId":"replacement.atom"}); + let trace_sha = value_sha256(temp, "necessity-trace.json", &trace); + let necessity = evidence( + temp, + "c00_necessity", + json!({"outcome":"passed","decision":"admit","changeId":"ret-1","necessityTraceSha256":trace_sha}), + ); + let dependency = evidence( + temp, + "dependency_approval", + json!({"outcome":"passed","dependencyId":"D02","status":"approved","reviewer":"d02-reviewer"}), + ); + let subject = json!({ + "legacyBranch":{"capabilityId":"legacy.capability","branchId":"legacy.branch","callPath":"run-code-intel.ps1::legacy.branch","affectedFiles":["run-code-intel.ps1"],"owner":"owner-team","registryParticipantId":"legacy.registry"}, + "replacement":{"capabilityId":"replacement.atom","implementationId":"replacement.atom.compat","dependencies":["D02"],"atomEvidence":atom}, + "parity":{"golden":golden,"contract":contract,"effects":effects},"registryReconciliation":registry,"compatibilityWindow":window, + "rollback":{"command":"restore legacy.branch","executionEvidence":rollback},"usageObservation":usage,"necessityEvidence":necessity,"dependencyStates":[dependency],"lineReductionEvidence":false + }); + let subject_path = temp.join("subject.json"); + fs::write(&subject_path, serde_json::to_vec(&subject).unwrap()).unwrap(); + let subject_sha = sha256(&subject_path); + let approval = evidence( + temp, + "independent_approval", + json!({"outcome":"passed","approved":true,"authorIndependent":true,"subjectSha256":subject_sha,"reviewer":"code-intel-maintainers","authorityEvent":signed_event(temp, &subject_sha)}), + ); + let manifest = json!({"schema":"code-intel-compatibility-retirement-manifest.v1","snapshotIdentity":SNAPSHOT,"retirementId":"ret-1","approvalSubject":subject,"independentApproval":approval}); + let manifest_ref = write_artifact( + temp, + "manifest.json", + "code-intel-compatibility-retirement-manifest.v1", + "compatibility.retirement-manifest", + &manifest, + ); + let mut inputs = vec![manifest_ref]; + for name in [ + "replacement_atom", + "golden_parity", + "contract_parity", + "effect_parity", + "registry_reconciliation", + "compatibility_window", + "rollback_execution", + "usage_observation", + "c00_necessity", + "dependency_approval", + "independent_approval", + ] { + let path = temp.join(format!("{name}.json")); + inputs.push(json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":EVIDENCE_SCHEMA,"type":"compatibility.retirement-evidence","path":format!("{name}.json"),"sha256":sha256(&path),"consumedSnapshotIdentity":SNAPSHOT})); + } + (manifest, inputs, "rollback_execution.json".into()) +} +fn request(inputs: Vec) -> Value { + let d = declaration(); + json!({"schema":"code-intel-capability-request.v1","capability":"compatibility.retirement-gate","contractVersion":1,"implementation":d["implementation"],"snapshot":{"identity":SNAPSHOT,"repoIdentity":format!("content-v1:{}","c".repeat(64)),"head":"unversioned","workingTreePolicy":"explicit_overlay","scope":["."],"inputDigest":"d".repeat(64)},"options":{"evaluatedAt":NOW},"inputs":inputs,"effectPolicy":{"allowedEffects":d["allowedEffects"]}}) +} +fn run(temp: &Path, request: &Value, out: &str) -> std::process::Output { + let path = temp.join(format!("{out}-request.json")); + fs::write(&path, serde_json::to_vec(request).unwrap()).unwrap(); + Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "compatibility.retirement-gate", + "--request", + ]) + .arg(path) + .arg("--out") + .arg(temp.join(out)) + .arg("--artifact-root") + .arg(temp) + .output() + .unwrap() +} + +fn run_e01(temp: &Path, request: &Value, request_name: &str, out: &str) -> std::process::Output { + let path = temp.join(request_name); + fs::write(&path, serde_json::to_vec(request).unwrap()).unwrap(); + Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "compatibility.retirement-ticket-template", + "--request", + ]) + .arg(path) + .arg("--out") + .arg(temp.join(out)) + .arg("--artifact-root") + .arg(temp) + .output() + .unwrap() +} + +fn assert_schema_valid(document: &Path, schema: &Path) { + let output = Command::new("powershell") + .args([ + "-NoLogo", + "-NoProfile", + "-Command", + "param($Document,$Schema); if (-not (Get-Content -Raw -LiteralPath $Document | Test-Json -SchemaFile $Schema -ErrorAction Stop)) { exit 1 }", + ]) + .arg(document) + .arg(schema) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn a01_a03_gate_approves_complete_evidence_and_rejects_missing_rollback() { + let temp = Temp::new(); + let (manifest, inputs, rollback) = fixture(&temp.0); + let output = run(&temp.0, &request(inputs.clone()), "approved"); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let decision: Value = serde_json::from_slice( + &fs::read( + temp.0 + .join("approved/compatibility-retirement-decision.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(decision["decision"], "approved"); + assert_eq!( + decision["authorityBoundary"], + "approval_only_no_deletion_authority" + ); + assert_schema_valid( + &temp.0.join("manifest.json"), + &root().join( + "orchestration/schemas/code-intel-compatibility-retirement-manifest.v1.schema.json", + ), + ); + assert_schema_valid( + &temp.0.join("rollback_execution.json"), + &root().join( + "orchestration/schemas/code-intel-compatibility-retirement-evidence.v1.schema.json", + ), + ); + assert_schema_valid( + &temp + .0 + .join("approved/compatibility-retirement-decision.json"), + &root().join( + "orchestration/schemas/code-intel-compatibility-retirement-decision.v1.schema.json", + ), + ); + let replay = run(&temp.0, &request(inputs.clone()), "replay"); + assert_eq!(replay.status.code(), Some(0)); + assert_eq!( + fs::read( + temp.0 + .join("approved/compatibility-retirement-decision.json") + ) + .unwrap(), + fs::read(temp.0.join("replay/compatibility-retirement-decision.json")).unwrap() + ); + + let missing = inputs + .clone() + .into_iter() + .filter(|v| v["path"] != rollback) + .collect(); + let failed = run(&temp.0, &request(missing), "missing"); + assert_eq!(failed.status.code(), Some(65)); + assert!(!temp + .0 + .join("missing/compatibility-retirement-decision.json") + .exists()); + + let mut tampered_manifest = manifest; + tampered_manifest["unexpected"] = json!(true); + let manifest_path = temp.0.join("manifest.json"); + fs::write( + &manifest_path, + serde_json::to_vec(&tampered_manifest).unwrap(), + ) + .unwrap(); + let mut tampered_inputs = inputs.clone(); + tampered_inputs[0]["sha256"] = json!(sha256(&manifest_path)); + let closed_manifest_failure = run( + &temp.0, + &request(tampered_inputs), + "closed-manifest-failure", + ); + assert_eq!(closed_manifest_failure.status.code(), Some(65)); + assert!(!temp + .0 + .join("closed-manifest-failure/compatibility-retirement-decision.json") + .exists()); +} + +#[test] +fn evaluated_time_and_trusted_authority_hash_are_enforced_end_to_end() { + let temp = Temp::new(); + let (mut manifest, mut inputs, _) = fixture(&temp.0); + let mut missing_time = request(inputs.clone()); + missing_time["options"] = json!({}); + let invalid_options = run(&temp.0, &missing_time, "missing-evaluated-at"); + assert_eq!(invalid_options.status.code(), Some(64)); + assert!(!temp + .0 + .join("missing-evaluated-at/compatibility-retirement-decision.json") + .exists()); + + let approval_path = temp.0.join("independent_approval.json"); + let mut approval: Value = serde_json::from_slice(&fs::read(&approval_path).unwrap()).unwrap(); + approval["details"]["authorityEvent"]["attestation"]["digest"] = json!("0".repeat(64)); + fs::write(&approval_path, serde_json::to_vec(&approval).unwrap()).unwrap(); + let approval_sha = sha256(&approval_path); + manifest["independentApproval"]["sha256"] = json!(approval_sha.clone()); + let manifest_path = temp.0.join("manifest.json"); + fs::write(&manifest_path, serde_json::to_vec(&manifest).unwrap()).unwrap(); + inputs[0]["sha256"] = json!(sha256(&manifest_path)); + let approval_ref = inputs + .iter_mut() + .find(|value| value["path"] == "independent_approval.json") + .unwrap(); + approval_ref["sha256"] = json!(approval_sha); + let output = run(&temp.0, &request(inputs), "forged-authority"); + assert_eq!(output.status.code(), Some(0)); + let decision: Value = serde_json::from_slice( + &fs::read( + temp.0 + .join("forged-authority/compatibility-retirement-decision.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(decision["decision"], "blocked"); + assert!(decision["blockers"] + .as_array() + .unwrap() + .contains(&json!("unproven_independent_approval"))); +} + +#[test] +fn e01_ticket_is_content_bound_to_the_approved_e00_subject() { + let temp = Temp::new(); + let (manifest, inputs, _) = fixture(&temp.0); + let gate = run(&temp.0, &request(inputs.clone()), "e01-source"); + assert_eq!( + gate.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&gate.stderr) + ); + let decision_path = temp + .0 + .join("e01-source/compatibility-retirement-decision.json"); + let decision: Value = serde_json::from_slice(&fs::read(&decision_path).unwrap()).unwrap(); + assert_eq!(decision["decision"], "approved"); + fs::copy(&decision_path, temp.0.join("decision.json")).unwrap(); + let deletion = deletion_diff(&temp.0, &["run-code-intel.ps1"]); + let deletion_ref = write_artifact( + &temp.0, + "deletion.json", + "code-intel-compatibility-retirement-deletion-diff.v1", + "compatibility.retirement-deletion-diff", + &deletion, + ); + assert_schema_valid( + &temp.0.join("deletion.json"), + &root().join( + "orchestration/schemas/code-intel-compatibility-retirement-deletion-diff.v1.schema.json", + ), + ); + let source_ref = |schema: &str, kind: &str, path: &str| json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":schema,"type":kind,"path":path,"sha256":sha256(&temp.0.join(path)),"consumedSnapshotIdentity":SNAPSHOT}); + let subject = &manifest["approvalSubject"]; + let ticket = json!({ + "schema":"code-intel-compatibility-retirement-ticket-template.v1","snapshotIdentity":SNAPSHOT,"ticketId":"ticket-ret-1","retirementId":"ret-1", + "legacyBranch":{"capabilityId":"legacy.capability","branchId":"legacy.branch","callPath":"run-code-intel.ps1::legacy.branch"}, + "replacement":{"capabilityId":"replacement.atom","dependencies":["D02"]},"affectedFiles":["run-code-intel.ps1"], + "evidence":{"golden":subject["parity"]["golden"],"contract":subject["parity"]["contract"],"effects":subject["parity"]["effects"],"usage":subject["usageObservation"],"rollbackRehearsal":subject["rollback"]["executionEvidence"],"deletionDiff":deletion_ref}, + "source":{"retirementDecision":source_ref("code-intel-compatibility-retirement-decision.v1","compatibility.retirement-decision","decision.json"),"retirementManifest":inputs[0]}, + "owner":"executor-a","verifier":"verifier-b","observationExpiry":NOW+100,"status":"draft","authorityBoundary":"template_only_no_approval_or_deletion_authority" + }); + let ticket_ref = write_artifact( + &temp.0, + "ticket.json", + "code-intel-compatibility-retirement-ticket-template.v1", + "compatibility.retirement-ticket-template", + &ticket, + ); + assert_schema_valid( + &temp.0.join("ticket.json"), + &root().join( + "orchestration/schemas/code-intel-compatibility-retirement-ticket-template.v1.schema.json", + ), + ); + let d = declaration_for("compatibility.retirement-ticket-template"); + let req = json!({"schema":"code-intel-capability-request.v1","capability":"compatibility.retirement-ticket-template","contractVersion":1,"implementation":d["implementation"],"snapshot":{"identity":SNAPSHOT,"repoIdentity":format!("content-v1:{}","c".repeat(64)),"head":"unversioned","workingTreePolicy":"explicit_overlay","scope":["."],"inputDigest":"d".repeat(64)},"options":{"evaluatedAt":NOW},"inputs":[ticket_ref,inputs[0],source_ref("code-intel-compatibility-retirement-decision.v1","compatibility.retirement-decision","decision.json"),source_ref("code-intel-compatibility-retirement-deletion-diff.v1","compatibility.retirement-deletion-diff","deletion.json")],"effectPolicy":{"allowedEffects":d["allowedEffects"]}}); + let output = run_e01(&temp.0, &req, "e01-request.json", "e01-ticket"); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read( + temp.0 + .join("e01-ticket/compatibility-retirement-ticket.json") + ) + .unwrap(), + fs::read(temp.0.join("ticket.json")).unwrap() + ); + + let mut forged_deletion = deletion.clone(); + forged_deletion["patch"]["files"][0]["resultText"] = json!("keep-0\nsmuggled()\n"); + forged_deletion["patch"]["files"][0]["resultBlobSha256"] = json!(bytes_sha256( + &temp.0, + "forged-result.txt", + b"keep-0\nsmuggled()\n", + )); + forged_deletion["patch"]["files"][0]["hunks"][0]["newLines"] = json!(1); + forged_deletion["patch"]["files"][0]["hunks"][0]["addedLines"] = json!(["smuggled()"]); + forged_deletion["patch"]["sha256"] = json!(value_sha256( + &temp.0, + "forged-patch-files.json", + &forged_deletion["patch"]["files"], + )); + let forged_deletion_ref = write_artifact( + &temp.0, + "forged-deletion.json", + "code-intel-compatibility-retirement-deletion-diff.v1", + "compatibility.retirement-deletion-diff", + &forged_deletion, + ); + let mut forged_ticket = ticket.clone(); + forged_ticket["evidence"]["deletionDiff"] = forged_deletion_ref.clone(); + let forged_ticket_ref = write_artifact( + &temp.0, + "forged-ticket.json", + "code-intel-compatibility-retirement-ticket-template.v1", + "compatibility.retirement-ticket-template", + &forged_ticket, + ); + let mut forged_req = req.clone(); + forged_req["inputs"][0] = forged_ticket_ref; + forged_req["inputs"][3] = forged_deletion_ref; + let forged = run_e01( + &temp.0, + &forged_req, + "forged-e01-request.json", + "forged-e01-ticket", + ); + assert_eq!( + forged.status.code(), + Some(65), + "forged added code was not rejected: {}", + String::from_utf8_lossy(&forged.stderr) + ); + + let hidden_deletion = deletion_diff(&temp.0, &["run-code-intel.ps1", "second-branch.ps1"]); + let hidden_deletion_ref = write_artifact( + &temp.0, + "hidden-deletion.json", + "code-intel-compatibility-retirement-deletion-diff.v1", + "compatibility.retirement-deletion-diff", + &hidden_deletion, + ); + let mut hidden_ticket = ticket.clone(); + hidden_ticket["affectedFiles"] = json!(["run-code-intel.ps1", "second-branch.ps1"]); + hidden_ticket["evidence"]["deletionDiff"] = hidden_deletion_ref.clone(); + let hidden_ticket_ref = write_artifact( + &temp.0, + "hidden-ticket.json", + "code-intel-compatibility-retirement-ticket-template.v1", + "compatibility.retirement-ticket-template", + &hidden_ticket, + ); + let mut hidden_req = req.clone(); + hidden_req["inputs"][0] = hidden_ticket_ref; + hidden_req["inputs"][3] = hidden_deletion_ref; + let hidden = run_e01( + &temp.0, + &hidden_req, + "hidden-e01-request.json", + "hidden-e01-ticket", + ); + assert_eq!( + hidden.status.code(), + Some(65), + "unapproved second path was not rejected: {}", + String::from_utf8_lossy(&hidden.stderr) + ); + + let mut wrong_decision = decision.clone(); + wrong_decision["approvalSubjectSha256"] = json!("0".repeat(64)); + let wrong_decision_ref = write_artifact( + &temp.0, + "wrong-subject-decision.json", + "code-intel-compatibility-retirement-decision.v1", + "compatibility.retirement-decision", + &wrong_decision, + ); + let mut wrong_subject_ticket = ticket.clone(); + wrong_subject_ticket["source"]["retirementDecision"] = wrong_decision_ref.clone(); + let wrong_subject_ticket_ref = write_artifact( + &temp.0, + "wrong-subject-ticket.json", + "code-intel-compatibility-retirement-ticket-template.v1", + "compatibility.retirement-ticket-template", + &wrong_subject_ticket, + ); + let mut wrong_subject_req = req.clone(); + wrong_subject_req["inputs"][0] = wrong_subject_ticket_ref; + wrong_subject_req["inputs"][2] = wrong_decision_ref; + let wrong_subject = run_e01( + &temp.0, + &wrong_subject_req, + "wrong-subject-e01-request.json", + "wrong-subject-e01-ticket", + ); + assert_eq!( + wrong_subject.status.code(), + Some(65), + "wrong E00 approval subject was not rejected: {}", + String::from_utf8_lossy(&wrong_subject.stderr) + ); + + let mut missing_input_req = req.clone(); + missing_input_req["inputs"].as_array_mut().unwrap().pop(); + let missing_input = run_e01( + &temp.0, + &missing_input_req, + "missing-input-e01-request.json", + "missing-input-e01-ticket", + ); + assert_eq!(missing_input.status.code(), Some(65)); + assert!(!temp + .0 + .join("missing-input-e01-ticket/compatibility-retirement-ticket.json") + .exists()); + + let mut extra_input_req = req.clone(); + let fifth = extra_input_req["inputs"][3].clone(); + extra_input_req["inputs"] + .as_array_mut() + .unwrap() + .push(fifth); + let extra_input = run_e01( + &temp.0, + &extra_input_req, + "extra-input-e01-request.json", + "extra-input-e01-ticket", + ); + assert_eq!(extra_input.status.code(), Some(65)); + assert!(!temp + .0 + .join("extra-input-e01-ticket/compatibility-retirement-ticket.json") + .exists()); + + let mut wrong_call_path_ticket = ticket.clone(); + wrong_call_path_ticket["legacyBranch"]["callPath"] = json!("other-entry.ps1::legacy.branch"); + let wrong_call_path_ticket_ref = write_artifact( + &temp.0, + "wrong-call-path-ticket.json", + "code-intel-compatibility-retirement-ticket-template.v1", + "compatibility.retirement-ticket-template", + &wrong_call_path_ticket, + ); + let mut wrong_call_path_req = req.clone(); + wrong_call_path_req["inputs"][0] = wrong_call_path_ticket_ref; + let wrong_call_path = run_e01( + &temp.0, + &wrong_call_path_req, + "wrong-call-path-e01-request.json", + "wrong-call-path-e01-ticket", + ); + assert_eq!(wrong_call_path.status.code(), Some(65)); + assert!(!temp + .0 + .join("wrong-call-path-e01-ticket/compatibility-retirement-ticket.json") + .exists()); +} diff --git a/crates/code-intel-cli/tests/compatibility_retirement_ticket_template.rs b/crates/code-intel-cli/tests/compatibility_retirement_ticket_template.rs new file mode 100644 index 0000000..b99801d --- /dev/null +++ b/crates/code-intel-cli/tests/compatibility_retirement_ticket_template.rs @@ -0,0 +1,99 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde_json::{json, Value}; + +fn artifact_ref(schema: &str, kind: &str) -> Value { + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":schema, + "type":kind, + "path":"evidence.json", + "sha256":"a".repeat(64), + "consumedSnapshotIdentity":"snapshot-1" + }) +} + +fn ticket() -> Value { + let evidence = artifact_ref( + "code-intel-compatibility-retirement-evidence.v1", + "compatibility.retirement-evidence", + ); + json!({ + "schema":"code-intel-compatibility-retirement-ticket-template.v1", + "snapshotIdentity":"snapshot-1", + "ticketId":"ticket-ret-1", + "retirementId":"ret-1", + "legacyBranch":{"capabilityId":"legacy.capability","branchId":"legacy.branch","callPath":"run-code-intel.ps1::legacy.branch"}, + "replacement":{"capabilityId":"replacement.atom","dependencies":["D02"]}, + "affectedFiles":["run-code-intel.ps1"], + "evidence":{"golden":evidence,"contract":evidence,"effects":evidence,"usage":evidence,"rollbackRehearsal":evidence,"deletionDiff":artifact_ref("code-intel-compatibility-retirement-deletion-diff.v1","compatibility.retirement-deletion-diff")}, + "source":{"retirementDecision":artifact_ref("code-intel-compatibility-retirement-decision.v1","compatibility.retirement-decision"),"retirementManifest":artifact_ref("code-intel-compatibility-retirement-manifest.v1","compatibility.retirement-manifest")}, + "owner":"executor-a","verifier":"verifier-b","observationExpiry":4000000, + "status":"draft", + "authorityBoundary":"template_only_no_approval_or_deletion_authority" + }) +} + +fn write(root: &Path, name: &str, value: &Value) -> PathBuf { + let path = root.join(name); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + path +} + +#[test] +fn lint_accepts_one_branch_and_rejects_multi_branch_ambiguity_and_expiry() { + let root = std::env::temp_dir().join(format!("code-intel-e01-{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).unwrap(); + let valid = write(&root, "valid.json", &ticket()); + let ok = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["compatibility", "retirement-ticket", "lint", "--ticket"]) + .arg(&valid) + .args(["--evaluated-at", "3000000"]) + .output() + .unwrap(); + assert_eq!( + ok.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&ok.stderr) + ); + + let mut multi = ticket(); + multi["legacyBranches"] = json!([multi["legacyBranch"].clone()]); + let multi = write(&root, "multi.json", &multi); + let rejected = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["compatibility", "retirement-ticket", "lint", "--ticket"]) + .arg(&multi) + .args(["--evaluated-at", "3000000"]) + .output() + .unwrap(); + assert_eq!(rejected.status.code(), Some(65)); + + let mut expired = ticket(); + expired["observationExpiry"] = json!(2_999_999); + let expired = write(&root, "expired.json", &expired); + let rejected = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["compatibility", "retirement-ticket", "lint", "--ticket"]) + .arg(&expired) + .args(["--evaluated-at", "3000000"]) + .output() + .unwrap(); + assert_eq!(rejected.status.code(), Some(65)); + + for field in ["owner", "verifier"] { + let mut missing = ticket(); + missing.as_object_mut().unwrap().remove(field); + let path = write(&root, &format!("missing-{field}.json"), &missing); + let rejected = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["compatibility", "retirement-ticket", "lint", "--ticket"]) + .arg(path) + .args(["--evaluated-at", "3000000"]) + .output() + .unwrap(); + assert_eq!(rejected.status.code(), Some(65)); + } + let _ = fs::remove_dir_all(root); +} diff --git a/crates/code-intel-cli/tests/dag_coordinator.rs b/crates/code-intel-cli/tests/dag_coordinator.rs new file mode 100644 index 0000000..6418764 --- /dev/null +++ b/crates/code-intel-cli/tests/dag_coordinator.rs @@ -0,0 +1,522 @@ +mod artifact_ref { + pub(crate) struct VerifiedArtifact { + artifact_schema: String, + artifact_type: String, + sha256: String, + consumed_snapshot_identity: String, + } + + impl VerifiedArtifact { + pub(crate) fn artifact_schema(&self) -> &str { + &self.artifact_schema + } + + pub(crate) fn artifact_type(&self) -> &str { + &self.artifact_type + } + + pub(crate) fn sha256(&self) -> &str { + &self.sha256 + } + + pub(crate) fn consumed_snapshot_identity(&self) -> &str { + &self.consumed_snapshot_identity + } + } +} + +#[path = "../src/dag_coordinator.rs"] +mod dag_coordinator; + +use std::collections::BTreeSet; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; + +use dag_coordinator::{ + Coordinator, CoordinatorErrorKind, DagSpec, DomainVerdict, EdgeSpec, ExecutionFailure, + NodeExecutor, NodeOutcome, NodeSpec, NodeState, RunCheckpoint, VerifiedArtifactRef, +}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + +fn node(id: &str) -> NodeSpec { + NodeSpec::new(id, format!("fixture.{id}"), format!("request-v1:{id}")) +} + +fn dag(nodes: &[&str], edges: &[(&str, &str)], max_concurrency: usize) -> DagSpec { + DagSpec::new( + SNAPSHOT, + max_concurrency, + nodes.iter().map(|id| node(id)).collect(), + edges + .iter() + .map(|(from, to)| EdgeSpec::new(*from, *to)) + .collect(), + ) +} + +#[test] +fn rejects_duplicate_unknown_and_cyclic_graphs_before_execution() { + let cases = [ + ( + DagSpec::new(SNAPSHOT, 1, vec![node("a"), node("a")], vec![]), + CoordinatorErrorKind::DuplicateNode, + ), + ( + dag(&["a"], &[("missing", "a")], 1), + CoordinatorErrorKind::UnknownNode, + ), + ( + dag(&["a", "b"], &[("a", "b"), ("a", "b")], 1), + CoordinatorErrorKind::DuplicateEdge, + ), + ( + dag(&["a", "b"], &[("a", "b"), ("b", "a")], 1), + CoordinatorErrorKind::Cycle, + ), + ]; + + for (spec, expected) in cases { + let error = Coordinator::new(spec).unwrap_err(); + assert_eq!(error.kind(), expected); + } +} + +#[test] +fn ready_batches_are_sorted_bounded_and_carry_only_verified_dependency_refs() { + let mut coordinator = Coordinator::new(dag( + &["snapshot", "inventory", "independent"], + &[("snapshot", "inventory")], + 2, + )) + .unwrap(); + + let first = coordinator.next_batch().unwrap(); + assert_eq!( + first + .iter() + .map(|item| item.node_id.as_str()) + .collect::>(), + vec!["independent", "snapshot"] + ); + assert!(first.iter().all(|item| item.inputs.is_empty())); + + coordinator + .record( + "snapshot", + NodeOutcome::success( + DomainVerdict::Pass, + vec![VerifiedArtifactRef::verified_for_test( + "code-intel-repository-snapshot.v1", + "repository.snapshot", + "snapshot.json", + "b".repeat(64), + SNAPSHOT, + ) + .unwrap()], + ), + ) + .unwrap(); + coordinator + .record( + "independent", + NodeOutcome::success(DomainVerdict::Pass, vec![]), + ) + .unwrap(); + + let second = coordinator.next_batch().unwrap(); + assert_eq!(second.len(), 1); + assert_eq!(second[0].node_id, "inventory"); + assert_eq!(second[0].inputs.len(), 1); + assert_eq!(second[0].inputs[0].path(), "snapshot.json"); +} + +#[test] +fn domain_and_process_failures_block_only_descendants_and_preserve_taxonomy() { + let mut coordinator = Coordinator::new(dag( + &["domain", "domain_child", "process", "process_child", "free"], + &[("domain", "domain_child"), ("process", "process_child")], + 3, + )) + .unwrap(); + let first = coordinator.next_batch().unwrap(); + assert_eq!(first.len(), 3); + + coordinator + .record("domain", NodeOutcome::domain_fail("quality gate failed")) + .unwrap(); + coordinator + .record( + "process", + NodeOutcome::process_failure(ExecutionFailure::Unavailable, "tool missing"), + ) + .unwrap(); + coordinator + .record("free", NodeOutcome::success(DomainVerdict::Pass, vec![])) + .unwrap(); + + assert!(matches!( + coordinator.state("domain"), + Some(NodeState::DomainFailed { .. }) + )); + assert!(matches!( + coordinator.state("process"), + Some(NodeState::ProcessFailed { + failure: ExecutionFailure::Unavailable, + .. + }) + )); + assert!(matches!( + coordinator.state("domain_child"), + Some(NodeState::DependencyBlocked { .. }) + )); + assert!(matches!( + coordinator.state("process_child"), + Some(NodeState::DependencyBlocked { .. }) + )); + assert!(matches!( + coordinator.state("free"), + Some(NodeState::Succeeded { .. }) + )); + assert!(coordinator.next_batch().unwrap().is_empty()); + assert!(coordinator.is_terminal()); +} + +struct BranchOutcomeProbe; + +impl NodeExecutor for BranchOutcomeProbe { + fn execute(&self, dispatch: dag_coordinator::Dispatch) -> NodeOutcome { + if dispatch.node_id == "failing" { + NodeOutcome::domain_fail("independent quality branch failed") + } else { + NodeOutcome::success(DomainVerdict::Pass, vec![]) + } + } +} + +#[test] +fn scheduler_continues_an_independent_branch_after_domain_failure() { + let manifest = Coordinator::new(dag( + &["failing", "blocked_child", "independent"], + &[("failing", "blocked_child")], + 2, + )) + .unwrap() + .run_to_completion(&BranchOutcomeProbe) + .unwrap(); + + assert_eq!(manifest.outcome.as_str(), "domain_failed"); + assert!(matches!( + manifest.nodes["failing"], + NodeState::DomainFailed { .. } + )); + assert!(matches!( + manifest.nodes["blocked_child"], + NodeState::DependencyBlocked { .. } + )); + assert!(matches!( + manifest.nodes["independent"], + NodeState::Succeeded { .. } + )); +} + +#[test] +fn direct_fail_verdicts_and_empty_diagnostics_still_emit_schema_valid_failure_states() { + let mut coordinator = Coordinator::new(dag(&["domain", "process"], &[], 2)).unwrap(); + assert_eq!(coordinator.next_batch().unwrap().len(), 2); + coordinator + .record( + "domain", + NodeOutcome::Success { + verdict: DomainVerdict::Fail, + artifacts: vec![], + }, + ) + .unwrap(); + coordinator + .record( + "process", + NodeOutcome::process_failure(ExecutionFailure::Internal, " "), + ) + .unwrap(); + + let manifest = coordinator.manifest().to_json(); + assert_eq!(manifest["nodes"]["domain"]["status"], "domain_failed"); + assert_eq!( + manifest["nodes"]["domain"]["diagnostic"], + "executor returned a domain fail verdict" + ); + assert_eq!(manifest["nodes"]["process"]["status"], "process_failed"); + assert_eq!( + manifest["nodes"]["process"]["diagnostic"], + "process failure without diagnostic" + ); +} + +#[test] +fn terminal_unknown_verdict_is_not_reported_as_completed() { + let mut coordinator = Coordinator::new(dag(&["evidence", "independent"], &[], 2)).unwrap(); + assert_eq!(coordinator.next_batch().unwrap().len(), 2); + coordinator + .record( + "evidence", + NodeOutcome::success(DomainVerdict::Unknown, vec![]), + ) + .unwrap(); + coordinator + .record( + "independent", + NodeOutcome::success(DomainVerdict::Pass, vec![]), + ) + .unwrap(); + + let manifest = coordinator.manifest().to_json(); + assert_eq!(manifest["outcome"], "domain_unknown"); + assert_eq!(manifest["nodes"]["evidence"]["status"], "succeeded"); + assert_eq!(manifest["nodes"]["evidence"]["verdict"], "unknown"); +} + +#[test] +fn resume_requires_matching_identity_and_replays_only_unfinished_nodes() { + let spec = dag(&["a", "b", "c"], &[("a", "b")], 2); + let mut first = Coordinator::new(spec.clone()).unwrap(); + let identity = first.run_identity().to_string(); + let batch = first.next_batch().unwrap(); + assert_eq!( + batch + .iter() + .map(|item| item.node_id.as_str()) + .collect::>(), + vec!["a", "c"] + ); + first + .record("a", NodeOutcome::success(DomainVerdict::Pass, vec![])) + .unwrap(); + let checkpoint = first.checkpoint(); + + let mut resumed = Coordinator::resume(spec.clone(), checkpoint.clone()).unwrap(); + assert_eq!(resumed.run_identity(), identity); + assert!(matches!( + resumed.state("a"), + Some(NodeState::Succeeded { .. }) + )); + assert_eq!( + resumed + .next_batch() + .unwrap() + .iter() + .map(|item| item.node_id.as_str()) + .collect::>(), + vec!["b", "c"] + ); + + let changed = dag(&["a", "b", "c", "d"], &[("a", "b")], 2); + let error = Coordinator::resume(changed, checkpoint).unwrap_err(); + assert_eq!(error.kind(), CoordinatorErrorKind::ResumeIdentityMismatch); +} + +#[test] +fn identity_ignores_declaration_order_and_resume_rejects_impossible_history() { + let left = dag(&["a", "b", "c"], &[("a", "c"), ("b", "c")], 2); + let right = dag(&["c", "a", "b"], &[("b", "c"), ("a", "c")], 2); + let left_coordinator = Coordinator::new(left.clone()).unwrap(); + assert_eq!(left_coordinator.run_identity().len(), 71); + assert!(left_coordinator.run_identity().starts_with("dag-v1:")); + assert_eq!( + left_coordinator.run_identity(), + Coordinator::new(right).unwrap().run_identity() + ); + + let mut nodes = left_coordinator.checkpoint().nodes; + nodes.insert( + "c".to_string(), + NodeState::Succeeded { + verdict: DomainVerdict::Pass, + artifacts: vec![], + }, + ); + let forged = RunCheckpoint { + schema: dag_coordinator::RUN_STATE_SCHEMA, + run_identity: left_coordinator.run_identity().to_string(), + nodes, + }; + let error = Coordinator::resume(left, forged).unwrap_err(); + assert_eq!(error.kind(), CoordinatorErrorKind::ResumeIdentityMismatch); +} + +#[test] +fn resume_rejects_every_forged_terminal_child_while_parent_is_pending() { + let spec = dag(&["parent", "child"], &[("parent", "child")], 1); + let coordinator = Coordinator::new(spec.clone()).unwrap(); + let forged_states = [ + NodeState::DomainFailed { + diagnostic: "forged domain result".to_string(), + artifacts: vec![], + }, + NodeState::ProcessFailed { + failure: ExecutionFailure::Internal, + diagnostic: "forged process result".to_string(), + }, + NodeState::DependencyBlocked { + blocked_by: vec!["parent".to_string()], + }, + NodeState::Succeeded { + verdict: DomainVerdict::Pass, + artifacts: vec![], + }, + ]; + for forged_state in forged_states { + let mut nodes = coordinator.checkpoint().nodes; + nodes.insert("child".to_string(), forged_state); + let checkpoint = RunCheckpoint { + schema: dag_coordinator::RUN_STATE_SCHEMA, + run_identity: coordinator.run_identity().to_string(), + nodes, + }; + let error = Coordinator::resume(spec.clone(), checkpoint).unwrap_err(); + assert_eq!(error.kind(), CoordinatorErrorKind::ResumeIdentityMismatch); + } +} + +struct ProbeExecutor { + active: AtomicUsize, + peak: AtomicUsize, + seen: Mutex>, +} + +impl NodeExecutor for ProbeExecutor { + fn execute(&self, dispatch: dag_coordinator::Dispatch) -> NodeOutcome { + let active = self.active.fetch_add(1, Ordering::SeqCst) + 1; + self.peak.fetch_max(active, Ordering::SeqCst); + thread::sleep(Duration::from_millis(30)); + self.seen.lock().unwrap().insert(dispatch.node_id); + self.active.fetch_sub(1, Ordering::SeqCst); + NodeOutcome::success(DomainVerdict::Pass, vec![]) + } +} + +#[test] +fn run_uses_bounded_parallelism_and_manifest_is_deterministic_and_complete() { + let spec = dag(&["d", "b", "a", "c"], &[], 2); + let identity = Coordinator::new(spec.clone()) + .unwrap() + .run_identity() + .to_string(); + let executor = ProbeExecutor { + active: AtomicUsize::new(0), + peak: AtomicUsize::new(0), + seen: Mutex::new(BTreeSet::new()), + }; + let manifest = Coordinator::new(spec) + .unwrap() + .run_to_completion(&executor) + .unwrap(); + + assert_eq!(executor.peak.load(Ordering::SeqCst), 2); + assert_eq!(executor.seen.lock().unwrap().len(), 4); + assert_eq!(manifest.run_identity, identity); + assert_eq!(manifest.snapshot_identity, SNAPSHOT); + assert_eq!(manifest.outcome.as_str(), "completed"); + assert_eq!(manifest.nodes.len(), 4); + assert_eq!( + manifest + .nodes + .keys() + .map(String::as_str) + .collect::>(), + vec!["a", "b", "c", "d"] + ); +} + +#[test] +fn node_contract_has_no_implicit_provider_command_or_tool_authority() { + let value = node("safe").to_json(); + assert_eq!(value["id"], "safe"); + assert_eq!(value["capability"], "fixture.safe"); + for forbidden in ["provider", "command", "executable", "tool", "effects"] { + assert!( + value.get(forbidden).is_none(), + "forbidden authority field: {forbidden}" + ); + } +} + +#[test] +fn production_source_exposes_no_field_based_verified_artifact_constructor() { + let source = include_str!("../src/dag_coordinator.rs"); + let artifact_source = include_str!("../src/artifact_ref.rs"); + assert!(!source.contains("pub(crate) fn from_verified_fields")); + assert!(!source.contains("pub fn from_verified_fields")); + let verified_struct = artifact_source + .split("pub(crate) struct VerifiedArtifact") + .nth(1) + .and_then(|tail| tail.split('}').next()) + .expect("VerifiedArtifact declaration"); + assert!( + !verified_struct + .lines() + .any(|line| line.trim_start().starts_with("pub")), + "every A03 token field must be private so siblings cannot forge the token" + ); + for field in [ + "bytes", + "artifact_schema", + "artifact_type", + "sha256", + "consumed_snapshot_identity", + "stable_file_id", + ] { + assert!( + verified_struct.contains(&format!("{field}:")), + "static forge-chain test must cover token field {field}" + ); + } + assert!(source.contains("verified: &crate::artifact_ref::VerifiedArtifact")); + assert_eq!( + artifact_source.matches("Ok(VerifiedArtifact {").count(), + 1, + "only the A03 verifier may construct a successful VerifiedArtifact token" + ); +} + +#[test] +fn checked_in_dag_schema_is_closed_and_contains_no_execution_authority_fields() { + let schema: serde_json::Value = serde_json::from_str(include_str!( + "../../../orchestration/schemas/code-intel-run-dag.v1.schema.json" + )) + .unwrap(); + assert_eq!( + schema["$id"], + "https://code-intel.local/schemas/code-intel-run-dag.v1.schema.json" + ); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["properties"]["nodes"]["items"]["additionalProperties"], + false + ); + let node_properties = schema["properties"]["nodes"]["items"]["properties"] + .as_object() + .unwrap(); + assert_eq!( + node_properties + .keys() + .map(String::as_str) + .collect::>(), + BTreeSet::from(["capability", "id", "requestIdentity"]) + ); + + for checked_in in [ + include_str!("../../../orchestration/schemas/code-intel-run-state.v1.schema.json"), + include_str!("../../../orchestration/schemas/code-intel-run-manifest.v1.schema.json"), + ] { + let schema: serde_json::Value = serde_json::from_str(checked_in).unwrap(); + assert_eq!(schema["additionalProperties"], false); + assert!(schema["definitions"]["nodeState"].is_object()); + assert_ne!( + schema["properties"]["nodes"]["additionalProperties"], + serde_json::Value::Bool(true) + ); + } +} diff --git a/crates/code-intel-cli/tests/dag_run.rs b/crates/code-intel-cli/tests/dag_run.rs new file mode 100644 index 0000000..74eb048 --- /dev/null +++ b/crates/code-intel-cli/tests/dag_run.rs @@ -0,0 +1,555 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +fn temp_dir() -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!("code-intel-a09-run-{}-{nonce}", std::process::id())) +} + +fn doctor_tool_fixture(root: &Path, conforming_sentrux: bool) -> PathBuf { + let bin = root.join(if conforming_sentrux { + "doctor-tools-ready" + } else { + "doctor-tools-nonconforming" + }); + fs::create_dir_all(&bin).unwrap(); + #[cfg(windows)] + { + for name in ["rg", "git", "python", "repowise"] { + fs::write( + bin.join(format!("{name}.cmd")), + "@echo off\r\nexit /b 0\r\n", + ) + .unwrap(); + } + let sentrux = if conforming_sentrux { + "@echo off\r\necho Enforce architectural rules\r\necho Tier: pro\r\nexit /b 0\r\n" + } else { + "@echo off\r\necho fixture nonconforming\r\nexit /b 0\r\n" + }; + fs::write(bin.join("sentrux.cmd"), sentrux).unwrap(); + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + for name in ["rg", "git", "python", "repowise"] { + let path = bin.join(name); + fs::write(&path, "#!/bin/sh\nexit 0\n").unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap(); + } + let path = bin.join("sentrux"); + let sentrux = if conforming_sentrux { + "#!/bin/sh\necho 'Enforce architectural rules'\necho 'Tier: pro'\nexit 0\n" + } else { + "#!/bin/sh\necho 'fixture nonconforming'\nexit 0\n" + }; + fs::write(&path, sentrux).unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap(); + } + bin +} + +#[test] +fn production_run_route_executes_snapshot_then_inventory() { + let root = temp_dir(); + let repo = root.join("repo"); + let out = root.join("run"); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + fs::write(repo.join("src/lib.rs"), "pub fn fixture() {}\n").unwrap(); + let doctor_tools = doctor_tool_fixture(&root, true); + + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(&repo) + .arg("--out") + .arg(&out) + .arg("--doctor-tool-path-prefix") + .arg(&doctor_tools) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let manifest: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(manifest["schema"], "code-intel-run-manifest.v1"); + assert_eq!(manifest["outcome"], "completed", "manifest={manifest}"); + assert!(out.join("run-manifest.json").is_file()); + assert!(out.join("run-manifest-ref.json").is_file()); + let manifest_ref: Value = + serde_json::from_slice(&fs::read(out.join("run-manifest-ref.json")).unwrap()).unwrap(); + assert_eq!(manifest_ref["artifactSchema"], "code-intel-run-manifest.v1"); + assert_eq!(manifest_ref["type"], "run.manifest"); + assert_eq!(manifest_ref["path"], "run-manifest.json"); + assert_eq!( + manifest_ref["consumedSnapshotIdentity"], + manifest["snapshotIdentity"] + ); + assert!(out.join("repo.snapshot/snapshot.json").is_file()); + assert!(out.join("inventory.rg/files.txt").is_file()); + assert_eq!( + fs::read(out.join("inventory.rg/files.txt")).unwrap(), + b"README.md\nsrc/lib.rs\n", + "A09 inventory must preserve the A00 normalized rg artifact" + ); + + let snapshot_request: Value = + serde_json::from_slice(&fs::read(out.join("repo.snapshot.request.json")).unwrap()).unwrap(); + let snapshot_result: Value = + serde_json::from_slice(&fs::read(out.join("repo.snapshot.result.json")).unwrap()).unwrap(); + let inventory_request: Value = + serde_json::from_slice(&fs::read(out.join("inventory.rg.request.json")).unwrap()).unwrap(); + let inventory_result: Value = + serde_json::from_slice(&fs::read(out.join("inventory.rg.result.json")).unwrap()).unwrap(); + let doctor_result: Value = + serde_json::from_slice(&fs::read(out.join("doctor.result.json")).unwrap()).unwrap(); + let native_result: Value = + serde_json::from_slice(&fs::read(out.join("evidence.native-code.result.json")).unwrap()) + .unwrap(); + for envelope in [&snapshot_request, &inventory_request] { + assert_eq!(envelope["schema"], "code-intel-capability-request.v1"); + } + for envelope in [ + &snapshot_result, + &doctor_result, + &inventory_result, + &native_result, + ] { + assert_eq!(envelope["schema"], "code-intel-capability-result.v1"); + assert_eq!(envelope["status"], "completed"); + assert_eq!(envelope["verdict"], "pass"); + assert_eq!(envelope["domainVerdict"], "pass"); + } + assert_eq!(snapshot_request["capability"], "repo.snapshot"); + assert_eq!(inventory_request["capability"], "inventory.rg"); + assert_eq!(inventory_request["inputs"].as_array().unwrap().len(), 1); + assert_eq!( + inventory_request["inputs"][0]["artifactSchema"], + "code-intel-repository-snapshot.v1" + ); + assert_eq!( + inventory_request["inputs"][0]["sha256"], + snapshot_result["artifacts"][0]["sha256"] + ); + assert_eq!( + manifest["nodes"]["repo.snapshot"]["artifacts"][0]["path"], + "repo.snapshot/snapshot.json" + ); + assert_eq!( + manifest["nodes"]["inventory.rg"]["artifacts"][0]["path"], + "inventory.rg/files.txt" + ); + assert_eq!( + manifest["nodes"]["doctor"]["artifacts"][0]["path"], + "doctor/doctor-observation.json" + ); + assert_eq!( + manifest["nodes"]["evidence.native-code"]["status"], + "succeeded" + ); + assert_eq!(manifest["nodes"]["evidence.graph"]["status"], "succeeded"); + assert_eq!(manifest["nodes"]["evidence.sentrux"]["status"], "succeeded"); + assert_eq!( + manifest["nodes"]["diagnosis.hospital"]["status"], + "succeeded" + ); + for node in ["evidence.graph", "evidence.sentrux", "diagnosis.hospital"] { + assert_eq!(manifest["nodes"][node]["verdict"], "pass", "node={node}"); + } + assert!(manifest["nodes"]["evidence.graph"]["artifacts"] + .as_array() + .unwrap() + .iter() + .any(|artifact| artifact["type"] == "observed.evidence.payload")); + assert!(manifest["nodes"]["evidence.sentrux"]["artifacts"] + .as_array() + .unwrap() + .iter() + .any(|artifact| artifact["type"] == "provider.sentrux.command-observation")); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn production_dag_output_commits_and_enters_the_authoritative_index() { + let root = temp_dir(); + let repo = root.join("repo"); + let source = root.join("a09-source"); + let artifact_root = root.join("artifacts"); + let repo_authority = artifact_root.join("fixture-repo"); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::create_dir_all(repo.join("tests")).unwrap(); + fs::create_dir_all(&repo_authority).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + fs::write(repo.join("src/lib.rs"), "pub fn fixture() {}\n").unwrap(); + fs::write( + repo.join("tests/lib_test.rs"), + "use crate::lib;\n#[test]\nfn covers_fixture() {}\n", + ) + .unwrap(); + let doctor_tools = doctor_tool_fixture(&root, true); + + let dag = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(&repo) + .arg("--out") + .arg(&source) + .arg("--doctor-tool-path-prefix") + .arg(&doctor_tools) + .output() + .unwrap(); + assert_eq!( + dag.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&dag.stdout), + String::from_utf8_lossy(&dag.stderr) + ); + + let commit = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "commit", "--source-root"]) + .arg(&source) + .arg("--authority-root") + .arg(&repo_authority) + .arg("--manifest-ref") + .arg(source.join("run-manifest-ref.json")) + .args(["--final-name", "run-001"]) + .output() + .unwrap(); + assert_eq!( + commit.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&commit.stdout), + String::from_utf8_lossy(&commit.stderr) + ); + + let index = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["artifact", "index", "--artifact-root"]) + .arg(&artifact_root) + .output() + .unwrap(); + assert_eq!( + index.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&index.stdout), + String::from_utf8_lossy(&index.stderr) + ); + let index: Value = serde_json::from_slice(&index.stdout).unwrap(); + assert_eq!(index["entries"].as_array().unwrap().len(), 1); + assert_eq!(index["entries"][0]["repo"], "fixture-repo"); + assert_eq!(index["entries"][0]["run"], "run-001"); + assert!( + index["entries"][0]["artifactRefs"] + .as_array() + .unwrap() + .iter() + .any(|artifact| artifact["type"] == "repository.snapshot"), + "index={index}" + ); + + let query = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["artifact", "query", "--artifact-root"]) + .arg(&artifact_root) + .args(["--repo", "fixture-repo", "--repo-path"]) + .arg(&repo) + .args(["--type", "inventory.files", "--contains", "src/lib.rs"]) + .output() + .unwrap(); + assert_eq!( + query.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&query.stdout), + String::from_utf8_lossy(&query.stderr) + ); + let query: Value = serde_json::from_slice(&query.stdout).unwrap(); + assert_eq!(query["schema"], "code-intel-evidence-query.v1"); + assert_eq!(query["runOutcome"], "completed"); + assert_eq!(query["authority"]["status"], "committed"); + assert_eq!(query["coverage"]["status"], "complete"); + assert_eq!(query["coverage"]["requestedEvidenceStatus"], "available"); + assert_eq!(query["confidence"], "high"); + assert_eq!(query["freshness"]["status"], "current"); + assert_eq!(query["matches"].as_array().unwrap().len(), 1); + assert_eq!( + query["matches"][0]["artifactRef"]["type"], + "inventory.files" + ); + + let freshness_unknown = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["artifact", "query", "--artifact-root"]) + .arg(&artifact_root) + .args(["--repo", "fixture-repo", "--type", "inventory.files"]) + .output() + .unwrap(); + assert_eq!(freshness_unknown.status.code(), Some(0)); + let freshness_unknown: Value = serde_json::from_slice(&freshness_unknown.stdout).unwrap(); + assert_eq!(freshness_unknown["freshness"]["status"], "unknown"); + assert_eq!(freshness_unknown["confidence"], "limited"); + + let impact = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["change", "impact", "--artifact-root"]) + .arg(&artifact_root) + .args(["--repo", "fixture-repo", "--repo-path"]) + .arg(&repo) + .args(["--changed", "src/lib.rs"]) + .output() + .unwrap(); + assert_eq!( + impact.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&impact.stdout), + String::from_utf8_lossy(&impact.stderr) + ); + let impact: Value = serde_json::from_slice(&impact.stdout).unwrap(); + assert_eq!(impact["schema"], "code-intel-change-impact.v1"); + assert_eq!(impact["runOutcome"], "completed"); + assert_eq!(impact["freshness"]["status"], "current"); + assert_eq!( + impact["testSelection"]["files"], + json!(["tests/lib_test.rs"]) + ); + assert_eq!(impact["testSelection"]["commands"], json!(["cargo test"])); + + fs::write(repo.join("src/lib.rs"), "pub fn changed() {}\n").unwrap(); + let stale = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["artifact", "query", "--artifact-root"]) + .arg(&artifact_root) + .args(["--repo", "fixture-repo", "--repo-path"]) + .arg(&repo) + .args(["--type", "inventory.files"]) + .output() + .unwrap(); + assert_eq!(stale.status.code(), Some(0)); + let stale: Value = serde_json::from_slice(&stale.stdout).unwrap(); + assert_eq!(stale["freshness"]["status"], "stale"); + assert_eq!(stale["confidence"], "limited"); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn production_run_preserves_doctor_domain_failure_and_completes_unrelated_branch() { + let root = temp_dir(); + let repo = root.join("repo"); + let out = root.join("run"); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + fs::write(repo.join("src/lib.rs"), "pub fn fixture() {}\n").unwrap(); + let doctor_tools = doctor_tool_fixture(&root, false); + + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(&repo) + .arg("--out") + .arg(&out) + .arg("--doctor-tool-path-prefix") + .arg(&doctor_tools) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(10)); + let manifest: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(manifest["outcome"], "domain_failed"); + assert_eq!(manifest["nodes"]["doctor"]["status"], "domain_failed"); + let doctor_artifacts = manifest["nodes"]["doctor"]["artifacts"] + .as_array() + .expect("domain failure must retain verified doctor evidence"); + assert!(!doctor_artifacts.is_empty()); + assert!(doctor_artifacts + .iter() + .any(|artifact| artifact["type"] == "doctor.observation")); + assert_eq!(manifest["nodes"]["repo.snapshot"]["status"], "succeeded"); + assert_eq!(manifest["nodes"]["inventory.rg"]["status"], "succeeded"); + assert_eq!( + manifest["nodes"]["evidence.native-code"]["status"], + "succeeded" + ); + assert_eq!(manifest["nodes"]["evidence.graph"]["status"], "succeeded"); + assert_eq!(manifest["nodes"]["evidence.sentrux"]["status"], "succeeded"); + assert_eq!( + manifest["nodes"]["diagnosis.hospital"]["status"], + "succeeded" + ); + let doctor_result: Value = + serde_json::from_slice(&fs::read(out.join("doctor.result.json")).unwrap()).unwrap(); + assert_eq!(doctor_result["status"], "completed"); + assert_eq!(doctor_result["verdict"], "fail"); + assert_eq!(doctor_result["domainVerdict"], "fail"); + assert_eq!(doctor_result["exitCode"], 10); + assert!(doctor_result["diagnostics"][0] + .as_str() + .unwrap() + .contains("provider conformance failed")); + + let artifact_root = root.join("artifacts"); + let authority = artifact_root.join("fixture-repo"); + fs::create_dir_all(&authority).unwrap(); + let commit = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "commit", "--source-root"]) + .arg(&out) + .arg("--authority-root") + .arg(&authority) + .arg("--manifest-ref") + .arg(out.join("run-manifest-ref.json")) + .args(["--final-name", "failed-001"]) + .output() + .unwrap(); + assert_eq!(commit.status.code(), Some(0)); + let committed_root = authority.join("failed-001"); + let marker: Value = + serde_json::from_slice(&fs::read(committed_root.join("run-complete.json")).unwrap()) + .unwrap(); + let committed_manifest: Value = serde_json::from_slice( + &fs::read(committed_root.join(marker["manifest"]["path"].as_str().unwrap())).unwrap(), + ) + .unwrap(); + let committed_doctor_artifact = committed_manifest["nodes"]["doctor"]["artifacts"] + .as_array() + .unwrap() + .iter() + .find(|artifact| artifact["type"] == "doctor.observation") + .unwrap(); + assert!(committed_root + .join(committed_doctor_artifact["path"].as_str().unwrap()) + .is_file()); + + let index = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["artifact", "index", "--artifact-root"]) + .arg(&artifact_root) + .output() + .unwrap(); + assert_eq!(index.status.code(), Some(0)); + let index: Value = serde_json::from_slice(&index.stdout).unwrap(); + assert_eq!(index["entries"], json!([])); + assert!(index["diagnostics"].as_array().unwrap().iter().any(|item| { + item["run"] == "failed-001" + && item["classification"] == "non_completed" + && item["reason"] + .as_str() + .is_some_and(|reason| reason.contains("domain_failed")) + })); + + let query = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["artifact", "query", "--artifact-root"]) + .arg(&artifact_root) + .args(["--repo", "fixture-repo", "--type", "code_evidence.files"]) + .output() + .unwrap(); + assert_eq!(query.status.code(), Some(65)); + assert!(String::from_utf8_lossy(&query.stderr) + .contains("no committed authoritative run is indexed")); + + let impact = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["change", "impact", "--artifact-root"]) + .arg(&artifact_root) + .args(["--repo", "fixture-repo", "--repo-path"]) + .arg(&repo) + .args(["--changed", "src/lib.rs"]) + .output() + .unwrap(); + assert_eq!(impact.status.code(), Some(65)); + assert!(String::from_utf8_lossy(&impact.stderr) + .contains("no committed authoritative run is indexed")); + + let _ = fs::remove_dir_all(root); +} + +#[test] +fn optional_session_evidence_is_snapshot_bound_a03_verified_and_manifested() { + let root = temp_dir(); + let repo = root.join("repo"); + let out = root.join("run"); + let trace = root.join("trace.json"); + let session = root.join("session-evidence.json"); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::write(repo.join("src/lib.rs"), "pub fn fixture() {}\n").unwrap(); + fs::write( + &trace, + serde_json::to_vec(&json!({ + "version":1, + "session":{"id":"private-session","harness":"Codex Desktop","cwd":repo}, + "events":[{ + "seq":1, + "tool":"read_file", + "action":"read", + "targets":[{"path":"src/lib.rs","touch":"read"}], + "outside":[], + "isError":false + }], + "stats":{"observability":{"reads":"exact","errors":"exact"}} + })) + .unwrap(), + ) + .unwrap(); + let adapted = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["provider", "session-adapt", "--repo"]) + .arg(&repo) + .arg("--trace") + .arg(&trace) + .arg("--out") + .arg(&session) + .output() + .unwrap(); + assert_eq!( + adapted.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&adapted.stdout), + String::from_utf8_lossy(&adapted.stderr) + ); + + let doctor_tools = doctor_tool_fixture(&root, true); + let run = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(&repo) + .arg("--out") + .arg(&out) + .arg("--session-evidence") + .arg(&session) + .arg("--doctor-tool-path-prefix") + .arg(&doctor_tools) + .output() + .unwrap(); + assert_eq!( + run.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + let manifest: Value = serde_json::from_slice(&run.stdout).unwrap(); + let node = &manifest["nodes"]["verification.session-evidence"]; + assert_eq!(node["status"], "succeeded"); + assert_eq!(node["verdict"], "pass"); + assert_eq!( + node["artifacts"][0]["artifactSchema"], + "code-intel-session-evidence.v1" + ); + assert_eq!( + node["artifacts"][0]["type"], + "verification.session-evidence" + ); + assert_eq!( + node["artifacts"][0]["consumedSnapshotIdentity"], + manifest["snapshotIdentity"] + ); + assert!(out + .join("verification.session-evidence/session-evidence.json") + .is_file()); + + let _ = fs::remove_dir_all(root); +} diff --git a/crates/code-intel-cli/tests/decision_gap.rs b/crates/code-intel-cli/tests/decision_gap.rs new file mode 100644 index 0000000..f04293f --- /dev/null +++ b/crates/code-intel-cli/tests/decision_gap.rs @@ -0,0 +1,188 @@ +#[path = "../src/decision_gap.rs"] +mod decision_gap; + +use std::fs; +use std::path::PathBuf; + +use serde_json::{json, Value}; + +fn root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn fixture(name: &str) -> Value { + serde_json::from_slice( + &fs::read( + root() + .join("tests/fixtures/decision-gap") + .join(format!("{name}.json")), + ) + .unwrap(), + ) + .unwrap() +} + +#[test] +fn unresolved_risk_acceptance_blocks_only_publication() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let result = decision_gap::detect(&fixture("risk-acceptance"), &rules).unwrap(); + + assert_eq!( + result["schema"], + "code-intel-decision-gap-detection-result.v1" + ); + assert_eq!(result["gaps"].as_array().unwrap().len(), 1); + let gap = &result["gaps"][0]; + assert_eq!(gap["kind"], "risk_acceptance"); + assert_eq!(gap["blockedDecision"], "publish release with residual risk"); + assert_eq!(gap["recommendedAnswer"]["kind"], "proposal"); + assert_eq!(gap["affectedBranches"], json!(["publication"])); + assert_eq!(gap["authorityRequired"], true); + + assert_eq!(result["branches"][0]["branchId"], "inventory"); + assert_eq!(result["branches"][0]["status"], "completed"); + assert_eq!(result["branches"][1]["branchId"], "publication"); + assert_eq!(result["branches"][1]["status"], "blocked_decision_gap"); + assert_eq!(result["answersRecorded"], false); + assert_eq!(result["authorityEvents"], json!([])); + assert_eq!(result["adoptionDecisions"], json!([])); + assert_eq!(result["committedEngineeringPlans"], json!([])); +} + +#[test] +fn missing_fact_is_discovery_work_not_a_decision_gap() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let result = decision_gap::detect(&fixture("missing-fact"), &rules).unwrap(); + + assert_eq!(result["gaps"], json!([])); + assert_eq!( + result["factDiscovery"][0]["blockerId"], + "fact-release-owner" + ); + assert_eq!( + result["factDiscovery"][0]["missingFactIds"], + json!(["fact-owner"]) + ); + assert_eq!(result["branches"][0]["status"], "fact_discovery_required"); +} + +#[test] +fn multiple_gaps_and_branches_have_stable_order() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let mut request = fixture("risk-acceptance"); + let second = json!({ + "id": "gap-priority-a", + "kind": "priority", + "blockedDecision": "choose the first migration branch", + "discoverableFactsChecked": [{"factId": "fact-dependencies", "status": "resolved"}], + "missingFactIds": [], + "options": [ + {"id": "zeta", "label": "Migrate publication", "consequence": "publication changes first"}, + {"id": "alpha", "label": "Migrate inventory", "consequence": "inventory changes first"} + ], + "recommendedOptionId": "alpha", + "recommendationRationale": "reduce dependency fan-out", + "affectedBranches": ["inventory"] + }); + request["branches"][0]["blockers"] = json!([second]); + request["branches"].as_array_mut().unwrap().reverse(); + + let first = decision_gap::detect(&request, &rules).unwrap(); + let second = decision_gap::detect(&request, &rules).unwrap(); + + assert_eq!(first, second); + assert_eq!(first["gaps"][0]["id"], "gap-priority-a"); + assert_eq!(first["gaps"][1]["id"], "gap-risk-release"); + assert_eq!(first["gaps"][0]["options"][0]["id"], "alpha"); + assert_eq!(first["branches"][0]["branchId"], "inventory"); + assert_eq!(first["branches"][1]["branchId"], "publication"); +} + +#[test] +fn duplicate_branch_and_blocker_ids_are_rejected() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let mut duplicate_branch = fixture("risk-acceptance"); + duplicate_branch["branches"][1]["branchId"] = json!("inventory"); + assert!(decision_gap::detect(&duplicate_branch, &rules) + .unwrap_err() + .to_string() + .contains("duplicate branchId")); + + let mut duplicate_blocker = fixture("risk-acceptance"); + let blocker = duplicate_blocker["branches"][1]["blockers"][0].clone(); + duplicate_blocker["branches"][0]["blockers"] = json!([blocker]); + assert!(decision_gap::detect(&duplicate_blocker, &rules) + .unwrap_err() + .to_string() + .contains("duplicate blocker id")); +} + +#[test] +fn unknown_affected_branch_is_rejected() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let mut request = fixture("risk-acceptance"); + request["branches"][1]["blockers"][0]["affectedBranches"] = json!(["unknown"]); + + assert!(decision_gap::detect(&request, &rules) + .unwrap_err() + .to_string() + .contains("unknown branch")); +} + +#[test] +fn resolved_missing_fact_blocker_is_rejected_instead_of_blocking_work() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let mut request = fixture("missing-fact"); + request["branches"][0]["blockers"][0]["discoverableFactsChecked"][0]["status"] = + json!("resolved"); + request["branches"][0]["blockers"][0]["missingFactIds"] = json!([]); + + assert!(decision_gap::detect(&request, &rules) + .unwrap_err() + .to_string() + .contains("must name an unresolved fact")); +} + +#[test] +fn unknown_kind_with_missing_fact_is_rejected() { + let rules = + decision_gap::load_rule_table(&root().join("orchestration/decision-gap-rules.v1.json")) + .unwrap(); + let mut request = fixture("missing-fact"); + request["branches"][0]["blockers"][0]["kind"] = json!("invented_kind"); + + assert!(decision_gap::detect(&request, &rules) + .unwrap_err() + .to_string() + .contains("unknown blocker kind")); +} + +#[test] +fn detector_has_no_interactive_or_decision_authority_surface() { + let source = + fs::read_to_string(root().join("crates/code-intel-cli/src/decision_gap.rs")).unwrap(); + for forbidden in [ + "std::io::stdin", + "read_line(", + "evaluate_batch(", + "authority::", + "answersRecorded\": true", + ] { + assert!( + !source.contains(forbidden), + "found forbidden surface: {forbidden}" + ); + } +} diff --git a/crates/code-intel-cli/tests/decision_port.rs b/crates/code-intel-cli/tests/decision_port.rs new file mode 100644 index 0000000..a065b38 --- /dev/null +++ b/crates/code-intel-cli/tests/decision_port.rs @@ -0,0 +1,577 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/decision_port.rs"] +mod decision_port; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +use decision_port::{ + DecisionExchange, DecisionRequestResponsePort, FileDecisionPort, InMemoryDecisionPort, + NativeStructuredDecisionPort, PlainTextDecisionPort, +}; + +fn fixture(name: &str) -> Value { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/fixtures/decision-port") + .join(name); + serde_json::from_slice(&fs::read(path).expect("fixture should be readable")) + .expect("fixture should be valid JSON") +} + +fn branches(result: &Value) -> Vec<(&str, &str)> { + result["branches"] + .as_array() + .unwrap() + .iter() + .map(|branch| { + ( + branch["branchId"].as_str().unwrap(), + branch["status"].as_str().unwrap(), + ) + }) + .collect() +} + +fn temp_root(label: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + std::env::temp_dir().join(format!("code-intel-c06-{label}-{stamp}")) +} + +#[test] +fn in_memory_port_correlates_one_gap_and_preserves_branch_locality() { + let request = fixture("request.json"); + let response = fixture("valid-response.json"); + let mut port = InMemoryDecisionPort::default(); + port.supply_response(response).unwrap(); + let mut exchange = DecisionExchange::default(); + + let result = exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap(); + + assert_eq!(result["status"], "resolved"); + assert_eq!(result["acceptedAnswer"]["optionId"], "accept-risk"); + assert_eq!(result["authorityProvenance"]["actorId"], "release-owner"); + assert_eq!( + branches(&result), + vec![("inventory", "continues"), ("publication", "ready")] + ); + assert_eq!(result["effects"], json!([])); +} + +#[test] +fn in_memory_port_resolves_after_an_asynchronous_pending_poll() { + let request = fixture("request.json"); + let mut port = InMemoryDecisionPort::default(); + let mut exchange = DecisionExchange::default(); + let pending = exchange + .advance( + &request, + &mut port, + 1_700_000_015, + &["inventory", "publication"], + ) + .unwrap(); + assert_eq!(pending["status"], "pending"); + + port.supply_response(fixture("valid-response.json")) + .unwrap(); + let resolved = exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap(); + assert_eq!(resolved["status"], "resolved"); +} + +#[test] +fn pending_and_timeout_block_only_the_dependent_branch() { + let request = fixture("request.json"); + let mut port = InMemoryDecisionPort::default(); + let mut exchange = DecisionExchange::default(); + let pending = exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap(); + assert_eq!(pending["status"], "pending"); + assert_eq!( + branches(&pending), + vec![ + ("inventory", "continues"), + ("publication", "blocked_pending_response") + ] + ); + + let timeout = exchange + .advance( + &request, + &mut port, + 1_700_000_101, + &["inventory", "publication"], + ) + .unwrap(); + assert_eq!(timeout["status"], "timeout"); + assert_eq!( + branches(&timeout), + vec![ + ("inventory", "continues"), + ("publication", "blocked_timeout") + ] + ); +} + +#[test] +fn expired_processing_future_timestamp_and_wrong_correlation_are_rejected() { + let request = fixture("request.json"); + + let mut expired_port = InMemoryDecisionPort::default(); + expired_port + .supply_response(fixture("valid-response.json")) + .unwrap(); + assert!(DecisionExchange::default() + .advance( + &request, + &mut expired_port, + 1_700_000_100, + &["inventory", "publication"] + ) + .unwrap_err() + .to_string() + .contains("expired")); + + let mut future = fixture("valid-response.json"); + future["timestamp"] = json!(1_700_000_021_u64); + let mut future_port = InMemoryDecisionPort::default(); + future_port.supply_response(future).unwrap(); + assert!(DecisionExchange::default() + .advance( + &request, + &mut future_port, + 1_700_000_020, + &["inventory", "publication"] + ) + .unwrap_err() + .to_string() + .contains("future")); + + let mut wrong = fixture("valid-response.json"); + wrong["correlationId"] = json!("other-correlation"); + let mut wrong_port = InMemoryDecisionPort::default(); + wrong_port.supply_response(wrong).unwrap(); + assert!(DecisionExchange::default() + .advance( + &request, + &mut wrong_port, + 1_700_000_020, + &["inventory", "publication"] + ) + .unwrap_err() + .to_string() + .contains("correlation")); +} + +#[test] +fn wrong_gap_actor_and_stale_evidence_fail_closed_without_consuming_request() { + for fixture_name in [ + "wrong-gap-response.json", + "wrong-actor-response.json", + "stale-response.json", + ] { + let request = fixture("request.json"); + let mut port = InMemoryDecisionPort::default(); + port.supply_response(fixture(fixture_name)).unwrap(); + let mut exchange = DecisionExchange::default(); + assert!(exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"] + ) + .is_err()); + + port.supply_response(fixture("valid-response.json")) + .unwrap(); + let recovered = exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap(); + assert_eq!(recovered["status"], "resolved"); + } +} + +#[test] +fn replay_and_cancellation_fail_closed() { + let request = fixture("request.json"); + let mut port = InMemoryDecisionPort::default(); + port.supply_response(fixture("valid-response.json")) + .unwrap(); + let mut exchange = DecisionExchange::default(); + exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap(); + assert!(exchange + .advance( + &request, + &mut port, + 1_700_000_021, + &["inventory", "publication"] + ) + .unwrap_err() + .to_string() + .contains("replay")); + + let mut cancel_port = InMemoryDecisionPort::default(); + cancel_port + .supply_cancellation(fixture("cancellation.json")) + .unwrap(); + let mut cancel_exchange = DecisionExchange::default(); + let cancelled = cancel_exchange + .advance( + &request, + &mut cancel_port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap(); + assert_eq!(cancelled["status"], "cancelled"); + assert_eq!( + branches(&cancelled), + vec![ + ("inventory", "continues"), + ("publication", "blocked_cancelled") + ] + ); + assert_eq!(cancelled["effects"], json!([])); +} + +fn resolve_with(mut port: P) -> Value { + let request = fixture("request.json"); + let mut exchange = DecisionExchange::default(); + exchange + .advance( + &request, + &mut port, + 1_700_000_020, + &["inventory", "publication"], + ) + .unwrap() +} + +#[test] +fn native_plain_text_and_file_adapters_are_substitutable() { + let response = fixture("valid-response.json"); + + let mut native = NativeStructuredDecisionPort::default(); + native.supply_response(response.clone()).unwrap(); + assert_eq!(resolve_with(native)["status"], "resolved"); + + let mut plain = PlainTextDecisionPort::default(); + plain + .supply_line("choice\tdecision-42\trisk-gap-7\taccept-risk\trelease-owner\trisk_acceptance\tlocal-cli\t1700000020") + .unwrap(); + let plain_result = resolve_with(plain); + assert_eq!(plain_result["status"], "resolved"); + + let root = temp_root("file"); + fs::create_dir_all(&root).unwrap(); + let response_path = root.join("decision-42.response.json"); + fs::write( + &response_path, + serde_json::to_vec_pretty(&response).unwrap(), + ) + .unwrap(); + let file_result = resolve_with(FileDecisionPort::new(root.clone())); + fs::remove_dir_all(root).unwrap(); + assert_eq!(file_result["status"], "resolved"); +} + +#[test] +fn plain_text_request_is_lossless_for_every_authority_decision_field() { + let request = fixture("request.json"); + let mut port = PlainTextDecisionPort::default(); + port.submit(&request).unwrap(); + let rendered = port.outbox().join("\n"); + for required in [ + "recommendation", + "accept-risk", + "rollback is proven", + "options", + "consequence", + "evidenceRefs", + "risk-report", + "authorityNeeded", + "risk_acceptance", + "expiresAt", + "1700000100", + ] { + assert!( + rendered.contains(required), + "plain-text request dropped {required}: {rendered}" + ); + } +} + +#[test] +fn malformed_request_and_free_form_response_are_closed_and_supported() { + let mut invalid = fixture("request.json"); + invalid["questions"] = json!(["second question"]); + let mut exchange = DecisionExchange::default(); + let mut port = InMemoryDecisionPort::default(); + assert!(exchange + .advance( + &invalid, + &mut port, + 1_700_000_020, + &["inventory", "publication"] + ) + .is_err()); + + let mut free_form = fixture("valid-response.json"); + free_form["answer"] = json!({"kind": "free-form", "text": "accept with a seven-day review"}); + let mut free_port = InMemoryDecisionPort::default(); + free_port.supply_response(free_form).unwrap(); + let result = resolve_with(free_port); + assert_eq!(result["acceptedAnswer"]["kind"], "free-form"); +} + +#[test] +fn schemas_docs_and_registry_binding_exist() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + for path in [ + "orchestration/schemas/code-intel-decision-request.v1.schema.json", + "orchestration/schemas/code-intel-decision-response.v1.schema.json", + "orchestration/schemas/code-intel-decision-cancellation.v1.schema.json", + "orchestration/schemas/code-intel-decision-exchange-result.v1.schema.json", + "docs/decision-request-response-port.md", + ] { + assert!(root.join(path).is_file(), "missing {path}"); + } + let registry: Value = + serde_json::from_slice(&fs::read(root.join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let integration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "decision.request-response-port") + .expect("C06 production registry binding"); + assert_eq!( + integration["entrypoint"], + "crates/code-intel-cli/src/decision_port.rs" + ); + assert_eq!(integration["kind"], "internal-port"); + for contract in [ + "orchestration/schemas/code-intel-decision-cancellation.v1.schema.json", + "orchestration/schemas/code-intel-decision-exchange-result.v1.schema.json", + ] { + assert!(integration["artifactContract"] + .as_array() + .unwrap() + .iter() + .any(|item| item == contract)); + } +} + +#[test] +fn production_cli_routes_through_the_native_structured_adapter() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let fixtures = root.join("tests/fixtures/decision-port"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "decision", + "request-response", + "--request", + fixtures.join("request.json").to_str().unwrap(), + "--response", + fixtures.join("valid-response.json").to_str().unwrap(), + "--now", + "1700000020", + "--branch", + "inventory", + "--branch", + "publication", + ]) + .output() + .expect("production CLI should execute"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["status"], "resolved"); + assert_eq!(result["correlationId"], "decision-42"); +} + +#[test] +fn production_cli_rejects_duplicate_keys_with_a_machine_envelope() { + let root = temp_root("duplicate-json"); + fs::create_dir_all(&root).unwrap(); + let fixture_root = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/decision-port"); + let request_text = fs::read_to_string(fixture_root.join("request.json")).unwrap(); + let duplicate = request_text.replacen( + "\"question\":", + "\"question\": \"forged hidden question\",\n \"question\":", + 1, + ); + let request_path = root.join("duplicate-request.json"); + fs::write(&request_path, duplicate).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "decision", + "request-response", + "--request", + request_path.to_str().unwrap(), + "--response", + fixture_root.join("valid-response.json").to_str().unwrap(), + "--now", + "1700000020", + "--branch", + "inventory", + "--branch", + "publication", + ]) + .output() + .unwrap(); + fs::remove_dir_all(root).unwrap(); + assert_eq!(output.status.code(), Some(65)); + let envelope: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(envelope["status"], "rejected"); + assert!(envelope["diagnostics"][0] + .as_str() + .unwrap() + .contains("duplicate")); +} + +#[test] +fn file_response_and_cancellation_ingress_reject_duplicate_keys() { + let request = fixture("request.json"); + let fixture_root = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/decision-port"); + + let response_root = temp_root("duplicate-response"); + fs::create_dir_all(&response_root).unwrap(); + let response = fs::read_to_string(fixture_root.join("valid-response.json")) + .unwrap() + .replacen( + "\"answer\":", + "\"answer\": {\"kind\":\"choice\",\"optionId\":\"defer\"},\n \"answer\":", + 1, + ); + fs::write(response_root.join("decision-42.response.json"), response).unwrap(); + let mut response_port = FileDecisionPort::new(response_root.clone()); + assert!(DecisionExchange::default() + .advance( + &request, + &mut response_port, + 1_700_000_020, + &["inventory", "publication"] + ) + .unwrap_err() + .to_string() + .contains("duplicate")); + fs::remove_dir_all(response_root).unwrap(); + + let cancel_root = temp_root("duplicate-cancel"); + fs::create_dir_all(&cancel_root).unwrap(); + let cancellation = fs::read_to_string(fixture_root.join("cancellation.json")) + .unwrap() + .replacen("\"reason\":", "\"reason\": \"forged\",\n \"reason\":", 1); + fs::write(cancel_root.join("decision-42.cancel.json"), cancellation).unwrap(); + let mut cancel_port = FileDecisionPort::new(cancel_root.clone()); + assert!(DecisionExchange::default() + .advance( + &request, + &mut cancel_port, + 1_700_000_020, + &["inventory", "publication"] + ) + .unwrap_err() + .to_string() + .contains("duplicate")); + fs::remove_dir_all(cancel_root).unwrap(); +} + +#[test] +fn production_cli_exit_policy_distinguishes_pending_timeout_and_cancelled() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let fixtures = root.join("tests/fixtures/decision-port"); + let request_path = fixtures.join("request.json").to_string_lossy().into_owned(); + let cancellation_path = fixtures + .join("cancellation.json") + .to_string_lossy() + .into_owned(); + let cases = [ + (Vec::::new(), "1700000020", 10, "pending"), + (Vec::::new(), "1700000100", 11, "timeout"), + ( + vec!["--cancel".to_string(), cancellation_path], + "1700000020", + 12, + "cancelled", + ), + ]; + for (extra, now, exit, status) in cases { + let mut args = vec![ + "decision".to_string(), + "request-response".to_string(), + "--request".to_string(), + request_path.clone(), + ]; + args.extend(extra); + args.extend([ + "--now".to_string(), + now.to_string(), + "--branch".to_string(), + "inventory".to_string(), + "--branch".to_string(), + "publication".to_string(), + ]); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(args) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(exit)); + let envelope: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(envelope["status"], status); + } +} diff --git a/crates/code-intel-cli/tests/decision_record.rs b/crates/code-intel-cli/tests/decision_record.rs new file mode 100644 index 0000000..5205721 --- /dev/null +++ b/crates/code-intel-cli/tests/decision_record.rs @@ -0,0 +1,549 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/authority.rs"] +mod authority; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/decision_port.rs"] +mod decision_port; +#[path = "../src/decision_record.rs"] +mod decision_record; +#[path = "../src/run_commit.rs"] +mod run_commit; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; +#[path = "../src/staged_artifact.rs"] +mod staged_artifact; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const EVIDENCE: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +static NONCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-c07-{}-{now}-{}", + std::process::id(), + NONCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn resolution() -> Value { + let options = json!([ + {"id":"safe","label":"Use safe path","consequence":"slower delivery"}, + {"id":"fast","label":"Use fast path","consequence":"accept migration risk"} + ]); + let evidence = json!([{ + "refId":"ev-current","sha256":EVIDENCE,"observedAt":1900,"expiresAt":2200 + }]); + json!({ + "schema":"code-intel-decision-record-request.v1", + "gap":{ + "schema":"code-intel-decision-gap.v1","id":"gap-risk","kind":"risk_acceptance", + "blockedDecision":"choose migration path","discoverableFactsChecked":[{"factId":"fact-cost","status":"resolved"}], + "options":options,"recommendedAnswer":{"kind":"proposal","optionId":"safe","rationale":"preserve rollback"}, + "affectedBranches":["deploy"],"authorityRequired":true,"authorityState":"unresolved","effects":[] + }, + "request":{ + "schema":"code-intel-decision-request.v1","correlationId":"corr-risk-1","gapId":"gap-risk", + "question":"Which migration path is authorized?","recommendation":{"optionId":"safe","rationale":"preserve rollback"}, + "evidenceRefs":evidence,"options":options,"authorityNeeded":{"kind":"release_owner","actorIds":["alice"]}, + "issuedAt":1950,"expiresAt":2150,"affectedBranches":["deploy"] + }, + "response":{ + "schema":"code-intel-decision-response.v1","correlationId":"corr-risk-1","gapId":"gap-risk", + "answer":{"kind":"choice","optionId":"safe"}, + "actorProvenance":{"actorId":"alice","authorityKind":"release_owner","source":"native-ui"},"timestamp":2000 + }, + "authorityEvent":{ + "schema":"code-intel-authority-event.v1","id":"authority-risk-1","decision":"approved", + "approver":{"id":"alice","role":"release_owner"},"evidenceIds":["ev-current"],"issuedAt":1990,"expiresAt":2100 + }, + "snapshotIdentity":SNAPSHOT, + "recordedAt":2000 + }) +} + +fn replay_query() -> Value { + json!({ + "schema":"code-intel-decision-replay-query.v1","gapId":"gap-risk", + "snapshotIdentity":SNAPSHOT, + "evidenceRefs":[{"refId":"ev-current","sha256":EVIDENCE,"observedAt":1900,"expiresAt":2200}], + "affectedBranches":["deploy"],"now":2050 + }) +} + +fn committed_runs(root: &Path) -> Vec { + let mut runs = fs::read_dir(root) + .unwrap() + .filter_map(Result::ok) + .filter(|entry| { + entry.file_type().unwrap().is_dir() + && entry + .file_name() + .to_str() + .is_some_and(|name| name.starts_with("decision-")) + }) + .map(|entry| entry.path()) + .collect::>(); + runs.sort(); + runs +} + +fn spawn_record(resolution: &Path, store: &Path) -> std::process::Child { + Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["decision", "record", "--resolution"]) + .arg(resolution) + .arg("--store") + .arg(store) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap() +} + +fn wait_record_pair(mut children: [std::process::Child; 2]) -> [std::process::Output; 2] { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let exited = children + .iter_mut() + .map(|child| child.try_wait().unwrap()) + .collect::>(); + if exited.iter().all(Option::is_some) { + return children.map(|child| child.wait_with_output().unwrap()); + } + if Instant::now() >= deadline { + for (child, status) in children.iter_mut().zip(exited) { + if status.is_none() { + let _ = child.kill(); + } + } + let outputs = children.map(|child| child.wait_with_output().unwrap()); + panic!( + "decision record child pair timed out; stderr: {:?}", + outputs.map(|output| String::from_utf8_lossy(&output.stderr).into_owned()) + ); + } + std::thread::sleep(Duration::from_millis(10)); + } +} + +#[test] +fn resolve_once_then_replay_without_question() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + let resolution = resolution(); + let mut port = decision_port::InMemoryDecisionPort::default(); + port.supply_response(resolution["response"].clone()) + .unwrap(); + let exchange = decision_port::DecisionExchange::default() + .advance( + &resolution["request"], + &mut port, + resolution["recordedAt"].as_u64().unwrap(), + &["deploy", "unrelated"], + ) + .unwrap(); + assert_eq!(exchange["status"], "resolved"); + assert_eq!(exchange["branches"][1]["status"], "continues"); + + let recorded = store.record(&resolution).unwrap(); + assert_eq!(recorded["status"], "recorded"); + assert_eq!(recorded["record"]["acceptedChoice"]["optionId"], "safe"); + assert_eq!( + recorded["record"]["consequences"], + json!(["slower delivery"]) + ); + + let replayed = store.replay(&replay_query()).unwrap(); + assert_eq!(replayed["status"], "replay"); + assert_eq!(replayed["questionRequired"], false); + assert_eq!( + replayed["record"]["response"]["correlationId"], + "corr-risk-1" + ); + + let duplicate = store.record(&resolution).unwrap(); + assert_eq!(duplicate["status"], "replay"); + assert_eq!(duplicate["questionRequired"], false); +} + +#[test] +fn changed_evidence_or_snapshot_requires_reopen() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + store.record(&resolution()).unwrap(); + + let mut changed = replay_query(); + changed["evidenceRefs"][0]["sha256"] = json!("c".repeat(64)); + let result = store.replay(&changed).unwrap(); + assert_eq!(result["status"], "reopen"); + assert_eq!(result["reason"], "evidence_changed"); + assert_eq!(result["questionRequired"], true); + + let mut changed_snapshot = replay_query(); + changed_snapshot["snapshotIdentity"] = json!("d".repeat(64)); + assert_eq!( + store.replay(&changed_snapshot).unwrap()["reason"], + "snapshot_changed" + ); +} + +#[test] +fn stale_evidence_requires_reopen() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + store.record(&resolution()).unwrap(); + let mut query = replay_query(); + query["now"] = json!(2201); + assert_eq!(store.replay(&query).unwrap()["reason"], "evidence_stale"); +} + +#[test] +fn forged_authority_and_replayed_authority_event_are_rejected() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + let mut forged = resolution(); + forged["authorityEvent"]["approver"]["id"] = json!("mallory"); + assert!(store + .record(&forged) + .unwrap_err() + .to_string() + .contains("approver")); + + store.record(&resolution()).unwrap(); + let mut second = resolution(); + second["gap"]["id"] = json!("gap-second"); + second["request"]["gapId"] = json!("gap-second"); + second["response"]["gapId"] = json!("gap-second"); + second["request"]["correlationId"] = json!("corr-second"); + second["response"]["correlationId"] = json!("corr-second"); + assert!(store + .record(&second) + .unwrap_err() + .to_string() + .contains("replay")); +} + +#[test] +fn wrong_response_correlation_and_gap_are_rejected() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + let mut wrong_correlation = resolution(); + wrong_correlation["response"]["correlationId"] = json!("corr-wrong"); + assert!(store + .record(&wrong_correlation) + .unwrap_err() + .to_string() + .contains("correlation")); + + let mut wrong_gap = resolution(); + wrong_gap["response"]["gapId"] = json!("gap-wrong"); + assert!(store + .record(&wrong_gap) + .unwrap_err() + .to_string() + .contains("gap")); +} + +#[test] +fn branch_scope_mismatch_is_rejected_not_replayed() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + store.record(&resolution()).unwrap(); + let mut query = replay_query(); + query["affectedBranches"] = json!(["unrelated"]); + assert!(store + .replay(&query) + .unwrap_err() + .to_string() + .contains("branch scope")); +} + +#[test] +fn invalid_stored_records_are_ignored_with_diagnosis() { + let root = Temp::new(); + fs::write(root.0.join("forged.json"), b"{\"schema\":\"forged\"}").unwrap(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + let result = store.replay(&replay_query()).unwrap(); + assert_eq!(result["status"], "reopen"); + assert_eq!(result["reason"], "no_record"); + assert_eq!(result["diagnostics"].as_array().unwrap().len(), 1); +} + +#[test] +fn committed_store_uses_a06_a07_manifest_marker_and_real_schema_validation() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.join("records")); + let result = store.record(&resolution()).unwrap(); + let record = &result["record"]; + let bytes = serde_json::to_vec_pretty(record).unwrap(); + decision_record::validate_decision_record_artifact(&bytes).unwrap(); + + let runs = committed_runs(&root.0.join("records")); + assert_eq!(runs.len(), 1); + assert_eq!(run_commit::classify(&runs[0]), "committed"); + let (marker, manifest) = run_commit::validate_committed_run(&runs[0]).unwrap(); + assert_eq!(marker["schema"], "code-intel-run-commit.v1"); + assert_eq!(manifest["schema"], "code-intel-run-manifest.v1"); + assert!(runs[0].join("run-complete.json").is_file()); + let artifact = &manifest["nodes"]["decision_record"]["artifacts"][0]; + assert_eq!(artifact["artifactSchema"], "code-intel-decision-record.v1"); + assert_eq!(artifact["type"], "decision.record"); + let artifact_path = runs[0].join(artifact["path"].as_str().unwrap()); + let emitted = fs::read(&artifact_path).unwrap(); + decision_record::validate_decision_record_artifact(&emitted).unwrap(); + assert_eq!(serde_json::from_slice::(&emitted).unwrap(), *record); + + let schema_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/schemas"); + let checked_in_schema: Value = serde_json::from_slice( + &fs::read(schema_dir.join("code-intel-decision-record.v1.schema.json")).unwrap(), + ) + .unwrap(); + assert_eq!( + checked_in_schema["$id"], + "code-intel-decision-record.v1.schema.json" + ); + assert_eq!(checked_in_schema["additionalProperties"], false); + assert_eq!( + checked_in_schema["properties"]["schema"]["const"], + "code-intel-decision-record.v1" + ); + + let mut malformed = record.clone(); + malformed["consequences"] = json!([1]); + assert!(decision_record::validate_decision_record_artifact( + &serde_json::to_vec(&malformed).unwrap() + ) + .is_err()); + + let reopened = decision_record::DecisionRecordStore::new(root.0.join("records")); + assert_eq!( + reopened.replay(&replay_query()).unwrap()["status"], + "replay" + ); +} + +#[test] +fn store_lock_serializes_same_binding_and_authority_reuse_across_processes() { + let root = Temp::new(); + let same_store = root.0.join("same-store"); + let same_path = root.0.join("same.json"); + fs::write(&same_path, serde_json::to_vec(&resolution()).unwrap()).unwrap(); + let first = spawn_record(&same_path, &same_store); + let second = spawn_record(&same_path, &same_store); + let outputs = wait_record_pair([first, second]); + assert!(outputs.iter().all(|output| output.status.success())); + let mut statuses = outputs + .iter() + .map(|output| { + serde_json::from_slice::(&output.stdout).unwrap()["status"] + .as_str() + .unwrap() + .to_string() + }) + .collect::>(); + statuses.sort(); + assert_eq!(statuses, ["recorded", "replay"]); + assert_eq!(committed_runs(&same_store).len(), 1); + + let authority_store = root.0.join("authority-store"); + let first_path = root.0.join("first.json"); + let second_path = root.0.join("second.json"); + fs::write(&first_path, serde_json::to_vec(&resolution()).unwrap()).unwrap(); + let mut second_resolution = resolution(); + second_resolution["gap"]["id"] = json!("gap-second"); + second_resolution["request"]["gapId"] = json!("gap-second"); + second_resolution["response"]["gapId"] = json!("gap-second"); + second_resolution["request"]["correlationId"] = json!("corr-second"); + second_resolution["response"]["correlationId"] = json!("corr-second"); + fs::write( + &second_path, + serde_json::to_vec(&second_resolution).unwrap(), + ) + .unwrap(); + let first = spawn_record(&first_path, &authority_store); + let second = spawn_record(&second_path, &authority_store); + let outputs = wait_record_pair([first, second]); + assert_eq!( + outputs + .iter() + .filter(|output| output.status.success()) + .count(), + 1 + ); + assert_eq!(committed_runs(&authority_store).len(), 1); + let rejected = outputs + .iter() + .find(|output| !output.status.success()) + .unwrap(); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("authority event replay")); +} + +#[test] +fn binding_and_replay_reject_temporal_backdating() { + let root = Temp::new(); + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + let mut late = resolution(); + late["recordedAt"] = json!(2160); + late["authorityEvent"]["expiresAt"] = json!(2190); + assert!(store + .record(&late) + .unwrap_err() + .to_string() + .contains("outside request")); + + store.record(&resolution()).unwrap(); + let mut before_record = replay_query(); + before_record["now"] = json!(1999); + assert!(store + .replay(&before_record) + .unwrap_err() + .to_string() + .contains("precedes")); + let mut before_observation = replay_query(); + before_observation["evidenceRefs"][0]["observedAt"] = json!(2050); + before_observation["now"] = json!(2025); + assert!(store + .replay(&before_observation) + .unwrap_err() + .to_string() + .contains("observed evidence")); +} + +#[test] +fn store_entry_failures_and_symlinks_are_reported_not_silently_filtered() { + let root = Temp::new(); + fs::write(root.0.join("forged.json"), b"{\"schema\":\"forged\"}").unwrap(); + fs::create_dir(root.0.join("uncommitted-run")).unwrap(); + let mut expected = 2; + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(root.0.join("forged.json"), root.0.join("record-link")).unwrap(); + expected += 1; + } + #[cfg(windows)] + { + use std::os::windows::fs::symlink_file; + if symlink_file(root.0.join("forged.json"), root.0.join("record-link")).is_ok() { + expected += 1; + } + } + let store = decision_record::DecisionRecordStore::new(root.0.clone()); + let result = store.replay(&replay_query()).unwrap(); + let diagnostics = result["diagnostics"].as_array().unwrap(); + assert_eq!(diagnostics.len(), expected); + let text = serde_json::to_string(diagnostics).unwrap(); + assert!(text.contains("forged.json")); + assert!(text.contains("uncommitted-run")); + if expected == 3 { + assert!(text.contains("symlink")); + } +} + +#[test] +fn production_cli_registry_and_schema_are_wired() { + let root = Temp::new(); + let resolution_path = root.0.join("resolution.json"); + let query_path = root.0.join("query.json"); + let store_path = root.0.join("records"); + fs::write(&resolution_path, serde_json::to_vec(&resolution()).unwrap()).unwrap(); + fs::write(&query_path, serde_json::to_vec(&replay_query()).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "decision", + "record", + "--resolution", + resolution_path.to_str().unwrap(), + "--store", + store_path.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + serde_json::from_slice::(&output.stdout).unwrap()["status"], + "recorded" + ); + let replay = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "decision", + "replay", + "--query", + query_path.to_str().unwrap(), + "--store", + store_path.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!( + replay.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&replay.stderr) + ); + let replay: Value = serde_json::from_slice(&replay.stdout).unwrap(); + assert_eq!(replay["status"], "replay"); + assert_eq!(replay["questionRequired"], false); + + let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest: Value = + serde_json::from_slice(&fs::read(repo.join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let entry = manifest["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "decision.record") + .unwrap(); + assert_eq!(entry["required"], true); + assert!(entry["commands"]["record"] + .as_str() + .unwrap() + .contains("decision record")); + let schema: Value = serde_json::from_slice( + &fs::read(repo.join("orchestration/schemas/code-intel-decision-record.v1.schema.json")) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["properties"]["reopenRule"]["additionalProperties"], + false + ); +} diff --git a/crates/code-intel-cli/tests/delivery_light_speed.rs b/crates/code-intel-cli/tests/delivery_light_speed.rs new file mode 100644 index 0000000..0fa5f91 --- /dev/null +++ b/crates/code-intel-cli/tests/delivery_light_speed.rs @@ -0,0 +1,717 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = + std::env::temp_dir().join(format!("code-intel-d04-{}-{nonce}", std::process::id())); + fs::create_dir(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn sha256(path: &Path) -> String { + sha256_hex(&fs::read(path).unwrap()) +} + +fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0); + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (index, word) in chunk.chunks_exact(4).enumerate() { + w[index] = u32::from_be_bytes(word.try_into().unwrap()); + } + for index in 16..64 { + let s0 = w[index - 15].rotate_right(7) + ^ w[index - 15].rotate_right(18) + ^ (w[index - 15] >> 3); + let s1 = w[index - 2].rotate_right(17) + ^ w[index - 2].rotate_right(19) + ^ (w[index - 2] >> 10); + w[index] = w[index - 16] + .wrapping_add(s0) + .wrapping_add(w[index - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for index in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ (!e & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[index]) + .wrapping_add(w[index]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value); + } + } + h.iter().map(|value| format!("{value:08x}")).collect() +} + +fn event( + id: &str, + kind: &str, + subject: &str, + start: u64, + end: u64, + mandatory: bool, + coordination_need: Option<&str>, + predecessor: Option<&str>, +) -> Value { + event_with_predecessors( + id, + kind, + subject, + start, + end, + mandatory, + coordination_need, + predecessor.into_iter().collect(), + ) +} + +fn event_with_predecessors( + id: &str, + kind: &str, + subject: &str, + start: u64, + end: u64, + mandatory: bool, + coordination_need: Option<&str>, + predecessors: Vec<&str>, +) -> Value { + json!({ + "id":id, + "kind":kind, + "subject":subject, + "startedAtMs":start, + "completedAtMs":end, + "mandatory":mandatory, + "coordinationNeed":coordination_need, + "predecessors":predecessors + }) +} + +fn artifact_ref(schema: &str, artifact_type: &str, path: &str, sha256: &str) -> Value { + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":schema, + "type":artifact_type, + "path":path, + "sha256":sha256, + "consumedSnapshotIdentity":SNAPSHOT + }) +} + +fn write_artifact(root: &Path, relative: &str, value: &Value) -> Value { + let path = root.join(relative); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + json!({"path":relative.replace('\\', "/"),"sha256":sha256(&path)}) +} + +fn committed_trace(root: &Path, label: &str, run: &str, events: Vec) -> (Value, Vec) { + let manifest = json!({ + "schema":"code-intel-run-manifest.v1", + "runIdentity":run, + "snapshotIdentity":SNAPSHOT, + "outcome":"completed", + "nodes":{"measurement":{"status":"succeeded","verdict":"pass","artifacts":[]}} + }); + let manifest_bytes = serde_json::to_vec(&manifest).unwrap(); + let manifest_sha = sha256_hex(&manifest_bytes); + let manifest_path = format!("objects/sha256/{manifest_sha}"); + fs::create_dir_all(root.join("objects/sha256")).unwrap(); + fs::write(root.join(&manifest_path), manifest_bytes).unwrap(); + let manifest_ref = artifact_ref( + "code-intel-run-manifest.v1", + "run.manifest", + &manifest_path, + &manifest_sha, + ); + let commit = json!({ + "schema":"code-intel-run-commit.v1", + "runIdentity":run, + "snapshotIdentity":SNAPSHOT, + "manifest":{"path":manifest_path,"sha256":manifest_sha} + }); + let commit_meta = write_artifact(root, &format!("commits/{label}.json"), &commit); + let commit_ref = artifact_ref( + "code-intel-run-commit.v1", + "run.commit", + commit_meta["path"].as_str().unwrap(), + commit_meta["sha256"].as_str().unwrap(), + ); + ( + json!({"commitRef":commit_ref,"events":events}), + vec![commit_ref, manifest_ref], + ) +} + +fn method_inputs(root: &Path) -> Vec { + [ + ( + "code-intel-method-catalog.v1", + "method.catalog", + "methods/catalog.v1.json", + "../../orchestration/methods/catalog.v1.json", + ), + ( + "code-intel-method-card.v1", + "method.card", + "methods/cards/critical-path-pert.v1.json", + "../../orchestration/methods/cards/critical-path-pert.v1.json", + ), + ( + "code-intel-method-card.v1", + "method.card", + "methods/cards/value-stream-queue-delay.v1.json", + "../../orchestration/methods/cards/value-stream-queue-delay.v1.json", + ), + ] + .into_iter() + .map(|(schema, artifact_type, relative, source)| { + let source = Path::new(env!("CARGO_MANIFEST_DIR")).join(source); + let target = root.join(relative); + fs::create_dir_all(target.parent().unwrap()).unwrap(); + fs::copy(source, &target).unwrap(); + artifact_ref(schema, artifact_type, relative, &sha256(&target)) + }) + .collect() +} + +fn request(inputs: Vec, snapshot: &str) -> Value { + let registry: Value = serde_json::from_slice( + &fs::read( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../orchestration/integrations.json"), + ) + .unwrap(), + ) + .unwrap(); + let declaration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "delivery.light-speed-measure"); + let implementation = declaration + .map(|entry| entry["capabilityDeclaration"]["implementation"].clone()) + .unwrap_or_else(|| { + json!({"id":"delivery.light-speed-measure.compat","version":"1.0.0","toolchainDigests":["f".repeat(64)]}) + }); + json!({ + "schema":"code-intel-capability-request.v1", + "capability":"delivery.light-speed-measure", + "contractVersion":1, + "implementation":implementation, + "snapshot":{ + "identity":snapshot, + "repoIdentity":format!("content-v1:{}", "c".repeat(64)), + "head":"d04-current", + "workingTreePolicy":"explicit_overlay", + "scope":["."], + "inputDigest":"d".repeat(64) + }, + "options":{}, + "inputs":inputs, + "effectPolicy":{"allowedEffects":["local_write"]} + }) +} + +fn execute(root: &Path, request: &Value, name: &str) -> (i32, Vec, Vec, PathBuf) { + let request_path = root.join(format!("{name}-request.json")); + fs::write(&request_path, serde_json::to_vec(request).unwrap()).unwrap(); + let out = root.join(format!("{name}-out")); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "delivery.light-speed-measure", + "--request", + ]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .arg("--artifact-root") + .arg(root) + .output() + .unwrap(); + ( + output.status.code().unwrap_or(70), + output.stdout, + output.stderr, + out, + ) +} + +#[test] +fn committed_trace_attributes_queue_protects_mandatory_tests_and_reports_deterministic_delta() { + let temp = Temp::new(); + let (baseline, mut inputs) = committed_trace( + &temp.0, + "baseline", + &format!("dag-v1:{}", "1a".repeat(16)), + vec![ + event( + "b01", + "technical_work", + "implementation", + 0, + 100, + false, + None, + None, + ), + event( + "b02", + "queue", + "review-queue", + 100, + 150, + false, + None, + Some("b01"), + ), + event( + "b03", + "handoff", + "developer-reviewer", + 150, + 170, + false, + None, + Some("b02"), + ), + event( + "b04", + "understanding", + "core", + 170, + 200, + false, + None, + Some("b03"), + ), + event( + "b05", + "understanding", + "core", + 200, + 225, + false, + None, + Some("b04"), + ), + event( + "b06", + "test", + "mandatory-regression", + 225, + 265, + true, + None, + Some("b05"), + ), + event( + "b07", + "rework", + "defect-repair", + 265, + 280, + false, + None, + Some("b06"), + ), + event( + "b08", + "coordination", + "status-sync", + 280, + 290, + false, + Some("unnecessary"), + Some("b07"), + ), + event( + "b09", + "coordination", + "release-approval", + 290, + 295, + true, + Some("required"), + Some("b08"), + ), + ], + ); + let (current, current_inputs) = committed_trace( + &temp.0, + "current", + &format!("dag-v1:{}", "2b".repeat(16)), + vec![ + event( + "c01", + "technical_work", + "implementation", + 0, + 110, + false, + None, + None, + ), + event( + "c02", + "queue", + "review-queue", + 110, + 130, + false, + None, + Some("c01"), + ), + event( + "c03", + "handoff", + "developer-reviewer", + 130, + 140, + false, + None, + Some("c02"), + ), + event( + "c04", + "understanding", + "core", + 140, + 165, + false, + None, + Some("c03"), + ), + event( + "c05", + "understanding", + "core", + 165, + 170, + false, + None, + Some("c04"), + ), + event( + "c06", + "test", + "mandatory-regression", + 170, + 220, + true, + None, + Some("c05"), + ), + event( + "c07", + "rework", + "defect-repair", + 220, + 225, + false, + None, + Some("c06"), + ), + event( + "c08", + "coordination", + "release-approval", + 225, + 230, + true, + Some("required"), + Some("c07"), + ), + ], + ); + let telemetry = json!({ + "schema":"code-intel-run-timing-events.v1", + "measurementSnapshotIdentity":SNAPSHOT, + "telemetry":{"mode":"local_opt_in","clock":"monotonic_elapsed_ms","externalPlatform":false}, + "baseline":baseline, + "current":current + }); + inputs.extend(current_inputs); + inputs.extend(method_inputs(&temp.0)); + let telemetry_path = temp.0.join("timing-events.json"); + fs::write(&telemetry_path, serde_json::to_vec(&telemetry).unwrap()).unwrap(); + inputs.push(artifact_ref( + "code-intel-run-timing-events.v1", + "delivery.run-timing-events", + "timing-events.json", + &sha256(&telemetry_path), + )); + let valid_request = request(inputs.clone(), SNAPSHOT); + + let (left_exit, _, left_stderr, left_out) = execute(&temp.0, &valid_request, "left"); + assert_eq!(left_exit, 0, "{}", String::from_utf8_lossy(&left_stderr)); + let (right_exit, _, right_stderr, right_out) = execute(&temp.0, &valid_request, "right"); + assert_eq!(right_exit, 0, "{}", String::from_utf8_lossy(&right_stderr)); + let left_bytes = fs::read(left_out.join("light-speed-report.json")).unwrap(); + let right_bytes = fs::read(right_out.join("light-speed-report.json")).unwrap(); + assert_eq!( + left_bytes, right_bytes, + "same committed traces must replay byte-for-byte" + ); + let report: Value = serde_json::from_slice(&left_bytes).unwrap(); + assert_eq!(report["schema"], "code-intel-delivery-light-speed.v1"); + assert_eq!( + report["authority"], + "derived_measurement_no_schedule_commitment" + ); + assert_eq!(report["baseline"]["categories"]["queueMs"], 50); + assert_eq!( + report["baseline"]["categories"]["mandatoryVerificationMs"], + 40 + ); + assert_eq!( + report["baseline"]["categories"]["repeatedUnderstandingMs"], + 25 + ); + assert_eq!( + report["baseline"]["categories"]["unnecessaryCoordinationMs"], + 10 + ); + assert_eq!(report["baseline"]["avoidableDelayMs"], 120); + assert_eq!(report["baseline"]["criticalPath"]["queueMs"], 50); + assert_eq!( + report["baseline"]["criticalPath"]["mandatoryVerificationMs"], + 40 + ); + assert_eq!(report["delta"]["avoidableDelayMs"], -80); + assert_eq!(report["delta"]["leadTimeMs"], -65); + assert_eq!( + report["baseline"]["commitRef"], + telemetry["baseline"]["commitRef"] + ); + assert_eq!( + report["method"]["provenance"]["methodCardSha256"] + .as_array() + .unwrap() + .len(), + 2 + ); + assert!(report.get("schedulePromise").is_none()); + let queue_provenance = report["baseline"]["provenance"]["queueMs"] + .as_object() + .unwrap(); + assert_eq!( + queue_provenance["sourceArtifactSha256"], + sha256(&telemetry_path) + ); + assert_eq!(queue_provenance["eventIds"], json!(["b02"])); + assert!(report["rules"].as_array().unwrap().iter().any(|rule| { + rule["id"] == "mandatory-verification-protection" + && rule["methodCardIds"] == json!(["critical-path-pert", "value-stream-queue-delay"]) + })); + + let mismatch = request(inputs.clone(), &"9".repeat(64)); + let (mismatch_exit, _, _, mismatch_out) = execute(&temp.0, &mismatch, "mismatch"); + assert_eq!(mismatch_exit, 65); + assert!(!mismatch_out.join("light-speed-report.json").exists()); + + let mut unprotected = telemetry.clone(); + unprotected["current"]["events"][5]["mandatory"] = json!(false); + let unprotected_path = temp.0.join("unprotected-timing-events.json"); + fs::write(&unprotected_path, serde_json::to_vec(&unprotected).unwrap()).unwrap(); + let mut unprotected_inputs = inputs.clone(); + let timing_input = unprotected_inputs.last_mut().unwrap(); + timing_input["path"] = json!("unprotected-timing-events.json"); + timing_input["sha256"] = json!(sha256(&unprotected_path)); + let unprotected_request = request(unprotected_inputs, SNAPSHOT); + let (unprotected_exit, _, _, unprotected_out) = + execute(&temp.0, &unprotected_request, "unprotected"); + assert_eq!(unprotected_exit, 65); + assert!(!unprotected_out.join("light-speed-report.json").exists()); + + let missing_commit_path = temp.0.join("commits/baseline.json"); + let missing_commit_bytes = fs::read(&missing_commit_path).unwrap(); + fs::remove_file(&missing_commit_path).unwrap(); + let (missing_exit, _, _, missing_out) = execute(&temp.0, &valid_request, "missing-commit"); + assert_eq!(missing_exit, 65); + assert!(!missing_out.join("light-speed-report.json").exists()); + fs::write(&missing_commit_path, missing_commit_bytes).unwrap(); + + let mut missing_manifest_inputs = inputs.clone(); + let manifest_index = missing_manifest_inputs + .iter() + .position(|input| input["artifactSchema"] == "code-intel-run-manifest.v1") + .expect("A07 manifest Artifact Ref must be present in the valid request"); + missing_manifest_inputs.remove(manifest_index); + let missing_manifest_request = request(missing_manifest_inputs, SNAPSHOT); + let (missing_manifest_exit, _, _, missing_manifest_out) = execute( + &temp.0, + &missing_manifest_request, + "missing-a07-manifest-object", + ); + assert_eq!(missing_manifest_exit, 65); + assert!(!missing_manifest_out + .join("light-speed-report.json") + .exists()); + assert!(!missing_manifest_out.join("light-speed-report.md").exists()); + + let tampered_card = temp + .0 + .join("methods/cards/value-stream-queue-delay.v1.json"); + let mut card: Value = serde_json::from_slice(&fs::read(&tampered_card).unwrap()).unwrap(); + card["deterministicSteps"][1]["action"] = json!("tampered but structurally valid formula"); + fs::write(&tampered_card, serde_json::to_vec(&card).unwrap()).unwrap(); + let mut tampered_inputs = inputs.clone(); + let card_ref = tampered_inputs + .iter_mut() + .find(|input| input["path"] == "methods/cards/value-stream-queue-delay.v1.json") + .unwrap(); + card_ref["sha256"] = json!(sha256(&tampered_card)); + let tampered_request = request(tampered_inputs, SNAPSHOT); + let (tampered_exit, _, _, tampered_out) = execute(&temp.0, &tampered_request, "tampered-card"); + assert_eq!(tampered_exit, 65); + assert!(!tampered_out.join("light-speed-report.json").exists()); +} + +#[test] +fn critical_path_is_predecessor_closed_for_a_diamond_join() { + let temp = Temp::new(); + let (baseline, mut inputs) = committed_trace( + &temp.0, + "diamond-baseline", + &format!("dag-v1:{}", "3c".repeat(16)), + vec![event( + "base", + "technical_work", + "base", + 0, + 5, + false, + None, + None, + )], + ); + let (current, current_inputs) = committed_trace( + &temp.0, + "diamond-current", + &format!("dag-v1:{}", "4d".repeat(16)), + vec![ + event("a", "technical_work", "root", 0, 10, false, None, None), + event( + "b", + "technical_work", + "left", + 10, + 30, + false, + None, + Some("a"), + ), + event( + "c", + "technical_work", + "right", + 10, + 25, + false, + None, + Some("a"), + ), + event_with_predecessors( + "d", + "verification", + "join", + 30, + 40, + true, + None, + vec!["b", "c"], + ), + ], + ); + inputs.extend(current_inputs); + inputs.extend(method_inputs(&temp.0)); + let telemetry = json!({ + "schema":"code-intel-run-timing-events.v1", + "measurementSnapshotIdentity":SNAPSHOT, + "telemetry":{"mode":"local_opt_in","clock":"monotonic_elapsed_ms","externalPlatform":false}, + "baseline":baseline, + "current":current + }); + let timing = write_artifact(&temp.0, "diamond-timing.json", &telemetry); + inputs.push(artifact_ref( + "code-intel-run-timing-events.v1", + "delivery.run-timing-events", + timing["path"].as_str().unwrap(), + timing["sha256"].as_str().unwrap(), + )); + let (exit, _, stderr, out) = execute(&temp.0, &request(inputs, SNAPSHOT), "diamond"); + assert_eq!(exit, 0, "{}", String::from_utf8_lossy(&stderr)); + let report: Value = + serde_json::from_slice(&fs::read(out.join("light-speed-report.json")).unwrap()).unwrap(); + assert_eq!( + report["current"]["criticalPath"]["eventIds"], + json!(["a", "b", "c", "d"]) + ); + assert_eq!(report["current"]["criticalPath"]["durationMs"], 55); +} diff --git a/crates/code-intel-cli/tests/doctor_envelope.rs b/crates/code-intel-cli/tests/doctor_envelope.rs new file mode 100644 index 0000000..69979e7 --- /dev/null +++ b/crates/code-intel-cli/tests/doctor_envelope.rs @@ -0,0 +1,290 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +fn temp_dir() -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + std::env::temp_dir().join(format!("code-intel-b10-{}-{nonce}", std::process::id())) +} + +fn manifest_path() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("orchestration") + .join("integrations.json") +} + +fn declaration(id: &str) -> Value { + let registry: Value = + serde_json::from_slice(&fs::read(manifest_path()).expect("read registry")) + .expect("parse registry"); + registry["integrations"] + .as_array() + .expect("integrations") + .iter() + .find_map(|integration| { + (integration + .pointer("/capabilityDeclaration/id") + .and_then(Value::as_str) + == Some(id)) + .then(|| integration["capabilityDeclaration"].clone()) + }) + .expect("registered declaration") +} + +fn snapshot(repo: &Path) -> Value { + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["snapshot", "identity", "--repo"]) + .arg(repo) + .args(["--working-tree-policy", "explicit_overlay", "--scope", "."]) + .output() + .expect("snapshot identity"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice::(&output.stdout).expect("snapshot JSON")["snapshot"].clone() +} + +fn request(capability: &str, snapshot: &Value, options: Value, inputs: Value) -> Value { + let declaration = declaration(capability); + json!({ + "schema":"code-intel-capability-request.v1", + "capability":capability, + "contractVersion":1, + "implementation":declaration["implementation"], + "snapshot":snapshot, + "options":options, + "inputs":inputs, + "effectPolicy":{"allowedEffects":declaration["allowedEffects"]} + }) +} + +fn exec( + request: &Value, + capability: &str, + request_path: &Path, + out: &Path, + artifact_root: Option<&Path>, + path_prefix: Option<&Path>, +) -> std::process::Output { + fs::write(request_path, serde_json::to_vec(request).unwrap()).expect("write request"); + let mut command = Command::new(env!("CARGO_BIN_EXE_code-intel")); + command + .args(["capability", "exec", capability, "--request"]) + .arg(request_path) + .arg("--out") + .arg(out) + .arg("--manifest") + .arg(manifest_path()); + if let Some(root) = artifact_root { + command.arg("--artifact-root").arg(root); + } + if let Some(prefix) = path_prefix { + let mut paths = vec![prefix.to_path_buf()]; + paths.extend(std::env::split_paths( + &std::env::var_os("PATH").unwrap_or_default(), + )); + command.env("PATH", std::env::join_paths(paths).expect("fixture PATH")); + } + command.output().expect("capability exec") +} + +#[test] +fn doctor_cli_emits_one_envelope_and_snapshot_bound_redacted_observation() { + let root = temp_dir(); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + let snapshot = snapshot(&repo); + + let snapshot_out = root.join("snapshot"); + let snapshot_result = exec( + &request( + "repo.snapshot", + &snapshot, + json!({"repoPath":repo}), + json!([]), + ), + "repo.snapshot", + &root.join("snapshot-request.json"), + &snapshot_out, + None, + None, + ); + assert_eq!(snapshot_result.status.code(), Some(0)); + let snapshot_envelope: Value = serde_json::from_slice(&snapshot_result.stdout).unwrap(); + let mut input = snapshot_envelope["artifacts"][0].clone(); + input["path"] = json!("snapshot/snapshot.json"); + + let doctor_out = root.join("doctor"); + let doctor_result = exec( + &request( + "doctor", + &snapshot, + json!({"repoPath":repo,"requireRepowise":false,"requireUnderstand":false}), + json!([input]), + ), + "doctor", + &root.join("doctor-request.json"), + &doctor_out, + Some(&root), + None, + ); + assert!(matches!(doctor_result.status.code(), Some(0 | 10))); + let text = String::from_utf8(doctor_result.stdout).unwrap(); + let mut stream = serde_json::Deserializer::from_str(&text).into_iter::(); + let envelope = stream.next().unwrap().unwrap(); + assert!( + stream.next().is_none(), + "stdout must contain exactly one JSON result" + ); + assert_eq!(envelope["capability"], "doctor"); + assert_eq!(envelope["status"], "completed"); + assert_eq!(envelope["artifacts"].as_array().unwrap().len(), 1); + assert_eq!( + envelope["artifacts"][0]["consumedSnapshotIdentity"], + snapshot["identity"] + ); + let observation_text = fs::read_to_string(doctor_out.join("doctor-observation.json")).unwrap(); + let observation: Value = serde_json::from_str(&observation_text).unwrap(); + assert_eq!(observation["schema"], "code-intel-doctor-observation.v1"); + assert_eq!(observation["engineeringFacts"], json!([])); + assert!(!observation_text.to_ascii_lowercase().contains("password=")); + fs::remove_dir_all(root).ok(); +} + +#[test] +fn doctor_cli_fails_closed_without_verified_snapshot_input() { + let root = temp_dir(); + let repo = root.join("repo"); + fs::create_dir_all(&repo).unwrap(); + let snapshot = snapshot(&repo); + let out = root.join("doctor"); + let output = exec( + &request("doctor", &snapshot, json!({"repoPath":repo}), json!([])), + "doctor", + &root.join("request.json"), + &out, + None, + None, + ); + assert_eq!(output.status.code(), Some(65)); + let envelope: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(envelope["status"], "failed"); + assert_eq!(envelope["verdict"], "unknown"); + assert!(envelope["artifacts"].as_array().unwrap().is_empty()); + assert!(!out.exists()); + fs::remove_dir_all(root).ok(); +} + +#[test] +fn doctor_cli_reports_nonconforming_provider_and_manifest_drift_as_domain_failure() { + let root = temp_dir(); + let repo = root.join("repo"); + let fixture_bin = root.join("fixture-bin"); + fs::create_dir_all(&repo).unwrap(); + fs::create_dir_all(&fixture_bin).unwrap(); + fs::write(repo.join("README.md"), "fixture\n").unwrap(); + fs::write( + fixture_bin.join("sentrux.cmd"), + "@echo off\r\necho Authorization: Bearer fixture-secret-token\r\nexit /b 0\r\n", + ) + .unwrap(); + + let mut drift: Value = serde_json::from_slice(&fs::read(manifest_path()).unwrap()).unwrap(); + let doctor = drift["integrations"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry.pointer("/capabilityDeclaration/id") == Some(&json!("doctor"))) + .unwrap(); + doctor["capabilityDeclaration"]["implementation"]["adapter"] = json!("doctor.envelope.drifted"); + let drift_path = root.join("integrations-drift.json"); + fs::write(&drift_path, serde_json::to_vec(&drift).unwrap()).unwrap(); + + let snapshot = snapshot(&repo); + let snapshot_out = root.join("snapshot"); + let snapshot_result = exec( + &request( + "repo.snapshot", + &snapshot, + json!({"repoPath":repo}), + json!([]), + ), + "repo.snapshot", + &root.join("snapshot-request.json"), + &snapshot_out, + None, + None, + ); + assert_eq!(snapshot_result.status.code(), Some(0)); + let snapshot_envelope: Value = serde_json::from_slice(&snapshot_result.stdout).unwrap(); + let mut input = snapshot_envelope["artifacts"][0].clone(); + input["path"] = json!("snapshot/snapshot.json"); + + let doctor_out = root.join("doctor"); + let doctor_result = exec( + &request( + "doctor", + &snapshot, + json!({ + "repoPath":repo, + "manifestPath":drift_path, + "requireRepowise":false, + "requireUnderstand":false + }), + json!([input]), + ), + "doctor", + &root.join("doctor-request.json"), + &doctor_out, + Some(&root), + Some(&fixture_bin), + ); + + assert_eq!(doctor_result.status.code(), Some(10)); + assert!(doctor_result.stderr.is_empty()); + let stdout = String::from_utf8(doctor_result.stdout).unwrap(); + let mut stream = serde_json::Deserializer::from_str(&stdout).into_iter::(); + let envelope = stream.next().unwrap().unwrap(); + assert!(stream.next().is_none(), "stdout must be one JSON result"); + assert_eq!(envelope["status"], "completed"); + assert_eq!(envelope["verdict"], "fail"); + assert_eq!(envelope["exitCode"], 10); + assert_eq!(envelope["artifacts"].as_array().unwrap().len(), 1); + assert_eq!( + envelope["artifacts"][0]["consumedSnapshotIdentity"], + snapshot["identity"] + ); + let diagnostic_text = serde_json::to_string(&envelope["diagnostics"]).unwrap(); + assert!(diagnostic_text.contains("provider conformance failed")); + assert!(diagnostic_text.contains("manifest reconciliation failed")); + assert!(!diagnostic_text.contains("fixture-secret-token")); + + let observation_text = fs::read_to_string(doctor_out.join("doctor-observation.json")).unwrap(); + let observation: Value = serde_json::from_str(&observation_text).unwrap(); + let sentrux = observation["providers"] + .as_array() + .unwrap() + .iter() + .find(|provider| provider["id"] == "sentrux") + .unwrap(); + assert_eq!(sentrux["presence"], "present"); + assert_eq!(sentrux["readiness"], "unavailable"); + assert_eq!(sentrux["conformance"], "nonconforming"); + assert_eq!(sentrux["admissibility"], "not_evaluated"); + assert_eq!(observation["manifest"]["reconciled"], false); + assert_eq!(observation["engineeringFacts"], json!([])); + assert!(!observation_text.contains("fixture-secret-token")); + assert!(!observation_text.contains(root.to_string_lossy().as_ref())); + fs::remove_dir_all(root).ok(); +} diff --git a/crates/code-intel-cli/tests/evidence_admissibility.rs b/crates/code-intel-cli/tests/evidence_admissibility.rs new file mode 100644 index 0000000..7b12382 --- /dev/null +++ b/crates/code-intel-cli/tests/evidence_admissibility.rs @@ -0,0 +1,343 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const PAYLOAD: &[u8] = br#"{"schema":"code-intel-evidence-payload.v1","data":{"files":3}}"#; +const PAYLOAD_SHA: &str = "4669d0e1cbe1105783b6eeaaae64e00b392860c50c65cdb2cdd953fa8c2fdbca"; +static SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-a04-{}-{nonce}-{}", + std::process::id(), + SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn good_request(provider: &str) -> Value { + json!({ + "schema":"code-intel-evidence-admissibility-request.v1", + "expectedSnapshotIdentity":SNAPSHOT, + "policy":{"evaluatedAt":1_700_000_100u64,"maxAgeSeconds":300u64}, + "observation":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{"id":provider,"implementation":{"id":"synthetic.adapter","version":"1.2.3","digest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}, + "source":{"revision":"fixture-r17"}, + "consumedSnapshotIdentity":SNAPSHOT, + "observedAt":1_700_000_000u64, + "completeness":"complete", + "claimedComplete":true, + "payload":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-evidence-payload.v1","type":"observed.evidence.payload","path":"payload.json","sha256":PAYLOAD_SHA,"consumedSnapshotIdentity":SNAPSHOT}, + "provenance":{"collectionId":"fixture-1","command":"synthetic --emit","startedAt":1_699_999_999u64,"completedAt":1_700_000_000u64}, + "failure":{"kind":"none"} + } + }) +} + +fn run(root: &Path, request: &Value) -> (i32, Value, String) { + run_with_payload(root, request, PAYLOAD) +} + +fn run_with_payload(root: &Path, request: &Value, payload: &[u8]) -> (i32, Value, String) { + fs::write(root.join("payload.json"), payload).unwrap(); + let request_path = root.join("request.json"); + fs::write(&request_path, serde_json::to_vec(request).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "evidence", + "validate", + "--request", + request_path.to_str().unwrap(), + "--artifact-root", + root.to_str().unwrap(), + ]) + .output() + .unwrap(); + let value = serde_json::from_slice(&output.stdout).unwrap_or_else(|_| { + panic!( + "stdout: {} stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + }); + ( + output.status.code().unwrap(), + value, + String::from_utf8_lossy(&output.stderr).into_owned(), + ) +} + +#[test] +fn unknown_synthetic_provider_is_admitted_without_registry_branching() { + let temp = Temp::new(); + let (exit, result, stderr) = run(&temp.0, &good_request("provider.never-registered")); + assert_eq!(exit, 0, "{stderr}"); + assert_eq!(result["status"], "admitted"); + assert_eq!(result["domainVerdict"], "observed"); + assert_eq!(result["engineeringFacts"], json!([])); + assert_eq!(result["verifiedPayload"]["data"], json!({"files":3})); + assert_eq!( + result["evidence"]["provider"]["id"], + "provider.never-registered" + ); +} + +#[test] +fn snapshot_and_digest_mismatch_fail_closed_without_fact() { + for mutation in ["snapshot", "digest"] { + let temp = Temp::new(); + let mut request = good_request("synthetic"); + if mutation == "snapshot" { + request["observation"]["consumedSnapshotIdentity"] = json!("c".repeat(64)); + } else { + request["observation"]["payload"]["sha256"] = json!("d".repeat(64)); + } + let (exit, result, _) = run(&temp.0, &request); + assert_eq!(exit, 65, "mutation={mutation}"); + assert_eq!(result["status"], "rejected"); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["engineeringFacts"], json!([])); + } +} + +#[test] +fn stale_malformed_and_incomplete_as_complete_fail_closed() { + let cases: Vec> = vec![ + Box::new(|v| v["policy"]["evaluatedAt"] = json!(1_700_000_301u64)), + Box::new(|v| { + v["observation"]["provider"] + .as_object_mut() + .unwrap() + .remove("implementation"); + }), + Box::new(|v| { + v["observation"]["provider"]["implementation"]["digest"] = json!("not-a-digest") + }), + Box::new(|v| { + v["observation"]["completeness"] = json!("partial"); + v["observation"]["claimedComplete"] = json!(true); + }), + Box::new(|v| { + v["observation"]["failure"]["kind"] = json!("provider_unavailable"); + }), + Box::new(|v| { + v["observation"]["source"] = json!({}); + }), + Box::new(|v| { + v["observation"]["source"] = json!({"revision":"r1","endpointIdentity":"endpoint@1"}); + }), + Box::new(|v| v["observation"]["provenance"]["collectionId"] = json!("")), + Box::new(|v| { + v["observation"]["provenance"] + .as_object_mut() + .unwrap() + .remove("command"); + }), + Box::new(|v| { + v["observation"]["provenance"]["completedAt"] = json!(1_699_999_998u64); + }), + ]; + for mutate in cases { + let temp = Temp::new(); + let mut request = good_request("synthetic"); + mutate(&mut request); + let (exit, result, _) = run(&temp.0, &request); + assert_eq!(exit, 65); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["engineeringFacts"], json!([])); + } +} + +#[test] +fn freshness_boundaries_clock_types_and_replay_identity_are_deterministic() { + let temp = Temp::new(); + let mut boundary = good_request("synthetic.clock"); + boundary["policy"]["evaluatedAt"] = json!(1_700_000_300u64); + let (exit, first, _) = run(&temp.0, &boundary); + assert_eq!(exit, 0); + let (exit, replay, _) = run(&temp.0, &boundary); + assert_eq!(exit, 0); + assert_eq!(first["admissionIdentity"], replay["admissionIdentity"]); + assert_eq!(first["engineeringFacts"], json!([])); + + let cases: Vec> = vec![ + Box::new(|v| v["policy"]["evaluatedAt"] = json!(1_700_000_301u64)), + Box::new(|v| v["policy"]["evaluatedAt"] = json!(1_699_999_999u64)), + Box::new(|v| v["policy"]["evaluatedAt"] = json!("1700000100")), + Box::new(|v| v["policy"]["maxAgeSeconds"] = json!(0)), + ]; + for mutate in cases { + let temp = Temp::new(); + let mut request = good_request("synthetic.clock"); + mutate(&mut request); + let (exit, result, _) = run(&temp.0, &request); + assert_eq!(exit, 65); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["engineeringFacts"], json!([])); + } +} + +#[test] +fn completeness_and_failure_semantics_are_exhaustive() { + for (completeness, claimed, kind, message, accepted, verdict) in [ + ("complete", true, "none", None, true, "observed"), + ("partial", false, "none", None, true, "unknown"), + ( + "partial", + false, + "domain_unknown", + Some("not covered"), + true, + "unknown", + ), + ( + "partial", + false, + "provider_unavailable", + Some("offline"), + true, + "unknown", + ), + ( + "partial", + false, + "process_failure", + Some("crashed"), + false, + "unknown", + ), + ( + "complete", + true, + "domain_unknown", + Some("contradiction"), + false, + "unknown", + ), + ] { + let temp = Temp::new(); + let mut request = good_request("synthetic.failure"); + request["observation"]["completeness"] = json!(completeness); + request["observation"]["claimedComplete"] = json!(claimed); + request["observation"]["failure"] = match message { + Some(m) => json!({"kind":kind,"message":m}), + None => json!({"kind":kind}), + }; + let (exit, result, _) = run(&temp.0, &request); + assert_eq!(exit, if accepted { 0 } else { 65 }, "{completeness}/{kind}"); + assert_eq!(result["domainVerdict"], verdict); + assert_eq!(result["engineeringFacts"], json!([])); + } +} + +#[test] +fn payload_schema_is_checked_after_digest_verification() { + let temp = Temp::new(); + let payload = br#"{"schema":"wrong.v1","data":{"files":3}}"#; + let mut request = good_request("synthetic.payload"); + request["observation"]["payload"]["sha256"] = + json!("cfa6e883b9208641a7de6bd394db4ff63c8950a8712593a704d192cdd71556b9"); + let (exit, result, stderr) = run_with_payload(&temp.0, &request, payload); + assert_eq!(exit, 65); + assert!(stderr.contains("schema/data"), "{stderr}"); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["engineeringFacts"], json!([])); +} + +#[test] +fn endpoint_identity_can_replace_source_revision_and_partial_is_preserved() { + let temp = Temp::new(); + let mut request = good_request("synthetic.endpoint"); + request["observation"]["source"] = + json!({"endpointIdentity":"https://fixture.invalid/api@etag-7"}); + request["observation"]["completeness"] = json!("partial"); + request["observation"]["claimedComplete"] = json!(false); + request["observation"]["failure"] = + json!({"kind":"domain_unknown","message":"coverage unavailable"}); + let (exit, result, stderr) = run(&temp.0, &request); + assert_eq!(exit, 0, "{stderr}"); + assert_eq!(result["status"], "admitted"); + assert_eq!(result["evidence"]["completeness"], "partial"); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["engineeringFacts"], json!([])); +} + +#[test] +fn checked_in_provider_protocol_fixture_is_executable() { + let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("tests/fixtures/evidence-admissibility/good"); + let request: Value = + serde_json::from_slice(&fs::read(fixture.join("request.json")).unwrap()).unwrap(); + let request_copy = Temp::new(); + fs::copy( + fixture.join("payload.json"), + request_copy.0.join("payload.json"), + ) + .unwrap(); + let request_path = request_copy.0.join("request.json"); + fs::write(&request_path, serde_json::to_vec(&request).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "evidence", + "validate", + "--request", + request_path.to_str().unwrap(), + "--artifact-root", + request_copy.0.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn duplicate_key_malformed_request_returns_unknown_without_fact() { + let temp = Temp::new(); + fs::write(temp.0.join("payload.json"), PAYLOAD).unwrap(); + let request_path = temp.0.join("request.json"); + fs::write( + &request_path, + br#"{"schema":"code-intel-evidence-admissibility-request.v1","schema":"duplicate"}"#, + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "evidence", + "validate", + "--request", + request_path.to_str().unwrap(), + "--artifact-root", + temp.0.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(65)); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["engineeringFacts"], json!([])); +} diff --git a/crates/code-intel-cli/tests/file_boundary.rs b/crates/code-intel-cli/tests/file_boundary.rs new file mode 100644 index 0000000..0a813c0 --- /dev/null +++ b/crates/code-intel-cli/tests/file_boundary.rs @@ -0,0 +1,180 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use serde_json::{json, Value}; + +#[path = "../src/file_boundary.rs"] +mod file_boundary; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const OTHER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const SOURCE: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + +fn request(target: &str) -> Value { + json!({ + "schema":"code-intel-file-boundary-request.v1", + "targetFile":target, + "expectedSnapshotIdentity":SNAPSHOT, + "policy":{"evaluatedAt":2_000,"maxAgeSeconds":100}, + "document":{ + "schema":"code-intel-file-boundary-document.v1", + "snapshotIdentity":SNAPSHOT, + "observedAt":1_950, + "source":{ + "kind":"local_boundary_document", + "path":".aigx/files.aigx", + "sha256":SOURCE + }, + "entries":[{ + "path":"src/lib.rs", + "role":"library boundary", + "forbid":[{"id":"BOUNDARY-001","summary":"Do not import provider storage"}], + "gotcha":[{"id":"GOTCHA-001","summary":"Preserve snapshot identity"}], + "checks":[{"id":"CHECK-001","command":"cargo test -p code-intel --test file_boundary"}] + }], + "unsupportedConstructs":[] + } + }) +} + +#[test] +fn exact_file_resolution_is_normalized_snapshot_bound_and_complete() { + let result = file_boundary::resolve(&request(".\\src\\lib.rs")).unwrap(); + assert_eq!(result["status"], "resolved"); + assert_eq!(result["normalizedTargetFile"], "src/lib.rs"); + assert_eq!(result["freshness"], "current"); + assert_eq!(result["completeness"], "complete"); + assert_eq!(result["boundary"]["path"], "src/lib.rs"); + assert_eq!(result["boundary"]["forbid"][0]["id"], "BOUNDARY-001"); + assert_eq!(result["boundary"]["gotcha"][0]["id"], "GOTCHA-001"); + assert_eq!(result["boundary"]["checks"][0]["id"], "CHECK-001"); + assert_eq!(result["provenance"]["sourcePath"], ".aigx/files.aigx"); + assert_eq!(result["diagnostics"], json!([])); +} + +#[test] +fn missing_file_and_unsupported_constructs_are_explicit_unknowns() { + let mut value = request("src/missing.rs"); + value["document"]["unsupportedConstructs"] = json!(["selector:owner-group"]); + let result = file_boundary::resolve(&value).unwrap(); + assert_eq!(result["status"], "unknown"); + assert_eq!(result["completeness"], "unknown"); + assert!(result["boundary"].is_null()); + assert_eq!(result["diagnostics"][0]["code"], "unsupported_construct"); + assert_eq!(result["diagnostics"][1]["code"], "no_matching_boundary"); + + let mut matched = request("src/lib.rs"); + matched["document"]["unsupportedConstructs"] = json!(["selector:owner-group"]); + let matched_result = file_boundary::resolve(&matched).unwrap(); + assert_eq!(matched_result["status"], "resolved"); + assert_eq!(matched_result["completeness"], "partial"); +} + +#[test] +fn snapshot_mismatch_stale_future_and_unsafe_paths_fail_closed() { + let mut mismatch = request("src/lib.rs"); + mismatch["document"]["snapshotIdentity"] = json!(OTHER); + assert!(file_boundary::resolve(&mismatch) + .unwrap_err() + .contains("snapshot mismatch")); + + let mut stale = request("src/lib.rs"); + stale["document"]["observedAt"] = json!(1_899); + assert!(file_boundary::resolve(&stale) + .unwrap_err() + .contains("stale")); + + let mut future = request("src/lib.rs"); + future["document"]["observedAt"] = json!(2_001); + assert!(file_boundary::resolve(&future) + .unwrap_err() + .contains("future")); + + assert!(file_boundary::resolve(&request("../outside.rs")) + .unwrap_err() + .contains("escape")); + assert!(file_boundary::resolve(&request("C:\\outside.rs")) + .unwrap_err() + .contains("repository-relative")); +} + +#[test] +fn duplicate_case_folded_paths_wildcards_and_duplicate_ids_fail_closed() { + let mut duplicate = request("src/lib.rs"); + let mut second = duplicate["document"]["entries"][0].clone(); + second["path"] = json!("SRC/LIB.RS"); + duplicate["document"]["entries"] + .as_array_mut() + .unwrap() + .push(second); + assert!(file_boundary::resolve(&duplicate) + .unwrap_err() + .contains("duplicate or ambiguous")); + + let mut wildcard = request("src/lib.rs"); + wildcard["document"]["entries"][0]["path"] = json!("src/*.rs"); + assert!(file_boundary::resolve(&wildcard) + .unwrap_err() + .contains("exact paths")); + + let mut ids = request("src/lib.rs"); + ids["document"]["entries"][0]["checks"][0]["id"] = json!("BOUNDARY-001"); + assert!(file_boundary::resolve(&ids) + .unwrap_err() + .contains("rule IDs")); +} + +#[test] +fn unknown_fields_are_rejected_without_echoing_their_values() { + let mut top_level = request("src/lib.rs"); + top_level["apiToken"] = json!("BOUNDARY-SECRET-SENTINEL"); + let top_error = file_boundary::resolve(&top_level).unwrap_err(); + assert!(top_error.contains("fields are invalid")); + assert!(!top_error.contains("BOUNDARY-SECRET-SENTINEL")); + + let mut entry = request("src/lib.rs"); + entry["document"]["entries"][0]["ownerDatabase"] = json!("C:/private/index.db"); + let entry_error = file_boundary::resolve(&entry).unwrap_err(); + assert!(entry_error.contains("fields are invalid")); + assert!(!entry_error.contains("private/index.db")); +} + +#[test] +fn request_document_and_result_schemas_are_closed_and_docs_keep_aigx_optional() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + for name in [ + "code-intel-file-boundary-document.v1.schema.json", + "code-intel-file-boundary-request.v1.schema.json", + "code-intel-file-boundary-result.v1.schema.json", + ] { + let schema: Value = serde_json::from_slice( + &fs::read(root.join("orchestration/schemas").join(name)).unwrap(), + ) + .unwrap(); + assert_eq!(schema["additionalProperties"], false, "{name}"); + } + + let result_schema: Value = serde_json::from_slice( + &fs::read( + root.join("orchestration/schemas/code-intel-file-boundary-result.v1.schema.json"), + ) + .unwrap(), + ) + .unwrap(); + let required = result_schema["required"] + .as_array() + .unwrap() + .iter() + .map(Value::as_str) + .collect::>>() + .unwrap(); + assert!(required.contains("provenance")); + assert!(required.contains("freshness")); + assert!(required.contains("diagnostics")); + + let docs = fs::read_to_string(root.join("docs/file-boundary-observation.md")).unwrap(); + assert!(docs.contains("does not require AIGX")); + assert!(docs.contains("exact repository-relative path")); + assert!(docs.contains("fail closed")); +} diff --git a/crates/code-intel-cli/tests/fixtures/codenexus-adapter/full-current.json b/crates/code-intel-cli/tests/fixtures/codenexus-adapter/full-current.json new file mode 100644 index 0000000..68c96f7 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/codenexus-adapter/full-current.json @@ -0,0 +1,9 @@ +{ + "providerMode": "full", + "status": "current", + "providerId": "codenexus.full", + "implementationId": "codenexus.service.v1", + "sourceRevision": "full-revision-7", + "activation": "primary", + "effects": ["network_provider", "read_provider_artifact"] +} diff --git a/crates/code-intel-cli/tests/fixtures/codenexus-adapter/lite-current.json b/crates/code-intel-cli/tests/fixtures/codenexus-adapter/lite-current.json new file mode 100644 index 0000000..4495ee9 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/codenexus-adapter/lite-current.json @@ -0,0 +1,9 @@ +{ + "providerMode": "lite", + "status": "current", + "providerId": "codenexus.lite-compat", + "implementationId": "invoke-codenexus-lite.ps1", + "sourceRevision": "lite-revision-4", + "activation": "explicit_fallback", + "effects": ["read_repository", "read_git_history", "read_sentrux_artifacts", "write_compatibility_artifact"] +} diff --git a/crates/code-intel-cli/tests/fixtures/codenexus-adapter/unavailable.json b/crates/code-intel-cli/tests/fixtures/codenexus-adapter/unavailable.json new file mode 100644 index 0000000..e86eb40 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/codenexus-adapter/unavailable.json @@ -0,0 +1,9 @@ +{ + "providerMode": "full", + "status": "unavailable", + "providerId": "codenexus.full", + "implementationId": "codenexus.service.v1", + "sourceRevision": "unavailable-at-full-revision-7", + "activation": "primary", + "effects": ["network_provider"] +} diff --git a/crates/code-intel-cli/tests/fixtures/graph-adapter/external-current.json b/crates/code-intel-cli/tests/fixtures/graph-adapter/external-current.json new file mode 100644 index 0000000..abd7b20 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/graph-adapter/external-current.json @@ -0,0 +1,11 @@ +{ + "providerMode": "external", + "status": "current", + "sourceRevision": "understand-r9", + "fallback": { + "identity": "understand-anything.compat.v1", + "activation": "explicit_fallback", + "reason": "operator selected compatibility provider" + }, + "graphPresent": true +} diff --git a/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-current.json b/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-current.json new file mode 100644 index 0000000..f9719d9 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-current.json @@ -0,0 +1,7 @@ +{ + "providerMode": "internal", + "status": "current", + "sourceRevision": "internal-r17", + "fallback": null, + "graphPresent": true +} diff --git a/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-missing.json b/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-missing.json new file mode 100644 index 0000000..a77ffd6 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-missing.json @@ -0,0 +1,7 @@ +{ + "providerMode": "internal", + "status": "missing", + "sourceRevision": "internal-r17", + "fallback": null, + "graphPresent": false +} diff --git a/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-partial.json b/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-partial.json new file mode 100644 index 0000000..efd42b5 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/graph-adapter/internal-partial.json @@ -0,0 +1,7 @@ +{ + "providerMode": "internal", + "status": "partial", + "sourceRevision": "internal-r17", + "fallback": null, + "graphPresent": true +} diff --git a/crates/code-intel-cli/tests/fixtures/method-select/dependency-delay.json b/crates/code-intel-cli/tests/fixtures/method-select/dependency-delay.json new file mode 100644 index 0000000..15291a5 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/method-select/dependency-delay.json @@ -0,0 +1,15 @@ +{ + "facts": [ + { + "id": "dependency-fact", + "signalIds": ["dependency-congestion"], + "evidenceKinds": ["activity-duration-estimates", "activity-network"] + }, + { + "id": "queue-fact", + "signalIds": ["handoff-wait"], + "evidenceKinds": ["item-state-timestamps", "work-demand-and-capacity"] + } + ], + "evidenceGaps": [] +} diff --git a/crates/code-intel-cli/tests/fixtures/method-select/insufficient-evidence.json b/crates/code-intel-cli/tests/fixtures/method-select/insufficient-evidence.json new file mode 100644 index 0000000..bada87d --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/method-select/insufficient-evidence.json @@ -0,0 +1,10 @@ +{ + "facts": [ + { + "id": "dependency-fact", + "signalIds": ["dependency-congestion"], + "evidenceKinds": ["activity-network"] + } + ], + "evidenceGaps": ["activity-duration-estimates"] +} diff --git a/crates/code-intel-cli/tests/fixtures/model-routing/ready-local.json b/crates/code-intel-cli/tests/fixtures/model-routing/ready-local.json new file mode 100644 index 0000000..c4a2395 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/model-routing/ready-local.json @@ -0,0 +1,37 @@ +{ + "schema": "code-intel-model-routing-request.v1", + "inventory": { + "schema": "code-intel-model-channel-inventory-result.v1", + "candidates": [ + { + "id": "ollama-local", + "channelKind": "ollama", + "provider": "ollama", + "model": "fixture-model", + "costScope": "local_compute", + "endpointConfigured": true, + "discovered": true, + "executableVerified": true, + "authPresent": "not_applicable", + "modelAvailable": "available", + "externalEgress": false, + "source": "local_discovery", + "diagnostics": [] + } + ], + "configurationBrokers": [] + }, + "policy": { + "consumptionAuthorization": { + "status": "granted", + "scopes": ["local_compute"] + }, + "externalData": { "status": "denied" }, + "paidSpend": { "status": "denied" }, + "selection": { + "pinnedAdapter": null, + "fallbackPolicy": "denied" + } + }, + "workload": { "requiresExternalData": true } +} diff --git a/crates/code-intel-cli/tests/fixtures/project-orientation/missing-purpose.json b/crates/code-intel-cli/tests/fixtures/project-orientation/missing-purpose.json new file mode 100644 index 0000000..79b9eda --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/project-orientation/missing-purpose.json @@ -0,0 +1,92 @@ +{ + "snapshot": { + "schema": "code-intel-repository-snapshot.v1", + "snapshot": { + "identity": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "repoIdentity": "content-v1:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "head": "unversioned", + "workingTreePolicy": "explicit_overlay", + "scope": ["."], + "inputDigest": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + "dirtyOverlay": { + "present": true, + "digest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "paths": ["src/main.rs"], + "members": { + "trackedModified": [], + "trackedDeleted": [], + "untracked": ["src/main.rs"], + "renamed": [], + "typeChanged": [], + "staged": [] + }, + "ignoredPolicy": "excluded_by_git_ignore" + }, + "repository": {"kind": "unversioned"} + }, + "inventory": "Cargo.toml\nREADME.md\nsrc/lib.rs\nsrc/main.rs\ntests/orientation.rs\n", + "survival": { + "schema": "code-intel-repository-survival-scan-result.v1", + "status": "completed", + "snapshotIdentity": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "repository": { + "kind": "unversioned", + "identity": "content-v1:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "revision": "unversioned", + "dirty": true, + "sourceSha256": "1111111111111111111111111111111111111111111111111111111111111111" + }, + "inventory": { + "fileCount": 5, + "extensions": {"md": 1, "rs": 3, "toml": 1}, + "sourceSha256": "2222222222222222222222222222222222222222222222222222222222222222" + }, + "providerDiagnosis": { + "providerId": "codenexus.full", + "status": "provider_unavailable", + "domainVerdict": "unknown" + }, + "completeness": "reduced", + "structuralVerdict": "unknown", + "limitations": [ + "only repository identity and basic file inventory are available", + "deeper structural perception requires an admitted provider result" + ], + "engineeringFacts": [ + {"kind": "repository_identity", "value": "content-v1:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "sourceSha256": "1111111111111111111111111111111111111111111111111111111111111111"}, + {"kind": "repository_revision", "value": "unversioned", "sourceSha256": "1111111111111111111111111111111111111111111111111111111111111111"}, + {"kind": "inventory_file_count", "value": 5, "sourceSha256": "2222222222222222222222222222222222222222222222222222222222222222"} + ] + }, + "nativeFiles": { + "schema": "code-evidence-files.v1", + "files": [ + {"path": "Cargo.toml", "language": "text", "bytes": 40, "lines": 4, "textHash": "3333333333333333333333333333333333333333333333333333333333333333", "source": "native-minimal"}, + {"path": "README.md", "language": "text", "bytes": 0, "lines": 0, "textHash": "4444444444444444444444444444444444444444444444444444444444444444", "source": "native-minimal"}, + {"path": "src/lib.rs", "language": "rust", "bytes": 20, "lines": 1, "textHash": "5555555555555555555555555555555555555555555555555555555555555555", "source": "native-minimal"}, + {"path": "src/main.rs", "language": "rust", "bytes": 13, "lines": 1, "textHash": "6666666666666666666666666666666666666666666666666666666666666666", "source": "native-minimal"}, + {"path": "tests/orientation.rs", "language": "rust", "bytes": 18, "lines": 1, "textHash": "7777777777777777777777777777777777777777777777777777777777777777", "source": "native-minimal"} + ] + }, + "nativeCoverage": { + "schema": "code-evidence-coverage.v1", + "producer": "native-minimal", + "parserKind": "line-heuristic", + "supportedHeuristics": ["rust"], + "unsupportedFiles": ["Cargo.toml", "README.md"], + "symbolPrecision": "heuristic", + "importPrecision": "heuristic", + "relationshipPrecision": "unknown", + "callGraph": "unknown", + "effects": ["repo_read", "local_write"] + }, + "nativeRanking": { + "schema": "agent-code-slice-ranking.v1", + "strategy": "native-evidence-default", + "files": [ + {"path": "src/main.rs", "language": "rust", "score": 40, "reasons": ["entrypoint"], "symbols": null, "imports": null}, + {"path": "tests/orientation.rs", "language": "rust", "score": 35, "reasons": ["test"], "symbols": null, "imports": null} + ] + } +} diff --git a/crates/code-intel-cli/tests/fixtures/sentrux-adapter/complete.json b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/complete.json new file mode 100644 index 0000000..03e0d93 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/complete.json @@ -0,0 +1,12 @@ +{ + "status": "complete", + "authoritativeRules": [ + { "kind": "max_cc", "status": "evaluated", "verdict": "pass", "failure": { "kind": "none" } }, + { "kind": "max_cycles", "status": "evaluated", "verdict": "pass", "failure": { "kind": "none" } }, + { "kind": "max_coupling", "status": "evaluated", "verdict": "fail", "failure": { "kind": "none" } }, + { "kind": "no_god_files", "status": "evaluated", "verdict": "pass", "failure": { "kind": "none" } }, + { "kind": "layer_order", "status": "evaluated", "verdict": "pass", "failure": { "kind": "none" } }, + { "kind": "boundary_dependency", "status": "evaluated", "verdict": "pass", "failure": { "kind": "none" } } + ], + "nativeFailure": { "kind": "none" } +} diff --git a/crates/code-intel-cli/tests/fixtures/sentrux-adapter/crashed.json b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/crashed.json new file mode 100644 index 0000000..b9b6b53 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/crashed.json @@ -0,0 +1,5 @@ +{ + "status": "crashed", + "authoritativeRules": [], + "nativeFailure": { "kind": "provider_unavailable", "message": "provider process exited before normalization" } +} diff --git a/crates/code-intel-cli/tests/fixtures/sentrux-adapter/partial.json b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/partial.json new file mode 100644 index 0000000..f0fc0c3 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/partial.json @@ -0,0 +1,8 @@ +{ + "status": "partial", + "authoritativeRules": [ + { "kind": "max_cc", "status": "evaluated", "verdict": "fail", "failure": { "kind": "none" } }, + { "kind": "max_cycles", "status": "not_evaluated", "verdict": "unknown", "failure": { "kind": "domain_unknown", "message": "provider did not emit cycle evaluation" } } + ], + "nativeFailure": { "kind": "none" } +} diff --git a/crates/code-intel-cli/tests/fixtures/sentrux-adapter/unknown-kind.json b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/unknown-kind.json new file mode 100644 index 0000000..df5cf59 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/sentrux-adapter/unknown-kind.json @@ -0,0 +1,7 @@ +{ + "status": "complete", + "authoritativeRules": [ + { "kind": "future_architecture_rule", "status": "evaluated", "verdict": "pass", "failure": { "kind": "none" } } + ], + "nativeFailure": { "kind": "none" } +} diff --git a/crates/code-intel-cli/tests/fixtures/survival-scan/unavailable-expectations.json b/crates/code-intel-cli/tests/fixtures/survival-scan/unavailable-expectations.json new file mode 100644 index 0000000..3e9d012 --- /dev/null +++ b/crates/code-intel-cli/tests/fixtures/survival-scan/unavailable-expectations.json @@ -0,0 +1,6 @@ +{ + "providerStatus": "provider_unavailable", + "completeness": "reduced", + "structuralVerdict": "unknown", + "forbiddenClaims": ["graph", "impact", "current anatomy"] +} diff --git a/crates/code-intel-cli/tests/graph_adapter.rs b/crates/code-intel-cli/tests/graph_adapter.rs new file mode 100644 index 0000000..c595447 --- /dev/null +++ b/crates/code-intel-cli/tests/graph_adapter.rs @@ -0,0 +1,455 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/admissibility.rs"] +mod admissibility; +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/graph_adapter.rs"] +mod graph_adapter; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +const CURRENT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const WRONG: &str = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; +const IMPLEMENTATION_DIGEST: &str = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +static SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-b02-{}-{nonce}-{}", + std::process::id(), + SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn descriptor(name: &str) -> Value { + let text = match name { + "internal-current" => include_str!("fixtures/graph-adapter/internal-current.json"), + "external-current" => include_str!("fixtures/graph-adapter/external-current.json"), + "internal-partial" => include_str!("fixtures/graph-adapter/internal-partial.json"), + "internal-missing" => include_str!("fixtures/graph-adapter/internal-missing.json"), + _ => panic!("unknown fixture {name}"), + }; + serde_json::from_str(text).unwrap() +} + +fn graph_document() -> Value { + json!({ + "schema":"code-intel-understand-graph.v1", + "summary":{"files":2,"symbols":3}, + "nodes":[], + "edges":[], + "symbols":[] + }) +} + +fn build_case( + root: &Path, + fixture: &Value, + expected_snapshot: &str, + source_snapshot: &str, + observed_at: u64, +) -> Value { + let mode = fixture["providerMode"].as_str().unwrap(); + let implementation_id = if mode == "internal" { + "architecture-graph.internal-rust" + } else { + "architecture-graph.understand-compat" + }; + let fallback_identity = fixture["fallback"]["identity"].clone(); + let graph = if fixture["graphPresent"] == true { + graph_document() + } else { + Value::Null + }; + let completeness = if fixture["status"] == "current" { + "complete" + } else { + "partial" + }; + let payload = json!({ + "schema":"code-intel-evidence-payload.v1", + "data":{ + "architectureGraph":{ + "schema":"code-intel-architecture-graph-evidence.v1", + "snapshotIdentity":source_snapshot, + "provider":{ + "mode":mode, + "implementationId":implementation_id, + "fallbackIdentity":fallback_identity + }, + "provenance":{ + "sourceRevision":fixture["sourceRevision"], + "observedAt":observed_at + }, + "completeness":completeness, + "graph":graph + } + } + }); + let bytes = serde_json::to_vec(&payload).unwrap(); + fs::write(root.join("payload.json"), &bytes).unwrap(); + json!({ + "schema":"code-intel-graph-provider-native.v1", + "providerMode":mode, + "status":fixture["status"], + "implementation":{ + "id":implementation_id, + "version":"1.0.0", + "digest":IMPLEMENTATION_DIGEST + }, + "sourceRevision":fixture["sourceRevision"], + "expectedSnapshotIdentity":expected_snapshot, + "sourceSnapshotIdentity":source_snapshot, + "collectedAt":observed_at - 1, + "observedAt":observed_at, + "payload":{ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-evidence-payload.v1", + "type":"observed.evidence.payload", + "path":"payload.json", + "sha256":capability::sha256_hex(&bytes), + "consumedSnapshotIdentity":source_snapshot + }, + "fallback":fixture["fallback"] + }) +} + +fn keys(value: &Value) -> BTreeSet<&str> { + value + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect() +} + +fn route(root: &Path, native: &Value) -> (i32, Value, String) { + let request = root.join("native.json"); + fs::write(&request, serde_json::to_vec(native).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "provider", + "graph-adapt", + "--request", + request.to_str().unwrap(), + "--artifact-root", + root.to_str().unwrap(), + "--evaluated-at", + "2000", + "--max-age-seconds", + "100", + ]) + .output() + .unwrap(); + let stdout: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|_| { + panic!( + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + }); + ( + output.status.code().unwrap(), + stdout, + String::from_utf8_lossy(&output.stderr).into_owned(), + ) +} + +#[test] +fn internal_and_external_current_outputs_share_one_port_and_provenance_schema() { + let internal_root = Temp::new(); + let external_root = Temp::new(); + let internal_native = build_case( + &internal_root.0, + &descriptor("internal-current"), + CURRENT, + CURRENT, + 1_950, + ); + let external_native = build_case( + &external_root.0, + &descriptor("external-current"), + CURRENT, + CURRENT, + 1_950, + ); + let internal = graph_adapter::translate(&internal_native, 2_000, 100).unwrap(); + let external = graph_adapter::translate(&external_native, 2_000, 100).unwrap(); + + assert_eq!(keys(&internal["port"]), keys(&external["port"])); + assert_eq!( + keys(&internal["port"]["provenance"]), + keys(&external["port"]["provenance"]) + ); + assert_eq!( + internal["port"]["schema"], + "code-intel-architecture-graph-port.v1" + ); + assert_eq!(internal["port"]["provider"]["mode"], "internal"); + assert!(internal["port"]["provider"]["fallbackIdentity"].is_null()); + assert_eq!(external["port"]["provider"]["mode"], "external"); + assert_eq!( + external["port"]["provider"]["fallbackIdentity"], + "understand-anything.compat.v1" + ); + + for (root, adapter) in [(&internal_root.0, &internal), (&external_root.0, &external)] { + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], root).unwrap(); + assert_eq!(admitted.result()["domainVerdict"], "observed"); + graph_adapter::validate_admitted_payload(admitted.payload(), adapter).unwrap(); + } +} + +#[test] +fn wrong_head_and_stale_graphs_are_rejected_by_a04() { + let wrong_root = Temp::new(); + let wrong_native = build_case( + &wrong_root.0, + &descriptor("internal-current"), + CURRENT, + WRONG, + 1_950, + ); + let wrong = graph_adapter::translate(&wrong_native, 2_000, 100).unwrap(); + assert_eq!(wrong["port"]["freshness"], "snapshot_mismatch"); + assert_eq!(wrong["port"]["anatomyUsable"], false); + assert!( + admissibility::validate_for_consumer(&wrong["evidence"]["request"], &wrong_root.0) + .err() + .unwrap() + .contains("consumed snapshot mismatch") + ); + + let stale_root = Temp::new(); + let stale_native = build_case( + &stale_root.0, + &descriptor("internal-current"), + CURRENT, + CURRENT, + 1_800, + ); + let stale = graph_adapter::translate(&stale_native, 2_000, 100).unwrap(); + assert_eq!(stale["port"]["freshness"], "stale"); + assert!( + admissibility::validate_for_consumer(&stale["evidence"]["request"], &stale_root.0) + .err() + .unwrap() + .contains("stale") + ); +} + +#[test] +fn missing_partial_and_current_matrix_preserves_unknown_without_facts() { + for (name, verdict, completeness) in [ + ("internal-missing", "unknown", "partial"), + ("internal-partial", "unknown", "partial"), + ("internal-current", "observed", "complete"), + ] { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor(name), CURRENT, CURRENT, 1_950); + let adapter = graph_adapter::translate(&native, 2_000, 100).unwrap(); + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + graph_adapter::validate_admitted_payload(admitted.payload(), &adapter).unwrap(); + assert_eq!(adapter["port"]["completeness"], completeness, "{name}"); + assert_eq!(adapter["port"]["anatomyUsable"], false, "{name}"); + assert_eq!(admitted.result()["domainVerdict"], verdict, "{name}"); + assert_eq!(admitted.result()["engineeringFacts"], json!([]), "{name}"); + assert_eq!( + adapter["factPromotion"]["engineeringFacts"], + json!([]), + "{name}" + ); + } +} + +#[test] +fn fallback_identity_and_payload_identity_cannot_be_relabelled() { + let root = Temp::new(); + let mut external = build_case( + &root.0, + &descriptor("external-current"), + CURRENT, + CURRENT, + 1_950, + ); + external["fallback"]["activation"] = json!("legacy_rollback"); + let rollback = graph_adapter::translate(&external, 2_000, 100).unwrap(); + assert_eq!( + rollback["port"]["provider"]["fallbackIdentity"], + "understand-anything.compat.v1" + ); + external["fallback"]["activation"] = json!("automatic_primary"); + assert!(graph_adapter::translate(&external, 2_000, 100) + .unwrap_err() + .contains("explicit fallback/rollback identity")); + external["fallback"] = Value::Null; + assert!(graph_adapter::translate(&external, 2_000, 100) + .unwrap_err() + .contains("external graph fallback")); + + let mut internal = build_case( + &root.0, + &descriptor("internal-current"), + CURRENT, + CURRENT, + 1_950, + ); + internal["fallback"] = json!({ + "identity":"illegal", + "activation":"explicit_fallback", + "reason":"illegal internal fallback" + }); + assert!(graph_adapter::translate(&internal, 2_000, 100) + .unwrap_err() + .contains("internal graph provider")); + + let native = build_case( + &root.0, + &descriptor("external-current"), + CURRENT, + CURRENT, + 1_950, + ); + let adapter = graph_adapter::translate(&native, 2_000, 100).unwrap(); + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + let mut relabelled = admitted.payload().clone(); + relabelled["data"]["architectureGraph"]["provider"]["fallbackIdentity"] = + json!("different-fallback"); + assert!( + graph_adapter::validate_admitted_payload(&relabelled, &adapter) + .unwrap_err() + .contains("provider/fallback identity mismatch") + ); +} + +#[test] +fn public_route_accepts_current_and_rejects_wrong_head_without_secret_or_fact() { + let current_root = Temp::new(); + let current_native = build_case( + ¤t_root.0, + &descriptor("internal-current"), + CURRENT, + CURRENT, + 1_950, + ); + let (exit, result, stderr) = route(¤t_root.0, ¤t_native); + assert_eq!(exit, 0, "{stderr}"); + assert_eq!(result["schema"], "code-intel-graph-route-result.v1"); + assert_eq!(result["status"], "completed"); + assert_eq!(result["adapter"]["port"]["anatomyUsable"], true); + assert_eq!(result["engineeringFacts"], json!([])); + + let wrong_root = Temp::new(); + let wrong_native = build_case( + &wrong_root.0, + &descriptor("internal-current"), + CURRENT, + WRONG, + 1_950, + ); + let (exit, result, _) = route(&wrong_root.0, &wrong_native); + assert_eq!(exit, 65); + assert_eq!(result["status"], "rejected"); + assert_eq!(result["engineeringFacts"], json!([])); + + let secret_root = Temp::new(); + let mut secret_native = build_case( + &secret_root.0, + &descriptor("internal-current"), + CURRENT, + CURRENT, + 1_950, + ); + secret_native["apiToken"] = json!("B02-SECRET-SENTINEL"); + let (exit, result, stderr) = route(&secret_root.0, &secret_native); + assert_eq!(exit, 65); + let rendered = format!("{result}{stderr}"); + assert!(!rendered.contains("B02-SECRET-SENTINEL")); +} + +#[test] +fn public_route_usage_registry_facade_and_schemas_are_real() { + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["provider", "graph-adapt"]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(64)); + assert!(output.stdout.is_empty()); + + let validation = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["provider", "--action", "Validate", "--json"]) + .output() + .unwrap(); + let validation_json: Value = serde_json::from_slice(&validation.stdout).unwrap(); + assert_eq!(validation.status.code(), Some(0)); + assert_eq!(validation_json["ok"], true, "{validation_json}"); + + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest: Value = + serde_json::from_slice(&fs::read(root.join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let integration = manifest["integrations"] + .as_array() + .unwrap() + .iter() + .find(|item| item["id"] == "provider.graph-adapt") + .expect("provider.graph-adapt registry entry"); + assert_eq!(integration["required"], true); + assert!(integration["commands"]["adapt"] + .as_str() + .unwrap() + .contains("provider graph-adapt")); + assert!(integration["commands"]["facade"] + .as_str() + .unwrap() + .contains("-GraphAdapterRequest")); + + let facade = fs::read_to_string(root.join("run-code-intel.ps1")).unwrap(); + assert!(facade.contains("provider graph-adapt")); + assert!(facade.contains("GraphAdapterMaxAgeSeconds")); + + for schema in [ + "code-intel-architecture-graph-port.v1.schema.json", + "code-intel-graph-route-result.v1.schema.json", + ] { + let value: Value = serde_json::from_slice( + &fs::read(root.join("orchestration/schemas").join(schema)).unwrap(), + ) + .unwrap(); + assert_eq!(value["additionalProperties"], false, "{schema}"); + } +} diff --git a/crates/code-intel-cli/tests/hospital_diagnosis.rs b/crates/code-intel-cli/tests/hospital_diagnosis.rs new file mode 100644 index 0000000..f949999 --- /dev/null +++ b/crates/code-intel-cli/tests/hospital_diagnosis.rs @@ -0,0 +1,929 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-b09-{}-{nonce}-{}", + std::process::id(), + TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn artifact_ref(root: &Path, path: &str, schema: &str, kind: &str, value: &Value) -> Value { + let bytes = serde_json::to_vec(value).unwrap(); + let full_path = root.join(path); + fs::write(&full_path, &bytes).unwrap(); + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":schema, + "type":kind, + "path":path, + "sha256":file_sha256(&full_path), + "consumedSnapshotIdentity":SNAPSHOT + }) +} + +fn file_sha256(path: &Path) -> String { + sha256_hex(&fs::read(path).unwrap()) +} + +fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0); + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (index, word) in chunk.chunks_exact(4).enumerate() { + w[index] = u32::from_be_bytes(word.try_into().unwrap()); + } + for index in 16..64 { + let s0 = w[index - 15].rotate_right(7) + ^ w[index - 15].rotate_right(18) + ^ (w[index - 15] >> 3); + let s1 = w[index - 2].rotate_right(17) + ^ w[index - 2].rotate_right(19) + ^ (w[index - 2] >> 10); + w[index] = w[index - 16] + .wrapping_add(s0) + .wrapping_add(w[index - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for index in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ (!e & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[index]) + .wrapping_add(w[index]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value); + } + } + h.iter().map(|value| format!("{value:08x}")).collect() +} + +fn admission( + root: &Path, + name: &str, + provider: &str, + domain_verdict: &str, + failure_kind: &str, + data: Value, +) -> Value { + let payload = json!({"schema":"code-intel-evidence-payload.v1","data":data}); + let payload_ref = artifact_ref( + root, + &format!("{name}-payload.json"), + "code-intel-evidence-payload.v1", + "observed.evidence.payload", + &payload, + ); + let admission_seed = root.join(format!("{name}-identity.json")); + fs::write( + &admission_seed, + serde_json::to_vec(&json!({"provider":provider,"name":name,"data":payload})).unwrap(), + ) + .unwrap(); + let admission_identity = file_sha256(&admission_seed); + let result = json!({ + "schema":"code-intel-evidence-admissibility-result.v1", + "status":"admitted", + "domainVerdict":domain_verdict, + "admissionIdentity":admission_identity, + "evidence":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{"id":provider,"implementation":{"id":format!("{provider}.fixture"),"version":"1.0.0","digest":"b".repeat(64)}}, + "source":{"revision":"fixture-r1"}, + "consumedSnapshotIdentity":SNAPSHOT, + "observedAt":1700000000, + "completeness":if domain_verdict == "observed" { "complete" } else { "partial" }, + "claimedComplete":domain_verdict == "observed", + "payload":payload_ref, + "provenance":{"collectionId":format!("fixture-{name}"),"command":"fixture","startedAt":1699999999,"completedAt":1700000000}, + "failure":if failure_kind == "none" { json!({"kind":"none"}) } else { json!({"kind":failure_kind,"message":failure_kind}) } + }, + "verifiedPayload":{ + "sha256":payload_ref["sha256"], + "artifactSchema":"code-intel-evidence-payload.v1", + "type":"observed.evidence.payload", + "consumedSnapshotIdentity":SNAPSHOT, + "data":payload["data"] + }, + "engineeringFacts":[] + }); + artifact_ref( + root, + &format!("{name}-admission.json"), + "code-intel-evidence-admissibility-result.v1", + "evidence.admission", + &result, + ) +} + +fn snapshot() -> Value { + json!({ + "identity":SNAPSHOT, + "repoIdentity":format!("content-v1:{}", "c".repeat(64)), + "head":"unversioned", + "workingTreePolicy":"explicit_overlay", + "scope":["."], + "inputDigest":"d".repeat(64) + }) +} + +fn registry_integration() -> Value { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let registry: Value = + serde_json::from_slice(&fs::read(root.join("orchestration/integrations.json")).unwrap()) + .unwrap(); + registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "diagnosis.hospital") + .unwrap() + .clone() +} + +fn run(root: &Path, inputs: Vec, out_name: &str) -> (i32, Value, PathBuf, String) { + let integration = registry_integration(); + let request = json!({ + "schema":"code-intel-capability-request.v1", + "capability":"diagnosis.hospital", + "contractVersion":1, + "implementation":integration["capabilityDeclaration"]["implementation"], + "snapshot":snapshot(), + "options":{}, + "inputs":inputs, + "effectPolicy":{"allowedEffects":["local_write"]} + }); + let request_path = root.join(format!("{out_name}-request.json")); + fs::write(&request_path, serde_json::to_vec(&request).unwrap()).unwrap(); + let out = root.join(out_name); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "diagnosis.hospital", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .arg("--artifact-root") + .arg(root) + .output() + .unwrap(); + let value = serde_json::from_slice(&output.stdout).unwrap_or(Value::Null); + ( + output.status.code().unwrap(), + value, + out, + String::from_utf8_lossy(&output.stderr).into_owned(), + ) +} + +fn graph(root: &Path, current: bool) -> Value { + admission( + root, + if current { + "graph-current" + } else { + "graph-missing" + }, + "architecture-graph.internal", + if current { "observed" } else { "unknown" }, + if current { "none" } else { "domain_unknown" }, + json!({"architectureGraph":{ + "schema":"code-intel-architecture-graph-evidence.v1", + "snapshotIdentity":SNAPSHOT, + "completeness":if current { "complete" } else { "partial" }, + "graph":if current { json!({"nodes":[],"edges":[]}) } else { Value::Null } + }}), + ) +} + +fn structural(root: &Path, name: &str, verdict: Option<&str>, trusted: bool) -> Value { + let rules = verdict + .map(|value| json!([{"kind":"boundary_dependency","status":"evaluated","verdict":value,"failure":{"kind":"none"}}])) + .unwrap_or_else(|| json!([])); + admission( + root, + name, + "structural-evidence.sentrux", + if trusted { "observed" } else { "unknown" }, + if trusted { "none" } else { "domain_unknown" }, + json!({"structuralEvidence":{ + "schema":"code-intel-structural-evidence-payload.v1", + "snapshotIdentity":SNAPSHOT, + "completeness":if trusted { "complete" } else { "partial" }, + "rules":rules + }}), + ) +} + +fn native(root: &Path, debt: bool) -> Value { + admission( + root, + if debt { "native-debt" } else { "native-clean" }, + "native-code-evidence", + "observed", + "none", + json!({"nativeCode":{"modernizationDebt":debt,"topTarget":if debt { "src/legacy.rs" } else { "" }}}), + ) +} + +fn diagnosis(root: &Path, inputs: Vec, name: &str) -> (i32, String, String, PathBuf) { + let (exit, _, out, stderr) = run(root, inputs, name); + if !out.join("hospital-report.json").is_file() { + return (exit, stderr, String::new(), out); + } + let value: Value = + serde_json::from_slice(&fs::read(out.join("hospital-report.json")).unwrap()).unwrap(); + ( + exit, + value["triage"]["primary_diagnosis"] + .as_str() + .unwrap() + .into(), + value["triage"]["next_protocol"].as_str().unwrap().into(), + out, + ) +} + +#[test] +fn provider_quota_precedes_missing_current_graph_and_is_replay_stable() { + let temp = Temp::new(); + let repowise = admission( + &temp.0, + "repowise-docs", + "repowise.docs", + "unknown", + "provider_unavailable", + json!({"repowise":{"channel":"docs","status":"quota"}}), + ); + let graph = admission( + &temp.0, + "graph", + "architecture-graph.internal", + "unknown", + "domain_unknown", + json!({"architectureGraph":{"schema":"code-intel-architecture-graph-evidence.v1","snapshotIdentity":SNAPSHOT,"completeness":"partial","graph":null}}), + ); + + let (exit, envelope, out, stderr) = + run(&temp.0, vec![repowise.clone(), graph.clone()], "first"); + assert_eq!(exit, 0, "{stderr}"); + assert_eq!(envelope["status"], "completed"); + assert_eq!(envelope["observedEffects"], json!(["local_write"])); + let emitted = envelope["artifacts"] + .as_array() + .expect("B09 emits Artifact Refs"); + assert_eq!(emitted.len(), 4); + assert_eq!( + emitted + .iter() + .map(|artifact| artifact["type"].as_str().unwrap()) + .collect::>(), + vec![ + "diagnosis.hospital", + "diagnosis.hospital-view", + "diagnosis.surgery-plan", + "diagnosis.surgery-plan-view", + ] + ); + let machine: Value = + serde_json::from_slice(&fs::read(out.join("hospital-report.json")).unwrap()).unwrap(); + assert_eq!(machine["schema"], "code-intel-hospital.v1"); + assert_eq!(machine["domainVerdict"], "unknown"); + assert_eq!(machine["triage"]["status"], "unknown"); + assert_eq!( + machine["triage"]["primary_diagnosis"], + "provider quota exhausted" + ); + assert_eq!(machine["triage"]["disposition"], "admit"); + assert_eq!(machine["triage"]["next_protocol"], "triage"); + assert!(machine["treatment"]["plan"] + .as_array() + .unwrap() + .iter() + .any(|v| v.as_str().unwrap().contains("provider quota"))); + assert_eq!(machine["surgery_plan"]["status"], "not_required"); + + let (replay_exit, _, replay_out, replay_stderr) = run(&temp.0, vec![graph, repowise], "replay"); + assert_eq!(replay_exit, 0, "{replay_stderr}"); + assert_eq!( + fs::read(out.join("hospital-report.json")).unwrap(), + fs::read(replay_out.join("hospital-report.json")).unwrap(), + "input order and output path must not change the machine diagnosis" + ); + let markdown = fs::read_to_string(out.join("hospital.md")).unwrap(); + assert!(markdown.contains("Primary diagnosis: provider quota exhausted")); + let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let schema = Command::new("pwsh") + .args(["-NoLogo", "-NoProfile", "-Command", "param($Document,$Schema); if (-not (Get-Content -Raw -LiteralPath $Document | Test-Json -SchemaFile $Schema -ErrorAction Stop)) { exit 1 }"]) + .arg(out.join("hospital-report.json")) + .arg(repo.join("orchestration/schemas/code-intel-hospital.v1.schema.json")) + .output() + .unwrap(); + assert!( + schema.status.success(), + "{}", + String::from_utf8_lossy(&schema.stderr) + ); +} + +#[test] +fn provider_identity_spoofs_cannot_supply_any_admitted_modality() { + let temp = Temp::new(); + let cases = [ + ( + "graph-spoof", + "repowise.docs-graph-spoof", + json!({"architectureGraph":{"schema":"code-intel-architecture-graph-evidence.v1","snapshotIdentity":SNAPSHOT,"completeness":"complete","graph":{"nodes":[],"edges":[]}}}), + ), + ( + "structural-spoof", + "structural-evidence.sentrux-spoof", + json!({"structuralEvidence":{"schema":"code-intel-structural-evidence-payload.v1","snapshotIdentity":SNAPSHOT,"completeness":"complete","rules":[{"kind":"boundary_dependency","status":"evaluated","verdict":"pass","failure":{"kind":"none"}}]}}), + ), + ( + "native-spoof", + "native-code-evidence-spoof", + json!({"nativeCode":{"modernizationDebt":false,"topTarget":""}}), + ), + ]; + for (name, provider, data) in cases { + let spoof = admission(&temp.0, name, provider, "observed", "none", data); + let (exit, _, out, _) = run(&temp.0, vec![spoof], &format!("{name}-out")); + assert_eq!(exit, 65, "spoof provider {provider} must fail closed"); + assert!(!out.join("hospital-report.json").exists()); + assert!(!out.join("hospital.md").exists()); + assert!(!out.join("surgery-plan.json").exists()); + assert!(!out.join("surgery-plan.md").exists()); + } +} + +#[test] +fn repowise_quota_signal_requires_an_exact_provider_identity() { + let temp = Temp::new(); + let spoof = admission( + &temp.0, + "repowise-spoof", + "repowise.docs-spoof", + "unknown", + "provider_unavailable", + json!({"repowise":{"channel":"docs","status":"quota"}}), + ); + let (exit, _, out, _) = run(&temp.0, vec![spoof, graph(&temp.0, false)], "quota-spoof"); + assert_eq!(exit, 65); + assert!(!out.join("hospital-report.json").exists()); + assert!(!out.join("hospital.md").exists()); + assert!(!out.join("surgery-plan.json").exists()); + assert!(!out.join("surgery-plan.md").exists()); +} + +#[test] +fn precedence_matrix_matches_the_legacy_stable_diagnoses_and_fails_closed() { + let temp = Temp::new(); + let cases = vec![ + ( + "local-first", + vec![ + admission( + &temp.0, + "local", + "repowise.index", + "unknown", + "local_tool_error", + json!({"repowise":{"status":"unavailable"}}), + ), + admission( + &temp.0, + "quota-2", + "repowise.docs", + "unknown", + "provider_unavailable", + json!({"repowise":{"status":"quota"}}), + ), + ], + "local tool failure", + "triage", + ), + ( + "gate-before-graph", + vec![ + graph(&temp.0, false), + structural(&temp.0, "structural-fail", Some("fail"), true), + ], + "architecture gate failure", + "govern", + ), + ( + "graph-missing", + vec![ + graph(&temp.0, false), + structural(&temp.0, "structural-pass-1", Some("pass"), true), + ], + "architecture graph missing", + "diagnose", + ), + ( + "authoritative-missing", + vec![graph(&temp.0, true), native(&temp.0, false)], + "authoritative structural evidence unavailable", + "diagnose", + ), + ( + "authoritative-untrusted", + vec![ + graph(&temp.0, true), + structural(&temp.0, "structural-unknown", Some("unknown"), false), + native(&temp.0, false), + ], + "authoritative structural evidence unavailable", + "diagnose", + ), + ( + "ungoverned", + vec![ + graph(&temp.0, true), + structural(&temp.0, "structural-empty", None, true), + ], + "ungoverned structural scope", + "govern", + ), + ( + "modernization", + vec![ + graph(&temp.0, true), + structural(&temp.0, "structural-pass-2", Some("pass"), true), + native(&temp.0, true), + ], + "known modernization debt", + "surgery_plan", + ), + ( + "clean", + vec![ + graph(&temp.0, true), + structural(&temp.0, "structural-pass-3", Some("pass"), true), + native(&temp.0, false), + ], + "clean snapshot", + "post_op", + ), + ]; + for (name, inputs, expected_diagnosis, expected_protocol) in cases { + let (exit, actual_diagnosis, actual_protocol, _) = diagnosis(&temp.0, inputs, name); + let expected_exit = if matches!(expected_protocol, "govern" | "surgery_plan") { + 10 + } else { + 0 + }; + assert_eq!(exit, expected_exit, "case={name}: {actual_diagnosis}"); + assert_eq!(actual_diagnosis, expected_diagnosis, "case={name}"); + assert_eq!(actual_protocol, expected_protocol, "case={name}"); + } +} + +#[test] +fn missing_or_non_admitted_authority_is_rejected_and_enrichment_never_overrides_it() { + let temp = Temp::new(); + let (exit, diagnosis_text, protocol, _) = diagnosis( + &temp.0, + vec![graph(&temp.0, true), native(&temp.0, true)], + "missing-authority", + ); + assert_eq!(exit, 0); + assert_eq!( + diagnosis_text, + "authoritative structural evidence unavailable" + ); + assert_eq!(protocol, "diagnose"); + + let (untrusted_exit, untrusted_diagnosis, untrusted_protocol, untrusted_out) = diagnosis( + &temp.0, + vec![ + graph(&temp.0, true), + structural(&temp.0, "untrusted-with-target", Some("unknown"), false), + native(&temp.0, true), + ], + "untrusted-authority-with-enrichment", + ); + assert_eq!(untrusted_exit, 0); + assert_eq!( + untrusted_diagnosis, + "authoritative structural evidence unavailable" + ); + assert_eq!(untrusted_protocol, "diagnose"); + let untrusted_machine: Value = + serde_json::from_slice(&fs::read(untrusted_out.join("hospital-report.json")).unwrap()) + .unwrap(); + assert_eq!(untrusted_machine["domainVerdict"], "unknown"); + assert_eq!(untrusted_machine["surgery_plan"]["status"], "not_required"); + + let mut rejected = structural(&temp.0, "rejected-structural", Some("pass"), true); + let path = temp.0.join(rejected["path"].as_str().unwrap()); + let mut value: Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); + value["status"] = json!("rejected"); + fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap(); + rejected["sha256"] = json!(file_sha256(&path)); + let (rejected_exit, _, out, stderr) = run( + &temp.0, + vec![graph(&temp.0, true), rejected, native(&temp.0, true)], + "rejected-authority", + ); + assert_eq!(rejected_exit, 65, "{stderr}"); + assert!(!out.join("hospital-report.json").exists()); +} + +#[test] +fn conflicting_or_provider_injected_modalities_fail_closed_independent_of_input_order() { + let temp = Temp::new(); + let first = admission( + &temp.0, + "graph-provider-a", + "architecture-graph.provider-a", + "observed", + "none", + json!({"architectureGraph":{"completeness":"complete","graph":{"nodes":[],"edges":[]}}}), + ); + let second = admission( + &temp.0, + "graph-provider-b", + "architecture-graph.provider-b", + "unknown", + "domain_unknown", + json!({"architectureGraph":{"completeness":"partial","graph":null}}), + ); + let (forward_exit, forward, forward_out, forward_stderr) = run( + &temp.0, + vec![first.clone(), second.clone()], + "conflict-forward", + ); + let (reverse_exit, reverse, reverse_out, reverse_stderr) = + run(&temp.0, vec![second, first], "conflict-reverse"); + assert_eq!(forward_exit, 65, "{forward_stderr}"); + assert_eq!(reverse_exit, 65, "{reverse_stderr}"); + assert_eq!(forward["status"], reverse["status"]); + assert_eq!(forward["verdict"], reverse["verdict"]); + assert_eq!(forward["diagnostics"], reverse["diagnostics"]); + assert!(!forward_out.join("hospital-report.json").exists()); + assert!(!reverse_out.join("hospital-report.json").exists()); + + let injected = admission( + &temp.0, + "injected-graph", + "repowise.docs", + "observed", + "none", + json!({"architectureGraph":{"completeness":"complete","graph":{"nodes":[],"edges":[]}}}), + ); + let (injected_exit, _, injected_out, injected_stderr) = + run(&temp.0, vec![injected], "provider-injected"); + assert_eq!(injected_exit, 65, "{injected_stderr}"); + assert!(!injected_out.join("hospital-report.json").exists()); +} + +#[test] +fn markdown_is_a_rebuildable_view_and_cannot_change_the_machine_verdict() { + let temp = Temp::new(); + let inputs = vec![ + graph(&temp.0, true), + structural(&temp.0, "render-pass", Some("pass"), true), + ]; + let (exit, _, _, out) = diagnosis(&temp.0, inputs.clone(), "render-first"); + assert_eq!(exit, 0); + let machine = fs::read(out.join("hospital-report.json")).unwrap(); + fs::write( + out.join("hospital.md"), + "# forged\nPrimary diagnosis: clean snapshot\n", + ) + .unwrap(); + let (replay_exit, _, _, replay_out) = diagnosis(&temp.0, inputs, "render-rebuilt"); + assert_eq!(replay_exit, 0); + assert_eq!( + machine, + fs::read(replay_out.join("hospital-report.json")).unwrap() + ); + let rebuilt = fs::read_to_string(replay_out.join("hospital.md")).unwrap(); + assert!(rebuilt.contains("Primary diagnosis: clean snapshot")); + assert!(!rebuilt.contains("# forged")); +} + +fn dynamic_artifact_ref( + root: &Path, + path: &str, + schema: &str, + kind: &str, + snapshot_identity: &str, + value: &Value, +) -> Value { + let full_path = root.join(path); + fs::write(&full_path, serde_json::to_vec(value).unwrap()).unwrap(); + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":schema, + "type":kind, + "path":path, + "sha256":file_sha256(&full_path), + "consumedSnapshotIdentity":snapshot_identity + }) +} + +fn dynamic_admission( + root: &Path, + name: &str, + provider: &str, + snapshot_identity: &str, + data: Value, +) -> Value { + let payload = json!({"schema":"code-intel-evidence-payload.v1","data":data}); + let payload_ref = dynamic_artifact_ref( + root, + &format!("{name}-payload.json"), + "code-intel-evidence-payload.v1", + "observed.evidence.payload", + snapshot_identity, + &payload, + ); + let result = json!({ + "schema":"code-intel-evidence-admissibility-result.v1", + "status":"admitted", + "domainVerdict":"observed", + "admissionIdentity":payload_ref["sha256"], + "evidence":{ + "provider":{"id":provider}, + "consumedSnapshotIdentity":snapshot_identity, + "failure":{"kind":"none"}, + "payload":payload_ref + }, + "verifiedPayload":{ + "sha256":payload_ref["sha256"], + "artifactSchema":"code-intel-evidence-payload.v1", + "type":"observed.evidence.payload", + "consumedSnapshotIdentity":snapshot_identity, + "data":payload["data"] + }, + "engineeringFacts":[] + }); + dynamic_artifact_ref( + root, + &format!("{name}-admission.json"), + "code-intel-evidence-admissibility-result.v1", + "evidence.admission", + snapshot_identity, + &result, + ) +} + +#[test] +fn a09_seeded_path_executes_hospital_through_a01_and_rejects_snapshot_mismatch() { + let temp = Temp::new(); + let repo = temp.0.join("repo"); + let seed = temp.0.join("seed"); + fs::create_dir_all(&repo).unwrap(); + fs::create_dir_all(&seed).unwrap(); + fs::write(repo.join("source.txt"), "stable fixture\n").unwrap(); + let snapshot_output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "snapshot", + "identity", + "--repo", + repo.to_str().unwrap(), + "--working-tree-policy", + "explicit_overlay", + "--scope", + ".", + ]) + .output() + .unwrap(); + assert!(snapshot_output.status.success()); + let snapshot_document: Value = serde_json::from_slice(&snapshot_output.stdout).unwrap(); + let snapshot_identity = snapshot_document["snapshot"]["identity"].as_str().unwrap(); + let inputs = vec![ + dynamic_admission( + &seed, + "graph", + "architecture-graph.internal", + snapshot_identity, + json!({"architectureGraph":{"completeness":"complete","graph":{"nodes":[],"edges":[]}}}), + ), + dynamic_admission( + &seed, + "structure", + "structural-evidence.sentrux", + snapshot_identity, + json!({"structuralEvidence":{"completeness":"complete","rules":[{"verdict":"pass"}]}}), + ), + ]; + let inputs_path = temp.0.join("diagnosis-inputs.json"); + fs::write(&inputs_path, serde_json::to_vec(&inputs).unwrap()).unwrap(); + let out = temp.0.join("a09-run"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(&repo) + .arg("--out") + .arg(&out) + .arg("--diagnosis-inputs") + .arg(&inputs_path) + .arg("--seed-artifact-root") + .arg(&seed) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let manifest: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(manifest["schema"], "code-intel-run-manifest.v1"); + let hospital_path = out.join("diagnosis.hospital/hospital-report.json"); + let hospital: Value = serde_json::from_slice(&fs::read(hospital_path).unwrap()).unwrap(); + assert_eq!(hospital["triage"]["primary_diagnosis"], "clean snapshot"); + + let mut mismatched = inputs; + mismatched[0]["consumedSnapshotIdentity"] = json!("f".repeat(64)); + let mismatched_path = temp.0.join("mismatched-inputs.json"); + fs::write(&mismatched_path, serde_json::to_vec(&mismatched).unwrap()).unwrap(); + let failed_out = temp.0.join("a09-failed"); + let failed = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(&repo) + .arg("--out") + .arg(&failed_out) + .arg("--diagnosis-inputs") + .arg(&mismatched_path) + .arg("--seed-artifact-root") + .arg(&seed) + .output() + .unwrap(); + assert_eq!( + failed.status.code(), + Some(65), + "stdout={} stderr={} failed_out={} exists={}", + String::from_utf8_lossy(&failed.stdout), + String::from_utf8_lossy(&failed.stderr), + failed_out.display(), + failed_out.exists() + ); + assert!(!failed_out + .join("diagnosis.hospital/hospital-report.json") + .exists()); +} + +#[test] +fn legacy_facade_and_rust_execute_the_same_fixture_with_stable_machine_parity() { + let temp = Temp::new(); + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let source = fs::read_to_string(root.join("run-code-intel.ps1")).unwrap(); + let main = source + .find("\n$configData = $null") + .expect("legacy function boundary"); + let function_source = source[..main].replace( + "$PSScriptRoot", + &format!("'{}'", root.to_string_lossy().replace('\\', "/")), + ); + let legacy_out = temp.0.join("legacy"); + fs::create_dir(&legacy_out).unwrap(); + let seam = format!( + "{}\n{}", + function_source, + r#" +$out = $ArtifactRoot +$failureCounts = [ordered]@{ localToolError = 1; providerQuota = 0; sentruxFail = 0; graphMissing = 0 } +$steps = @( + [pscustomobject]@{ name='git status'; status='failed'; output=''; error='fixture'; exitCode=1; durationMs=1 }, + [pscustomobject]@{ name='rg file inventory'; status='ok'; output='2'; error=''; exitCode=0; durationMs=1 }, + [pscustomobject]@{ name='understand graph'; status='ok'; output=''; error=''; exitCode=0; durationMs=1 }, + [pscustomobject]@{ name='repowise fixture'; status='ok'; output=''; error=''; exitCode=0; durationMs=1 }, + [pscustomobject]@{ name='sentrux check'; status='ok'; output=''; error=''; exitCode=0; durationMs=1 }, + [pscustomobject]@{ name='sentrux gate fixture'; status='ok'; output=''; error=''; exitCode=0; durationMs=1 } +) +$hospital = New-CodeIntelHospitalReport -RepoPath $RepoPath -Mode 'normal' -RunDir $out -ReportPath (Join-Path $out 'report.json') -SummaryPath (Join-Path $out 'summary.md') -UnderstandingPath (Join-Path $out 'understanding.md') -Steps $steps -FailureCounts $failureCounts -SentruxInsight ([ordered]@{}) -SentruxDsmSummary $null -SentruxFileDetailsSummary $null -SentruxHotspotsSummary $null -SentruxEvolutionSummary $null -SentruxWhatIfSummary $null -CodeNexusContextSummary $null -UnderstandCommand 'fixture' -ToolState ([ordered]@{}) -GitHubResearch $null +$surgery = New-CodeIntelSurgeryPlan -Hospital $hospital -RepoPath $RepoPath -SentruxTargetPath $RepoPath -HotspotsPath '' -WhatIfPath '' -CodeNexusPath '' +$hospital['surgery_plan'] = [ordered]@{ path=(Join-Path $out 'surgery-plan.json'); markdown=(Join-Path $out 'surgery-plan.md'); status=$surgery.status; primary_target='' } +$hospital | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $out 'hospital-report.json') -Encoding utf8NoBOM +Convert-HospitalReportToMarkdown $hospital | Set-Content -LiteralPath (Join-Path $out 'hospital.md') -Encoding utf8NoBOM +$surgery | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath (Join-Path $out 'surgery-plan.json') -Encoding utf8NoBOM +Convert-SurgeryPlanToMarkdown $surgery | Set-Content -LiteralPath (Join-Path $out 'surgery-plan.md') -Encoding utf8NoBOM +[ordered]@{ primary_diagnosis=$hospital.triage.primary_diagnosis; disposition=$hospital.triage.disposition; next_protocol=$hospital.triage.next_protocol; surgery_status=$surgery.status } | ConvertTo-Json -Compress +"#, + ); + let seam_path = temp.0.join("legacy-hospital-seam.ps1"); + fs::write(&seam_path, seam).unwrap(); + let legacy = Command::new("pwsh") + .args(["-NoLogo", "-NoProfile", "-File"]) + .arg(&seam_path) + .arg("-RepoPath") + .arg(&temp.0) + .arg("-ArtifactRoot") + .arg(&legacy_out) + .output() + .unwrap(); + assert!( + legacy.status.success(), + "{}", + String::from_utf8_lossy(&legacy.stderr) + ); + let legacy_machine: Value = + serde_json::from_slice(legacy.stdout.trim_ascii()).expect("legacy machine JSON"); + + let local_failure = admission( + &temp.0, + "legacy-parity-local", + "repowise.index", + "unknown", + "local_tool_error", + json!({"repowise":{"status":"unavailable"}}), + ); + let (exit, _, rust_out, stderr) = run(&temp.0, vec![local_failure], "rust-parity"); + assert_eq!(exit, 0, "{stderr}"); + let rust_machine: Value = + serde_json::from_slice(&fs::read(rust_out.join("hospital-report.json")).unwrap()).unwrap(); + assert_eq!( + legacy_machine["primary_diagnosis"], + rust_machine["triage"]["primary_diagnosis"] + ); + assert_eq!( + legacy_machine["disposition"], + rust_machine["triage"]["disposition"] + ); + assert_eq!( + legacy_machine["next_protocol"], + rust_machine["triage"]["next_protocol"] + ); + assert_eq!( + legacy_machine["surgery_status"], + rust_machine["surgery_plan"]["status"] + ); + for file in [ + "hospital-report.json", + "hospital.md", + "surgery-plan.json", + "surgery-plan.md", + ] { + assert!(legacy_out.join(file).is_file(), "legacy omitted {file}"); + assert!(rust_out.join(file).is_file(), "Rust omitted {file}"); + } +} diff --git a/crates/code-intel-cli/tests/internalization_record.rs b/crates/code-intel-cli/tests/internalization_record.rs new file mode 100644 index 0000000..6ae70ae --- /dev/null +++ b/crates/code-intel-cli/tests/internalization_record.rs @@ -0,0 +1,1637 @@ +#[path = "../src/authority.rs"] +mod authority; +#[path = "../src/internalization_record.rs"] +mod internalization_record; + +use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use serde_json::{json, Value}; + +const NOW: u64 = 1_700_000_100; +static NEXT_SCHEMA_CHECK: AtomicUsize = AtomicUsize::new(1); +static SCHEMA_CHECK_LOCK: Mutex<()> = Mutex::new(()); + +fn root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn complete() -> Value { + serde_json::from_slice( + &fs::read(root().join("tests/fixtures/internalization/complete.json")).unwrap(), + ) + .unwrap() +} + +fn advisory_candidate(name: &str) -> Value { + serde_json::from_slice( + &fs::read( + root() + .join("orchestration/internalization") + .join(format!("{name}.json")), + ) + .unwrap(), + ) + .unwrap() +} + +fn c03_measurements() -> Value { + serde_json::from_slice( + &fs::read(root().join("orchestration/internalization/c03-r05-r12-measurements.json")) + .unwrap(), + ) + .unwrap() +} + +fn integration_exists(id: &str) -> bool { + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + registry["integrations"] + .as_array() + .unwrap() + .iter() + .any(|entry| entry["id"] == id) +} + +fn known(record: &Value) -> Vec { + internalization_record::record_evidence_ids(record) + .unwrap() + .into_iter() + .collect() +} + +#[test] +fn complete_record_enables_production_and_projects_reuse_notice_and_provenance() { + let record = complete(); + let evaluation = internalization_record::evaluate_record(&record, NOW, &known(&record), &[]) + .expect("complete record must validate"); + assert_eq!(evaluation["researchAllowed"], true); + assert_eq!(evaluation["productionEnabled"], true); + assert_eq!(evaluation["status"], "production_enabled"); + assert_eq!(evaluation["diagnostics"], json!([])); + assert_eq!( + evaluation["consumedAuthorityEventId"], + "authority-enable-example" + ); + + let reuse = internalization_record::project_reuse_record(&record, &evaluation).unwrap(); + assert_eq!(reuse["schema"], "code-intel-reuse-record.v1"); + assert_eq!(reuse["sourceRevision"], "0123456789abcdef"); + assert_eq!(reuse["adoptionRung"], "adapt"); + assert_eq!(reuse["economics"]["benefit"]["value"], 2); + assert_eq!( + reuse["compatibilityEvidence"]["evidenceIds"], + json!(["ev-compatibility"]) + ); + assert_eq!( + reuse["conformanceEvidence"]["evidenceIds"], + json!(["ev-conformance"]) + ); + assert_eq!( + reuse["assurance"]["securityEvidence"]["evidenceIds"], + json!(["ev-security"]) + ); + assert_eq!(reuse["productionEnabled"], true); + assert_eq!(reuse["engineeringFacts"], json!([])); + + let notice = internalization_record::project_notice_provenance(&record, &evaluation).unwrap(); + assert_eq!(notice["schema"], "code-intel-notice-provenance.v1"); + assert!(notice["noticeText"] + .as_str() + .unwrap() + .contains("Apache-2.0")); + assert_eq!(notice["provenance"]["revision"], "0123456789abcdef"); + assert_eq!( + notice["provenance"]["ownedModifications"] + .as_array() + .unwrap() + .len(), + 1 + ); +} + +#[test] +fn each_required_evidence_class_independently_blocks_production_but_not_research() { + let cases = [ + "/adoption/necessityEvidence", + "/adoption/compatibilityEvidence", + "/adoption/conformanceEvidence", + "/economics/benefitEvidence", + "/economics/costEvidence", + "/assurance/maintenanceEvidence", + "/assurance/securityEvidence", + "/update/evidence", + "/rollback/evidence", + "/exit/evidence", + "/retirement/evidence", + "/ownedModifications/0", + "/lifecycle", + ]; + for pointer in cases { + let mut record = complete(); + record + .pointer_mut(&format!("{pointer}/evidenceIds")) + .unwrap() + .as_array_mut() + .unwrap() + .clear(); + let evaluation = + internalization_record::evaluate_record(&record, NOW, &known(&record), &[]).unwrap(); + assert_eq!( + evaluation["researchAllowed"], true, + "{pointer}: {evaluation}" + ); + assert_eq!( + evaluation["productionEnabled"], false, + "{pointer}: {evaluation}" + ); + assert!( + evaluation["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|value| value.as_str().unwrap().contains("evidence")), + "{pointer}: {evaluation}" + ); + } +} + +#[test] +fn source_license_and_measurements_are_structural_requirements() { + for (pointer, invalid) in [ + ("/subject/source/revision", json!("")), + ("/subject/license/obligations", json!([])), + ("/economics/benefit/value", json!(-1)), + ("/economics/cost/unit", json!("")), + ] { + let mut record = complete(); + *record.pointer_mut(pointer).unwrap() = invalid; + assert!( + internalization_record::evaluate_record(&record, NOW, &[], &[]).is_err(), + "{pointer} must be rejected structurally" + ); + } +} + +#[test] +fn operation_trace_rejects_empty_duplicate_and_malformed_digest_entries() { + let valid = json!({ + "integrationId": "provider.example", + "operation": "adapt", + "command": "code-intel provider example-adapt", + "implementationIdentity": { + "providerId": "example.provider", + "implementationId": "example.v1", + "activation": "primary" + }, + "source": { + "path": "src/example.rs", + "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "conformance": { + "path": "tests/example.rs", + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "testName": "example_conformance" + } + }); + + let mut empty = complete(); + empty["operationTrace"] = json!([]); + assert!(internalization_record::evaluate_record(&empty, NOW, &[], &[]).is_err()); + + let mut duplicate = complete(); + duplicate["operationTrace"] = json!([valid.clone(), valid.clone()]); + assert!(internalization_record::evaluate_record(&duplicate, NOW, &[], &[]).is_err()); + + let mut malformed = complete(); + malformed["operationTrace"] = json!([valid]); + malformed["operationTrace"][0]["source"]["sha256"] = json!("ABC123"); + assert!(internalization_record::evaluate_record(&malformed, NOW, &[], &[]).is_err()); +} + +#[test] +fn expired_unknown_and_update_due_evidence_fail_closed_for_production() { + let mut expired = complete(); + expired["assurance"]["securityEvidence"]["expiresAt"] = json!(NOW - 1); + let expired_eval = + internalization_record::evaluate_record(&expired, NOW, &known(&expired), &[]).unwrap(); + assert_eq!(expired_eval["productionEnabled"], false); + assert!(expired_eval["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("expired"))); + + let record = complete(); + let mut incomplete_known = known(&record); + incomplete_known.retain(|id| id != "ev-conformance"); + let unknown_eval = + internalization_record::evaluate_record(&record, NOW, &incomplete_known, &[]).unwrap(); + assert_eq!(unknown_eval["productionEnabled"], false); + assert!(unknown_eval["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("unknown"))); + + let mut due = complete(); + due["update"]["nextCheckAt"] = json!(NOW - 1); + let due_eval = internalization_record::evaluate_record(&due, NOW, &known(&due), &[]).unwrap(); + assert_eq!(due_eval["productionEnabled"], false); + assert!(due_eval["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("update check"))); +} + +#[test] +fn lifecycle_changes_require_a05_authority_and_reject_replay_or_illegal_edges() { + let mut missing = complete(); + missing["lifecycle"]["authorityEvent"] = Value::Null; + let missing_eval = + internalization_record::evaluate_record(&missing, NOW, &known(&missing), &[]).unwrap(); + assert_eq!(missing_eval["productionEnabled"], false); + assert!(missing_eval["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("authority"))); + + let record = complete(); + let replay = internalization_record::evaluate_record( + &record, + NOW, + &known(&record), + &["authority-enable-example".to_string()], + ) + .unwrap(); + assert_eq!(replay["productionEnabled"], false); + assert!(replay["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("replay"))); + + let mut expired_authority = complete(); + expired_authority["lifecycle"]["authorityEvent"]["expiresAt"] = json!(NOW - 1); + let expired_authority_eval = internalization_record::evaluate_record( + &expired_authority, + NOW, + &known(&expired_authority), + &[], + ) + .unwrap(); + assert_eq!(expired_authority_eval["productionEnabled"], false); + assert!(expired_authority_eval["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("expired"))); + + let mut illegal = complete(); + illegal["lifecycle"]["previousStatus"] = json!("retired"); + let illegal_eval = + internalization_record::evaluate_record(&illegal, NOW, &known(&illegal), &[]).unwrap(); + assert_eq!(illegal_eval["productionEnabled"], false); + assert!(illegal_eval["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|d| d.as_str().unwrap().contains("transition"))); +} + +#[test] +fn generic_v1_out_of_scope_event_remains_compatible_but_declared_repository_sign_off_is_required() { + let mut generic = complete(); + generic["lifecycle"]["status"] = json!("out_of_scope"); + let accepted = + internalization_record::evaluate_record(&generic, NOW, &known(&generic), &[]).unwrap(); + assert_eq!(accepted["lifecycleAccepted"], true); + assert_eq!( + accepted["consumedAuthorityEventId"], + "authority-enable-example" + ); + + generic["authorityRequirements"] = json!({"repositoryGovernedAttestation":true}); + let rejected = + internalization_record::evaluate_record(&generic, NOW, &known(&generic), &[]).unwrap(); + assert_eq!(rejected["lifecycleAccepted"], false); + assert!(rejected["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|value| value.as_str().unwrap().contains("attestation is required"))); +} + +#[test] +fn rejected_incomplete_and_expired_lifecycle_attempts_do_not_consume_authority() { + let mut rejected = complete(); + rejected["lifecycle"]["previousStatus"] = json!("retired"); + + let mut incomplete = complete(); + incomplete["adoption"]["conformanceEvidence"]["evidenceIds"] = json!([]); + + let mut expired = complete(); + expired["update"]["evidence"]["expiresAt"] = json!(NOW - 1); + + for (label, record) in [ + ("rejected", rejected), + ("incomplete", incomplete), + ("expired", expired), + ] { + let evaluation = + internalization_record::evaluate_record(&record, NOW, &known(&record), &[]).unwrap(); + assert_eq!(evaluation["productionEnabled"], false, "{label}"); + assert_eq!( + evaluation["consumedAuthorityEventId"], + Value::Null, + "{label}: {evaluation}" + ); + } +} + +#[test] +fn candidate_retirement_and_expired_update_or_retirement_are_retryable() { + let mut cases = Vec::new(); + + let mut candidate = complete(); + candidate["lifecycle"]["previousStatus"] = json!("production_enabled"); + candidate["lifecycle"]["status"] = json!("retired"); + candidate["retirement"]["status"] = json!("candidate"); + let mut completed = candidate.clone(); + completed["retirement"]["status"] = json!("completed"); + cases.push(("candidate retirement", candidate, completed)); + + let mut expired_update = complete(); + expired_update["update"]["evidence"]["expiresAt"] = json!(NOW - 1); + let mut renewed_update = expired_update.clone(); + renewed_update["update"]["evidence"]["expiresAt"] = json!(NOW + 100); + cases.push(("expired update", expired_update, renewed_update)); + + let mut expired_retirement = complete(); + expired_retirement["retirement"]["evidence"]["expiresAt"] = json!(NOW - 1); + let mut renewed_retirement = expired_retirement.clone(); + renewed_retirement["retirement"]["evidence"]["expiresAt"] = json!(NOW + 100); + cases.push(("expired retirement", expired_retirement, renewed_retirement)); + + for (label, first, repaired) in cases { + let first_evaluation = + internalization_record::evaluate_record(&first, NOW, &known(&first), &[]).unwrap(); + assert_eq!(first_evaluation["productionEnabled"], false, "{label}"); + assert_eq!( + first_evaluation["consumedAuthorityEventId"], + Value::Null, + "{label}: {first_evaluation}" + ); + + let retry = internalization_record::evaluate_record(&repaired, NOW, &known(&repaired), &[]) + .unwrap(); + assert_eq!( + retry["consumedAuthorityEventId"], "authority-enable-example", + "{label}: {retry}" + ); + assert_eq!(retry["lifecycleAccepted"], true, "{label}: {retry}"); + } +} + +#[test] +fn replacement_rollback_and_retirement_require_their_specific_closure() { + let mut replacement = complete(); + replacement["lifecycle"]["previousStatus"] = json!("production_enabled"); + replacement["lifecycle"]["status"] = json!("replaced"); + replacement["lifecycle"]["replacementRecordId"] = Value::Null; + let replacement_eval = + internalization_record::evaluate_record(&replacement, NOW, &known(&replacement), &[]) + .unwrap(); + assert_eq!(replacement_eval["lifecycleAccepted"], false); + + let mut rollback = complete(); + rollback["lifecycle"]["previousStatus"] = json!("production_enabled"); + rollback["lifecycle"]["status"] = json!("rollback"); + rollback["rollback"]["evidence"]["evidenceIds"] = json!([]); + let rollback_eval = + internalization_record::evaluate_record(&rollback, NOW, &known(&rollback), &[]).unwrap(); + assert_eq!(rollback_eval["lifecycleAccepted"], false); + + let mut retired = complete(); + retired["lifecycle"]["previousStatus"] = json!("production_enabled"); + retired["lifecycle"]["status"] = json!("retired"); + retired["retirement"]["status"] = json!("candidate"); + let retired_eval = + internalization_record::evaluate_record(&retired, NOW, &known(&retired), &[]).unwrap(); + assert_eq!(retired_eval["lifecycleAccepted"], false); +} + +#[test] +fn rollback_replacement_retirement_and_audit_only_research_have_positive_paths() { + for (status, replacement, retirement) in [ + ("rollback", Value::Null, "active"), + ("replaced", json!("reuse-replacement"), "active"), + ("retired", Value::Null, "completed"), + ] { + let mut record = complete(); + record["lifecycle"]["previousStatus"] = json!("production_enabled"); + record["lifecycle"]["status"] = json!(status); + record["lifecycle"]["replacementRecordId"] = replacement; + record["retirement"]["status"] = json!(retirement); + let evaluation = + internalization_record::evaluate_record(&record, NOW, &known(&record), &[]).unwrap(); + assert_eq!( + evaluation["lifecycleAccepted"], true, + "{status}: {evaluation}" + ); + assert_eq!(evaluation["productionEnabled"], false); + } + + let mut research = complete(); + research["lifecycle"]["previousStatus"] = Value::Null; + research["lifecycle"]["status"] = json!("research"); + research["lifecycle"]["authorityEvent"] = Value::Null; + research["adoption"]["conformanceEvidence"]["evidenceIds"] = json!([]); + let evaluation = + internalization_record::evaluate_record(&research, NOW, &known(&research), &[]).unwrap(); + assert_eq!(evaluation["researchAllowed"], true); + assert_eq!(evaluation["lifecycleAccepted"], true); + assert_eq!(evaluation["productionEnabled"], false); +} + +#[test] +fn deterministic_store_rejects_duplicates_and_does_not_make_adoption_decisions() { + let record = complete(); + let mut store = internalization_record::RecordStore::default(); + store.insert(record.clone()).unwrap(); + assert!(store + .insert(record.clone()) + .unwrap_err() + .contains("duplicate")); + let projected = store + .project_reuse_records(NOW, &known(&record), &[]) + .unwrap(); + assert_eq!(projected.len(), 1); + let text = serde_json::to_string(&projected).unwrap(); + assert!(!text.contains("adoption_decision")); + assert!(!text.contains("install")); +} + +#[test] +fn projections_use_sealed_evaluations_bound_to_the_exact_record() { + let record = complete(); + let evaluation = + internalization_record::evaluate_record(&record, NOW, &known(&record), &[]).unwrap(); + assert_ne!( + std::any::type_name_of_val(&evaluation), + std::any::type_name::() + ); + + let mut changed_after_evaluation = record.clone(); + changed_after_evaluation["lifecycle"]["authorityEvent"]["id"] = json!("forged-authority-event"); + assert!( + internalization_record::project_reuse_record(&changed_after_evaluation, &evaluation) + .is_err() + ); + assert!(internalization_record::project_notice_provenance( + &changed_after_evaluation, + &evaluation + ) + .is_err()); +} + +fn assert_research_candidate(record: &Value, expected_id: &str) { + assert_eq!(record["id"], expected_id); + assert_eq!(record["lifecycle"]["status"], "research"); + assert!(record["subject"]["source"]["revision"] + .as_str() + .unwrap() + .contains("unverified-upstream")); + assert_eq!(record["subject"]["license"]["id"], "UNKNOWN-RESEARCH-ONLY"); + + let evidence_ids = known(record); + let gaps = evidence_ids + .iter() + .filter(|id| id.starts_with("gap:")) + .cloned() + .collect::>(); + assert!(!gaps.is_empty()); + let admitted = evidence_ids + .into_iter() + .filter(|id| !id.starts_with("gap:")) + .collect::>(); + let evaluation = + internalization_record::evaluate_record(record, 1_783_900_800, &admitted, &[]).unwrap(); + assert_eq!(evaluation["researchAllowed"], true); + assert_eq!(evaluation["productionEnabled"], false); + assert_eq!(evaluation["consumedAuthorityEventId"], Value::Null); + let diagnostics = evaluation["diagnostics"] + .as_array() + .unwrap() + .iter() + .map(|diagnostic| diagnostic.as_str().unwrap()) + .collect::>() + .join("\n"); + assert!(diagnostics.contains("unknown evidence")); + assert!(gaps.iter().any(|gap| gap.contains(":license"))); + assert!(gaps.iter().any(|gap| gap.contains(":upstream-revision"))); + + let reuse = internalization_record::project_reuse_record(record, &evaluation).unwrap(); + let notice = internalization_record::project_notice_provenance(record, &evaluation).unwrap(); + assert_eq!(reuse["productionEnabled"], false); + assert!(notice["noticeText"] + .as_str() + .unwrap() + .contains("UNKNOWN-RESEARCH-ONLY")); + assert_checked_schema(record, "code-intel-internalization-record.v1.schema.json"); + assert_checked_schema(&reuse, "code-intel-reuse-record.v1.schema.json"); + assert_checked_schema(¬ice, "code-intel-notice-provenance.v1.schema.json"); +} + +fn integration(id: &str) -> Value { + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == id) + .unwrap_or_else(|| panic!("missing integration {id}")) + .clone() +} + +fn production_participant(id: &str) -> Value { + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + registry["productionRegistry"]["participants"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["capabilityId"] == id) + .unwrap_or_else(|| panic!("missing production participant {id}")) + .clone() +} + +fn assert_recomputable_sha(record: &Value, relative: &str, label: &str) { + let digest = recompute_sha(relative); + assert!( + record["subject"]["source"]["revision"] + .as_str() + .unwrap() + .contains(&format!("{label}:{digest}")), + "{relative} digest is not bound into the record" + ); +} + +fn assert_research_record_projects(record: &Value, expected_id: &str) { + assert_eq!(record["id"], expected_id); + assert_eq!(record["lifecycle"]["status"], "research"); + assert_eq!(record["lifecycle"]["authorityEvent"], Value::Null); + let evidence = known(record); + let admitted = evidence + .iter() + .filter(|id| !id.starts_with("gap:")) + .cloned() + .collect::>(); + assert!(evidence.iter().any(|id| id.starts_with("gap:"))); + let evaluation = + internalization_record::evaluate_record(record, 1_783_900_800, &admitted, &[]).unwrap(); + assert_eq!(evaluation["researchAllowed"], true); + assert_eq!(evaluation["productionEnabled"], false); + assert_eq!(evaluation["consumedAuthorityEventId"], Value::Null); + let reuse = internalization_record::project_reuse_record(record, &evaluation).unwrap(); + let notice = internalization_record::project_notice_provenance(record, &evaluation).unwrap(); + assert_eq!(reuse["productionEnabled"], false); + assert_checked_schema(record, "code-intel-internalization-record.v1.schema.json"); + assert_checked_schema(&reuse, "code-intel-reuse-record.v1.schema.json"); + assert_checked_schema(¬ice, "code-intel-notice-provenance.v1.schema.json"); +} + +fn assert_signed_out_of_scope_record_projects(record: &Value, expected_id: &str) { + assert_eq!(record["id"], expected_id); + assert_eq!(record["lifecycle"]["status"], "out_of_scope"); + assert_eq!( + record["lifecycle"]["authorityEvent"]["approver"], + json!({"id":"code-intel-maintainers","role":"repository_governance"}) + ); + assert_eq!( + record["lifecycle"]["authorityEvent"]["attestation"]["scheme"], + "repository-governed-sha256-v1" + ); + assert!( + record["lifecycle"]["authorityEvent"]["expiresAt"] + .as_u64() + .unwrap() + > 1_783_900_800 + ); + + let evidence = known(record); + let evaluation = + internalization_record::evaluate_record(record, 1_783_900_800, &evidence, &[]).unwrap(); + assert_eq!(evaluation["lifecycleAccepted"], true); + assert_eq!(evaluation["productionEnabled"], false); + assert_eq!( + evaluation["consumedAuthorityEventId"], + record["lifecycle"]["authorityEvent"]["id"] + ); + + let mut unsigned = record.clone(); + unsigned["lifecycle"]["authorityEvent"] + .as_object_mut() + .unwrap() + .remove("attestation"); + let rejected = + internalization_record::evaluate_record(&unsigned, 1_783_900_800, &known(&unsigned), &[]) + .unwrap(); + assert_eq!(rejected["lifecycleAccepted"], false); + assert!(rejected["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|value| value.as_str().unwrap().contains("attestation is required"))); + assert_checked_schema(record, "code-intel-internalization-record.v1.schema.json"); +} + +fn recompute_sha(relative: &str) -> String { + let path = root().join(relative); + let mut command = Command::new("pwsh"); + command.args([ + "-NoProfile", + "-CommandWithArgs", + "(Get-FileHash -LiteralPath $args[0] -Algorithm SHA256).Hash.ToLowerInvariant()", + path.to_str().unwrap(), + ]); + let output = run_command_with_timeout(&mut command); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let digest = String::from_utf8(output.stdout).unwrap(); + let digest = digest.trim().to_string(); + assert_eq!(digest.len(), 64); + digest +} + +fn assert_operation_trace_exact(record: &Value, integration_ids: &[&str]) { + let mut expected = BTreeMap::new(); + for integration_id in integration_ids { + let entry = integration(integration_id); + for (operation, command) in entry["commands"].as_object().unwrap() { + expected.insert( + ((*integration_id).to_string(), operation.clone()), + command.as_str().unwrap().to_string(), + ); + } + } + + let traces = record["operationTrace"].as_array().unwrap(); + let mut actual = BTreeMap::new(); + for trace in traces { + let integration_id = trace["integrationId"].as_str().unwrap(); + let operation = trace["operation"].as_str().unwrap(); + let key = (integration_id.to_string(), operation.to_string()); + assert!( + actual.insert(key.clone(), trace).is_none(), + "duplicate operation trace {key:?}" + ); + + let source_path = trace["source"]["path"].as_str().unwrap(); + assert_eq!(trace["source"]["sha256"], recompute_sha(source_path)); + let test_path = trace["conformance"]["path"].as_str().unwrap(); + assert_eq!(trace["conformance"]["sha256"], recompute_sha(test_path)); + let test_name = trace["conformance"]["testName"].as_str().unwrap(); + assert!( + fs::read_to_string(root().join(test_path)) + .unwrap() + .contains(test_name), + "{test_path} does not contain named conformance {test_name}" + ); + } + + assert_eq!( + actual.keys().collect::>(), + expected.keys().collect::>(), + "operation trace coverage must exactly match registry commands" + ); + for (key, command) in expected { + assert_eq!( + actual[&key]["command"].as_str().unwrap(), + command, + "trace command differs from registry for {key:?}" + ); + } +} + +#[test] +fn ticket_r01_repowise_record_traces_b01_operations_and_stays_research_only() { + let record = advisory_candidate("repowise"); + assert_research_candidate(&record, "internalization.repowise-record"); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/repowise_adapter.rs", + "local-adapter-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/repowise_route.rs", + "local-conformance-sha256", + ); + assert_operation_trace_exact(&record, &["provider.repowise-adapt", "memory.repowise"]); + let route = integration("provider.repowise-adapt"); + assert!(route["commands"]["adapt"] + .as_str() + .unwrap() + .contains("provider repowise-adapt")); + assert!(route["commands"]["facade"] + .as_str() + .unwrap() + .contains("-RepowiseAdapterRequest")); + assert_eq!(record["economics"]["benefit"]["value"], 2); + let evidence = serde_json::to_string(&record).unwrap(); + assert!(evidence.contains("local:b01:production-operation-trace")); + assert!(evidence.contains("gap:repowise:representative-value-measurement")); + assert!(evidence.contains("gap:repowise:quota-data-handling-review")); +} + +#[test] +fn ticket_r02_graph_record_keeps_internal_and_external_evidence_separate() { + let record = advisory_candidate("graph"); + assert_research_candidate(&record, "internalization.graph-record"); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/graph_adapter.rs", + "local-adapter-sha256", + ); + assert_operation_trace_exact( + &record, + &[ + "provider.graph-adapt", + "graph.code-intel-understand", + "graph.understand-external", + ], + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/graph_adapter.rs", + "local-conformance-sha256", + ); + let route = integration("provider.graph-adapt"); + assert!(route["commands"]["adapt"] + .as_str() + .unwrap() + .contains("provider graph-adapt")); + assert!(route["commands"]["facade"] + .as_str() + .unwrap() + .contains("-GraphAdapterRequest")); + let internal = integration("graph.code-intel-understand"); + let external = integration("graph.understand-external"); + assert_eq!(internal["kind"], "internal-rust-provider"); + assert_eq!(external["kind"], "compatibility-fallback"); + assert!(internal["commands"]["refresh"] + .as_str() + .unwrap() + .contains("code-intel.exe graph")); + assert!(external["commands"]["refresh"] + .as_str() + .unwrap() + .starts_with("/understand")); + let evidence = serde_json::to_string(&record).unwrap(); + assert!(evidence.contains("local:b02:internal-operation-trace")); + assert!(evidence.contains("local:b02:external-fallback-operation-trace")); + assert!(evidence.contains("gap:graph:external-upstream-conformance")); +} + +#[test] +fn ticket_r03_sentrux_record_blocks_shim_retirement_on_windows_and_plugin_gaps() { + let record = advisory_candidate("sentrux"); + assert_research_candidate(&record, "internalization.sentrux-record"); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/sentrux_adapter.rs", + "local-adapter-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/sentrux_adapter.rs", + "local-conformance-sha256", + ); + assert_operation_trace_exact(&record, &["provider.sentrux-adapt", "structure.sentrux"]); + let route = integration("provider.sentrux-adapt"); + assert!(route["commands"]["adapt"] + .as_str() + .unwrap() + .contains("provider sentrux-adapt")); + assert!(route["commands"]["facade"] + .as_str() + .unwrap() + .contains("-SentruxAdapterRequest")); + let runtime = integration("structure.sentrux"); + assert!(runtime["commands"]["rustScan"] + .as_str() + .unwrap() + .contains("code-intel.exe sentrux")); + assert_eq!( + runtime["commands"]["rustDsm"], + "target/debug/code-intel.exe sentrux dsm " + ); + assert_eq!(record["retirement"]["status"], "candidate"); + let retirement = serde_json::to_string(&record["retirement"]).unwrap(); + assert!(retirement.contains("gap:sentrux:upstream-windows-conformance")); + assert!(retirement.contains("gap:sentrux:upstream-plugin-conformance")); + let owned = record["ownedModifications"].as_array().unwrap(); + assert_eq!(owned.len(), 3); + assert!(owned + .iter() + .any(|entry| { entry["path"] == "crates/code-intel-cli/src/sentrux_analysis.rs" })); +} + +#[test] +fn ticket_r04_codenexus_record_traces_full_lite_swap_without_vendoring_semantics() { + let record = advisory_candidate("codenexus"); + assert_research_candidate(&record, "internalization.codenexus-record"); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/codenexus_adapter.rs", + "local-adapter-sha256", + ); + assert_operation_trace_exact( + &record, + &[ + "provider.codenexus-adapt", + "runtime.code-nexus-lite", + "localization.codenexus-lite", + ], + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/codenexus_adapter.rs", + "local-conformance-sha256", + ); + let route = integration("provider.codenexus-adapt"); + assert!(route["commands"]["adapt"] + .as_str() + .unwrap() + .contains("provider codenexus-adapt")); + assert!(route["commands"]["facade"] + .as_str() + .unwrap() + .contains("-CodeNexusAdapterRequest")); + assert_eq!(integration("runtime.code-nexus-lite")["required"], false); + assert_eq!( + integration("localization.codenexus-lite")["kind"], + "compatibility-adapter" + ); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + for provider_owned in [ + "process", + "indexing", + "storage", + "retrieval", + "impact semantics", + ] { + assert!( + boundary.contains(provider_owned), + "missing {provider_owned}" + ); + } + let evidence = serde_json::to_string(&record).unwrap(); + assert!(evidence.contains("local:b04:full-lite-swap-trace")); + assert!(evidence.contains("gap:codenexus:measured-localization-value")); + assert!(evidence.contains("gap:codenexus:full-provider-security-review")); +} + +#[test] +fn ticket_r04_codenexus_compat_command_executes_and_writes_context() { + let temp = std::env::temp_dir().join(format!( + "code-intel-r04-compat-{}-{}", + std::process::id(), + NEXT_SCHEMA_CHECK.fetch_add(1, Ordering::Relaxed) + )); + let repo = temp.join("repo"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("README.md"), "# R04 compat fixture\n").unwrap(); + + let mut command = Command::new("pwsh"); + command.args([ + "-NoProfile", + "-File", + root().join("Invoke-CodeNexusLite.ps1").to_str().unwrap(), + "-RepoPath", + repo.to_str().unwrap(), + "-Quiet", + ]); + let output = run_command_with_timeout(&mut command); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let context_path = repo.join(".code-intel/codenexus-context.json"); + let context: Value = serde_json::from_slice(&fs::read(&context_path).unwrap()).unwrap(); + assert_eq!(context["tool"], "codenexus-lite"); + let emitted_repo = PathBuf::from(context["repo"].as_str().unwrap()); + assert_eq!( + fs::canonicalize(emitted_repo).unwrap(), + fs::canonicalize(&repo).unwrap(), + "compatibility context must identify the same repository across Windows short/long path aliases" + ); + fs::remove_dir_all(temp).unwrap(); +} + +#[test] +fn ticket_r05_repomix_is_a_measured_reviewed_deletion_not_fake_production() { + let record = advisory_candidate("repomix"); + assert_research_record_projects(&record, "internalization.repomix-record"); + assert_recomputable_sha( + &record, + "orchestration/internalization/c03-r05-r12-measurements.json", + "measurement-sha256", + ); + let participant = production_participant("pack.repomix"); + assert_eq!(participant["status"], "deleted"); + assert!(participant["reviewedDeletion"]["evidence"] + .as_str() + .unwrap() + .contains("no Repomix executable")); + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + assert!(!registry["integrations"] + .as_array() + .unwrap() + .iter() + .any(|entry| entry["id"] == "pack.repomix")); + assert!(record.get("operationTrace").is_none()); + assert_eq!(record["retirement"]["status"], "completed"); + assert_eq!(record["economics"]["benefit"]["value"], 0); + assert_eq!(record["economics"]["cost"]["value"], 0); + let measurements = c03_measurements(); + assert_eq!( + measurements["observations"]["r05Repomix"]["npmRegistryMetadataEntries"], + 3 + ); + assert_eq!( + measurements["observations"]["r05Repomix"]["npmPackageTarballEntries"], + 3 + ); + assert_eq!( + measurements["observations"]["r05Repomix"]["npmInstalledOrExtractedExecutableEntries"], + 0 + ); + let source = fs::read_to_string(root().join("run-code-intel.ps1")).unwrap(); + assert!(!source.contains("$repomixTool = Join-Path")); + assert!(source.contains("Repomix production participation was reviewed and removed")); +} + +#[test] +fn ticket_r06_native_record_binds_b08_parity_size_coverage_and_latency() { + let record = advisory_candidate("native-code-evidence"); + assert_research_record_projects(&record, "internalization.native-code-evidence-record"); + assert_recomputable_sha( + &record, + "orchestration/internalization/c03-r05-r12-measurements.json", + "measurement-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/native_code_evidence.rs", + "local-runtime-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/native_code_evidence.rs", + "local-conformance-sha256", + ); + assert_operation_trace_exact(&record, &["evidence.native-code"]); + assert_eq!(record["economics"]["benefit"]["value"], 6); + assert_eq!(record["economics"]["cost"]["value"], 27110); + let text = serde_json::to_string(&record).unwrap(); + for fact in [ + "normalized-parity-artifacts-6", + "artifact-refs-8", + "fixture-files-2", + ] { + assert!(text.contains(fact), "missing measured fact {fact}"); + } + let measurements = c03_measurements(); + let labeled = &measurements["observations"]["r06NativeCodeEvidence"]["labeledCorpus"]; + assert_eq!(labeled["samples"], 12); + assert_eq!(labeled["truePositives"], 6); + assert_eq!(labeled["falsePositives"], 2); + assert_eq!(labeled["falseNegatives"], 2); + assert_eq!(labeled["precision"], 0.75); + assert_eq!(labeled["recall"], 0.75); + assert_eq!(labeled["supportedCoverage"], 0.833333); + assert!(labeled["elapsedMs"] + .as_f64() + .is_some_and(|value| value >= 0.0)); + assert!(text.contains("labeled-corpus-samples-12")); + assert!(!text.contains("gap:native-code:representative-precision-recall")); + assert!(text.contains("line-heuristic")); +} + +#[test] +fn ticket_r07_cocoindex_is_a_reviewed_retirement_with_no_production_path() { + let record = advisory_candidate("cocoindex"); + assert_research_record_projects(&record, "internalization.cocoindex-record"); + assert_recomputable_sha( + &record, + "orchestration/internalization/c03-r05-r12-measurements.json", + "measurement-sha256", + ); + assert!(record.get("operationTrace").is_none()); + assert_eq!( + production_participant("evidence.cocoindex-code")["status"], + "deleted" + ); + assert!(!integration_exists("evidence.cocoindex-code")); + assert_eq!(record["retirement"]["status"], "completed"); + assert_eq!(record["economics"]["benefit"]["value"], 0); + assert_eq!(record["economics"]["cost"]["value"], 0); + let text = serde_json::to_string(&record).unwrap(); + for boundary in [ + "production-semantic-invocations:0", + "reviewed-deletion", + "native-baseline-independent", + ] { + assert!(text.contains(boundary), "missing {boundary}"); + } + let source = fs::read_to_string(root().join("run-code-intel.ps1")).unwrap(); + assert!(!source.contains("Get-JsonProperty $adapters \"cocoindex-code\"")); + assert!(!source.contains("Test-CommandAvailable $cocoCommand")); +} + +#[test] +fn ticket_r08_github_research_failed_live_reproduction_is_reviewed_out() { + let record = advisory_candidate("github-research"); + assert_research_record_projects(&record, "internalization.github-research-record"); + assert_recomputable_sha( + &record, + "orchestration/internalization/c03-r05-r12-measurements.json", + "measurement-sha256", + ); + assert!(record.get("operationTrace").is_none()); + assert_eq!( + production_participant("research.github-solution")["status"], + "deleted" + ); + assert!(!integration_exists("research.github-solution")); + assert_eq!(record["retirement"]["status"], "completed"); + assert_eq!(record["economics"]["benefit"]["value"], 0); + let measurements = c03_measurements(); + let live = &measurements["observations"]["r08GitHubResearch"]["representativeLiveRun"]; + assert_eq!(live["status"], "manual_required"); + assert_eq!(live["candidates"], 0); + assert_eq!(live["reproducible"], false); + assert_eq!(live["externalWrites"], 0); + assert_eq!(live["invocationExitCodes"], serde_json::json!([1, 1, 0, 0])); + assert_eq!(live["returnedUrls"], serde_json::json!([])); + assert_eq!(live["returnedSourceRevisions"], serde_json::json!([])); + assert_recomputable_sha( + &record, + "orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json", + "run-artifact-sha256", + ); + let text = serde_json::to_string(&record).unwrap(); + assert!(text.contains("invalid-query")); + let source = fs::read_to_string(root().join("run-code-intel.ps1")).unwrap(); + assert!(!source.contains("$githubResearchScript")); +} + +#[test] +fn ticket_r10_git_is_versioned_measured_and_read_only() { + let record = advisory_candidate("git"); + assert_research_record_projects(&record, "internalization.git-record"); + assert_recomputable_sha( + &record, + "orchestration/internalization/c03-r05-r12-measurements.json", + "measurement-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/snapshot.rs", + "local-snapshot-source-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/snapshot_identity.rs", + "local-conformance-sha256", + ); + assert_operation_trace_exact(&record, &["repository.snapshot-identity"]); + assert_eq!(record["economics"]["benefit"]["value"], 12); + assert_eq!(record["economics"]["cost"]["value"], 422.294); + let text = serde_json::to_string(&record).unwrap(); + assert!(text.contains("installed-version:2.54.0.windows.1")); + assert!(text.contains("rev-parse-samples-20")); + assert!(text.contains("mutation-commands-0")); + assert!(text.contains("local-license-sha256")); + assert!(text.contains("alternate-vcs-contract-fixture")); + assert!(text.contains("alternate-vcs-mismatch-fail-closed")); + assert!(text.contains("alternate-vcs-adapter-actually-executed")); + assert!(text.contains("rollback-to-git-or-unversioned")); + assert!(!text.contains("gap:git:local-license-copy")); + assert!(!text.contains("gap:git:alternate-vcs-port")); +} + +#[test] +fn ticket_r12_unverified_greenfield_plugin_is_retired_from_production() { + let record = advisory_candidate("greenfield"); + assert_research_record_projects(&record, "internalization.greenfield-record"); + assert_recomputable_sha( + &record, + "orchestration/internalization/c03-r05-r12-measurements.json", + "measurement-sha256", + ); + assert!(record.get("operationTrace").is_none()); + assert!(!integration_exists("spec.greenfield")); + let registry = fs::read_to_string(root().join("orchestration/integrations.json")).unwrap(); + assert!(!registry.contains("\"capabilityId\": \"spec.greenfield\"")); + assert_eq!(record["retirement"]["status"], "completed"); + assert_eq!(record["economics"]["benefit"]["value"], 2); + assert_eq!(record["economics"]["cost"]["value"], 2127.4285); + let text = serde_json::to_string(&record).unwrap(); + assert!(text.contains("auto-analyze-without-flag-0")); + assert!(text.contains("explicit-analyze-fixture-1")); + assert!(text.contains("external-plugin-retired")); + assert!(text.contains("pipeline-owned-plan-only")); + assert!(text.contains("never grant specification or implementation authority")); +} + +#[test] +fn ticket_r13_openspec_record_is_measured_removable_and_fail_closed() { + let record = advisory_candidate("openspec"); + assert_research_candidate(&record, "internalization.openspec-record"); + let atom = fs::read_to_string(root().join("OpenSpec-Detector.ps1")).unwrap(); + assert_eq!(atom.matches("openspec-opsx").count(), 5); + assert_eq!(record["economics"]["benefit"]["value"], 5); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no openspec init")); + assert!(record["rollback"]["strategy"] + .as_str() + .unwrap() + .contains("delete this candidate record")); +} + +#[test] +fn ticket_r14_spec_kit_record_requires_facts_and_has_no_auto_init_authority() { + let record = advisory_candidate("spec-kit"); + assert_research_candidate(&record, "internalization.spec-kit-record"); + let atom = fs::read_to_string(root().join("OpenSpec-Detector.ps1")).unwrap(); + assert_eq!(atom.matches("spec-kit").count(), 8); + assert_eq!(record["economics"]["benefit"]["value"], 8); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no specify init")); + assert!(record["exit"]["replacementCriteria"][0] + .as_str() + .unwrap() + .contains("repository facts rather than tool name alone")); +} + +#[test] +fn ticket_r15_matt_flow_record_traces_one_owned_branch_without_external_effects() { + let record = advisory_candidate("matt-flow"); + assert_research_candidate(&record, "internalization.matt-flow-record"); + let atom = fs::read_to_string(root().join("OpenSpec-Detector.ps1")).unwrap(); + assert_eq!(atom.matches("stack = \"matt-flow\"").count(), 1); + assert_eq!(record["economics"]["benefit"]["value"], 1); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no issue creation")); + assert!(boundary.contains("external-write authority")); +} + +#[test] +fn ticket_r16_gstack_record_exposes_source_gap_and_no_execution_authority() { + let record = advisory_candidate("gstack"); + assert_research_candidate(&record, "internalization.gstack-record"); + let atom = fs::read_to_string(root().join("OpenSpec-Detector.ps1")).unwrap(); + assert_eq!(atom.matches("stack = \"gstack\"").count(), 1); + assert_eq!(record["economics"]["benefit"]["value"], 1); + assert!(record["subject"]["source"]["uri"] + .as_str() + .unwrap() + .starts_with("unverified-upstream:")); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no skill execution")); + assert!(boundary.contains("external-write authority")); +} + +#[test] +fn ticket_r17_qiaomu_goal_record_traces_contract_and_stays_outside_scanner() { + let record = advisory_candidate("qiaomu-goal"); + assert_research_candidate(&record, "internalization.qiaomu-goal-record"); + assert_eq!(record["economics"]["benefit"]["value"], 7); + let intake = fs::read_to_string(root().join("docs/agent-goal-intake.md")).unwrap(); + for semantic in [ + "outcome", + "verification", + "constraints", + "boundaries", + "iteration policy", + "completion evidence", + "pause conditions", + ] { + assert!(intake.contains(semantic), "missing {semantic}"); + } + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no qiaomu runtime dependency")); + assert!(boundary.contains("scanner invocation")); + assert!(record["rollback"]["strategy"] + .as_str() + .unwrap() + .contains("retaining the versioned pipeline-owned Agent Goal Intake contract")); +} + +#[test] +fn ticket_r18_agent_loops_record_is_pinned_locally_and_removable_from_runtime() { + let record = advisory_candidate("agent-loops"); + assert_research_candidate(&record, "internalization.agent-loops-record"); + assert_eq!(record["economics"]["benefit"]["value"], 3); + let intake = fs::read_to_string(root().join("docs/agent-goal-intake.md")).unwrap(); + for pattern in ["`/goal`", "`/loop`", "`/schedule`"] { + assert!(intake.contains(pattern), "missing {pattern}"); + } + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no awesome-agent-loops runtime dependency")); + assert!(boundary.contains("scheduling authority")); + assert!(record["exit"]["replacementCriteria"][2] + .as_str() + .unwrap() + .contains("no scanner contract")); +} + +#[test] +fn ticket_r19_metaharness_record_has_expiring_authority_gap_and_no_runtime() { + let record = advisory_candidate("metaharness"); + assert_research_candidate(&record, "internalization.metaharness-record"); + assert_eq!(record["economics"]["benefit"]["value"], 6); + let harness = fs::read_to_string(root().join("docs/harness-factory-reference.md")).unwrap(); + for concern in [ + "branded `npx code-intel`", + "host configuration bundles", + "release-gate command shapes", + "SBOM, provenance", + "static repository analysis", + "package layout", + ] { + assert!(harness.contains(concern), "missing {concern}"); + } + let lifecycle = serde_json::to_string(&record["lifecycle"]["evidenceIds"]).unwrap(); + assert!(lifecycle.contains("gap:metaharness:retention-authority")); + assert_eq!(record["update"]["nextCheckAt"], 1_791_676_800u64); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no MetaHarness runtime dependency")); + assert!(record["retirement"]["triggers"][0] + .as_str() + .unwrap() + .contains("no authority-approved retention decision")); +} + +#[test] +fn ticket_r20_yao_record_rejects_local_doc_test_as_upstream_execution_proof() { + let record = advisory_candidate("yao-meta-skill"); + assert_research_candidate(&record, "internalization.yao-meta-skill-record"); + assert_eq!(record["economics"]["benefit"]["value"], 8); + let benchmark = fs::read_to_string(root().join("docs/skill-development-benchmark.md")).unwrap(); + for criterion in [ + "lean `SKILL.md` entrypoint", + "near-neighbor exclusions", + "multiple agent hosts", + "eval fixtures", + "failure library", + "review reports", + "release gates", + "adoption drift", + ] { + assert!(benchmark.contains(criterion), "missing {criterion}"); + } + assert!(benchmark.contains("This check does not run `yao-meta-skill`")); + let evidence = serde_json::to_string(&record).unwrap(); + assert!(evidence.contains("gap:yao-meta-skill:behavioral-measurement")); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("no yao-meta-skill runtime dependency")); + assert!(record["exit"]["replacementCriteria"][2] + .as_str() + .unwrap() + .contains("beyond a local documentation test pass")); +} + +#[test] +fn ticket_r22_mattpocock_skills_record_traces_each_retained_concept_and_exit() { + let record = advisory_candidate("mattpocock-skills"); + assert_research_candidate(&record, "internalization.mattpocock-skills-record"); + assert_eq!(record["economics"]["benefit"]["value"], 4); + assert_eq!(record["ownedModifications"].as_array().unwrap().len(), 4); + let project_management = + fs::read_to_string(root().join("docs/project-management-support.md")).unwrap(); + for concept in ["issue tracker", "triage", "domain documentation"] { + assert!(project_management.contains(concept), "missing {concept}"); + } + let workflow = fs::read_to_string(root().join("docs/openspec-detector.md")).unwrap(); + assert!(workflow.contains("idea→ship")); + assert!(record["exit"]["strategy"] + .as_str() + .unwrap() + .contains("retire the source reference independently")); +} + +#[test] +fn ticket_r09_rg_record_traces_every_registered_production_operation() { + let record = advisory_candidate("rg"); + assert_research_record_projects(&record, "internalization.rg-record"); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/capability_inventory.rs", + "local-native-source-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/capability_exec.rs", + "local-conformance-sha256", + ); + assert_operation_trace_exact(&record, &["inventory.rg"]); + let exact = BTreeMap::from([ + ( + ("inventory.rg", "run"), + "normalized_inventory_matches_real_legacy_runner_with_custom_exclude", + ), + ( + ("inventory.rg", "capabilityExec"), + "inventory_rg_exec_emits_one_result_and_stable_real_rg_artifact", + ), + ]); + for trace in record["operationTrace"].as_array().unwrap() { + let key = ( + trace["integrationId"].as_str().unwrap(), + trace["operation"].as_str().unwrap(), + ); + assert_eq!(trace["conformance"]["testName"], exact[&key]); + } + assert_eq!(record["adoption"]["rung"], "invoke"); + assert!(serde_json::to_string(&record) + .unwrap() + .contains("gap:rg:replacement-command-drill")); + assert!(serde_json::to_string(&record) + .unwrap() + .contains("gap:rg:latency-p50-p95-measurement")); +} + +#[test] +fn ticket_r11_tree_sitter_v_record_does_not_claim_sentrux_or_grammar_ownership() { + let record = advisory_candidate("tree-sitter-v"); + assert_research_record_projects(&record, "internalization.tree-sitter-v-record"); + assert_recomputable_sha( + &record, + "Install-SentruxVlangOverlay.ps1", + "local-installer-sha256", + ); + assert_recomputable_sha( + &record, + "overlays/sentrux/vlang/grammars/windows-x86_64.dll", + "local-compiled-windows-x86_64-sha256", + ); + assert_recomputable_sha( + &record, + "overlays/sentrux/vlang/src/sentrux_vlang_alias.c", + "local-abi-alias-source-sha256", + ); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("Sentrux owns parser/plugin loading")); + assert!(boundary.contains("upstream owns grammar implementation")); + assert!(boundary.contains("neither ownership is transferred")); + let record_text = serde_json::to_string(&record).unwrap(); + for gap in [ + "gap:tree-sitter-v:pinned-upstream-revision", + "gap:tree-sitter-v:reproducible-build", + "gap:tree-sitter-v:v-fixture-conformance", + "gap:tree-sitter-v:abi-symbol-verification", + ] { + assert!(record_text.contains(gap), "missing {gap}"); + } + assert!(!record_text.contains("gap:tree-sitter-v:compiled-artifact-digest")); + assert!(record["ownedModifications"] + .as_array() + .unwrap() + .iter() + .any(|entry| entry["path"] == "overlays/sentrux/vlang/src/sentrux_vlang_alias.c")); +} + +#[test] +fn ticket_r21_ponytail_record_binds_behavior_without_external_runtime_claim() { + let record = advisory_candidate("ponytail"); + assert_research_record_projects(&record, "internalization.ponytail-record"); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/src/ponytail_gate.rs", + "local-runtime-sha256", + ); + assert_recomputable_sha( + &record, + "crates/code-intel-cli/tests/ponytail_gate.rs", + "local-conformance-sha256", + ); + let boundary = serde_json::to_string(&record["adoption"]["ownedBoundary"]).unwrap(); + assert!(boundary.contains("Value Filter")); + assert!(boundary.contains("Necessity Trace")); + assert!(boundary.contains("not an external runtime")); + assert!( + boundary.contains("not an external runtime") + || boundary.contains("not an external Ponytail runtime") + ); + assert!(record["rollback"]["strategy"] + .as_str() + .unwrap() + .contains("report_only")); + let test_count = + fs::read_to_string(root().join("crates/code-intel-cli/tests/ponytail_gate.rs")) + .unwrap() + .lines() + .filter(|line| line.trim() == "#[test]") + .count(); + assert_eq!(test_count, 12); + assert_eq!(record["economics"]["benefit"]["value"], test_count); +} + +#[test] +fn ticket_r23_r24_r25_reference_records_add_no_external_write_ui_or_model_runtime() { + for (name, expected_id) in [ + ("linear", "internalization.linear-record"), + ("obsidian", "internalization.obsidian-record"), + ("llm-wiki", "internalization.llm-wiki-record"), + ] { + let record = advisory_candidate(name); + assert_signed_out_of_scope_record_projects(&record, expected_id); + assert_recomputable_sha( + &record, + "docs/project-management-support.md", + "local-policy-sha256", + ); + assert_recomputable_sha( + &record, + "test-project-management-support.ps1", + "local-boundary-test-sha256", + ); + assert_eq!(record["economics"]["benefit"]["value"], 0); + let text = serde_json::to_string(&record).unwrap(); + assert!(text.contains("no-current-use")); + assert!(text.contains("scope-authority")); + assert!(text.contains("code-intel-authority-event.v1")); + assert!(text.contains("repository-governed-sha256-v1")); + } + let linear = serde_json::to_string(&advisory_candidate("linear")).unwrap(); + assert!(linear.contains("no connector install")); + assert!(linear.contains("no-external-write")); + let obsidian = serde_json::to_string(&advisory_candidate("obsidian")).unwrap(); + assert!( + obsidian.contains("no UI/plugin/vault dependency") || obsidian.contains("no-ui-runtime") + ); + assert!(obsidian.contains("cannot replace scanner artifacts")); + let wiki = serde_json::to_string(&advisory_candidate("llm-wiki")).unwrap(); + assert!(wiki.contains("no model/provider call")); + assert!(wiki.contains("never Observed Evidence or Engineering Fact")); +} + +#[test] +fn ticket_r26_my_code_machine_record_defers_big_bang_and_host_effects() { + let record = advisory_candidate("my-code-machine"); + assert_signed_out_of_scope_record_projects(&record, "internalization.my-code-machine-record"); + assert_recomputable_sha( + &record, + "docs/adr/0001-merge-mcm-rust-unified.md", + "local-merge-proposal-sha256", + ); + assert_recomputable_sha( + &record, + "docs/adr/0010-tool-neutral-engineering-intelligence-core.md", + "local-no-big-bang-adr-sha256", + ); + assert!(!root().join("crates/machine").exists()); + assert!(!root().join("crates/sync").exists()); + let text = serde_json::to_string(&record).unwrap(); + for boundary in [ + "no-big-bang", + "no-host-mutation-authority", + "gap:my-code-machine:atom-parity", + "gap:my-code-machine:effect-contracts", + "gap:my-code-machine:adoption-authority", + ] { + assert!(text.contains(boundary), "missing {boundary}"); + } +} + +#[test] +fn checked_schemas_accept_complete_record_and_both_projections() { + let record = complete(); + let evaluation = + internalization_record::evaluate_record(&record, NOW, &known(&record), &[]).unwrap(); + assert_checked_schema(&record, "code-intel-internalization-record.v1.schema.json"); + assert_checked_schema( + &internalization_record::project_reuse_record(&record, &evaluation).unwrap(), + "code-intel-reuse-record.v1.schema.json", + ); + assert_checked_schema( + &internalization_record::project_notice_provenance(&record, &evaluation).unwrap(), + "code-intel-notice-provenance.v1.schema.json", + ); +} + +#[test] +fn claude_code_merge_queue_record_traces_optional_adapter_and_keeps_promotion_human_only() { + let record = advisory_candidate("claude-code-merge-queue"); + assert_research_record_projects(&record, "internalization.claude-code-merge-queue-record"); + assert_eq!(record["subject"]["license"]["id"], "MIT"); + assert!(record["subject"]["source"]["revision"] + .as_str() + .unwrap() + .contains("e7a76958dbd3953b84f12abbc2e6bd755aafce53")); + assert_recomputable_sha( + &record, + "Invoke-MultiAgentMergeQueue.ps1", + "local-adapter-sha256", + ); + assert_recomputable_sha( + &record, + "test-multi-agent-merge-queue.ps1", + "local-conformance-sha256", + ); + assert_recomputable_sha( + &record, + "orchestration/multi-agent-merge-queue-policy.v1.json", + "local-policy-sha256", + ); + assert_recomputable_sha( + &record, + "docs/multi-agent-merge-queue.md", + "local-doc-sha256", + ); + assert_operation_trace_exact(&record, &["delivery.multi-agent-merge-queue"]); + let integration = integration("delivery.multi-agent-merge-queue"); + assert_eq!(integration["required"], false); + assert_eq!(integration["stage"], "landing_coordination"); + assert!(integration["commands"].get("promote").is_none()); + assert!(integration["commands"]["land"] + .as_str() + .unwrap() + .contains("-AllowNetworkPush")); + let evidence = serde_json::to_string(&record).unwrap(); + assert!(evidence.contains("production promotion remains human-only")); + assert!(evidence.contains("gap:merge-queue:representative-cluster-throughput")); +} + +fn assert_checked_schema(value: &Value, schema: &str) { + // PowerShell's Test-Json schema engine is not reliable under concurrent + // invocations from this parallel test binary. Keep the external validator + // serialized while preserving unique payload paths for failure diagnosis. + let _guard = SCHEMA_CHECK_LOCK.lock().unwrap(); + let path = std::env::temp_dir().join(format!( + "code-intel-c03-schema-{}-{}.json", + std::process::id(), + NEXT_SCHEMA_CHECK.fetch_add(1, Ordering::Relaxed) + )); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + let schema_path = root().join("orchestration/schemas").join(schema); + let mut command = Command::new("pwsh"); + command.args([ + "-NoProfile", + "-CommandWithArgs", + "if (-not ((Get-Content -Raw -LiteralPath $args[0]) | Test-Json -SchemaFile $args[1])) { exit 1 }", + path.to_str().unwrap(), + schema_path.to_str().unwrap(), + ]); + let output = run_command_with_timeout(&mut command); + fs::remove_file(path).unwrap(); + assert!( + output.status.success(), + "{schema}: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +fn run_command_with_timeout(command: &mut Command) -> std::process::Output { + let mut child = command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(10); + loop { + if child.try_wait().unwrap().is_some() { + return child.wait_with_output().unwrap(); + } + if Instant::now() >= deadline { + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + panic!( + "subprocess timed out; stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + std::thread::sleep(Duration::from_millis(10)); + } +} diff --git a/crates/code-intel-cli/tests/method_catalog.rs b/crates/code-intel-cli/tests/method_catalog.rs new file mode 100644 index 0000000..5e7e439 --- /dev/null +++ b/crates/code-intel-cli/tests/method_catalog.rs @@ -0,0 +1,260 @@ +#[path = "../src/method_catalog.rs"] +mod method_catalog; + +use std::collections::BTreeSet; +use std::fs; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct TempTree(PathBuf); + +impl TempTree { + fn new(label: &str) -> Self { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "code-intel-method-catalog-{label}-{}-{nanos}-{sequence}", + std::process::id() + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for TempTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn root() -> PathBuf { + option_env!("CODE_INTEL_REPO_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")) +} + +fn seed_documents() -> (Value, Vec<(String, Value)>) { + let methods = root().join("orchestration/methods"); + let index: Value = + serde_json::from_slice(&fs::read(methods.join("catalog.v1.json")).unwrap()).unwrap(); + let cards = index["cards"] + .as_array() + .unwrap() + .iter() + .map(|entry| { + let path = entry["path"].as_str().unwrap().to_string(); + let card = serde_json::from_slice(&fs::read(methods.join(&path)).unwrap()).unwrap(); + (path, card) + }) + .collect(); + (index, cards) +} + +#[test] +fn seed_catalog_loads_all_nine_methods_in_stable_order() { + let catalog = method_catalog::load_catalog(&root().join("orchestration/methods")).unwrap(); + let ids = catalog + .cards() + .iter() + .map(|card| card["id"].as_str().unwrap()) + .collect::>(); + assert_eq!( + ids, + vec![ + "contract-testing", + "critical-path-pert", + "fault-tree-analysis", + "fmea", + "pdca", + "root-cause-analysis", + "spc", + "strangler-migration", + "value-stream-queue-delay", + ] + ); +} + +#[test] +fn every_card_has_the_frozen_engineering_fields_without_execution_claims() { + let catalog = method_catalog::load_catalog(&root().join("orchestration/methods")).unwrap(); + for card in catalog.cards() { + for field in [ + "problemSignals", + "requiredEvidence", + "assumptions", + "deterministicSteps", + "outputs", + "confidenceRules", + "contraindications", + "implementationPorts", + ] { + assert!( + !card[field].as_array().unwrap().is_empty(), + "{}:{field}", + card["id"] + ); + } + assert_eq!( + card["executionPolicy"], + "catalog_only_no_selection_or_execution" + ); + assert!(!serde_json::to_string(card) + .unwrap() + .to_ascii_lowercase() + .contains("method executed")); + } +} + +#[test] +fn missing_contraindications_or_confidence_rules_fail_closed() { + let (index, cards) = seed_documents(); + for missing in ["contraindications", "confidenceRules"] { + let mut candidate = cards.clone(); + candidate[0].1.as_object_mut().unwrap().remove(missing); + let error = method_catalog::validate_documents(&index, &candidate).unwrap_err(); + assert!(error.to_string().contains(missing), "{error}"); + } +} + +#[test] +fn unknown_fields_fail_closed_at_catalog_card_and_nested_levels() { + let (index, cards) = seed_documents(); + let mut bad_index = index.clone(); + bad_index["automaticSelection"] = json!(true); + assert!(method_catalog::validate_documents(&bad_index, &cards).is_err()); + + let mut bad_card = cards.clone(); + bad_card[0].1["selected"] = json!(true); + assert!(method_catalog::validate_documents(&index, &bad_card).is_err()); + + let mut bad_nested = cards.clone(); + bad_nested[0].1["cost"]["score"] = json!(99); + assert!(method_catalog::validate_documents(&index, &bad_nested).is_err()); +} + +#[test] +fn duplicate_ids_unsorted_index_and_unknown_method_references_are_rejected() { + let (index, cards) = seed_documents(); + + let mut duplicate = cards.clone(); + duplicate[1].1["id"] = duplicate[0].1["id"].clone(); + assert!(method_catalog::validate_documents(&index, &duplicate).is_err()); + + let mut unsorted = index.clone(); + unsorted["cards"].as_array_mut().unwrap().swap(0, 1); + assert!(method_catalog::validate_documents(&unsorted, &cards).is_err()); + + let mut unknown = cards.clone(); + unknown[0].1["relatedMethodIds"] = json!(["not-a-method"]); + assert!(method_catalog::validate_documents(&index, &unknown).is_err()); +} + +#[test] +fn unversioned_paths_and_unregistered_nested_cards_fail_closed() { + let (index, cards) = seed_documents(); + let mut unversioned_index = index.clone(); + let mut unversioned_cards = cards.clone(); + unversioned_index["cards"][0]["path"] = json!("cards/unversioned.json"); + unversioned_cards[0].0 = "cards/unversioned.json".to_string(); + assert!(method_catalog::validate_documents(&unversioned_index, &unversioned_cards).is_err()); + + let temp = TempTree::new("nested-card"); + let source = root().join("orchestration/methods"); + let target = temp.0.join("methods"); + fs::create_dir_all(target.join("cards/rogue")).unwrap(); + fs::copy( + source.join("catalog.v1.json"), + target.join("catalog.v1.json"), + ) + .unwrap(); + for entry in fs::read_dir(source.join("cards")).unwrap() { + let entry = entry.unwrap(); + fs::copy(entry.path(), target.join("cards").join(entry.file_name())).unwrap(); + } + fs::write(target.join("cards/rogue/unlisted.json"), b"{}\n").unwrap(); + let error = method_catalog::load_catalog(&target).unwrap_err(); + assert!(error.to_string().contains("unregistered non-card entry")); +} + +#[test] +fn step_references_are_closed_over_declared_evidence_steps_and_outputs() { + let (index, cards) = seed_documents(); + let mut unknown_evidence = cards.clone(); + unknown_evidence[0].1["deterministicSteps"][0]["requires"] = json!(["evidence:not-declared"]); + assert!(method_catalog::validate_documents(&index, &unknown_evidence).is_err()); + + let mut forward_step = cards.clone(); + let second_id = forward_step[0].1["deterministicSteps"][1]["id"] + .as_str() + .unwrap() + .to_string(); + forward_step[0].1["deterministicSteps"][0]["requires"] = json!([format!("step:{second_id}")]); + assert!(method_catalog::validate_documents(&index, &forward_step).is_err()); + + let mut unknown_output = cards.clone(); + unknown_output[0].1["deterministicSteps"][0]["produces"] = json!(["not-declared"]); + assert!(method_catalog::validate_documents(&index, &unknown_output).is_err()); +} + +#[test] +fn seed_cards_remain_distinct_and_preserve_method_specific_preconditions() { + let catalog = method_catalog::load_catalog(&root().join("orchestration/methods")).unwrap(); + let distinct_names = catalog + .cards() + .iter() + .map(|card| card["name"].as_str().unwrap()) + .collect::>(); + assert_eq!(distinct_names.len(), 9); + let required = catalog + .cards() + .iter() + .map(|card| { + ( + card["id"].as_str().unwrap(), + card["requiredEvidence"] + .as_array() + .unwrap() + .iter() + .map(|entry| entry["id"].as_str().unwrap()) + .collect::>(), + ) + }) + .collect::>(); + assert!(required["critical-path-pert"].contains("activity-duration-estimates")); + assert!(required["spc"].contains("time-ordered-measurements")); + assert!(required["fault-tree-analysis"].contains("top-event-definition")); + assert!(required["contract-testing"].contains("consumer-provider-contracts")); +} + +#[test] +fn checked_schema_is_versioned_closed_and_requires_the_frozen_fields() { + let schema: Value = serde_json::from_slice( + &fs::read(root().join("orchestration/schemas/code-intel-method-card.v1.schema.json")) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["$id"], "code-intel-method-card.v1"); + assert_eq!(schema["$defs"]["card"]["additionalProperties"], false); + assert_eq!(schema["$defs"]["cost"]["additionalProperties"], false); + let required = schema["$defs"]["card"]["required"] + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap()) + .collect::>(); + for field in [ + "contraindications", + "confidenceRules", + "implementationPorts", + ] { + assert!(required.contains(field)); + } +} diff --git a/crates/code-intel-cli/tests/method_select.rs b/crates/code-intel-cli/tests/method_select.rs new file mode 100644 index 0000000..c013452 --- /dev/null +++ b/crates/code-intel-cli/tests/method_select.rs @@ -0,0 +1,364 @@ +#[path = "../src/admissibility.rs"] +mod admissibility; +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/method_catalog.rs"] +mod method_catalog; +#[path = "../src/method_select.rs"] +mod method_select; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +static SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-c02-{}-{nonce}-{}", + std::process::id(), + SEQUENCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +struct Case { + root: Temp, + request: Value, +} + +fn repo_root() -> PathBuf { + option_env!("CODE_INTEL_REPO_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")) +} + +fn fixture(name: &str) -> Case { + let descriptor: Value = serde_json::from_slice( + &fs::read( + repo_root() + .join("crates/code-intel-cli/tests/fixtures/method-select") + .join(name), + ) + .unwrap(), + ) + .unwrap(); + build_case(&descriptor["facts"], &descriptor["evidenceGaps"]) +} + +fn base(signals: &[&str], evidence_kinds: &[&str]) -> Case { + build_case( + &json!([{ + "id":"fact-1", + "signalIds":signals, + "evidenceKinds":evidence_kinds + }]), + &json!([]), + ) +} + +fn build_case(facts: &Value, gaps: &Value) -> Case { + let root = Temp::new(); + let (envelope, admission_id) = make_envelope(&root.0, facts, "primary"); + let bound_facts = facts + .as_array() + .unwrap() + .iter() + .map(|fact| { + json!({ + "id":fact["id"], + "signalIds":fact["signalIds"], + "evidenceKinds":fact["evidenceKinds"], + "admissionIds":[admission_id] + }) + }) + .collect::>(); + Case { + root, + request: json!({ + "schema":"code-intel-method-selection-request.v1", + "snapshotIdentity":SNAPSHOT, + "evaluatedAt":2_000u64, + "maxEvidenceAgeSeconds":100u64, + "admissions":[envelope], + "facts":bound_facts, + "evidenceGaps":gaps + }), + } +} + +fn make_envelope(root: &Path, facts: &Value, tag: &str) -> (Value, String) { + let payload = json!({ + "schema":"code-intel-evidence-payload.v1", + "data":{"methodSelectionFacts":facts} + }); + let bytes = serde_json::to_vec(&payload).unwrap(); + let sha = capability::sha256_hex(&bytes); + let path = format!("payload-{tag}.json"); + fs::write(root.join(&path), bytes).unwrap(); + let request = json!({ + "schema":"code-intel-evidence-admissibility-request.v1", + "expectedSnapshotIdentity":SNAPSHOT, + "policy":{"evaluatedAt":2_000u64,"maxAgeSeconds":100u64}, + "observation":{ + "schema":"code-intel-observed-evidence.v1", + "provider":{"id":"method-selection-fixture","implementation":{"id":"fixture.adapter","version":"1.0.0","digest":"b".repeat(64)}}, + "source":{"revision":format!("fixture-{tag}")}, + "consumedSnapshotIdentity":SNAPSHOT, + "observedAt":1_950u64, + "completeness":"complete", + "claimedComplete":true, + "payload":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-evidence-payload.v1","type":"observed.evidence.payload","path":path,"sha256":sha,"consumedSnapshotIdentity":SNAPSHOT}, + "provenance":{"collectionId":format!("fixture-{tag}"),"command":"fixture --emit","startedAt":1_949u64,"completedAt":1_950u64}, + "failure":{"kind":"none"} + } + }); + let admitted = admissibility::validate_for_consumer(&request, root).unwrap(); + let result = admitted.result().clone(); + let identity = result["admissionIdentity"].as_str().unwrap().to_string(); + (json!({"request":request,"result":result}), identity) +} + +fn run(case: &Case) -> Value { + run_request(&case.request, &case.root.0).unwrap() +} + +fn run_error(case: &Case) -> String { + run_request(&case.request, &case.root.0) + .unwrap_err() + .to_string() +} + +fn run_request(request: &Value, artifact_root: &Path) -> Result { + let catalog = method_catalog::load_catalog(&repo_root().join("orchestration/methods")).unwrap(); + let rules = method_select::load_rule_table( + &repo_root().join("orchestration/method-selection-rules.v1.json"), + &catalog, + ) + .unwrap(); + method_select::select(request, artifact_root, &catalog, &rules) +} + +#[test] +fn dependency_delay_selects_critical_path_and_value_stream_as_tied_proposals() { + let result = run(&fixture("dependency-delay.json")); + assert_eq!(result["outcome"], "proposal"); + assert_eq!(result["tie"], true); + let methods = result["matches"] + .as_array() + .unwrap() + .iter() + .map(|item| item["methodId"].as_str().unwrap()) + .collect::>(); + assert_eq!( + methods, + vec!["critical-path-pert", "value-stream-queue-delay"] + ); + assert!(result["matches"].as_array().unwrap().iter().all(|item| { + item["outcome"] == "proposal" + && item["missingEvidence"].as_array().unwrap().is_empty() + && item["cost"].is_object() + && item["confidenceRules"].is_array() + })); +} + +#[test] +fn insufficient_evidence_returns_unknown_with_missing_evidence_explanation() { + let result = run(&fixture("insufficient-evidence.json")); + assert_eq!(result["outcome"], "unknown"); + assert_eq!(result["matches"][0]["outcome"], "unknown"); + assert!(!result["matches"][0]["missingEvidence"] + .as_array() + .unwrap() + .is_empty()); + assert_eq!(result["matches"][0]["selectionConfidence"], "unknown"); +} + +#[test] +fn unrelated_fact_is_a_false_positive_guard_and_returns_none() { + let result = run(&base( + &["unrelated-code-size-change"], + &["arbitrary-measurement"], + )); + assert_eq!(result["outcome"], "none"); + assert!(result["matches"].as_array().unwrap().is_empty()); +} + +#[test] +fn contraindication_blocks_proposal_but_preserves_the_explanation() { + let result = run(&base( + &["integration-drift", "no-observable-contract"], + &[ + "consumer-provider-contracts", + "provider-verification-context", + ], + )); + assert_eq!(result["outcome"], "none"); + assert_eq!(result["matches"][0]["methodId"], "contract-testing"); + assert_eq!(result["matches"][0]["outcome"], "none"); + assert!(!result["matches"][0]["triggeredContraindications"] + .as_array() + .unwrap() + .is_empty()); +} + +#[test] +fn selection_is_order_independent_and_stably_sorted() { + let mut case = fixture("dependency-delay.json"); + let extra_facts = json!([{ + "id":"schedule-fact", + "signalIds":["schedule-slippage"], + "evidenceKinds":["activity-duration-estimates", "activity-network"] + }]); + let (extra, extra_id) = make_envelope(&case.root.0, &extra_facts, "order-extra"); + case.request["admissions"] + .as_array_mut() + .unwrap() + .push(extra); + case.request["facts"].as_array_mut().unwrap().push(json!({ + "id":"schedule-fact", + "signalIds":["schedule-slippage"], + "evidenceKinds":["activity-duration-estimates", "activity-network"], + "admissionIds":[extra_id] + })); + let expected = run(&case); + case.request["facts"].as_array_mut().unwrap().reverse(); + case.request["admissions"].as_array_mut().unwrap().reverse(); + case.request["evidenceGaps"] + .as_array_mut() + .unwrap() + .reverse(); + assert_eq!(run(&case), expected); +} + +#[test] +fn stale_unadmitted_unknown_and_duplicate_admissions_fail_closed() { + let mut stale = base(&["dependency-congestion"], &["activity-network"]); + stale.request["admissions"][0]["request"]["observation"]["observedAt"] = json!(1_899u64); + stale.request["admissions"][0]["request"]["observation"]["provenance"]["startedAt"] = + json!(1_898u64); + stale.request["admissions"][0]["request"]["observation"]["provenance"]["completedAt"] = + json!(1_899u64); + assert!(run_error(&stale).contains("stale")); + + let mut unadmitted = base(&["dependency-congestion"], &["activity-network"]); + unadmitted.request["admissions"][0]["result"]["status"] = json!("rejected"); + assert!(run_error(&unadmitted).contains("forged")); + + let mut unknown = base(&["dependency-congestion"], &["activity-network"]); + unknown.request["facts"][0]["admissionIds"] = json!(["d".repeat(64)]); + assert!(run_error(&unknown).contains("unknown admission")); + + let mut duplicate = base(&["dependency-congestion"], &["activity-network"]); + let repeated = duplicate.request["admissions"][0].clone(); + duplicate.request["admissions"] + .as_array_mut() + .unwrap() + .push(repeated); + assert!(run_error(&duplicate).contains("duplicate admission identity")); +} + +#[test] +fn forged_minimal_a04_result_and_caller_relabeling_are_rejected() { + let mut forged = base( + &["dependency-congestion"], + &["activity-network", "activity-duration-estimates"], + ); + forged.request["admissions"][0]["result"] = json!({ + "schema":"code-intel-evidence-admissibility-result.v1", + "status":"admitted", + "domainVerdict":"observed", + "admissionIdentity":"b".repeat(64), + "evidence":{"consumedSnapshotIdentity":SNAPSHOT,"observedAt":1_950u64}, + "verifiedPayload":{"consumedSnapshotIdentity":SNAPSHOT}, + "engineeringFacts":[] + }); + assert!(run_error(&forged).contains("forged")); + + let mut relabeled = base(&["dependency-congestion"], &["activity-network"]); + relabeled.request["facts"][0]["signalIds"] = json!(["schedule-slippage"]); + assert!(run_error(&relabeled).contains("differ from A04 admitted payload")); +} + +#[test] +fn every_admission_must_be_referenced_by_a_payload_bound_fact() { + let mut case = fixture("dependency-delay.json"); + let unused_facts = json!([{ + "id":"unused-fact", + "signalIds":["process-instability"], + "evidenceKinds":["ordered-process-measurements"] + }]); + let (unused, _) = make_envelope(&case.root.0, &unused_facts, "unused"); + case.request["admissions"] + .as_array_mut() + .unwrap() + .push(unused); + assert!(run_error(&case).contains("unused admission")); +} + +#[test] +fn output_is_advisory_only_and_contains_no_execution_fact_or_decision_claim() { + let result = run(&fixture("dependency-delay.json")); + let text = serde_json::to_string(&result).unwrap().to_ascii_lowercase(); + assert!(!text.contains("adoption_decision")); + assert!(!text.contains("committed_engineering_plan")); + assert!(!text.contains("method_executed")); + assert!(result["matches"] + .as_array() + .unwrap() + .iter() + .all(|item| ["proposal", "unknown", "none"].contains(&item["outcome"].as_str().unwrap()))); +} + +#[test] +fn checked_rules_and_schema_are_closed_and_reference_c01_cards() { + let rules: Value = serde_json::from_slice( + &fs::read(repo_root().join("orchestration/method-selection-rules.v1.json")).unwrap(), + ) + .unwrap(); + assert_eq!(rules["schema"], "code-intel-method-selection-rules.v1"); + let schema: Value = serde_json::from_slice( + &fs::read( + repo_root().join("orchestration/schemas/code-intel-method-selection.v1.schema.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["$id"], "code-intel-method-selection.v1"); + for definition in [ + "request", + "admission", + "fact", + "match", + "result", + "cost", + "confidenceRule", + ] { + assert_eq!(schema["$defs"][definition]["additionalProperties"], false); + } +} diff --git a/crates/code-intel-cli/tests/mindwalk_internalization.rs b/crates/code-intel-cli/tests/mindwalk_internalization.rs new file mode 100644 index 0000000..4c59b14 --- /dev/null +++ b/crates/code-intel-cli/tests/mindwalk_internalization.rs @@ -0,0 +1,50 @@ +#[path = "../src/authority.rs"] +mod authority; +#[path = "../src/internalization_record.rs"] +mod internalization_record; + +use std::fs; +use std::path::PathBuf; + +use serde_json::Value; + +fn root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +#[test] +fn mindwalk_adapter_record_stays_optional_and_research_only() { + let record: Value = serde_json::from_slice( + &fs::read(root().join("orchestration/internalization/mindwalk.json")).unwrap(), + ) + .unwrap(); + let evidence = internalization_record::record_evidence_ids(&record).unwrap(); + let admitted = evidence + .into_iter() + .filter(|id| !id.starts_with("gap:")) + .collect::>(); + let evaluated_at = record["provenance"]["recordedAt"].as_u64().unwrap(); + let evaluation = + internalization_record::evaluate_record(&record, evaluated_at, &admitted, &[]).unwrap(); + + assert_eq!(evaluation["researchAllowed"], true); + assert_eq!(evaluation["productionEnabled"], false); + assert_eq!(evaluation["consumedAuthorityEventId"], Value::Null); + assert_eq!(record["lifecycle"]["authorityEvent"], Value::Null); + + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let integration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "provider.session-adapt") + .unwrap(); + assert_eq!(integration["required"], false); + assert_eq!(integration["stage"], "verification"); + assert_eq!( + record["operationTrace"][0]["command"], + integration["commands"]["adapt"] + ); +} diff --git a/crates/code-intel-cli/tests/native_code_evidence.rs b/crates/code-intel-cli/tests/native_code_evidence.rs new file mode 100644 index 0000000..16500fe --- /dev/null +++ b/crates/code-intel-cli/tests/native_code_evidence.rs @@ -0,0 +1,538 @@ +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::Value; + +struct Temp(PathBuf); + +impl Temp { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-b08-{label}-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn tool_fixture(root: &Path) -> PathBuf { + let bin = root.join("dag-tools"); + fs::create_dir_all(&bin).unwrap(); + #[cfg(windows)] + { + for name in ["rg", "git", "python", "repowise"] { + fs::write( + bin.join(format!("{name}.cmd")), + "@echo off\r\nexit /b 0\r\n", + ) + .unwrap(); + } + fs::write( + bin.join("sentrux.cmd"), + "@echo off\r\necho Enforce architectural rules\r\necho Tier: pro\r\nexit /b 0\r\n", + ) + .unwrap(); + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + for name in ["rg", "git", "python", "repowise", "sentrux"] { + let path = bin.join(name); + let content = if name == "sentrux" { + "#!/bin/sh\necho 'Enforce architectural rules'\necho 'Tier: pro'\nexit 0\n" + } else { + "#!/bin/sh\nexit 0\n" + }; + fs::write(&path, content).unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap(); + } + } + bin +} + +fn run_with_expected_code(repo: &Path, out: &Path, expected_code: i32) -> Value { + let tools = tool_fixture(out.parent().unwrap()); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(repo) + .arg("--out") + .arg(out) + .arg("--doctor-tool-path-prefix") + .arg(tools) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(expected_code), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() +} + +fn run(repo: &Path, out: &Path) -> Value { + run_with_expected_code(repo, out, 0) +} + +fn read_json(path: impl AsRef) -> Value { + serde_json::from_slice(&fs::read(path).unwrap()).unwrap() +} + +fn run_legacy(repo: &Path, root: &Path) -> PathBuf { + fs::create_dir_all(root).unwrap(); + let project_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let config = root.join("pipeline.config.json"); + fs::write( + &config, + serde_json::to_vec(&serde_json::json!({ + "artifactRoot":"", + "repowiseWorkspaceRoot":"", + "codeEvidence":{ + "enabled":true, + "nativeMinimal":true, + "adapters":{"cocoindex-code":{"enabled":false,"required":false,"command":"ccc"}} + }, + "inventoryExclude":[], + "repos":{} + })) + .unwrap(), + ) + .unwrap(); + let artifacts = root.join("legacy-artifacts"); + let output = Command::new("pwsh") + .arg("-NoProfile") + .arg("-File") + .arg(project_root.join("run-code-intel.ps1")) + .arg("-RepoPath") + .arg(repo) + .arg("-Config") + .arg(config) + .arg("-Mode") + .arg("lite") + .arg("-ArtifactRoot") + .arg(&artifacts) + .args([ + "-SkipRepowise", + "-SkipSentrux", + "-SkipGitHubResearch", + "-SkipRepomix", + ]) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "legacy stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let repo_runs = artifacts.join(repo.file_name().unwrap()); + fs::read_dir(repo_runs) + .unwrap() + .filter_map(Result::ok) + .map(|entry| entry.path()) + .find(|path| path.join("code-evidence/merged/full/files.json").is_file()) + .expect("legacy producer must publish code evidence") +} + +fn canonicalize_artifact(relative: &str, mut value: Value) -> Value { + let (field, key): (&str, fn(&Value) -> String) = match relative { + "code-evidence/merged/full/files.json" => ("files", |item| { + item["path"].as_str().unwrap_or_default().to_string() + }), + "code-evidence/merged/full/symbols.json" => ("symbols", |item| { + format!( + "{}\0{:020}\0{}\0{}", + item["file"].as_str().unwrap_or_default(), + item["startLine"].as_u64().unwrap_or_default(), + item["kind"].as_str().unwrap_or_default(), + item["name"].as_str().unwrap_or_default() + ) + }), + "code-evidence/merged/full/chunks.json" => ("chunks", |item| { + format!( + "{}\0{:020}\0{:020}\0{}", + item["file"].as_str().unwrap_or_default(), + item["startLine"].as_u64().unwrap_or_default(), + item["endLine"].as_u64().unwrap_or_default(), + item["id"].as_str().unwrap_or_default() + ) + }), + "code-evidence/merged/full/symbol-chunks.json" => ("mappings", |item| { + format!( + "{}\0{}", + item["symbolId"].as_str().unwrap_or_default(), + item["chunkId"].as_str().unwrap_or_default() + ) + }), + "code-evidence/merged/full/imports.json" => ("imports", |item| { + format!( + "{}\0{:020}\0{}", + item["file"].as_str().unwrap_or_default(), + item["line"].as_u64().unwrap_or_default(), + item["target"].as_str().unwrap_or_default() + ) + }), + "code-evidence/merged/agent/ranking.json" => ("files", |item| { + item["path"].as_str().unwrap_or_default().to_string() + }), + _ => return value, + }; + if let Some(items) = value.get_mut(field).and_then(Value::as_array_mut) { + items.sort_by_key(key); + } + value +} + +fn reverse_semantic_arrays(relative: &str, mut value: Value) -> Value { + let field = match relative { + "code-evidence/merged/full/files.json" | "code-evidence/merged/agent/ranking.json" => { + "files" + } + "code-evidence/merged/full/symbols.json" => "symbols", + "code-evidence/merged/full/chunks.json" => "chunks", + "code-evidence/merged/full/symbol-chunks.json" => "mappings", + "code-evidence/merged/full/imports.json" => "imports", + _ => return value, + }; + if let Some(items) = value.get_mut(field).and_then(Value::as_array_mut) { + items.reverse(); + } + value +} + +#[test] +fn native_atom_preserves_representative_v1_artifacts_through_a01_a03_a09() { + let temp = Temp::new("parity"); + let repo = temp.0.join("repo"); + let out = temp.0.join("run"); + fs::create_dir_all(&repo).unwrap(); + fs::write( + repo.join("index.js"), + "function greet(name) {\n return `hello ${name}`;\n}\n\nmodule.exports = { greet };\n", + ) + .unwrap(); + fs::write( + repo.join("index.test.js"), + "const { greet } = require(\"./index\");\n\ntest(\"greet\", () => greet(\"Ada\"));\n", + ) + .unwrap(); + + let manifest = run(&repo, &out); + assert_eq!( + manifest["nodes"]["evidence.native-code"]["status"], "succeeded", + "{}", + manifest["nodes"]["evidence.native-code"] + ); + + let result = read_json(out.join("evidence.native-code.result.json")); + assert_eq!(result["schema"], "code-intel-capability-result.v1"); + assert_eq!(result["capability"], "evidence.native-code"); + assert_eq!(result["status"], "completed"); + assert_eq!(result["verdict"], "pass"); + assert_eq!( + result["observedEffects"], + serde_json::json!(["repo_read", "local_write"]) + ); + assert_eq!(result["artifacts"].as_array().unwrap().len(), 8); + for artifact in result["artifacts"].as_array().unwrap() { + assert_eq!(artifact["schema"], "code-intel-artifact-ref.v1"); + assert_eq!( + artifact["consumedSnapshotIdentity"], + result["snapshotIdentity"] + ); + assert!(artifact["sha256"] + .as_str() + .is_some_and(|digest| digest.len() == 64)); + } + + let root = out.join("evidence.native-code/code-evidence"); + let files = read_json(root.join("merged/full/files.json")); + let symbols = read_json(root.join("merged/full/symbols.json")); + let imports = read_json(root.join("merged/full/imports.json")); + let scorecard = read_json(root.join("merged/scorecard.json")); + let ranking = read_json(root.join("merged/agent/ranking.json")); + assert_eq!(files["schema"], "code-evidence-files.v1"); + assert_eq!(symbols["schema"], "code-evidence-symbols.v1"); + assert!(symbols["symbols"].as_array().unwrap().iter().any(|symbol| { + symbol["name"] == "greet" + && symbol["kind"] == "function" + && symbol["source"] == "native-minimal" + })); + assert!(imports["imports"] + .as_array() + .unwrap() + .iter() + .any(|import| import["target"] == "./index")); + assert_eq!(scorecard["schema"], "code-evidence-scorecard.v1"); + assert_eq!(scorecard["metrics"]["files"], 2); + assert_eq!(scorecard["metrics"]["chunks"], 2); + assert_eq!(ranking["schema"], "agent-code-slice-ranking.v1"); + assert!(ranking["files"].as_array().unwrap().iter().any(|file| { + file["path"] == "index.js" + && file["reasons"] + .as_array() + .unwrap() + .contains(&Value::from("entrypoint")) + })); + let agent_index = fs::read_to_string(root.join("merged/agent/index.md")).unwrap(); + assert!(agent_index.contains("Call graph precision: unknown")); + assert!(root + .join("merged/agent/slices/native-retrieval.md") + .is_file()); +} + +#[test] +fn unsupported_language_is_explicit_unknown_without_relationship_fabrication() { + let temp = Temp::new("unsupported"); + let repo = temp.0.join("repo"); + let out = temp.0.join("run"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("mystery.zig"), "pub fn hidden() void {}\n").unwrap(); + + run(&repo, &out); + let root = out.join("evidence.native-code/code-evidence"); + let coverage = read_json(root.join("coverage.json")); + assert_eq!(coverage["schema"], "code-evidence-coverage.v1"); + assert_eq!(coverage["parserKind"], "line-heuristic"); + assert_eq!(coverage["relationshipPrecision"], "unknown"); + assert_eq!(coverage["callGraph"], "unknown"); + assert!(coverage["unsupportedFiles"] + .as_array() + .unwrap() + .contains(&Value::from("mystery.zig"))); + assert!(!root.join("full/call-graph.json").exists()); + let symbols = read_json(root.join("merged/full/symbols.json")); + assert!(symbols["symbols"].as_array().unwrap().is_empty()); +} + +#[test] +fn binary_non_source_is_preserved_as_explicit_unsupported_without_failing_the_run() { + let temp = Temp::new("binary-unsupported"); + let repo = temp.0.join("repo"); + let out = temp.0.join("run"); + fs::create_dir_all(repo.join("assets")).unwrap(); + fs::write(repo.join("src.rs"), "pub fn visible() {}\n").unwrap(); + fs::write(repo.join("assets/logo.png"), [0x89, 0x50, 0x4e, 0x47, 0xff]).unwrap(); + + let manifest = run(&repo, &out); + assert_eq!(manifest["outcome"], "completed"); + assert_eq!( + manifest["nodes"]["evidence.native-code"]["status"], + "succeeded" + ); + let evidence = out.join("evidence.native-code/code-evidence"); + let coverage = read_json(evidence.join("coverage.json")); + assert!(coverage["unsupportedFiles"] + .as_array() + .unwrap() + .contains(&Value::from("assets/logo.png"))); + let files = read_json(evidence.join("merged/full/files.json")); + let binary = files["files"] + .as_array() + .unwrap() + .iter() + .find(|file| file["path"] == "assets/logo.png") + .expect("binary inventory entry"); + assert_eq!(binary["language"], "text"); + assert_eq!(binary["lines"], 0); +} + +#[test] +fn non_utf8_supported_source_remains_a_visible_contract_failure() { + let temp = Temp::new("binary-supported"); + let repo = temp.0.join("repo"); + let out = temp.0.join("run"); + fs::create_dir_all(&repo).unwrap(); + fs::write(repo.join("broken.rs"), [0xff, 0xfe, 0xfd]).unwrap(); + + let manifest = run_with_expected_code(&repo, &out, 70); + assert_eq!(manifest["outcome"], "process_failed"); + assert_eq!( + manifest["nodes"]["evidence.native-code"]["status"], + "process_failed" + ); + assert!(manifest["nodes"]["evidence.native-code"]["diagnostic"] + .as_str() + .unwrap() + .contains("supported source file is not UTF-8 text: broken.rs")); +} + +#[test] +fn labeled_multilingual_corpus_quantifies_native_symbol_precision_recall_and_coverage() { + let started = std::time::Instant::now(); + let corpus: Value = read_json( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/internalization/fixtures/r06-native-labeled-corpus.json"), + ); + let temp = Temp::new("labeled-corpus"); + let repo = temp.0.join("repo"); + let out = temp.0.join("run"); + fs::create_dir_all(&repo).unwrap(); + for sample in corpus["samples"].as_array().unwrap() { + fs::write( + repo.join(sample["path"].as_str().unwrap()), + sample["content"].as_str().unwrap(), + ) + .unwrap(); + } + + run(&repo, &out); + let evidence = out.join("evidence.native-code/code-evidence"); + let symbols = read_json(evidence.join("merged/full/symbols.json")); + let predicted_files: HashSet<&str> = symbols["symbols"] + .as_array() + .unwrap() + .iter() + .filter_map(|symbol| symbol["file"].as_str()) + .collect(); + let coverage = read_json(evidence.join("coverage.json")); + let unsupported: HashSet<&str> = coverage["unsupportedFiles"] + .as_array() + .unwrap() + .iter() + .filter_map(Value::as_str) + .collect(); + + let mut tp = 0; + let mut fp = 0; + let mut fn_ = 0; + let mut tn = 0; + for sample in corpus["samples"].as_array().unwrap() { + let path = sample["path"].as_str().unwrap(); + let expected = sample["symbolPresent"].as_bool().unwrap(); + let predicted = predicted_files.contains(path); + match (expected, predicted) { + (true, true) => tp += 1, + (false, true) => fp += 1, + (true, false) => fn_ += 1, + (false, false) => tn += 1, + } + } + assert_eq!((tp, fp, fn_, tn), (6, 2, 2, 2)); + assert_eq!(unsupported.len(), 2); + assert_eq!(corpus["samples"].as_array().unwrap().len(), 12); + assert_eq!(tp as f64 / (tp + fp) as f64, 0.75); + assert_eq!(tp as f64 / (tp + fn_) as f64, 0.75); + assert!(started.elapsed().as_millis() < 120_000); +} + +#[test] +fn a01_a09_artifacts_match_the_real_legacy_producer_on_the_same_fixture() { + let temp = Temp::new("legacy-parity"); + let repo = temp.0.join("repo"); + let out = temp.0.join("native-run"); + fs::create_dir_all(&repo).unwrap(); + fs::write( + repo.join("main.js"), + "import first from \"./first.js\";\nimport second from \"./second.js\";\nfunction main() {}\nconst helper = () => {};\n", + ) + .unwrap(); + fs::write( + repo.join("single.js"), + "const dependency = require(\"./dependency.js\");\nfunction single() {}\n", + ) + .unwrap(); + fs::write(repo.join("plain.txt"), "plain text only\n").unwrap(); + + run(&repo, &out); + let native = out.join("evidence.native-code"); + let comparable = [ + "code-evidence/merged/full/files.json", + "code-evidence/merged/full/symbols.json", + "code-evidence/merged/full/chunks.json", + "code-evidence/merged/full/symbol-chunks.json", + "code-evidence/merged/full/imports.json", + "code-evidence/merged/scorecard.json", + "code-evidence/merged/agent/ranking.json", + "code-evidence/adapters/cocoindex-code/outcome.json", + ]; + let mut raw_file_orders = HashSet::new(); + let mut first_raw = Vec::new(); + for iteration in 0..10 { + let legacy_root = temp.0.join(format!("legacy-{iteration}")); + let legacy = run_legacy(&repo, &legacy_root); + for relative in comparable { + let legacy_raw = read_json(legacy.join(relative)); + let native_value = read_json(native.join(relative)); + assert_eq!( + canonicalize_artifact(relative, native_value.clone()), + native_value, + "native artifact is not in canonical order: {relative}" + ); + assert_eq!( + canonicalize_artifact(relative, legacy_raw.clone()), + native_value, + "legacy/A01-A09 canonical artifact drift on iteration {iteration}: {relative}" + ); + if iteration == 0 { + first_raw.push((relative, legacy_raw)); + } + } + let raw_files = read_json(legacy.join("code-evidence/merged/full/files.json")); + raw_file_orders.insert( + raw_files["files"] + .as_array() + .unwrap() + .iter() + .map(|file| file["path"].as_str().unwrap().to_string()) + .collect::>(), + ); + } + assert!(!raw_file_orders.is_empty()); + for (relative, raw) in first_raw { + let permuted = reverse_semantic_arrays(relative, raw.clone()); + assert_eq!( + canonicalize_artifact(relative, raw), + canonicalize_artifact(relative, permuted), + "canonical comparison must ignore non-semantic traversal order: {relative}" + ); + } + + let native_ranking = read_json(native.join("code-evidence/merged/agent/ranking.json")); + let ranked_files = native_ranking["files"].as_array().unwrap(); + assert_eq!( + ranked_files + .iter() + .map(|file| file["path"].as_str().unwrap()) + .collect::>(), + ["main.js", "plain.txt", "single.js"] + ); + assert_eq!(ranked_files[0]["score"], 60); + assert_eq!( + ranked_files[0]["reasons"], + serde_json::json!(["entrypoint", "symbols", "imports"]) + ); + assert_eq!(ranked_files[0]["symbols"].as_array().unwrap().len(), 2); + assert_eq!(ranked_files[0]["imports"].as_array().unwrap().len(), 2); + assert!(ranked_files[1]["symbols"].is_null()); + assert!(ranked_files[1]["imports"].is_null()); + assert_eq!(ranked_files[1]["score"], 1); + assert_eq!(ranked_files[1]["reasons"], serde_json::json!(["inventory"])); + assert!(ranked_files[2]["symbols"].is_string()); + assert!(ranked_files[2]["imports"].is_string()); + assert_eq!(ranked_files[2]["score"], 10); + assert_eq!( + ranked_files[2]["reasons"], + serde_json::json!(["symbols", "imports"]) + ); + let coverage = read_json(native.join("code-evidence/coverage.json")); + assert_eq!(coverage["relationshipPrecision"], "unknown"); + assert_eq!(coverage["callGraph"], "unknown"); +} diff --git a/crates/code-intel-cli/tests/pon_multilanguage_mapping.rs b/crates/code-intel-cli/tests/pon_multilanguage_mapping.rs new file mode 100644 index 0000000..acc5431 --- /dev/null +++ b/crates/code-intel-cli/tests/pon_multilanguage_mapping.rs @@ -0,0 +1,195 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::Value; + +struct Temp(PathBuf); + +impl Temp { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-pon-{label}-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn read_json(path: impl AsRef) -> Value { + serde_json::from_slice(&fs::read(path).unwrap()).unwrap() +} + +fn tool_fixture(root: &Path) -> PathBuf { + let bin = root.join("dag-tools"); + fs::create_dir_all(&bin).unwrap(); + #[cfg(windows)] + { + for name in ["rg", "git", "python", "repowise"] { + fs::write( + bin.join(format!("{name}.cmd")), + "@echo off\r\nexit /b 0\r\n", + ) + .unwrap(); + } + fs::write( + bin.join("sentrux.cmd"), + "@echo off\r\necho Enforce architectural rules\r\necho Tier: pro\r\nexit /b 0\r\n", + ) + .unwrap(); + } + #[cfg(not(windows))] + { + use std::os::unix::fs::PermissionsExt; + for name in ["rg", "git", "python", "repowise", "sentrux"] { + let path = bin.join(name); + let content = if name == "sentrux" { + "#!/bin/sh\necho 'Enforce architectural rules'\necho 'Tier: pro'\nexit 0\n" + } else { + "#!/bin/sh\nexit 0\n" + }; + fs::write(&path, content).unwrap(); + fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).unwrap(); + } + } + bin +} + +fn run(repo: &Path, out: &Path) { + let tools = tool_fixture(out.parent().unwrap()); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["run", "dag-coordinate", "--repo"]) + .arg(repo) + .arg("--out") + .arg(out) + .arg("--doctor-tool-path-prefix") + .arg(tools) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn pon_frontend_pattern_maps_supported_languages_into_one_code_evidence_contract() { + let fixture = read_json( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json"), + ); + let temp = Temp::new("multilanguage-mapping"); + let repo = temp.0.join("repo"); + let out = temp.0.join("run"); + fs::create_dir_all(&repo).unwrap(); + + for sample in fixture["samples"].as_array().unwrap() { + fs::write( + repo.join(sample["path"].as_str().unwrap()), + sample["content"].as_str().unwrap(), + ) + .unwrap(); + } + + run(&repo, &out); + let root = out.join("evidence.native-code/code-evidence"); + let files = read_json(root.join("merged/full/files.json")); + let symbols = read_json(root.join("merged/full/symbols.json")); + let imports = read_json(root.join("merged/full/imports.json")); + let coverage = read_json(root.join("coverage.json")); + + assert_eq!(files["schema"], "code-evidence-files.v1"); + assert_eq!(symbols["schema"], "code-evidence-symbols.v1"); + assert_eq!(imports["schema"], "code-evidence-imports.v1"); + assert_eq!(coverage["parserKind"], "line-heuristic"); + assert_eq!(coverage["relationshipPrecision"], "unknown"); + assert_eq!(coverage["callGraph"], "unknown"); + + let unsupported = coverage["unsupportedFiles"] + .as_array() + .unwrap() + .iter() + .filter_map(Value::as_str) + .collect::>(); + + for sample in fixture["samples"].as_array().unwrap() { + let path = sample["path"].as_str().unwrap(); + let language = sample["language"].as_str().unwrap(); + let supported = sample["supported"].as_bool().unwrap(); + let file = files["files"] + .as_array() + .unwrap() + .iter() + .find(|file| file["path"] == path) + .unwrap_or_else(|| panic!("missing file fact for {path}")); + + assert_eq!(file["language"], language, "language mismatch for {path}"); + assert_eq!(file["source"], "native-minimal"); + assert_eq!(unsupported.contains(path), !supported); + + let actual_symbols = symbols["symbols"] + .as_array() + .unwrap() + .iter() + .filter(|symbol| symbol["file"] == path) + .map(|symbol| { + assert_eq!(symbol["language"], language); + assert_eq!(symbol["confidence"], 0.55); + assert_eq!(symbol["source"], "native-minimal"); + ( + symbol["kind"].as_str().unwrap().to_string(), + symbol["name"].as_str().unwrap().to_string(), + ) + }) + .collect::>(); + let expected_symbols = sample["expectedSymbols"] + .as_array() + .unwrap() + .iter() + .map(|symbol| { + ( + symbol["kind"].as_str().unwrap().to_string(), + symbol["name"].as_str().unwrap().to_string(), + ) + }) + .collect::>(); + assert_eq!(actual_symbols, expected_symbols, "symbol drift for {path}"); + + let actual_imports = imports["imports"] + .as_array() + .unwrap() + .iter() + .filter(|import| import["file"] == path) + .map(|import| { + assert_eq!(import["language"], language); + assert_eq!(import["confidence"], 0.6); + assert_eq!(import["source"], "native-minimal"); + import["target"].as_str().unwrap().to_string() + }) + .collect::>(); + let expected_imports = sample["expectedImports"] + .as_array() + .unwrap() + .iter() + .map(|target| target.as_str().unwrap().to_string()) + .collect::>(); + assert_eq!(actual_imports, expected_imports, "import drift for {path}"); + } +} diff --git a/crates/code-intel-cli/tests/ponytail_gate.rs b/crates/code-intel-cli/tests/ponytail_gate.rs new file mode 100644 index 0000000..8ac46fd --- /dev/null +++ b/crates/code-intel-cli/tests/ponytail_gate.rs @@ -0,0 +1,458 @@ +#[path = "../src/authority.rs"] +mod authority; +#[path = "../src/ponytail_gate.rs"] +mod ponytail_gate; + +use std::collections::BTreeSet; +use std::io::Write; +use std::process::{Command, Stdio}; + +use serde_json::{json, Value}; + +const NOW: u64 = 2_000; + +fn lower(rung: &str) -> Value { + json!({ + "rung": rung, + "reason": format!("{rung} cannot satisfy the current contract"), + "evidenceIds": ["ev-plan"] + }) +} + +fn change(id: &str, kind: &str, operation: &str, source: &str, rung: &str) -> Value { + let ladder = [ + "do_nothing", + "repository_reuse", + "standard_library", + "platform_native", + "installed_dependency", + "one_liner", + "smallest_local_implementation", + ]; + let selected = ladder + .iter() + .position(|candidate| *candidate == rung) + .unwrap(); + json!({ + "id": id, + "kind": kind, + "operation": operation, + "valueSource": { + "kind": source, + "id": "C00", + "evidenceIds": ["ev-plan"] + }, + "requiredEvidenceIds": ["ev-protection-boundary"], + "firstSufficientRung": rung, + "lowerRungs": ladder[..selected].iter().map(|rung| lower(rung)).collect::>(), + "removedProtections": [] + }) +} + +fn request(mode: &str, changes: Vec) -> Value { + json!({ + "schema": "code-intel-ponytail-gate-request.v1", + "mode": mode, + "evaluatedAt": NOW, + "knownEvidenceIds": ["ev-plan", "ev-risk", "ev-approval", "ev-protection-boundary"], + "consumedAuthorityEventIds": [], + "changes": changes + }) +} + +fn bypass(id: &str, expires_at: u64, evidence: &[&str]) -> Value { + json!({ + "changeId": id, + "authorityEvent": { + "schema": "code-intel-authority-event.v1", + "id": format!("authority-{id}"), + "decision": "approved", + "approver": {"id": "architect-1", "role": "engineering_authority"}, + "evidenceIds": evidence, + "issuedAt": 1_900, + "expiresAt": expires_at + } + }) +} + +#[test] +fn future_maybe_dependency_is_rejected_but_committed_repository_reuse_is_accepted() { + let speculative = change( + "dependency-cache", + "dependency", + "add", + "future_maybe", + "installed_dependency", + ); + let reuse = change( + "reuse-authority-validator", + "abstraction", + "reuse", + "committed_engineering_plan_deliverable", + "repository_reuse", + ); + let result = ponytail_gate::evaluate(&request("enforce", vec![speculative, reuse])).unwrap(); + assert_eq!(result["wouldReject"], 1); + assert_eq!(result["enforcedBlock"], true); + assert_eq!(result["changes"][0]["status"], "rejected"); + assert_eq!(result["changes"][1]["status"], "accepted"); +} + +#[test] +fn report_only_retains_the_same_trace_without_blocking() { + let speculative = change( + "unused-process", + "process", + "add", + "future_maybe", + "smallest_local_implementation", + ); + let result = ponytail_gate::evaluate(&request("report_only", vec![speculative])).unwrap(); + assert_eq!(result["wouldReject"], 1); + assert_eq!(result["enforcedBlock"], false); + assert_eq!(result["changes"][0]["status"], "rejected"); + assert_eq!(result["changes"][0]["change"]["id"], "unused-process"); +} + +#[test] +fn valid_current_sources_cover_addition_deletion_reuse_test_doc_and_process() { + let cases = [ + ( + "artifact-add", + "artifact", + "add", + "operator_requested_outcome", + ), + ("file-delete", "file", "delete", "approved_debt_reduction"), + ( + "utility-reuse", + "abstraction", + "reuse", + "committed_engineering_plan_deliverable", + ), + ("test-add", "test", "add", "verified_defect_or_risk"), + ( + "doc-add", + "documentation", + "add", + "required_contract_or_gate", + ), + ("process-add", "process", "add", "evidence_closing_spike"), + ]; + for (id, kind, operation, source) in cases { + let result = ponytail_gate::evaluate(&request( + "enforce", + vec![change(id, kind, operation, source, "repository_reuse")], + )) + .unwrap(); + assert_eq!(result["wouldReject"], 0, "{id}: {result}"); + assert_eq!(result["changes"][0]["status"], "accepted"); + } +} + +#[test] +fn deleting_safety_evidence_or_verification_is_never_filterable_even_with_bypass() { + for protection in ["safety", "evidence", "verification"] { + let mut deletion = change( + &format!("delete-{protection}"), + "test", + "delete", + "approved_debt_reduction", + "repository_reuse", + ); + deletion["removedProtections"] = json!([protection]); + deletion["bypass"] = bypass( + deletion["id"].as_str().unwrap(), + 2_100, + &["ev-plan", "ev-approval"], + ); + let result = ponytail_gate::evaluate(&request("enforce", vec![deletion])).unwrap(); + assert_eq!(result["changes"][0]["status"], "rejected"); + assert_eq!(result["changes"][0]["authorityEventId"], Value::Null); + } +} + +#[test] +fn bypass_requires_a05_evidence_expiry_scope_and_single_use() { + let mut allowed = change( + "temporary-local-code", + "file", + "add", + "future_maybe", + "smallest_local_implementation", + ); + allowed["bypass"] = bypass( + "temporary-local-code", + 2_100, + &["ev-plan", "ev-approval", "ev-protection-boundary"], + ); + let result = ponytail_gate::evaluate(&request("enforce", vec![allowed.clone()])).unwrap(); + assert_eq!(result["changes"][0]["status"], "bypassed"); + assert_eq!(result["enforcedBlock"], false); + assert_eq!( + result["consumedAuthorityEventIds"], + json!(["authority-temporary-local-code"]) + ); + + let mut expired = allowed.clone(); + expired["bypass"] = bypass("temporary-local-code", 1_999, &["ev-plan", "ev-approval"]); + assert_eq!( + ponytail_gate::evaluate(&request("enforce", vec![expired])).unwrap()["changes"][0] + ["status"], + "rejected" + ); + + let mut missing_evidence = allowed.clone(); + missing_evidence["bypass"] = bypass("temporary-local-code", 2_100, &["ev-approval"]); + assert_eq!( + ponytail_gate::evaluate(&request("enforce", vec![missing_evidence])).unwrap()["changes"][0] + ["status"], + "rejected" + ); + + let mut replay_request = request("enforce", vec![allowed]); + replay_request["consumedAuthorityEventIds"] = json!(["authority-temporary-local-code"]); + assert_eq!( + ponytail_gate::evaluate(&replay_request).unwrap()["changes"][0]["status"], + "rejected" + ); +} + +#[test] +fn bypass_must_cover_value_lower_rung_and_all_required_trace_evidence() { + let mut missing_required = change( + "missing-protection-evidence", + "file", + "add", + "future_maybe", + "repository_reuse", + ); + missing_required["bypass"] = bypass( + "missing-protection-evidence", + 2_100, + &["ev-plan", "ev-approval"], + ); + let result = ponytail_gate::evaluate(&request("enforce", vec![missing_required])).unwrap(); + assert_eq!(result["changes"][0]["status"], "rejected"); + assert_eq!(result["consumedAuthorityEventIds"], json!([])); + + let mut unknown_rung_evidence = change( + "unknown-rung-evidence", + "file", + "add", + "future_maybe", + "repository_reuse", + ); + unknown_rung_evidence["lowerRungs"][0]["evidenceIds"] = json!(["ev-rung-unknown"]); + unknown_rung_evidence["bypass"] = bypass( + "unknown-rung-evidence", + 2_100, + &["ev-plan", "ev-approval", "ev-protection-boundary"], + ); + let result = ponytail_gate::evaluate(&request("enforce", vec![unknown_rung_evidence])).unwrap(); + assert_eq!(result["changes"][0]["status"], "rejected"); + assert_eq!(result["consumedAuthorityEventIds"], json!([])); + assert!(result["changes"][0]["diagnostics"] + .as_array() + .unwrap() + .iter() + .any(|message| message + .as_str() + .unwrap() + .contains("authority event evidence"))); +} + +#[test] +fn authority_cannot_bypass_a_malformed_or_unknown_protection_declaration() { + let mut malformed = change( + "malformed", + "file", + "add", + "future_maybe", + "repository_reuse", + ); + malformed["removedProtections"] = json!(["unknown_guard"]); + malformed["bypass"] = bypass("malformed", 2_100, &["ev-plan", "ev-approval"]); + let error = ponytail_gate::evaluate(&request("enforce", vec![malformed])).unwrap_err(); + assert!(error.contains("removedProtections"), "{error}"); +} + +#[test] +fn schema_invalid_value_source_kind_and_first_rung_fail_before_bypass() { + let mut invalid_source = change( + "invalid-source", + "file", + "add", + "not-a-schema-value-source", + "repository_reuse", + ); + invalid_source["bypass"] = bypass("invalid-source", 2_100, &["ev-plan", "ev-approval"]); + let source_error = + ponytail_gate::evaluate(&request("enforce", vec![invalid_source])).unwrap_err(); + assert!(source_error.contains("value source kind"), "{source_error}"); + + let mut invalid_rung = change( + "invalid-rung", + "file", + "add", + "required_contract_or_gate", + "repository_reuse", + ); + invalid_rung["firstSufficientRung"] = json!("not-a-schema-rung"); + invalid_rung["bypass"] = bypass("invalid-rung", 2_100, &["ev-plan", "ev-approval"]); + let rung_error = ponytail_gate::evaluate(&request("enforce", vec![invalid_rung])).unwrap_err(); + assert!(rung_error.contains("first sufficient rung"), "{rung_error}"); +} + +#[test] +fn first_sufficient_rung_requires_every_lower_rung_once_with_known_evidence() { + let mut missing = change( + "new-local-code", + "file", + "add", + "required_contract_or_gate", + "smallest_local_implementation", + ); + missing["lowerRungs"].as_array_mut().unwrap().remove(2); + let result = ponytail_gate::evaluate(&request("enforce", vec![missing])).unwrap(); + assert_eq!(result["changes"][0]["status"], "rejected"); + + let mut duplicate = change( + "duplicate-rung", + "file", + "add", + "required_contract_or_gate", + "repository_reuse", + ); + duplicate["lowerRungs"] + .as_array_mut() + .unwrap() + .push(lower("do_nothing")); + let error = ponytail_gate::evaluate(&request("enforce", vec![duplicate])).unwrap_err(); + assert!(error.contains("duplicate lowerRungs"), "{error}"); +} + +#[test] +fn checked_contracts_are_closed_and_runtime_policy_matches_the_rule_table() { + let schema: Value = serde_json::from_str(include_str!( + "../../../orchestration/schemas/code-intel-ponytail-gate.v1.schema.json" + )) + .unwrap(); + assert_eq!(schema["$defs"]["request"]["additionalProperties"], false); + assert_eq!(schema["$defs"]["change"]["additionalProperties"], false); + assert_eq!(schema["$defs"]["result"]["additionalProperties"], false); + assert!(schema["$defs"]["change"]["required"] + .as_array() + .unwrap() + .contains(&json!("requiredEvidenceIds"))); + + let checked: Value = serde_json::from_str(include_str!( + "../../../orchestration/ponytail-gate-policy.v1.json" + )) + .unwrap(); + assert_eq!(checked, ponytail_gate::policy_document()); + assert_eq!( + checked["allowedCurrentValueSources"] + .as_array() + .unwrap() + .len(), + 6 + ); + let protected = checked["nonFilterableRequirements"] + .as_array() + .unwrap() + .iter() + .map(|value| value.as_str().unwrap()) + .collect::>(); + assert!(BTreeSet::from(["safety", "evidence", "verification"]).is_subset(&protected)); +} + +#[test] +fn c00_self_trace_is_admitted_and_declares_no_dependency_addition() { + let fixture: Value = serde_json::from_str(include_str!( + "../../../tests/fixtures/ponytail/c00-necessity-trace.json" + )) + .unwrap(); + let result = ponytail_gate::evaluate(&fixture).unwrap(); + assert_eq!(result["wouldReject"], 0, "{result}"); + assert!(fixture["changes"] + .as_array() + .unwrap() + .iter() + .all(|change| change["kind"] != "dependency" || change["operation"] != "add")); +} + +#[test] +fn production_cli_consumes_stdin_and_registry_declares_the_gate() { + let mut speculative = change( + "cli-speculative-file", + "file", + "add", + "future_maybe", + "repository_reuse", + ); + speculative.as_object_mut().unwrap().remove("bypass"); + let input = serde_json::to_vec(&request("enforce", vec![speculative])).unwrap(); + let mut child = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["governance", "ponytail-gate", "--request", "-"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(&input).unwrap(); + let output = child.wait_with_output().unwrap(); + assert_eq!(output.status.code(), Some(2), "{:?}", output); + assert!(output.stderr.is_empty(), "{:?}", output); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["schema"], "code-intel-ponytail-gate-result.v1"); + assert_eq!(result["enforcedBlock"], true); + assert_eq!(result["changes"][0]["status"], "rejected"); + + let registry: Value = + serde_json::from_str(include_str!("../../../orchestration/integrations.json")).unwrap(); + let gate = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "governance.ponytail-gate") + .expect("production registry must declare governance.ponytail-gate"); + assert_eq!(gate["entrypoint"], "crates/code-intel-cli/Cargo.toml"); + assert_eq!( + gate["commands"]["evaluate"], + "target/debug/code-intel.exe governance ponytail-gate --request " + ); + + let mut invalid = change( + "invalid-cli-shape", + "file", + "add", + "not-a-schema-value-source", + "repository_reuse", + ); + invalid["bypass"] = bypass( + "invalid-cli-shape", + 2_100, + &["ev-plan", "ev-approval", "ev-protection-boundary"], + ); + let invalid_input = serde_json::to_vec(&request("enforce", vec![invalid])).unwrap(); + let mut invalid_child = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["governance", "ponytail-gate", "--request", "-"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + invalid_child + .stdin + .take() + .unwrap() + .write_all(&invalid_input) + .unwrap(); + let invalid_output = invalid_child.wait_with_output().unwrap(); + assert_eq!(invalid_output.status.code(), Some(65)); + assert!(invalid_output.stdout.is_empty()); + assert!(String::from_utf8(invalid_output.stderr) + .unwrap() + .contains("schema-invalid")); +} diff --git a/crates/code-intel-cli/tests/project_orientation.rs b/crates/code-intel-cli/tests/project_orientation.rs new file mode 100644 index 0000000..1b3291b --- /dev/null +++ b/crates/code-intel-cli/tests/project_orientation.rs @@ -0,0 +1,275 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = + std::env::temp_dir().join(format!("code-intel-d01-{}-{nonce}", std::process::id())); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn write_ref(root: &Path, path: &str, schema: &str, kind: &str, bytes: &[u8]) -> Value { + fs::write(root.join(path), bytes).unwrap(); + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":schema, + "type":kind, + "path":path, + "sha256":capability::sha256_hex(bytes), + "consumedSnapshotIdentity":SNAPSHOT + }) +} + +#[test] +fn fixture_composes_first_actionable_view_without_fabricating_purpose() { + let temp = Temp::new(); + let fixture: Value = serde_json::from_str(include_str!( + "fixtures/project-orientation/missing-purpose.json" + )) + .unwrap(); + let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let registry: Value = serde_json::from_slice( + &fs::read(repo_root.join("orchestration/integrations.json")).unwrap(), + ) + .unwrap(); + let integration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "project.orientation") + .expect("D01 must be registered as a real A01 atom"); + let bytes = |value: &Value| serde_json::to_vec(value).unwrap(); + let snapshot_ref = write_ref( + &temp.0, + "snapshot.json", + "code-intel-repository-snapshot.v1", + "repository.snapshot", + &bytes(&fixture["snapshot"]), + ); + let inventory_ref = write_ref( + &temp.0, + "files.txt", + "code-intel-file-inventory.v1", + "inventory.files", + fixture["inventory"].as_str().unwrap().as_bytes(), + ); + let mut survival = fixture["survival"].clone(); + survival["repository"]["sourceSha256"] = snapshot_ref["sha256"].clone(); + survival["inventory"]["sourceSha256"] = inventory_ref["sha256"].clone(); + let inputs = vec![ + snapshot_ref, + inventory_ref, + write_ref( + &temp.0, + "survival.json", + "code-intel-repository-survival-scan-result.v1", + "repository.survival-scan", + &bytes(&survival), + ), + write_ref( + &temp.0, + "native-files.json", + "code-evidence-files.v1", + "code_evidence.files", + &bytes(&fixture["nativeFiles"]), + ), + write_ref( + &temp.0, + "native-coverage.json", + "code-evidence-coverage.v1", + "code_evidence.coverage", + &bytes(&fixture["nativeCoverage"]), + ), + write_ref( + &temp.0, + "native-ranking.json", + "agent-code-slice-ranking.v1", + "code_evidence.agent_slice", + &bytes(&fixture["nativeRanking"]), + ), + ]; + let request = json!({ + "schema":"code-intel-capability-request.v1", + "capability":"project.orientation", + "contractVersion":1, + "implementation":integration["capabilityDeclaration"]["implementation"], + "snapshot":fixture["snapshot"]["snapshot"], + "options":{}, + "inputs":inputs, + "effectPolicy":{"allowedEffects":["local_write"]} + }); + let request_path = temp.0.join("request.json"); + fs::write(&request_path, serde_json::to_vec(&request).unwrap()).unwrap(); + let out = temp.0.join("out"); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "project.orientation", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(&out) + .arg("--artifact-root") + .arg(&temp.0) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let envelope: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(envelope["status"], "completed"); + assert_eq!(envelope["observedEffects"], json!(["local_write"])); + assert_eq!(envelope["artifacts"].as_array().unwrap().len(), 2); + let orientation: Value = + serde_json::from_slice(&fs::read(out.join("project-orientation.json")).unwrap()).unwrap(); + assert_eq!(orientation["schema"], "code-intel-project-orientation.v1"); + assert_eq!(orientation["purpose"]["status"], "unknown"); + assert_eq!(orientation["purpose"]["evidence"], json!([])); + assert_eq!(orientation["languages"][0]["name"], "rust"); + assert_eq!(orientation["languages"][0]["fileCount"], 3); + assert_eq!( + orientation["boundaries"], + json!([ + {"path":"src","kind":"top_level_directory","provenance":orientation["boundaries"][0]["provenance"]}, + {"path":"tests","kind":"top_level_directory","provenance":orientation["boundaries"][1]["provenance"]} + ]) + ); + assert_eq!(orientation["entryPoints"][0]["path"], "src/main.rs"); + assert_eq!(orientation["activeChange"]["status"], "dirty"); + assert_eq!(orientation["activeChange"]["paths"], json!(["src/main.rs"])); + assert!(orientation["risks"] + .as_array() + .unwrap() + .iter() + .any(|risk| risk["code"] == "structural_evidence_unavailable")); + assert!(orientation["unknowns"] + .as_array() + .unwrap() + .iter() + .any(|unknown| unknown["field"] == "purpose")); + assert_eq!(orientation["confidence"]["level"], "low"); + for field in ["identity", "purpose", "activeChange", "confidence"] { + assert!( + !orientation[field]["provenance"] + .as_array() + .unwrap() + .is_empty(), + "{field} lacks provenance" + ); + } + for field in [ + "languages", + "boundaries", + "entryPoints", + "evidenceAvailability", + "risks", + "unknowns", + ] { + assert!( + orientation[field] + .as_array() + .unwrap() + .iter() + .all(|claim| !claim["provenance"].as_array().unwrap().is_empty()), + "{field} contains a provenance-free claim" + ); + } + let summary = fs::read_to_string(out.join("project-orientation.md")).unwrap(); + for section in [ + "Identity", + "Purpose", + "Languages", + "Boundaries", + "Entry Points", + "Commands", + "Active Change", + "Risks", + "Unknowns", + "Confidence", + ] { + assert!( + summary.contains(&format!("## {section}")), + "summary projection lacks {section}" + ); + } + let schema_check = Command::new("pwsh") + .args(["-NoLogo", "-NoProfile", "-Command", "param($Document,$Schema); if (-not (Get-Content -Raw -LiteralPath $Document | Test-Json -SchemaFile $Schema -ErrorAction Stop)) { exit 1 }"]) + .arg(out.join("project-orientation.json")) + .arg(repo_root.join("orchestration/schemas/code-intel-project-orientation.v1.schema.json")) + .output() + .unwrap(); + assert!( + schema_check.status.success(), + "schema stderr={}", + String::from_utf8_lossy(&schema_check.stderr) + ); + + let mut incoherent_survival = survival; + incoherent_survival["repository"]["sourceSha256"] = json!("9".repeat(64)); + let incoherent_ref = write_ref( + &temp.0, + "incoherent-survival.json", + "code-intel-repository-survival-scan-result.v1", + "repository.survival-scan", + &bytes(&incoherent_survival), + ); + let mut incoherent_request = request; + let survival_input = incoherent_request["inputs"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|input| input["type"] == "repository.survival-scan") + .unwrap(); + *survival_input = incoherent_ref; + let incoherent_request_path = temp.0.join("incoherent-request.json"); + fs::write( + &incoherent_request_path, + serde_json::to_vec(&incoherent_request).unwrap(), + ) + .unwrap(); + let incoherent_out = temp.0.join("incoherent-out"); + let rejected = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "project.orientation", "--request"]) + .arg(&incoherent_request_path) + .arg("--out") + .arg(&incoherent_out) + .arg("--artifact-root") + .arg(&temp.0) + .output() + .unwrap(); + assert_eq!(rejected.status.code(), Some(65)); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("incoherent")); + assert!(!incoherent_out.join("project-orientation.json").exists()); +} diff --git a/crates/code-intel-cli/tests/project_orientation_benchmark.rs b/crates/code-intel-cli/tests/project_orientation_benchmark.rs new file mode 100644 index 0000000..774d1a3 --- /dev/null +++ b/crates/code-intel-cli/tests/project_orientation_benchmark.rs @@ -0,0 +1,416 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = + std::env::temp_dir().join(format!("code-intel-d02-{}-{nonce}", std::process::id())); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn sha256(path: &Path) -> String { + sha256_hex(&fs::read(path).unwrap()) +} + +fn sha256_hex(bytes: &[u8]) -> String { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + let mut data = bytes.to_vec(); + let bits = (data.len() as u64) * 8; + data.push(0x80); + while data.len() % 64 != 56 { + data.push(0); + } + data.extend_from_slice(&bits.to_be_bytes()); + let mut h = [ + 0x6a09e667u32, + 0xbb67ae85, + 0x3c6ef372, + 0xa54ff53a, + 0x510e527f, + 0x9b05688c, + 0x1f83d9ab, + 0x5be0cd19, + ]; + for chunk in data.chunks_exact(64) { + let mut w = [0u32; 64]; + for (index, word) in chunk.chunks_exact(4).enumerate() { + w[index] = u32::from_be_bytes(word.try_into().unwrap()); + } + for index in 16..64 { + let s0 = w[index - 15].rotate_right(7) + ^ w[index - 15].rotate_right(18) + ^ (w[index - 15] >> 3); + let s1 = w[index - 2].rotate_right(17) + ^ w[index - 2].rotate_right(19) + ^ (w[index - 2] >> 10); + w[index] = w[index - 16] + .wrapping_add(s0) + .wrapping_add(w[index - 7]) + .wrapping_add(s1); + } + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = h; + for index in 0..64 { + let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let ch = (e & f) ^ (!e & g); + let t1 = hh + .wrapping_add(s1) + .wrapping_add(ch) + .wrapping_add(K[index]) + .wrapping_add(w[index]); + let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let maj = (a & b) ^ (a & c) ^ (b & c); + let t2 = s0.wrapping_add(maj); + hh = g; + g = f; + f = e; + e = d.wrapping_add(t1); + d = c; + c = b; + b = a; + a = t1.wrapping_add(t2); + } + for (state, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) { + *state = state.wrapping_add(value); + } + } + h.iter().map(|value| format!("{value:08x}")).collect() +} + +#[test] +fn representative_corpus_measures_quality_latency_and_rejects_fast_provenance_free_output() { + let temp = Temp::new(); + let out = temp.0.join("benchmark"); + let measured = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "benchmark", + "orientation", + "--out", + out.to_str().unwrap(), + "--repetitions", + "2", + ]) + .output() + .unwrap(); + assert_eq!( + measured.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&measured.stdout), + String::from_utf8_lossy(&measured.stderr) + ); + let report: Value = + serde_json::from_slice(&fs::read(out.join("report.json")).unwrap()).unwrap(); + assert_eq!( + report["schema"], + "code-intel-project-orientation-benchmark.v1" + ); + assert_eq!(report["verdict"], "pass"); + assert_eq!(report["corpus"]["fixtureCount"], 9); + assert_eq!( + report["corpus"]["typicalDefinition"], + "small_and_medium_all_conditions" + ); + assert_eq!(report["method"]["repetitionsPerTemperature"], 2); + assert_eq!(report["method"]["llm"], "disabled"); + assert_eq!(report["quality"]["fieldCorrectness"], 1.0); + assert_eq!(report["quality"]["unknownPrecision"], 1.0); + assert_eq!(report["quality"]["unresolvedCoverage"], 1.0); + assert_eq!(report["quality"]["unsupportedCoverage"], 1.0); + assert_eq!(report["quality"]["deterministicReplayRate"], 1.0); + assert_eq!(report["quality"]["provenanceCompleteness"], 1.0); + assert!(report["artifactSize"]["typical"]["p50Bytes"] + .as_u64() + .is_some_and(|bytes| bytes > 0)); + assert!(report["artifactSize"]["all"]["maxBytes"] + .as_u64() + .is_some_and(|bytes| bytes > 0)); + assert!(report["latency"]["typical"]["p50WallTimeMs"].is_u64()); + assert!( + report["latency"]["typical"]["p95WallTimeMs"] + .as_u64() + .unwrap() + <= 60_000 + ); + assert!(report["costCenters"].as_array().unwrap().len() >= 2); + assert_eq!(report["environment"]["cleanMachine"], false); + assert!(report["limitations"] + .as_array() + .unwrap() + .iter() + .any(|item| item.as_str().unwrap().contains("clean machine"))); + + let observations_path = out.join("observations.json"); + let observations: Value = + serde_json::from_slice(&fs::read(&observations_path).unwrap()).unwrap(); + for size in ["small", "medium", "large"] { + let by_condition = |condition: &str| { + observations["fixtures"] + .as_array() + .unwrap() + .iter() + .find(|fixture| fixture["size"] == size && fixture["condition"] == condition) + .unwrap() + }; + let provider = |fixture: &Value| { + fixture["samples"]["warm"][0]["orientation"]["evidenceAvailability"] + .as_array() + .unwrap() + .iter() + .find(|item| item["evidence"] == "benchmark_provider") + .cloned() + .unwrap() + }; + let clean = by_condition("clean"); + let dirty = by_condition("dirty"); + let missing = by_condition("provider_missing"); + for fixture in [clean, dirty, missing] { + assert_eq!( + fixture["samples"]["warm"][0]["coverage"]["unsupportedFiles"], + json!(["Cargo.toml", "README.md"]) + ); + assert!(fixture["samples"]["warm"][0]["artifact"]["bytes"] + .as_u64() + .is_some_and(|bytes| bytes > 0)); + assert_eq!( + fixture["samples"]["warm"][0]["artifact"]["sha256"], + fixture["samples"]["warm"][1]["artifact"]["sha256"] + ); + } + assert_eq!(provider(clean)["status"], "available"); + assert_eq!(provider(dirty)["status"], "available"); + assert_eq!(provider(missing)["status"], "unavailable"); + assert_ne!( + provider(clean)["provenance"][0]["artifactSha256"], + provider(missing)["provenance"][0]["artifactSha256"], + "provider condition must alter committed evidence for {size}" + ); + assert!(!clean["samples"]["warm"][0]["orientation"]["risks"] + .as_array() + .unwrap() + .iter() + .any(|risk| risk["code"] == "structural_evidence_unavailable")); + assert!(missing["samples"]["warm"][0]["orientation"]["risks"] + .as_array() + .unwrap() + .iter() + .any(|risk| risk["code"] == "structural_evidence_unavailable")); + } + let registry: Value = serde_json::from_slice( + &fs::read( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../orchestration/integrations.json"), + ) + .unwrap(), + ) + .unwrap(); + let declaration = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "project.orientation-benchmark") + .unwrap(); + let snapshot = observations["snapshotIdentity"] + .as_str() + .unwrap() + .to_string(); + let request = json!({ + "schema":"code-intel-capability-request.v1", + "capability":"project.orientation-benchmark", + "contractVersion":1, + "implementation":declaration["capabilityDeclaration"]["implementation"], + "snapshot":{ + "identity":&snapshot, + "repoIdentity":format!("content-v1:{}", "c".repeat(64)), + "head":"benchmark-corpus-v1", + "workingTreePolicy":"explicit_overlay", + "scope":["."], + "inputDigest":"d".repeat(64) + }, + "options":{}, + "inputs":[{ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-project-orientation-benchmark-observations.v1", + "type":"benchmark.orientation-observations", + "path":"benchmark/observations.json", + "sha256":sha256(&observations_path), + "consumedSnapshotIdentity":&snapshot + }], + "effectPolicy":{"allowedEffects":["local_write"]} + }); + let valid_request_path = temp.0.join("valid-request.json"); + fs::write(&valid_request_path, serde_json::to_vec(&request).unwrap()).unwrap(); + let valid_out = temp.0.join("a01-valid"); + let valid = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "project.orientation-benchmark", + "--request", + ]) + .arg(&valid_request_path) + .arg("--out") + .arg(&valid_out) + .arg("--artifact-root") + .arg(&temp.0) + .output() + .unwrap(); + assert_eq!( + valid.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&valid.stdout), + String::from_utf8_lossy(&valid.stderr) + ); + assert!(valid_out.join("report.json").exists()); + assert!(valid_out.join("report.md").exists()); + + let mut wrong_count_observations = observations.clone(); + let expected_count = wrong_count_observations["fixtures"][0]["expected"]["fileCount"] + .as_u64() + .unwrap(); + wrong_count_observations["fixtures"][0]["expected"]["fileCount"] = json!(expected_count + 1); + let wrong_count_path = temp.0.join("wrong-count-observations.json"); + fs::write( + &wrong_count_path, + serde_json::to_vec(&wrong_count_observations).unwrap(), + ) + .unwrap(); + let mut wrong_count_request = request.clone(); + wrong_count_request["inputs"][0]["path"] = json!("wrong-count-observations.json"); + wrong_count_request["inputs"][0]["sha256"] = json!(sha256(&wrong_count_path)); + let wrong_count_request_path = temp.0.join("wrong-count-request.json"); + fs::write( + &wrong_count_request_path, + serde_json::to_vec(&wrong_count_request).unwrap(), + ) + .unwrap(); + let wrong_count_out = temp.0.join("wrong-count-out"); + let wrong_count = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "project.orientation-benchmark", + "--request", + ]) + .arg(&wrong_count_request_path) + .arg("--out") + .arg(&wrong_count_out) + .arg("--artifact-root") + .arg(&temp.0) + .output() + .unwrap(); + assert_eq!(wrong_count.status.code(), Some(0)); + let wrong_count_report: Value = + serde_json::from_slice(&fs::read(wrong_count_out.join("report.json")).unwrap()).unwrap(); + assert_eq!(wrong_count_report["verdict"], "fail"); + assert!( + wrong_count_report["quality"]["fieldCorrectness"] + .as_f64() + .unwrap() + < 1.0 + ); + + let mut forged_observations = observations; + forged_observations["fixtures"][0]["samples"]["warm"][0]["orientation"]["identity"] + ["provenance"] = json!([]); + let forged_path = temp.0.join("forged-observations.json"); + fs::write( + &forged_path, + serde_json::to_vec(&forged_observations).unwrap(), + ) + .unwrap(); + let mut forged_request = request; + forged_request["inputs"][0]["path"] = json!("forged-observations.json"); + forged_request["inputs"][0]["sha256"] = json!(sha256(&forged_path)); + let request_path = temp.0.join("forged-request.json"); + fs::write(&request_path, serde_json::to_vec(&forged_request).unwrap()).unwrap(); + let rejected_out = temp.0.join("rejected"); + let rejected = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "capability", + "exec", + "project.orientation-benchmark", + "--request", + ]) + .arg(&request_path) + .arg("--out") + .arg(&rejected_out) + .arg("--artifact-root") + .arg(&temp.0) + .output() + .unwrap(); + assert_eq!(rejected.status.code(), Some(65)); + assert!(!rejected_out.join("report.json").exists()); +} + +#[test] +fn historical_clean_machine_attestation_is_closed_and_cannot_claim_current_source() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let evidence_path = + root.join("orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json"); + let schema_path = root + .join("orchestration/schemas/code-intel-clean-machine-verifier-attestation.v1.schema.json"); + let powershell = if cfg!(windows) { "powershell" } else { "pwsh" }; + let output = Command::new(powershell) + .args([ + "-NoLogo", + "-NoProfile", + "-Command", + "param($Document,$Schema); if (-not (Get-Content -Raw -LiteralPath $Document | Test-Json -SchemaFile $Schema -ErrorAction Stop)) { exit 1 }", + ]) + .arg(&evidence_path) + .arg(&schema_path) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let evidence: Value = serde_json::from_slice(&fs::read(&evidence_path).unwrap()).unwrap(); + assert_eq!(evidence["environment"]["cleanMachine"], true); + assert_eq!(evidence["environment"]["mountCount"], 0); + assert_eq!(evidence["benchmark"]["fixtureCount"], 9); + assert_eq!(evidence["benchmark"]["sampleCount"], 54); + assert_eq!(evidence["productionReport"]["cleanMachine"], false); + assert_eq!( + evidence["productionReport"]["externalVerificationComplete"], + false + ); + assert_ne!( + evidence["source"]["sha256"], + sha256(&root.join("crates/code-intel-cli/src/project_orientation_benchmark.rs")) + ); + assert!(evidence["productionReport"]["statement"] + .as_str() + .unwrap() + .contains("requires a fresh independent clean-machine rerun")); +} diff --git a/crates/code-intel-cli/tests/repowise_adapter.rs b/crates/code-intel-cli/tests/repowise_adapter.rs new file mode 100644 index 0000000..f72bd25 --- /dev/null +++ b/crates/code-intel-cli/tests/repowise_adapter.rs @@ -0,0 +1,164 @@ +#[path = "../src/repowise_adapter.rs"] +mod repowise_adapter; + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use serde_json::{json, Value}; + +const EVALUATED_AT: u64 = 1_700_000_100; +const MAX_AGE: u64 = 300; +static NEXT_REQUEST: AtomicUsize = AtomicUsize::new(1); + +fn fixture_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("tests/fixtures/repowise-adapter") +} + +fn fixture(name: &str) -> Value { + serde_json::from_slice(&fs::read(fixture_root().join(name)).unwrap()).unwrap() +} + +fn evidence<'a>(translated: &'a Value, channel: &str) -> &'a Value { + translated["evidence"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["channel"] == channel) + .unwrap() +} + +fn admit(entry: &Value, artifact_root: &Path) -> (i32, Value) { + let stamp = NEXT_REQUEST.fetch_add(1, Ordering::Relaxed); + let request_path = std::env::temp_dir().join(format!( + "code-intel-repowise-admission-{}-{stamp}.json", + std::process::id() + )); + fs::write( + &request_path, + serde_json::to_vec(&entry["request"]).unwrap(), + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "evidence", + "validate", + "--request", + request_path.to_str().unwrap(), + "--artifact-root", + artifact_root.to_str().unwrap(), + ]) + .output() + .unwrap(); + fs::remove_file(request_path).unwrap(); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + (output.status.code().unwrap(), result) +} + +#[test] +fn repowise_success_keeps_health_separate_and_a04_admits_index_and_docs() { + let translated = + repowise_adapter::translate(&fixture("success.json"), EVALUATED_AT, MAX_AGE).unwrap(); + assert_eq!( + translated["schema"], + "code-intel-repowise-adapter-result.v1" + ); + assert_eq!(translated["health"]["kind"], "health"); + assert_eq!(translated["health"]["evidence"], false); + assert_eq!(translated["evidence"].as_array().unwrap().len(), 2); + assert_ne!( + translated["index"]["effects"], + translated["docs"]["effects"] + ); + assert_eq!(translated["factPromotion"]["eligible"], false); + + for channel in ["index", "docs"] { + let (exit, admitted) = admit(evidence(&translated, channel), &fixture_root()); + assert_eq!(exit, 0, "{channel}: {admitted}"); + assert_eq!(admitted["status"], "admitted"); + assert_eq!(admitted["domainVerdict"], "observed"); + assert_eq!(admitted["engineeringFacts"], json!([])); + } +} + +#[test] +fn docs_quota_is_partial_provider_unavailable_without_erasing_current_index() { + let translated = + repowise_adapter::translate(&fixture("quota.json"), EVALUATED_AT, MAX_AGE).unwrap(); + assert_eq!(translated["index"]["status"], "current"); + assert_eq!(translated["index"]["completeness"], "complete"); + assert_eq!(translated["docs"]["status"], "quota"); + assert_eq!(translated["docs"]["completeness"], "partial"); + assert_eq!(translated["docs"]["failureKind"], "provider_unavailable"); + + let (index_exit, index) = admit(evidence(&translated, "index"), &fixture_root()); + let (docs_exit, docs) = admit(evidence(&translated, "docs"), &fixture_root()); + assert_eq!(index_exit, 0, "{index}"); + assert_eq!(index["domainVerdict"], "observed"); + assert_eq!(docs_exit, 0, "{docs}"); + assert_eq!(docs["domainVerdict"], "unknown"); + assert_eq!(docs["evidence"]["failure"]["kind"], "provider_unavailable"); +} + +#[test] +fn index_only_emits_one_a04_request_and_docs_are_explicitly_not_requested() { + let translated = + repowise_adapter::translate(&fixture("index-only.json"), EVALUATED_AT, MAX_AGE).unwrap(); + assert_eq!(translated["evidence"].as_array().unwrap().len(), 1); + assert_eq!(translated["evidence"][0]["channel"], "index"); + assert_eq!(translated["docs"]["status"], "not_requested"); + assert_eq!(translated["docs"]["completeness"], "none"); +} + +#[test] +fn missing_cli_is_health_and_local_tool_diagnosis_not_fake_evidence() { + let mut native = fixture("success.json"); + native["cli"]["status"] = json!("missing"); + native["health"]["status"] = json!("unavailable"); + let translated = repowise_adapter::translate(&native, EVALUATED_AT, MAX_AGE).unwrap(); + assert_eq!(translated["health"]["status"], "unavailable"); + assert_eq!(translated["index"]["failureKind"], "local_tool_error"); + assert_eq!(translated["docs"]["failureKind"], "local_tool_error"); + assert!(translated["evidence"].as_array().unwrap().is_empty()); +} + +#[test] +fn stale_index_is_translated_but_a04_rejects_fact_promotion() { + let mut native = fixture("index-only.json"); + native["collectedAt"] = json!(1_699_999_000u64); + native["index"]["observedAt"] = json!(1_699_999_000u64); + let translated = repowise_adapter::translate(&native, EVALUATED_AT, MAX_AGE).unwrap(); + assert_eq!(translated["index"]["freshness"], "stale"); + let (exit, result) = admit(evidence(&translated, "index"), &fixture_root()); + assert_eq!(exit, 65, "{result}"); + assert_eq!(result["status"], "rejected"); + assert_eq!(result["engineeringFacts"], json!([])); +} + +#[test] +fn successful_but_incomplete_docs_remain_partial_and_domain_unknown() { + let mut native = fixture("success.json"); + native["docs"]["status"] = json!("partial"); + native["docs"]["payload"]["path"] = json!("partial-docs-payload.json"); + native["docs"]["payload"]["sha256"] = + json!("d7677aecabbe555f5aebe19cc411ac3cadb5bd77dbfe33dc9c402f55b1385ffa"); + let translated = repowise_adapter::translate(&native, EVALUATED_AT, MAX_AGE).unwrap(); + assert_eq!(translated["docs"]["completeness"], "partial"); + let (exit, result) = admit(evidence(&translated, "docs"), &fixture_root()); + assert_eq!(exit, 0, "{result}"); + assert_eq!(result["domainVerdict"], "unknown"); + assert_eq!(result["evidence"]["failure"]["kind"], "domain_unknown"); +} + +#[test] +fn translated_output_never_copies_native_secret_bearing_diagnostics() { + let native = fixture("success.json"); + let translated = repowise_adapter::translate(&native, EVALUATED_AT, MAX_AGE).unwrap(); + let text = serde_json::to_string(&translated).unwrap(); + assert!(!text.contains("super-secret")); + assert!(!text.contains("sk-live-123")); + assert!(!text.contains("ANTHROPIC_API_KEY")); +} diff --git a/crates/code-intel-cli/tests/repowise_route.rs b/crates/code-intel-cli/tests/repowise_route.rs new file mode 100644 index 0000000..652bd2e --- /dev/null +++ b/crates/code-intel-cli/tests/repowise_route.rs @@ -0,0 +1,186 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use serde_json::{json, Value}; + +const EVALUATED_AT: &str = "1700000100"; +const MAX_AGE: &str = "300"; +static NEXT_FILE: AtomicUsize = AtomicUsize::new(1); + +fn root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn fixtures() -> PathBuf { + root().join("tests/fixtures/repowise-adapter") +} + +fn run(request: &Path) -> Output { + Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "provider", + "repowise-adapt", + "--request", + request.to_str().unwrap(), + "--artifact-root", + fixtures().to_str().unwrap(), + "--evaluated-at", + EVALUATED_AT, + "--max-age-seconds", + MAX_AGE, + ]) + .output() + .unwrap() +} + +fn result(output: &Output) -> Value { + serde_json::from_slice(&output.stdout).unwrap_or_else(|error| { + panic!( + "stdout must be JSON: {error}; stdout={}; stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) + }) +} + +fn with_fixture(name: &str, edit: impl FnOnce(&mut Value)) -> PathBuf { + let mut value: Value = + serde_json::from_slice(&fs::read(fixtures().join(name)).unwrap()).unwrap(); + edit(&mut value); + let path = std::env::temp_dir().join(format!( + "code-intel-repowise-route-{}-{}.json", + std::process::id(), + NEXT_FILE.fetch_add(1, Ordering::Relaxed) + )); + fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap(); + path +} + +fn assert_admission(result: &Value, channel: &str, status: &str, verdict: &str) { + let admission = result["admissions"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["channel"] == channel) + .unwrap(); + assert_eq!(admission["result"]["status"], status); + assert_eq!(admission["result"]["domainVerdict"], verdict); +} + +#[test] +fn public_route_translates_and_a04_validates_success_quota_and_index_only() { + for (fixture, count) in [ + ("success.json", 2), + ("quota.json", 2), + ("index-only.json", 1), + ] { + let output = run(&fixtures().join(fixture)); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let value = result(&output); + assert_eq!(value["schema"], "code-intel-repowise-route-result.v1"); + assert_eq!( + value["adapter"]["schema"], + "code-intel-repowise-adapter-result.v1" + ); + assert_eq!(value["admissions"].as_array().unwrap().len(), count); + assert_admission(&value, "index", "admitted", "observed"); + if fixture == "quota.json" { + assert_eq!(value["adapter"]["index"]["status"], "current"); + assert_admission(&value, "docs", "admitted", "unknown"); + } + } +} + +#[test] +fn public_route_preserves_missing_cli_and_incomplete_docs_without_fake_facts() { + let missing = with_fixture("success.json", |native| { + native["cli"]["status"] = json!("missing"); + native["health"]["status"] = json!("unavailable"); + }); + let incomplete = with_fixture("success.json", |native| { + native["docs"]["status"] = json!("partial"); + native["docs"]["payload"]["path"] = json!("partial-docs-payload.json"); + native["docs"]["payload"]["sha256"] = + json!("d7677aecabbe555f5aebe19cc411ac3cadb5bd77dbfe33dc9c402f55b1385ffa"); + }); + let missing_output = run(&missing); + assert_eq!(missing_output.status.code(), Some(0)); + let missing_result = result(&missing_output); + assert_eq!(missing_result["adapter"]["health"]["status"], "unavailable"); + assert_eq!(missing_result["admissions"], json!([])); + assert_eq!( + missing_result["adapter"]["factPromotion"]["eligible"], + false + ); + + let incomplete_output = run(&incomplete); + assert_eq!(incomplete_output.status.code(), Some(0)); + let incomplete_result = result(&incomplete_output); + assert_admission(&incomplete_result, "docs", "admitted", "unknown"); + assert_eq!( + incomplete_result["adapter"]["factPromotion"]["engineeringFacts"], + json!([]) + ); + fs::remove_file(missing).unwrap(); + fs::remove_file(incomplete).unwrap(); +} + +#[test] +fn public_route_reports_stale_as_a04_rejection_and_never_leaks_native_diagnostics() { + let stale = with_fixture("index-only.json", |native| { + native["collectedAt"] = json!(1_699_999_000u64); + native["index"]["observedAt"] = json!(1_699_999_000u64); + }); + let output = run(&stale); + assert_eq!(output.status.code(), Some(65)); + let value = result(&output); + assert_admission(&value, "index", "rejected", "unknown"); + let text = String::from_utf8(output.stdout).unwrap(); + assert!(!text.contains("super-secret")); + assert!(!text.contains("sk-live-123")); + assert!(!text.contains("ANTHROPIC_API_KEY")); + fs::remove_file(stale).unwrap(); +} + +#[test] +fn registry_declares_the_exact_public_route() { + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let entry = registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["id"] == "provider.repowise-adapt") + .expect("provider.repowise-adapt must be registered"); + assert_eq!( + entry["commands"]["adapt"], + "target/debug/code-intel.exe provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds " + ); + + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "route", + "--action", + "List", + "--provider", + "repowise", + "--json", + ]) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(0)); + let public_registry: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert!(public_registry["routes"] + .as_array() + .unwrap() + .iter() + .any(|route| route["operation"] == "adapt")); +} diff --git a/crates/code-intel-cli/tests/run_commit.rs b/crates/code-intel-cli/tests/run_commit.rs new file mode 100644 index 0000000..244f908 --- /dev/null +++ b/crates/code-intel-cli/tests/run_commit.rs @@ -0,0 +1,282 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/run_commit.rs"] +mod run_commit; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; +#[path = "../src/staged_artifact.rs"] +mod staged_artifact; + +use run_commit::{CommitOptions, PublicationPhase}; +use staged_artifact::{ArtifactWriteContract, StagedWriter}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +static SEQ: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-a07-{label}-{}-{nonce}-{}", + std::process::id(), + SEQ.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn inventory_contract() -> ArtifactWriteContract { + ArtifactWriteContract { + artifact_schema: "code-intel-file-inventory.v1", + artifact_type: "inventory.files", + max_bytes: 1024, + validate_payload: |bytes| { + if bytes == b"portable evidence\n" { + Ok(()) + } else { + Err("invalid inventory".into()) + } + }, + } +} +fn manifest_contract() -> ArtifactWriteContract { + ArtifactWriteContract { + artifact_schema: "code-intel-run-manifest.v1", + artifact_type: "run.manifest", + max_bytes: 1024 * 1024, + validate_payload: run_commit::validate_run_manifest_bytes, + } +} + +fn staged(root: &Path) -> (staged_artifact::StagedArtifactSet, Value) { + let mut writer = StagedWriter::begin(root, SNAPSHOT).unwrap(); + let inventory = writer + .stage(b"portable evidence\n", inventory_contract()) + .unwrap() + .to_artifact_ref_value(); + let manifest = json!({ + "schema":"code-intel-run-manifest.v1", + "runIdentity":"dag-v1:aabb", + "snapshotIdentity":SNAPSHOT, + "outcome":"completed", + "nodes":{"inventory":{"status":"succeeded","verdict":"pass","artifacts":[inventory]}} + }); + let manifest_bytes = serde_json::to_vec(&manifest).unwrap(); + let manifest_ref = writer + .stage(&manifest_bytes, manifest_contract()) + .unwrap() + .to_artifact_ref_value(); + (writer.seal().unwrap(), manifest_ref) +} + +#[test] +fn staged_run_is_promoted_and_completion_marker_is_published_last() { + let tree = Temp::new("complete"); + let (set, manifest_ref) = staged(&tree.0); + let result = + run_commit::commit(set, &manifest_ref, "run-001", CommitOptions::default()).unwrap(); + assert_eq!(result.final_path, tree.0.join("run-001")); + assert_eq!(result.marker["runIdentity"], "dag-v1:aabb"); + assert_eq!(result.marker["snapshotIdentity"], SNAPSHOT); + assert_eq!(result.marker["manifest"]["sha256"], manifest_ref["sha256"]); + assert!(result.final_path.join("run-complete.json").is_file()); + assert_eq!(run_commit::classify(&result.final_path), "committed"); + assert!(!tree.0.join("run-001").join("artifact-index.json").exists()); +} + +#[test] +fn every_publication_phase_is_fail_closed_and_post_promotion_is_recoverable() { + for phase in [ + PublicationPhase::Prevalidate, + PublicationPhase::Rename, + PublicationPhase::DirectorySync, + PublicationPhase::MarkerTemp, + PublicationPhase::MarkerPublish, + PublicationPhase::PostMarkerVerify, + PublicationPhase::Rollback, + ] { + let tree = Temp::new(&format!("phase-{phase:?}")); + let (set, manifest_ref) = staged(&tree.0); + let error = run_commit::commit( + set, + &manifest_ref, + "run", + CommitOptions { + interrupt_before: Some(phase), + ..CommitOptions::default() + }, + ) + .unwrap_err(); + assert!(matches!(error, run_commit::CommitError::Interrupted(value) if value == phase)); + let final_path = tree.0.join("run"); + assert!(!final_path.join("run-complete.json").exists(), "{phase:?}"); + if phase == PublicationPhase::MarkerPublish { + assert!( + fs::read_dir(&final_path).unwrap().all(|entry| !entry + .unwrap() + .file_name() + .to_string_lossy() + .starts_with(".run-complete.json.tmp.")), + "owned marker temp was not cleaned" + ); + } + if matches!( + phase, + PublicationPhase::Prevalidate | PublicationPhase::Rename + ) { + assert!(!final_path.exists(), "{phase:?}"); + } else { + assert_eq!( + run_commit::classify(&final_path), + "legacy-uncommitted", + "{phase:?}" + ); + let recovered = + run_commit::recover(&final_path, &manifest_ref, CommitOptions::default()).unwrap(); + assert_eq!(run_commit::classify(&recovered.final_path), "committed"); + } + } +} + +#[test] +fn prevalidation_rechecks_digest_schema_snapshot_and_manifest_completeness() { + let tree = Temp::new("tamper"); + let (set, manifest_ref) = staged(&tree.0); + fs::write( + set.path().join(manifest_ref["path"].as_str().unwrap()), + b"{}\n", + ) + .unwrap(); + assert!(run_commit::commit(set, &manifest_ref, "run", CommitOptions::default()).is_err()); + assert!(!tree.0.join("run").exists()); + + let invalid = json!({"schema":"code-intel-run-manifest.v1","runIdentity":"dag-v1:aabb","snapshotIdentity":SNAPSHOT,"outcome":"incomplete","nodes":{"x":{"status":"running"}}}); + assert!( + run_commit::validate_run_manifest_bytes(&serde_json::to_vec(&invalid).unwrap()).is_err() + ); +} + +#[test] +fn competing_destination_and_marker_are_preserved() { + let tree = Temp::new("competitor-dir"); + let competitor = tree.0.join("run"); + fs::create_dir(&competitor).unwrap(); + fs::write(competitor.join("sentinel.txt"), b"competitor").unwrap(); + let (set, manifest_ref) = staged(&tree.0); + assert!(run_commit::commit(set, &manifest_ref, "run", CommitOptions::default()).is_err()); + assert_eq!( + fs::read(competitor.join("sentinel.txt")).unwrap(), + b"competitor" + ); + + let tree = Temp::new("competitor-marker"); + let (set, manifest_ref) = staged(&tree.0); + let _ = run_commit::commit( + set, + &manifest_ref, + "run", + CommitOptions { + interrupt_before: Some(PublicationPhase::MarkerPublish), + ..CommitOptions::default() + }, + ); + let marker = tree.0.join("run/run-complete.json"); + fs::write(&marker, b"competitor-marker").unwrap(); + assert!( + run_commit::recover(&tree.0.join("run"), &manifest_ref, CommitOptions::default()).is_err() + ); + assert_eq!(fs::read(marker).unwrap(), b"competitor-marker"); +} + +#[test] +fn post_publish_sync_and_read_failures_roll_back_owned_marker() { + for read_failure in [false, true] { + let tree = Temp::new(if read_failure { + "read-failure" + } else { + "sync-failure" + }); + let (set, manifest_ref) = staged(&tree.0); + let result = run_commit::commit( + set, + &manifest_ref, + "run", + CommitOptions { + fail_marker_sync: !read_failure, + fail_marker_read: read_failure, + ..CommitOptions::default() + }, + ); + assert!(result.is_err()); + let final_path = tree.0.join("run"); + assert!(!final_path.join("run-complete.json").exists()); + assert_eq!(run_commit::classify(&final_path), "legacy-uncommitted"); + } +} + +#[test] +fn legacy_timestamp_run_is_readable_but_uncommitted() { + let tree = Temp::new("legacy"); + let legacy = tree.0.join("20260713-120000"); + fs::create_dir(&legacy).unwrap(); + fs::write(legacy.join("report.json"), b"{}\n").unwrap(); + assert_eq!(run_commit::classify(&legacy), "legacy-uncommitted"); + assert_eq!(fs::read(legacy.join("report.json")).unwrap(), b"{}\n"); +} + +#[test] +fn production_run_commit_cli_restages_a09_refs_through_a06_and_publishes() { + let tree = Temp::new("cli"); + let source_authority = tree.0.join("source-authority"); + let publication_authority = tree.0.join("publication-authority"); + fs::create_dir(&source_authority).unwrap(); + fs::create_dir(&publication_authority).unwrap(); + let (set, manifest_ref) = staged(&source_authority); + let source = + run_commit::commit(set, &manifest_ref, "source", CommitOptions::default()).unwrap(); + let manifest_ref_path = tree.0.join("manifest-ref.json"); + fs::write( + &manifest_ref_path, + serde_json::to_vec(&manifest_ref).unwrap(), + ) + .unwrap(); + let raw = vec![ + "commit".to_string(), + "--source-root".to_string(), + source.final_path.display().to_string(), + "--authority-root".to_string(), + publication_authority.display().to_string(), + "--manifest-ref".to_string(), + manifest_ref_path.display().to_string(), + "--final-name".to_string(), + "published".to_string(), + ]; + assert_eq!(run_commit::run_raw(&raw), 0); + assert_eq!( + run_commit::classify(&publication_authority.join("published")), + "committed" + ); +} diff --git a/crates/code-intel-cli/tests/runtime_ci_evidence.rs b/crates/code-intel-cli/tests/runtime_ci_evidence.rs new file mode 100644 index 0000000..5eea52f --- /dev/null +++ b/crates/code-intel-cli/tests/runtime_ci_evidence.rs @@ -0,0 +1,259 @@ +#[path = "../src/runtime_ci_evidence.rs"] +mod runtime_ci_evidence; + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-runtime-ci-{}-{nonce}", + std::process::id() + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn signal(status: &str, observed: bool) -> Value { + json!({"status":status,"observed":observed,"summary":format!("fixture {status}")}) +} + +fn source(completeness: &str, tests: &str, build: &str, runtime: &str) -> Value { + json!({ + "schema":"code-intel-runtime-ci-observation.v1", + "provider":{"id":"fixture.local-json","runId":"run-42","sourceRevision":"abc123"}, + "provenance":{ + "collectorId":"fixture-exporter", + "collectorVersion":"1.0.0", + "collectionId":"collection-42", + "collectedAt":1950 + }, + "snapshotIdentity":SNAPSHOT, + "observedAt":1950, + "completeness":completeness, + "signals":{ + "tests":signal(tests, tests != "unknown"), + "build":signal(build, build != "unknown"), + "runtime":signal(runtime, runtime != "unknown") + } + }) +} + +fn write_request(root: &Path, source: &Value) -> Value { + let bytes = serde_json::to_vec(source).unwrap(); + fs::write(root.join("runtime-ci.json"), &bytes).unwrap(); + json!({ + "schema":"code-intel-runtime-ci-ingest-request.v1", + "expectedSnapshotIdentity":SNAPSHOT, + "artifact":{"path":"runtime-ci.json","sha256":runtime_ci_evidence::sha256_hex(&bytes)}, + "policy":{"evaluatedAt":2000,"maxAgeSeconds":100} + }) +} + +#[test] +fn only_complete_current_fully_positive_observations_are_green() { + let temp = Temp::new(); + let request = write_request(&temp.0, &source("complete", "passed", "passed", "healthy")); + let result = runtime_ci_evidence::ingest_request(&temp.0, &request).unwrap(); + assert_eq!(result["admission"], "accepted"); + assert_eq!(result["health"], "green"); + assert_eq!(result["failureKind"], "none"); + assert_eq!( + result["facts"], + json!([ + "tests_observed_passed", + "build_observed_passed", + "runtime_observed_healthy" + ]) + ); +} + +#[test] +fn missing_partial_and_unobserved_are_unknown_not_green() { + let temp = Temp::new(); + let missing_request = json!({ + "schema":"code-intel-runtime-ci-ingest-request.v1", + "expectedSnapshotIdentity":SNAPSHOT, + "artifact":{"path":"missing.json","sha256":"b".repeat(64)}, + "policy":{"evaluatedAt":2000,"maxAgeSeconds":100} + }); + let missing = runtime_ci_evidence::ingest_request(&temp.0, &missing_request).unwrap(); + assert_eq!(missing["health"], "unknown"); + assert_eq!(missing["failureKind"], "artifact_missing"); + assert_eq!(missing["facts"], json!([])); + + let partial = runtime_ci_evidence::normalize( + &source("partial", "passed", "unknown", "unknown"), + SNAPSHOT, + 2000, + 100, + ) + .unwrap(); + assert_eq!(partial["health"], "unknown"); + assert_eq!(partial["failureKind"], "partial_coverage"); + assert_eq!(partial["facts"], json!([])); +} + +#[test] +fn observed_failure_is_red_even_when_other_domains_are_unknown() { + let result = runtime_ci_evidence::normalize( + &source("partial", "failed", "unknown", "unknown"), + SNAPSHOT, + 2000, + 100, + ) + .unwrap(); + assert_eq!(result["health"], "red"); + assert_eq!(result["failureKind"], "observed_failure"); + assert_eq!(result["facts"], json!(["runtime_ci_observed_failure"])); +} + +#[test] +fn stale_snapshot_mismatch_and_digest_forgery_fail_closed() { + let mut stale_source = source("complete", "passed", "passed", "healthy"); + stale_source["observedAt"] = json!(1800); + let stale = runtime_ci_evidence::normalize(&stale_source, SNAPSHOT, 2000, 100).unwrap(); + assert_eq!(stale["admission"], "rejected"); + assert_eq!(stale["health"], "unknown"); + assert_eq!(stale["failureKind"], "stale"); + + let mut wrong = source("complete", "passed", "passed", "healthy"); + wrong["snapshotIdentity"] = json!("c".repeat(64)); + let wrong = runtime_ci_evidence::normalize(&wrong, SNAPSHOT, 2000, 100).unwrap(); + assert_eq!(wrong["failureKind"], "snapshot_mismatch"); + assert_eq!(wrong["facts"], json!([])); + + let temp = Temp::new(); + let mut request = write_request(&temp.0, &source("complete", "passed", "passed", "healthy")); + request["artifact"]["sha256"] = json!("d".repeat(64)); + assert!(runtime_ci_evidence::ingest_request(&temp.0, &request) + .unwrap_err() + .contains("digest mismatch")); +} + +#[test] +fn contracts_reject_unknown_fields_false_observation_claims_and_path_escape() { + let mut extra = source("complete", "passed", "passed", "healthy"); + extra["provider"]["secret"] = json!("must-not-be-accepted"); + assert!(runtime_ci_evidence::normalize(&extra, SNAPSHOT, 2000, 100) + .unwrap_err() + .contains("fields are invalid")); + + let mut false_claim = source("complete", "passed", "passed", "healthy"); + false_claim["signals"]["tests"]["observed"] = json!(false); + assert!( + runtime_ci_evidence::normalize(&false_claim, SNAPSHOT, 2000, 100) + .unwrap_err() + .contains("observed is false") + ); + + let temp = Temp::new(); + let request = json!({ + "schema":"code-intel-runtime-ci-ingest-request.v1", + "expectedSnapshotIdentity":SNAPSHOT, + "artifact":{"path":"../escape.json","sha256":"e".repeat(64)}, + "policy":{"evaluatedAt":2000,"maxAgeSeconds":100} + }); + assert!(runtime_ci_evidence::ingest_request(&temp.0, &request) + .unwrap_err() + .contains("without '..'")); +} + +#[test] +fn schemas_and_documentation_are_closed_and_provider_neutral() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + for relative in [ + "orchestration/schemas/code-intel-runtime-ci-ingest-request.v1.schema.json", + "orchestration/schemas/code-intel-runtime-ci-observation.v1.schema.json", + "orchestration/schemas/code-intel-runtime-ci-summary.v1.schema.json", + ] { + let schema: Value = + serde_json::from_slice(&fs::read(root.join(relative)).unwrap()).unwrap(); + assert_eq!(schema["additionalProperties"], false, "{relative}"); + } + let docs = fs::read_to_string(root.join("docs/runtime-ci-evidence.md")).unwrap(); + assert!(docs.contains("does not call a CI provider")); + assert!(docs.contains("Missing is not green")); + assert!(docs.contains("Hospital/PET")); +} + +#[test] +fn production_cli_reads_only_the_pinned_local_artifact() { + let temp = Temp::new(); + let request = write_request(&temp.0, &source("complete", "passed", "passed", "healthy")); + let request_path = temp.0.join("request.json"); + let output_path = temp.0.join("summary.json"); + fs::write(&request_path, serde_json::to_vec(&request).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["provider", "runtime-ci-evidence", "--artifact-root"]) + .arg(&temp.0) + .arg("--request") + .arg(&request_path) + .arg("--out") + .arg(&output_path) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let summary: Value = serde_json::from_slice(&fs::read(output_path).unwrap()).unwrap(); + assert_eq!(summary["schema"], "code-intel-runtime-ci-summary.v1"); + assert_eq!(summary["health"], "green"); +} + +#[test] +fn production_cli_rejects_duplicate_source_keys() { + let temp = Temp::new(); + let valid = serde_json::to_string(&source("complete", "passed", "passed", "healthy")).unwrap(); + let duplicate = valid.replacen( + "\"completeness\":\"complete\"", + "\"completeness\":\"complete\",\"completeness\":\"partial\"", + 1, + ); + assert!(duplicate.matches("\"completeness\"").count() >= 2); + fs::write(temp.0.join("runtime-ci.json"), duplicate.as_bytes()).unwrap(); + let request = json!({ + "schema":"code-intel-runtime-ci-ingest-request.v1", + "expectedSnapshotIdentity":SNAPSHOT, + "artifact":{"path":"runtime-ci.json","sha256":runtime_ci_evidence::sha256_hex(duplicate.as_bytes())}, + "policy":{"evaluatedAt":2000,"maxAgeSeconds":100} + }); + let request_path = temp.0.join("request.json"); + let output_path = temp.0.join("summary.json"); + fs::write(&request_path, serde_json::to_vec(&request).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["provider", "runtime-ci-evidence", "--artifact-root"]) + .arg(&temp.0) + .arg("--request") + .arg(&request_path) + .arg("--out") + .arg(&output_path) + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(65)); + assert!(!output_path.exists()); + assert!(String::from_utf8_lossy(&output.stderr).contains("duplicate JSON object key")); +} diff --git a/crates/code-intel-cli/tests/schema_lifecycle.rs b/crates/code-intel-cli/tests/schema_lifecycle.rs new file mode 100644 index 0000000..42c7585 --- /dev/null +++ b/crates/code-intel-cli/tests/schema_lifecycle.rs @@ -0,0 +1,250 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde_json::Value; + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("crate must live under /crates") + .to_path_buf() +} + +fn read_json(path: &Path) -> Value { + let bytes = fs::read(path).unwrap_or_else(|error| panic!("read {}: {error}", path.display())); + serde_json::from_slice(&bytes) + .unwrap_or_else(|error| panic!("parse {}: {error}", path.display())) +} + +fn strings<'a>(value: &'a Value, label: &str) -> Vec<&'a str> { + value + .as_array() + .unwrap_or_else(|| panic!("{label} must be an array")) + .iter() + .map(|item| { + item.as_str() + .unwrap_or_else(|| panic!("{label} items must be strings")) + }) + .collect() +} + +fn exact_keys(value: &Value, expected: &[&str], label: &str) { + let object = value + .as_object() + .unwrap_or_else(|| panic!("{label} must be an object")); + let actual = object.keys().map(String::as_str).collect::>(); + let expected = expected.iter().copied().collect::>(); + assert_eq!(actual, expected, "{label} keys differ"); +} + +fn versioned_schema_name(name: &str) -> bool { + let Some(stem) = name.strip_suffix(".schema.json") else { + return false; + }; + let Some((base, version)) = stem.rsplit_once(".v") else { + return false; + }; + !base.is_empty() + && !version.is_empty() + && !version.starts_with('0') + && version.bytes().all(|byte| byte.is_ascii_digit()) +} + +#[test] +fn all_schema_files_have_stable_unique_identity_and_registry_refs_resolve() { + let root = repo_root(); + let schema_root = root.join("orchestration/schemas"); + let mut ids = BTreeMap::new(); + let mut count = 0; + for entry in fs::read_dir(&schema_root).expect("read schema directory") { + let entry = entry.expect("read schema entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if !name.ends_with(".schema.json") { + continue; + } + count += 1; + assert!( + versioned_schema_name(&name), + "schema filename is not versioned: {name}" + ); + let schema = read_json(&entry.path()); + assert!( + matches!( + schema["$schema"].as_str(), + Some("https://json-schema.org/draft/2020-12/schema") + | Some("http://json-schema.org/draft-07/schema#") + ), + "unsupported JSON Schema draft in {name}" + ); + let id = schema["$id"] + .as_str() + .filter(|id| !id.is_empty()) + .unwrap_or_else(|| panic!("schema $id is required in {name}")); + assert!( + ids.insert(id.to_string(), name.clone()).is_none(), + "duplicate schema $id: {id}" + ); + } + assert!(count >= 12, "schema inventory unexpectedly small"); + + let registry = read_json(&root.join("orchestration/integrations.json")); + for integration in registry["integrations"] + .as_array() + .expect("integrations must be an array") + { + let id = integration["id"].as_str().expect("integration id"); + let Some(contracts) = integration.get("artifactContract") else { + continue; + }; + for contract in contracts.as_array().into_iter().flatten() { + let Some(path) = contract.as_str() else { + continue; + }; + if path.ends_with(".schema.json") { + assert!( + root.join(path).is_file(), + "integration {id} references missing schema {path}" + ); + } + } + } +} + +#[test] +fn lifecycle_catalog_is_coherent() { + let root = repo_root(); + let lifecycle = read_json(&root.join("orchestration/schema-lifecycle.v1.json")); + exact_keys(&lifecycle, &["schema", "policy", "contracts"], "lifecycle"); + assert_eq!(lifecycle["schema"], "code-intel-schema-lifecycle.v1"); + exact_keys( + &lifecycle["policy"], + &[ + "breakingChange", + "compatibleChange", + "retirement", + "coreRuntimeRule", + ], + "lifecycle policy", + ); + assert_eq!(lifecycle["policy"]["breakingChange"], "publish-new-version"); + assert_eq!(lifecycle["policy"]["compatibleChange"], "additive-only"); + assert_eq!( + lifecycle["policy"]["retirement"], + "evidence-backed-compatibility-window" + ); + assert_eq!( + lifecycle["policy"]["coreRuntimeRule"], + "implementation-and-tests-required" + ); + + let required = [ + "code-intel-schema-lifecycle.v1", + "code-intel-repository-snapshot.v1", + "code-intel-run-dag.v1", + "code-intel-run-state.v1", + "code-intel-run-manifest.v1", + "code-intel-staged-artifact-set.v1", + "code-intel-run-commit.v1", + "code-intel-artifact-index.v1", + "code-intel-evidence-query.v1", + "code-intel-change-impact.v1", + "code-evidence-files.v1", + "code-intel-session-evidence.v1", + ]; + let mut paths = BTreeSet::new(); + let mut logical = BTreeSet::new(); + for contract in lifecycle["contracts"] + .as_array() + .expect("contracts must be an array") + { + exact_keys( + contract, + &[ + "schemaPath", + "schemaId", + "logicalSchemas", + "shape", + "status", + "compatibilityPolicy", + "implementation", + ], + "lifecycle contract", + ); + assert_eq!(contract["status"], "active"); + assert_eq!(contract["compatibilityPolicy"], "additive-only"); + let relative = contract["schemaPath"].as_str().expect("schemaPath"); + assert!( + paths.insert(relative), + "duplicate lifecycle schemaPath: {relative}" + ); + let schema_path = root.join(relative); + let schema = read_json(&schema_path); + assert_eq!( + schema["$id"], contract["schemaId"], + "$id mismatch for {relative}" + ); + + let names = strings(&contract["logicalSchemas"], "logicalSchemas"); + for name in &names { + assert!(logical.insert(*name), "duplicate logical schema: {name}"); + } + match contract["shape"].as_str() { + Some("closed-object") => { + assert_eq!(schema["type"], "object", "{relative} must be an object"); + assert_eq!( + schema["additionalProperties"], false, + "{relative} must be closed" + ); + assert_eq!( + schema["properties"]["schema"]["const"], names[0], + "logical schema mismatch for {relative}" + ); + assert_eq!(names.len(), 1, "closed object must have one logical schema"); + } + Some("closed-composite") => { + assert!(schema["oneOf"] + .as_array() + .is_some_and(|items| !items.is_empty())); + let encoded = serde_json::to_string(&schema).expect("encode composite schema"); + for name in names { + assert!( + encoded.contains(name), + "{relative} omits logical schema {name}" + ); + } + } + other => panic!("unsupported lifecycle shape: {other:?}"), + } + + exact_keys( + &contract["implementation"], + &["source", "symbols", "tests"], + "contract implementation", + ); + let source_path = root.join( + contract["implementation"]["source"] + .as_str() + .expect("implementation source"), + ); + let source = fs::read_to_string(&source_path) + .unwrap_or_else(|error| panic!("read {}: {error}", source_path.display())); + for symbol in strings(&contract["implementation"]["symbols"], "symbols") { + assert!( + source.contains(symbol), + "implementation {} omits symbol {symbol}", + source_path.display() + ); + } + for test in strings(&contract["implementation"]["tests"], "tests") { + assert!( + root.join(test).is_file(), + "missing lifecycle test binding: {test}" + ); + } + } + for schema in required { + assert!(logical.contains(schema), "core lifecycle omits {schema}"); + } +} diff --git a/crates/code-intel-cli/tests/sentrux_adapter.rs b/crates/code-intel-cli/tests/sentrux_adapter.rs new file mode 100644 index 0000000..8b785ba --- /dev/null +++ b/crates/code-intel-cli/tests/sentrux_adapter.rs @@ -0,0 +1,248 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/admissibility.rs"] +mod admissibility; +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/sentrux_adapter.rs"] +mod sentrux_adapter; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +const CURRENT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const IMPL: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +static SEQ: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-b03-{}-{nonce}-{}", + std::process::id(), + SEQ.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn descriptor(name: &str) -> Value { + serde_json::from_str(match name { + "complete" => include_str!("fixtures/sentrux-adapter/complete.json"), + "partial" => include_str!("fixtures/sentrux-adapter/partial.json"), + "unknown" => include_str!("fixtures/sentrux-adapter/unknown-kind.json"), + "crashed" => include_str!("fixtures/sentrux-adapter/crashed.json"), + _ => panic!("unknown fixture"), + }) + .unwrap() +} + +fn build_case(root: &Path, fixture: &Value) -> Value { + let mut native = fixture.clone(); + native["schema"] = json!("code-intel-sentrux-provider-native.v1"); + native["implementation"] = json!({"id":"sentrux.shim.compat","version":"1.0.0","digest":IMPL}); + native["rollbackIdentity"] = json!("Invoke-SentruxAgentTool.ps1"); + native["sourceRevision"] = json!("revision-b03"); + native["expectedSnapshotIdentity"] = json!(CURRENT); + native["sourceSnapshotIdentity"] = json!(CURRENT); + native["collectedAt"] = json!(1940); + native["observedAt"] = json!(1950); + native["declaredEffects"] = json!(["repo_read", "local_write", "process_spawn"]); + native["observedEffects"] = native["declaredEffects"].clone(); + native["payload"] = json!({"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-evidence-payload.v1","type":"observed.evidence.payload","path":"payload.json","sha256":CURRENT,"consumedSnapshotIdentity":CURRENT}); + let first = sentrux_adapter::translate(&native, 2000, 100).unwrap(); + let payload = json!({"schema":"code-intel-evidence-payload.v1","data":{"structuralEvidence":{ + "schema":"code-intel-structural-evidence-payload.v1", + "snapshotIdentity":CURRENT, + "provider":first["port"]["provider"], + "provenance":first["port"]["provenance"], + "effects":first["port"]["effects"], + "completeness":first["port"]["completeness"], + "rules":first["port"]["rules"] + }}}); + let bytes = serde_json::to_vec(&payload).unwrap(); + fs::write(root.join("payload.json"), &bytes).unwrap(); + native["payload"]["sha256"] = json!(capability::sha256_hex(&bytes)); + native +} + +fn route(root: &Path, native: &Value) -> (i32, Value, String) { + let request = root.join("native.json"); + fs::write(&request, serde_json::to_vec(native).unwrap()).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "provider", + "sentrux-adapt", + "--request", + request.to_str().unwrap(), + "--artifact-root", + root.to_str().unwrap(), + "--evaluated-at", + "2000", + "--max-age-seconds", + "100", + ]) + .output() + .unwrap(); + let value = serde_json::from_slice(&output.stdout).unwrap(); + ( + output.status.code().unwrap(), + value, + String::from_utf8_lossy(&output.stderr).into_owned(), + ) +} + +#[test] +fn complete_normalizes_every_authoritative_kind_and_passes_a04() { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor("complete")); + let adapter = sentrux_adapter::translate(&native, 2000, 100).unwrap(); + assert_eq!(adapter["port"]["completeness"], "complete"); + assert_eq!( + adapter["port"]["rules"].as_array().unwrap().len(), + sentrux_adapter::AUTHORITATIVE_RULE_KINDS.len() + ); + for kind in sentrux_adapter::AUTHORITATIVE_RULE_KINDS { + assert!(adapter["port"]["rules"] + .as_array() + .unwrap() + .iter() + .any(|r| r["kind"] == kind)); + } + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + assert_eq!(admitted.result()["domainVerdict"], "observed"); + assert_eq!(admitted.result()["engineeringFacts"], json!([])); + sentrux_adapter::validate_admitted_payload(admitted.payload(), &adapter).unwrap(); +} + +#[test] +fn partial_unknown_and_crashed_never_become_diagnosis() { + for name in ["partial", "unknown", "crashed"] { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor(name)); + let adapter = sentrux_adapter::translate(&native, 2000, 100).unwrap(); + assert_eq!(adapter["port"]["completeness"], "partial", "{name}"); + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + assert_eq!(admitted.result()["domainVerdict"], "unknown", "{name}"); + let (code, result, _) = route(&root.0, &native); + assert_eq!(code, 0, "{name}"); + assert_eq!( + result["adapter"]["port"]["diagnosisEligible"], false, + "{name}" + ); + assert_eq!(result["engineeringFacts"], json!([]), "{name}"); + } +} + +#[test] +fn unknown_kind_is_fail_closed_even_when_provider_labels_it_pass() { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor("unknown")); + let adapter = sentrux_adapter::translate(&native, 2000, 100).unwrap(); + let rule = &adapter["port"]["rules"][0]; + assert_eq!(rule["status"], "unsupported"); + assert_eq!(rule["verdict"], "unknown"); + assert_eq!(rule["failure"]["kind"], "domain_unknown"); +} + +#[test] +fn effect_mismatch_and_inconsistent_rule_are_rejected() { + let root = Temp::new(); + let mut native = build_case(&root.0, &descriptor("complete")); + native["observedEffects"] = json!(["repo_read"]); + assert!(sentrux_adapter::translate(&native, 2000, 100) + .unwrap_err() + .contains("effects do not match")); + native["observedEffects"] = native["declaredEffects"].clone(); + native["authoritativeRules"][0]["failure"] = + json!({"kind":"domain_unknown","message":"bad relabel"}); + assert!(sentrux_adapter::translate(&native, 2000, 100) + .unwrap_err() + .contains("inconsistent")); +} + +#[test] +fn complete_missing_known_kind_is_downgraded_and_payload_relabel_is_rejected() { + let root = Temp::new(); + let mut fixture = descriptor("complete"); + fixture["authoritativeRules"].as_array_mut().unwrap().pop(); + let native = build_case(&root.0, &fixture); + let adapter = sentrux_adapter::translate(&native, 2000, 100).unwrap(); + assert_eq!(adapter["port"]["completeness"], "partial"); + let admitted = + admissibility::validate_for_consumer(&adapter["evidence"]["request"], &root.0).unwrap(); + assert_eq!(admitted.result()["domainVerdict"], "unknown"); + let mut payload = admitted.payload().clone(); + payload["data"]["structuralEvidence"]["completeness"] = json!("complete"); + assert!(sentrux_adapter::validate_admitted_payload(&payload, &adapter).is_err()); +} + +#[test] +fn complete_label_with_known_not_evaluated_rule_is_downgraded_before_diagnosis() { + let root = Temp::new(); + let mut fixture = descriptor("complete"); + fixture["authoritativeRules"][0]["status"] = json!("not_evaluated"); + fixture["authoritativeRules"][0]["verdict"] = json!("unknown"); + fixture["authoritativeRules"][0]["failure"] = + json!({"kind":"domain_unknown","message":"provider did not evaluate this rule"}); + let native = build_case(&root.0, &fixture); + let adapter = sentrux_adapter::translate(&native, 2000, 100).unwrap(); + assert_eq!(adapter["port"]["completeness"], "partial"); + assert_eq!( + adapter["evidence"]["request"]["observation"]["claimedComplete"], + false + ); + let (code, result, stderr) = route(&root.0, &native); + assert_eq!(code, 0, "{stderr}"); + assert_eq!(result["admission"]["domainVerdict"], "unknown"); + assert_eq!(result["adapter"]["port"]["diagnosisEligible"], false); +} + +#[test] +fn public_route_complete_is_eligible_but_never_emits_facts() { + let root = Temp::new(); + let native = build_case(&root.0, &descriptor("complete")); + let (code, result, stderr) = route(&root.0, &native); + assert_eq!(code, 0, "{stderr}"); + assert_eq!(result["schema"], "code-intel-sentrux-route-result.v1"); + assert_eq!(result["adapter"]["port"]["diagnosisEligible"], true); + assert_eq!(result["engineeringFacts"], json!([])); + assert_eq!( + result["admission"]["schema"], + "code-intel-evidence-admissibility-result.v1" + ); +} + +#[test] +fn secret_shaped_extra_input_is_rejected_without_echo() { + let root = Temp::new(); + let mut native = build_case(&root.0, &descriptor("complete")); + native["apiToken"] = json!("SENTINEL_DO_NOT_ECHO"); + let (code, result, stderr) = route(&root.0, &native); + assert_eq!(code, 65); + let rendered = format!("{result}{stderr}"); + assert!(!rendered.contains("SENTINEL_DO_NOT_ECHO")); +} diff --git a/crates/code-intel-cli/tests/sentrux_analysis.rs b/crates/code-intel-cli/tests/sentrux_analysis.rs index 11a9f96..26e7cda 100644 --- a/crates/code-intel-cli/tests/sentrux_analysis.rs +++ b/crates/code-intel-cli/tests/sentrux_analysis.rs @@ -80,6 +80,8 @@ fn dsm_snapshot_preserves_contract_and_excludes_non_governed_source() { "function Invoke-Ignored { if ($true) { 1 } }\n", ); fixture.write("target/ignored.rs", "fn ignored() {}\n"); + fixture.write(".gitignore", "work/\n"); + fixture.write("work/generated.rs", "fn generated() {}\n"); fixture.write("README.md", "not source\n"); let snapshot = sentrux_analysis::analyze(&fixture.root).expect("native DSM analysis"); @@ -88,7 +90,12 @@ fn dsm_snapshot_preserves_contract_and_excludes_non_governed_source() { assert_eq!(snapshot["default_color_mode"], "Risk"); assert_eq!(snapshot["color_modes"].as_array().unwrap().len(), 9); assert_eq!(snapshot["scope"]["included_files"], 2); - assert_eq!(snapshot["scope"]["excluded_files"], 2); + assert_eq!(snapshot["scope"]["excluded_files"], 3); + assert!(snapshot["scope"]["excluded_by_reason"] + .as_array() + .unwrap() + .iter() + .any(|entry| entry["reason"] == "repository_ignored" && entry["files"] == 1)); assert_eq!(snapshot["file_details"].as_array().unwrap().len(), 2); assert!(snapshot.get("modules").is_some()); assert!(snapshot.get("edges").is_some()); diff --git a/crates/code-intel-cli/tests/session_evidence.rs b/crates/code-intel-cli/tests/session_evidence.rs new file mode 100644 index 0000000..cef88d9 --- /dev/null +++ b/crates/code-intel-cli/tests/session_evidence.rs @@ -0,0 +1,228 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +static SEQ: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-session-evidence-{}-{nonce}-{}", + std::process::id(), + SEQ.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn fixture(root: &Path) -> (PathBuf, PathBuf, PathBuf) { + let repo = root.join("repo"); + fs::create_dir_all(repo.join("src")).unwrap(); + fs::write(repo.join("src/hot.rs"), "fn hot() {}\n").unwrap(); + let trace = root.join("trace.json"); + let trace_value = json!({ + "version":1, + "session":{ + "id":"private-session-id", + "harness":"Codex Desktop private-label", + "cwd":repo, + "eventCount":3, + "title":"SENTINEL_PRIVATE_TITLE", + "path":"C:/Users/private/session.jsonl" + }, + "events":[ + { + "seq":0, + "tool":"exec_command", + "action":"verify", + "targets":[], + "outside":[], + "resultBytes":10, + "isError":false, + "summary":"SENTINEL_PRIVATE_VERIFY_COMMAND" + }, + { + "seq":1, + "tool":"apply_patch", + "action":"edit", + "targets":[ + {"path":"src\\hot.rs","touch":"edit"}, + {"path":"..\\outside.txt","touch":"read"} + ], + "outside":[{"scope":"home","path":"C:/Users/private/secret.txt"}], + "resultBytes":20, + "isError":false, + "summary":"SENTINEL_PRIVATE_EDIT_COMMAND" + }, + { + "seq":2, + "tool":"wait_agent", + "action":"other", + "targets":[], + "outside":[], + "resultBytes":0, + "isError":true, + "summary":"SENTINEL_PRIVATE_ERROR" + } + ], + "marks":[{"seq":1,"type":"user-message","note":"SENTINEL_PRIVATE_PROMPT"}], + "stats":{ + "edited":1, + "observability":{"reads":"estimated","errors":"exact"} + } + }); + fs::write(&trace, serde_json::to_vec(&trace_value).unwrap()).unwrap(); + let hotspots = root.join("hotspots.json"); + fs::write( + &hotspots, + serde_json::to_vec(&json!({ + "files":[{ + "path":"src/hot.rs", + "maxComplexity":24, + "avgComplexity":8.0, + "loc":40, + "git":{"churn":7,"dirty":true} + }] + })) + .unwrap(), + ) + .unwrap(); + (repo, trace, hotspots) +} + +fn run( + repo: &Path, + trace: &Path, + hotspots: Option<&Path>, + out: Option<&Path>, +) -> std::process::Output { + let mut command = Command::new(env!("CARGO_BIN_EXE_code-intel")); + command.args([ + "provider", + "session-adapt", + "--repo", + repo.to_str().unwrap(), + "--trace", + trace.to_str().unwrap(), + ]); + if let Some(hotspots) = hotspots { + command.args(["--hotspots", hotspots.to_str().unwrap()]); + } + if let Some(out) = out { + command.args(["--out", out.to_str().unwrap()]); + } + command.output().unwrap() +} + +#[test] +fn normalizes_private_trace_and_joins_structural_evidence() { + let root = Temp::new(); + let (repo, trace, hotspots) = fixture(&root.0); + let output = run(&repo, &trace, Some(&hotspots), None); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let artifact: Value = serde_json::from_slice(&output.stdout).unwrap(); + + assert_eq!(artifact["schema"], "code-intel-session-evidence.v1"); + assert_eq!(artifact["status"], "partial"); + assert_eq!(artifact["reviewAuthority"], "advisory_only"); + assert_eq!(artifact["source"]["harness"], "codex"); + assert_eq!(artifact["summary"]["matchedTargets"], 1); + assert_eq!(artifact["summary"]["unsafeOrOutsideTargets"], 2); + assert_eq!(artifact["events"][1]["targets"][0]["path"], "src/hot.rs"); + assert_eq!( + artifact["events"][1]["targets"][0]["structural"]["maxComplexity"], + 24 + ); + assert!(artifact["signals"] + .as_array() + .unwrap() + .iter() + .any(|signal| { signal["kind"] == "unverified_structural_attention_edit" })); + + let rendered = serde_json::to_string(&artifact).unwrap(); + for private in [ + "private-session-id", + "SENTINEL_PRIVATE_TITLE", + "SENTINEL_PRIVATE_VERIFY_COMMAND", + "SENTINEL_PRIVATE_EDIT_COMMAND", + "SENTINEL_PRIVATE_ERROR", + "SENTINEL_PRIVATE_PROMPT", + "private-label", + "C:/Users/private", + ] { + assert!(!rendered.contains(private), "leaked {private}"); + } + assert_eq!(artifact["privacy"]["userMessageMarksConsumed"], false); + assert_eq!(artifact["privacy"]["eventSummariesConsumed"], false); + assert_eq!(artifact["privacy"]["absolutePathsEmitted"], false); +} + +#[test] +fn optional_enrichment_stays_unknown_and_output_is_non_overwriting() { + let root = Temp::new(); + let (repo, trace, _) = fixture(&root.0); + let out = root.0.join("session-evidence.json"); + let first = run(&repo, &trace, None, Some(&out)); + assert_eq!( + first.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&first.stderr) + ); + let artifact: Value = serde_json::from_slice(&fs::read(&out).unwrap()).unwrap(); + assert_eq!(artifact["summary"]["matchedTargets"], 0); + assert_eq!( + artifact["events"][1]["targets"][0]["structural"]["status"], + "unknown" + ); + + let second = run(&repo, &trace, None, Some(&out)); + assert_eq!(second.status.code(), Some(64)); + assert!(String::from_utf8_lossy(&second.stderr).contains("output already exists")); +} +#[test] +fn unsupported_trace_is_rejected_without_echoing_provider_content() { + let root = Temp::new(); + let repo = root.0.join("repo"); + fs::create_dir(&repo).unwrap(); + let trace = root.0.join("bad.json"); + fs::write( + &trace, + serde_json::to_vec(&json!({ + "version":2, + "secret":"SENTINEL_DO_NOT_ECHO" + })) + .unwrap(), + ) + .unwrap(); + let output = run(&repo, &trace, None, None); + assert_eq!(output.status.code(), Some(65)); + let rendered = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!(!rendered.contains("SENTINEL_DO_NOT_ECHO")); +} diff --git a/crates/code-intel-cli/tests/snapshot_identity.rs b/crates/code-intel-cli/tests/snapshot_identity.rs new file mode 100644 index 0000000..7648b60 --- /dev/null +++ b/crates/code-intel-cli/tests/snapshot_identity.rs @@ -0,0 +1,517 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::Value; + +struct TempTree(PathBuf); + +impl TempTree { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!("code-intel-{label}-{nonce}")); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for TempTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn git(repo: &Path, args: &[&str]) { + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output() + .unwrap(); + assert!( + output.status.success(), + "git {args:?}: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +fn init_repo(repo: &Path) { + git(repo, &["init", "--quiet"]); + git(repo, &["config", "user.name", "Snapshot Test"]); + git(repo, &["config", "user.email", "snapshot@example.invalid"]); + git(repo, &["config", "core.autocrlf", "false"]); + fs::create_dir_all(repo.join("src/子 目录")).unwrap(); + fs::create_dir_all(repo.join("docs")).unwrap(); + fs::write(repo.join("src/子 目录/main file.txt"), "alpha\n").unwrap(); + fs::write(repo.join("docs/readme.md"), "docs\n").unwrap(); + git(repo, &["add", "."]); + git(repo, &["commit", "--quiet", "-m", "fixture"]); +} + +fn snapshot(repo: &Path, policy: &str, scopes: &[&str]) -> Output { + let mut command = Command::new(env!("CARGO_BIN_EXE_code-intel")); + command + .arg("snapshot") + .arg("identity") + .arg("--repo") + .arg(repo) + .arg("--working-tree-policy") + .arg(policy); + for scope in scopes { + command.arg("--scope").arg(scope); + } + command.output().unwrap() +} + +fn ok_json(output: Output) -> Value { + assert!( + output.status.success(), + "snapshot failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output.stderr.is_empty()); + serde_json::from_slice(&output.stdout).unwrap() +} + +#[test] +fn identity_is_portable_scope_bound_and_dirty_overlay_explicit() { + let fixture = TempTree::new("snapshot fixture 空格"); + let source = fixture.0.join("源 repo"); + let copy = fixture.0.join("copy repo"); + fs::create_dir_all(&source).unwrap(); + init_repo(&source); + git( + &fixture.0, + &[ + "clone", + "--quiet", + "--no-hardlinks", + "-c", + "core.autocrlf=false", + source.to_str().unwrap(), + copy.to_str().unwrap(), + ], + ); + + let left = ok_json(snapshot(&source, "explicit_overlay", &["src/子 目录"])); + let right = ok_json(snapshot(©, "explicit_overlay", &["src/子 目录"])); + assert_eq!(left["snapshot"], right["snapshot"]); + assert_eq!(left["dirtyOverlay"]["present"], false); + assert_eq!( + left["snapshot"]["scope"], + serde_json::json!(["src/子 目录"]) + ); + let redundant = ok_json(snapshot( + &source, + "explicit_overlay", + &["src/子 目录", "src/子 目录/nested"], + )); + assert_eq!(left["snapshot"], redundant["snapshot"]); + let root_minimal = ok_json(snapshot(&source, "explicit_overlay", &[".", "src"])); + let root_only = ok_json(snapshot(&source, "explicit_overlay", &["."])); + assert_eq!(root_minimal["snapshot"], root_only["snapshot"]); + + let clean_head = ok_json(snapshot(©, "head_only", &["src/子 目录"])); + fs::write(copy.join("src/子 目录/main file.txt"), "bravo\n").unwrap(); + let dirty = ok_json(snapshot(©, "explicit_overlay", &["src/子 目录"])); + assert_ne!(left["snapshot"]["identity"], dirty["snapshot"]["identity"]); + assert_ne!( + left["snapshot"]["inputDigest"], + dirty["snapshot"]["inputDigest"] + ); + assert_eq!(dirty["dirtyOverlay"]["present"], true); + assert!(dirty["dirtyOverlay"]["paths"] + .as_array() + .unwrap() + .iter() + .any(|path| path == "src/子 目录/main file.txt")); + + let head_only = ok_json(snapshot(©, "head_only", &["src/子 目录"])); + assert_eq!( + clean_head["snapshot"]["identity"], + head_only["snapshot"]["identity"] + ); + assert_ne!( + left["snapshot"]["identity"], + head_only["snapshot"]["identity"] + ); + assert_eq!( + clean_head["snapshot"]["inputDigest"], + head_only["snapshot"]["inputDigest"] + ); + assert_eq!(head_only["dirtyOverlay"]["present"], false); + + let docs = ok_json(snapshot(©, "explicit_overlay", &["docs"])); + assert_ne!(dirty["snapshot"]["identity"], docs["snapshot"]["identity"]); + let docs_before = docs["snapshot"]["identity"].clone(); + fs::write(copy.join("src/子 目录/main file.txt"), "charlie\n").unwrap(); + let docs_after = ok_json(snapshot(©, "explicit_overlay", &["docs"])); + assert_eq!(docs_before, docs_after["snapshot"]["identity"]); +} + +#[test] +fn missing_git_is_content_addressed_and_rejects_head_only() { + let fixture = TempTree::new("snapshot unversioned"); + fs::create_dir_all(fixture.0.join("scope 空格")).unwrap(); + fs::write(fixture.0.join("scope 空格/file.txt"), "one\n").unwrap(); + + let first = ok_json(snapshot(&fixture.0, "explicit_overlay", &["scope 空格"])); + assert_eq!(first["snapshot"]["head"], "unversioned"); + assert!(first["snapshot"]["repoIdentity"] + .as_str() + .unwrap() + .starts_with("content-v1:")); + assert_eq!(first["dirtyOverlay"]["present"], true); + + fs::write(fixture.0.join("scope 空格/file.txt"), "two\n").unwrap(); + let second = ok_json(snapshot(&fixture.0, "explicit_overlay", &["scope 空格"])); + assert_ne!( + first["snapshot"]["identity"], + second["snapshot"]["identity"] + ); + + let rejected = snapshot(&fixture.0, "head_only", &["."]); + assert_eq!(rejected.status.code(), Some(69)); + assert!(rejected.stdout.is_empty()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("requires Git")); +} + +#[test] +fn alternate_vcs_contract_fixture_is_fail_closed_and_rolls_back_to_unversioned() { + let fixture_contract: Value = serde_json::from_slice( + &fs::read( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("orchestration/internalization/fixtures/r10-alternate-vcs-port.json"), + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(fixture_contract["mismatch"]["exitCode"], 65); + assert_eq!(fixture_contract["mismatch"]["publishesArtifacts"], false); + assert_eq!( + fixture_contract["rollback"]["routes"], + serde_json::json!(["git", "unversioned-explicit-overlay"]) + ); + + let tree = TempTree::new("alternate-vcs-rollback"); + fs::write(tree.0.join("source.txt"), "rollback content\n").unwrap(); + let adapter = tree.0.join("mismatched-adapter.ps1"); + fs::write( + &adapter, + "$request = [Console]::In.ReadToEnd() | ConvertFrom-Json\nif ($request.schema -ne 'code-intel-alternate-vcs-snapshot-request.v1') { exit 9 }\n'{\"snapshot\":{\"identity\":\"mismatch\"}}'\n", + ) + .unwrap(); + let rejected = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .arg("snapshot") + .arg("identity") + .arg("--repo") + .arg(&tree.0) + .arg("--working-tree-policy") + .arg("explicit_overlay") + .arg("--scope") + .arg(".") + .arg("--alternate-vcs-command") + .arg("pwsh") + .arg("--alternate-vcs-arg") + .arg("-NoProfile") + .arg("--alternate-vcs-arg") + .arg("-File") + .arg("--alternate-vcs-arg") + .arg(&adapter) + .output() + .unwrap(); + assert_eq!(rejected.status.code(), Some(65)); + assert!( + rejected.stdout.is_empty(), + "mismatch must publish no artifact" + ); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("does not match")); + + let rolled_back = ok_json(snapshot(&tree.0, "explicit_overlay", &["."])); + assert_eq!(rolled_back["snapshot"]["head"], "unversioned"); + assert_eq!( + rolled_back["snapshot"]["workingTreePolicy"], + "explicit_overlay" + ); + assert!(rolled_back["snapshot"]["identity"] + .as_str() + .is_some_and(|identity| identity.len() == 64)); +} + +#[test] +fn nonexistent_nonroot_scope_fails_closed_while_empty_root_scope_is_legal() { + let fixture = TempTree::new("snapshot scope existence"); + let typo = snapshot(&fixture.0, "explicit_overlay", &["typo/missing"]); + assert_eq!(typo.status.code(), Some(64)); + assert!(typo.stdout.is_empty()); + assert!(String::from_utf8_lossy(&typo.stderr).contains("does not exist")); + + let root = ok_json(snapshot(&fixture.0, "explicit_overlay", &["."])); + assert_eq!(root["snapshot"]["scope"], serde_json::json!(["."])); + assert_eq!(root["snapshot"]["head"], "unversioned"); +} + +#[test] +fn head_identity_ignores_checkout_state_time_and_attachment_but_binds_scope_and_mode() { + let fixture = TempTree::new("snapshot head rules"); + let repo = fixture.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + init_repo(&repo); + let attached = ok_json(snapshot(&repo, "head_only", &["src", "docs/."])); + let head = attached["snapshot"]["head"].as_str().unwrap().to_string(); + git(&repo, &["checkout", "--quiet", "--detach", &head]); + let detached = ok_json(snapshot(&repo, "head_only", &["docs", "src/."])); + assert_eq!(attached["snapshot"], detached["snapshot"]); + + fs::write(repo.join("src/子 目录/main file.txt"), b"binary\0\r\n").unwrap(); + let after_bytes = ok_json(snapshot(&repo, "head_only", &["src", "docs"])); + assert_eq!(attached["snapshot"], after_bytes["snapshot"]); + let metadata = fs::metadata(repo.join("docs/readme.md")).unwrap(); + let file = fs::OpenOptions::new() + .write(true) + .open(repo.join("docs/readme.md")) + .unwrap(); + file.set_times(std::fs::FileTimes::new().set_modified(metadata.modified().unwrap())) + .unwrap(); + let after_time = ok_json(snapshot(&repo, "head_only", &["src", "docs"])); + assert_eq!(attached["snapshot"], after_time["snapshot"]); + + let sub_scope = ok_json(snapshot(&repo, "head_only", &["src"])); + assert_ne!( + attached["snapshot"]["identity"], + sub_scope["snapshot"]["identity"] + ); + let escaped = snapshot(&repo, "head_only", &["../src"]); + assert_eq!(escaped.status.code(), Some(64)); + + git(&repo, &["update-index", "--chmod=+x", "docs/readme.md"]); + git(&repo, &["commit", "--quiet", "-m", "mode"]); + let mode_changed = ok_json(snapshot(&repo, "head_only", &["src", "docs"])); + assert_ne!( + attached["snapshot"]["identity"], + mode_changed["snapshot"]["identity"] + ); +} + +#[test] +fn overlay_classifies_delete_untracked_and_ignored_without_following_ignored_input() { + let fixture = TempTree::new("snapshot overlay rules"); + let repo = fixture.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + init_repo(&repo); + fs::write(repo.join(".gitignore"), "ignored.bin\nscratch/\n").unwrap(); + git(&repo, &["add", ".gitignore"]); + git(&repo, &["commit", "--quiet", "-m", "ignore"]); + let clean = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + + fs::remove_file(repo.join("docs/readme.md")).unwrap(); + fs::write(repo.join("new-data.txt"), [0, 1, 2, 255]).unwrap(); + fs::write(repo.join("ignored.bin"), "ignored-one").unwrap(); + let dirty = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + assert_ne!(clean["snapshot"]["identity"], dirty["snapshot"]["identity"]); + assert!(dirty["dirtyOverlay"]["members"]["trackedDeleted"] + .as_array() + .unwrap() + .iter() + .any(|path| path == "docs/readme.md")); + assert!( + dirty["dirtyOverlay"]["members"]["untracked"] + .as_array() + .unwrap() + .iter() + .any(|path| path == "new-data.txt"), + "{}", + dirty + ); + let before_ignored = dirty["snapshot"]["identity"].clone(); + fs::write(repo.join("ignored.bin"), "ignored-two").unwrap(); + let ignored_changed = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + assert_eq!(before_ignored, ignored_changed["snapshot"]["identity"]); + fs::create_dir_all(repo.join("scratch/nested")).unwrap(); + fs::write(repo.join("scratch/nested/.gitignore"), "*.bin\n").unwrap(); + fs::write( + repo.join("scratch/nested/generated.bin"), + vec![7_u8; 1024 * 1024], + ) + .unwrap(); + let ignored_tree_changed = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + assert_eq!( + ignored_changed["snapshot"]["identity"], ignored_tree_changed["snapshot"]["identity"], + "an ignored scratch subtree, including nested controls, must not become snapshot input" + ); + assert_eq!( + ignored_tree_changed["dirtyOverlay"]["ignoredPolicy"], + "excluded_by_git_ignore" + ); +} + +#[test] +fn intent_to_add_is_an_explicit_overlay_member() { + let fixture = TempTree::new("snapshot intent to add"); + let repo = fixture.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + init_repo(&repo); + fs::write(repo.join("intent.txt"), "intent").unwrap(); + git(&repo, &["add", "-N", "intent.txt"]); + let snapshot = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + assert_eq!(snapshot["dirtyOverlay"]["present"], true); + assert!(snapshot["dirtyOverlay"]["members"]["trackedModified"] + .as_array() + .unwrap() + .iter() + .any(|path| path == "intent.txt")); +} + +#[cfg(windows)] +#[test] +fn windows_scope_case_collision_fails_closed() { + let fixture = TempTree::new("snapshot scope collision"); + let repo = fixture.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + init_repo(&repo); + let output = snapshot(&repo, "head_only", &["src", "SRC"]); + assert_eq!(output.status.code(), Some(64)); + assert!(String::from_utf8_lossy(&output.stderr).contains("case collision")); + let overlapping = snapshot(&repo, "head_only", &["src", "SRC/nested"]); + assert_eq!(overlapping.status.code(), Some(64)); +} + +#[test] +fn unborn_git_is_explicit_content_snapshot_and_head_only_is_unavailable() { + let fixture = TempTree::new("snapshot unborn"); + git(&fixture.0, &["init", "--quiet"]); + fs::write(fixture.0.join("file.txt"), "unborn").unwrap(); + let explicit = ok_json(snapshot(&fixture.0, "explicit_overlay", &["."])); + assert_eq!(explicit["snapshot"]["head"], "unborn"); + assert_eq!(explicit["repository"]["kind"], "git_unborn"); + assert!(explicit["snapshot"]["repoIdentity"] + .as_str() + .unwrap() + .starts_with("content-v1:")); + let rejected = snapshot(&fixture.0, "head_only", &["."]); + assert_eq!(rejected.status.code(), Some(69)); +} + +#[test] +fn head_snapshot_binds_gitlink_lfs_pointer_and_case_sensitive_scope() { + let fixture = TempTree::new("snapshot special entries"); + let repo = fixture.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + init_repo(&repo); + let commit = String::from_utf8( + Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(&repo) + .output() + .unwrap() + .stdout, + ) + .unwrap(); + git( + &repo, + &[ + "update-index", + "--add", + "--cacheinfo", + &format!("160000,{},vendor/sub", commit.trim()), + ], + ); + fs::write( + repo.join("asset.lfs"), + "version https://git-lfs.github.com/spec/v1\noid sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\nsize 1\n", + ) + .unwrap(); + git(&repo, &["add", "asset.lfs"]); + git(&repo, &["commit", "--quiet", "-m", "special entries"]); + let first = ok_json(snapshot(&repo, "head_only", &["."])); + + fs::write( + repo.join("asset.lfs"), + "version https://git-lfs.github.com/spec/v1\noid sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\nsize 1\n", + ) + .unwrap(); + git(&repo, &["add", "asset.lfs"]); + git(&repo, &["commit", "--quiet", "-m", "new pointer"]); + let second = ok_json(snapshot(&repo, "head_only", &["."])); + assert_ne!( + first["snapshot"]["identity"], + second["snapshot"]["identity"] + ); + + let lower = ok_json(snapshot(&repo, "head_only", &["src"])); + let upper = ok_json(snapshot(&repo, "head_only", &["SRC"])); + assert_ne!(lower["snapshot"]["identity"], upper["snapshot"]["identity"]); +} + +#[test] +fn shallow_lineage_is_rejected_and_symlink_target_is_hashed_without_following() { + let fixture = TempTree::new("snapshot shallow symlink"); + let repo = fixture.0.join("repo"); + fs::create_dir_all(&repo).unwrap(); + init_repo(&repo); + fs::write(repo.join("docs/readme.md"), "second").unwrap(); + git(&repo, &["add", "."]); + git(&repo, &["commit", "--quiet", "-m", "second"]); + let shallow = fixture.0.join("shallow"); + let source_url = format!("file:///{}", repo.to_string_lossy().replace('\\', "/")); + git( + &fixture.0, + &[ + "clone", + "--quiet", + "--depth", + "1", + &source_url, + shallow.to_str().unwrap(), + ], + ); + let rejected = snapshot(&shallow, "head_only", &["."]); + assert_eq!(rejected.status.code(), Some(69)); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("shallow")); + + let outside = fixture.0.join("outside.txt"); + fs::write(&outside, "outside-one").unwrap(); + let link = repo.join("outside-link"); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_file(&outside, &link).is_ok(); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(&outside, &link).is_ok(); + #[cfg(not(any(windows, unix)))] + let linked = false; + if linked { + let before = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + fs::write(&outside, "outside-two").unwrap(); + let after_external = ok_json(snapshot(&repo, "explicit_overlay", &["."])); + assert_eq!( + before["snapshot"]["identity"], after_external["snapshot"]["identity"], + "symlink target bytes outside the repository must not be followed" + ); + } +} + +#[test] +fn missing_git_executable_is_unavailable_not_unversioned() { + let fixture = TempTree::new("snapshot missing git"); + fs::write(fixture.0.join("file.txt"), "content").unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "snapshot", + "identity", + "--repo", + fixture.0.to_str().unwrap(), + "--working-tree-policy", + "explicit_overlay", + "--scope", + ".", + ]) + .env("PATH", "") + .output() + .unwrap(); + assert_eq!(output.status.code(), Some(69)); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("cannot launch Git")); +} diff --git a/crates/code-intel-cli/tests/staged_artifact.rs b/crates/code-intel-cli/tests/staged_artifact.rs new file mode 100644 index 0000000..7eda657 --- /dev/null +++ b/crates/code-intel-cli/tests/staged_artifact.rs @@ -0,0 +1,380 @@ +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; + +#[path = "../src/staged_artifact.rs"] +mod staged_artifact; + +use std::fs; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::Value; +use staged_artifact::{ + ArtifactWriteContract, InterruptAfter, StageWriteError, StagedWriter, WriterOptions, +}; + +static SEQUENCE: AtomicU64 = AtomicU64::new(0); + +struct TempTree(PathBuf); + +impl TempTree { + fn new(label: &str) -> Self { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "code-intel-a06-{label}-{}-{nonce}-{sequence}", + std::process::id() + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for TempTree { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn contract(max_bytes: u64) -> ArtifactWriteContract { + ArtifactWriteContract { + artifact_schema: "code-intel-test-lines.v1", + artifact_type: "test.lines", + max_bytes, + validate_payload: |bytes| { + let text = std::str::from_utf8(bytes).map_err(|error| error.to_string())?; + if text.lines().all(|line| !line.is_empty()) && text.ends_with('\n') { + Ok(()) + } else { + Err("payload must contain non-empty newline-terminated lines".to_string()) + } + }, + } +} + +fn begin(root: &Path, nonce: &str) -> Result { + StagedWriter::begin_with_options( + root, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + WriterOptions { + nonce: nonce.to_string(), + interrupt_after: None, + before_publish: None, + }, + ) +} + +fn staging_children(root: &Path) -> Vec { + let staging = root.join(".staging"); + if !staging.is_dir() { + return Vec::new(); + } + fs::read_dir(staging) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect() +} + +#[test] +fn schema_failure_rolls_back_owned_staging_and_never_exposes_a_final_run() { + let tree = TempTree::new("schema-failure"); + let mut writer = begin(&tree.0, "schema-failure").unwrap(); + let error = writer.stage(b"invalid", contract(64)).unwrap_err(); + assert!(matches!(error, StageWriteError::Contract(_))); + assert!(staging_children(&tree.0).is_empty()); + assert!(writer.seal().is_err()); + assert!(!tree.0.join("run-complete.json").exists()); + assert!(fs::read_dir(&tree.0) + .unwrap() + .all(|entry| entry.unwrap().file_name() == ".staging")); +} + +#[test] +fn content_is_addressed_validated_and_duplicate_bytes_reuse_one_owned_object() { + let tree = TempTree::new("content-addressed"); + let mut writer = begin(&tree.0, "content-addressed").unwrap(); + let first = writer.stage(b"alpha\n", contract(64)).unwrap(); + let second = writer.stage(b"alpha\n", contract(64)).unwrap(); + + assert_eq!(first.sha256, second.sha256); + assert_eq!( + first.sha256, + "b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060" + ); + assert_eq!(first.path, second.path); + assert_eq!(first.size, 6); + assert_eq!(first.artifact_schema, "code-intel-test-lines.v1"); + assert_eq!(first.artifact_type, "test.lines"); + assert_eq!(first.consumed_snapshot_identity.len(), 64); + + let staged = writer.seal().unwrap(); + assert!(staged.path().starts_with(tree.0.join(".staging"))); + assert_eq!(staged.authority_root(), tree.0); + assert_eq!(staged.artifacts().len(), 2); + let manifest = staged.to_manifest_value(); + assert_eq!(manifest["schema"], "code-intel-staged-artifact-set.v1"); + assert_eq!(manifest["artifacts"].as_array().unwrap().len(), 2); + assert_eq!(manifest["artifacts"][0]["sha256"], first.sha256); + assert_eq!( + manifest["artifacts"][0]["path"], + format!("objects/sha256/{}", first.sha256) + ); + let objects = fs::read_dir(staged.path().join("objects/sha256")) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(objects.len(), 1); + assert!(!tree.0.join("run-complete.json").exists()); + + drop(staged); + assert!(staging_children(&tree.0).is_empty()); +} + +#[cfg(windows)] +#[test] +fn content_addressed_publication_supports_windows_paths_beyond_max_path() { + let tree = TempTree::new("windows-long-path"); + let mut root = tree.0.clone(); + for index in 0..3 { + root.push(format!( + "nested-authority-{index}-abcdefghijklmnopqrstuvwxyz0123456789" + )); + } + fs::create_dir_all(&root).unwrap(); + + let expected_target = root.join(format!( + ".staging/stage-long-path/objects/sha256/{}", + "b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060" + )); + assert!( + expected_target.as_os_str().encode_wide().count() > 260, + "test fixture must cross the legacy Windows MAX_PATH boundary" + ); + + let mut writer = begin(&root, "long-path").unwrap(); + let artifact = writer.stage(b"alpha\n", contract(64)).unwrap(); + assert_eq!( + fs::read(root.join(".staging/stage-long-path").join(artifact.path)).unwrap(), + b"alpha\n" + ); +} + +#[test] +fn production_begin_is_unique_and_reports_only_its_observed_local_write_effect() { + let tree = TempTree::new("production-begin"); + let first = StagedWriter::begin( + &tree.0, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .unwrap(); + let second = StagedWriter::begin( + &tree.0, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) + .unwrap(); + assert_eq!(first.observed_effects(), &["local_write"]); + assert_ne!(staging_children(&tree.0)[0], staging_children(&tree.0)[1]); + drop((first, second)); + assert!(staging_children(&tree.0).is_empty()); +} + +#[test] +fn commit_handoff_releases_handles_but_retains_owned_rollback() { + let tree = TempTree::new("handoff"); + let mut writer = begin(&tree.0, "handoff").unwrap(); + writer.stage(b"alpha\n", contract(64)).unwrap(); + let mut staged = writer.seal().unwrap(); + let path = staged.path().to_path_buf(); + staged.prepare_for_commit().unwrap(); + assert!(path.is_dir()); + drop(staged); + assert!(!path.exists()); +} + +#[test] +fn bounded_writes_accept_max_minus_one_and_max_but_reject_max_plus_one() { + let tree = TempTree::new("bounds"); + let mut writer = begin(&tree.0, "bounds").unwrap(); + assert!(writer.stage(b"a\n", contract(3)).is_ok()); + assert!(writer.stage(b"bb\n", contract(3)).is_ok()); + assert!(matches!( + writer.stage(b"ccc\n", contract(3)), + Err(StageWriteError::Contract(_)) + )); +} + +#[test] +fn every_interrupt_phase_rolls_back_only_the_owned_staging_tree() { + let phases = [ + InterruptAfter::StageCreated, + InterruptAfter::TempCreated, + InterruptAfter::FileSynced, + InterruptAfter::ObjectPublished, + InterruptAfter::DirectorySynced, + ]; + for phase in phases { + let tree = TempTree::new("interrupt"); + let writer = StagedWriter::begin_with_options( + &tree.0, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + WriterOptions { + nonce: format!("interrupt-{phase:?}"), + interrupt_after: Some(phase), + before_publish: None, + }, + ); + match phase { + InterruptAfter::StageCreated => { + assert!(matches!(writer, Err(StageWriteError::Interrupted(_)))); + } + _ => { + let mut writer = writer.unwrap(); + assert!(matches!( + writer.stage(b"alpha\n", contract(64)), + Err(StageWriteError::Interrupted(_)) + )); + drop(writer); + } + } + assert!(staging_children(&tree.0).is_empty(), "phase={phase:?}"); + assert!(!tree.0.join("run-complete.json").exists()); + } +} + +fn occupy_addressed_target_with_same_bytes(object_dir: &Path, digest: &str) -> Result<(), String> { + fs::write(object_dir.join(digest), b"alpha\n").map_err(|error| error.to_string()) +} + +fn occupy_addressed_target_with_different_bytes( + object_dir: &Path, + digest: &str, +) -> Result<(), String> { + fs::write(object_dir.join(digest), b"competitor\n").map_err(|error| error.to_string()) +} + +#[test] +fn publish_race_with_different_content_reports_collision_and_preserves_competitor() { + let tree = TempTree::new("publish-race-different"); + let nonce = "publish-race-different"; + let target = tree.0.join(format!( + ".staging/stage-{nonce}/objects/sha256/{}", + "b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060" + )); + let mut writer = StagedWriter::begin_with_options( + &tree.0, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + WriterOptions { + nonce: nonce.to_string(), + interrupt_after: None, + before_publish: Some(occupy_addressed_target_with_different_bytes), + }, + ) + .unwrap(); + + let error = writer.stage(b"alpha\n", contract(64)).unwrap_err(); + assert!(matches!(error, StageWriteError::Collision(_))); + assert!(error.to_string().contains("residual")); + assert_eq!(fs::read(&target).unwrap(), b"competitor\n"); + drop(writer); + assert_eq!(fs::read(&target).unwrap(), b"competitor\n"); +} + +#[test] +fn publish_race_with_same_content_deduplicates_without_taking_delete_ownership() { + let tree = TempTree::new("publish-race-same"); + let nonce = "publish-race-same"; + let target = tree.0.join(format!( + ".staging/stage-{nonce}/objects/sha256/{}", + "b6a98d9ce9a2d9149288fa3df42d377c3e42737afdcdaf714e33c0a100b51060" + )); + let mut writer = StagedWriter::begin_with_options( + &tree.0, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + WriterOptions { + nonce: nonce.to_string(), + interrupt_after: None, + before_publish: Some(occupy_addressed_target_with_same_bytes), + }, + ) + .unwrap(); + + let artifact = writer.stage(b"alpha\n", contract(64)).unwrap(); + assert!(!artifact.owned_by_stage); + assert_eq!( + artifact.sha256, + target.file_name().unwrap().to_string_lossy() + ); + let error = writer.seal().unwrap_err(); + assert!(matches!(error, StageWriteError::Collision(_))); + assert!(error.to_string().contains("residual")); + assert_eq!(fs::read(&target).unwrap(), b"alpha\n"); +} + +#[test] +fn nonce_collision_is_never_treated_as_owned_or_deleted() { + let tree = TempTree::new("collision"); + let collision = tree.0.join(".staging/stage-collision"); + fs::create_dir_all(&collision).unwrap(); + fs::write(collision.join("competitor.txt"), b"keep me").unwrap(); + + let error = begin(&tree.0, "collision").unwrap_err(); + assert!(matches!(error, StageWriteError::Collision(_))); + assert_eq!(error.kind(), "collision"); + assert_eq!( + fs::read(collision.join("competitor.txt")).unwrap(), + b"keep me" + ); +} + +#[test] +fn root_links_and_out_of_scope_authorities_fail_closed() { + let tree = TempTree::new("boundary"); + let real = tree.0.join("real"); + fs::create_dir(&real).unwrap(); + let link = tree.0.join("link"); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(&real, &link).is_ok(); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_dir(&real, &link).is_ok(); + if linked { + assert!(matches!( + begin(&link, "linked"), + Err(StageWriteError::Boundary(_)) + )); + } + + let file_root = tree.0.join("not-a-directory"); + fs::write(&file_root, b"x").unwrap(); + assert!(matches!( + begin(&file_root, "file-root"), + Err(StageWriteError::Boundary(_)) + )); +} + +#[test] +fn checked_in_staged_set_schema_is_closed_and_matches_the_runtime_manifest() { + let schema: Value = serde_json::from_slice( + &fs::read(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../orchestration/schemas/code-intel-staged-artifact-set.v1.schema.json" + )) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["$id"], "code-intel-staged-artifact-set.v1"); + assert_eq!(schema["additionalProperties"], false); + assert_eq!( + schema["$defs"]["artifactRef"]["additionalProperties"], + false + ); + assert_eq!( + schema["$defs"]["artifactRef"]["properties"]["path"]["pattern"], + "^objects/sha256/[0-9a-f]{64}$" + ); +} diff --git a/crates/code-intel-cli/tests/survival_scan.rs b/crates/code-intel-cli/tests/survival_scan.rs new file mode 100644 index 0000000..de434c1 --- /dev/null +++ b/crates/code-intel-cli/tests/survival_scan.rs @@ -0,0 +1,276 @@ +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +#[path = "../src/admissibility.rs"] +mod admissibility; +#[path = "../src/artifact_ref.rs"] +mod artifact_ref; +#[path = "../src/capability.rs"] +mod capability; +#[path = "../src/capability_inventory.rs"] +mod capability_inventory; +#[path = "../src/codenexus_adapter.rs"] +mod codenexus_adapter; +#[path = "../src/snapshot.rs"] +mod snapshot; +#[path = "../src/stable_artifact.rs"] +mod stable_artifact; +#[path = "../src/survival_scan.rs"] +mod survival_scan; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const IMPL: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +static NONCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); +impl Temp { + fn new() -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = std::env::temp_dir().join(format!( + "code-intel-b05-{}-{now}-{}", + std::process::id(), + NONCE.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn artifact(root: &Path, path: &str, schema: &str, kind: &str, bytes: &[u8]) -> Value { + fs::write(root.join(path), bytes).unwrap(); + json!({ + "schema":"code-intel-artifact-ref.v1", "artifactSchema":schema, "type":kind, + "path":path, "sha256":capability::sha256_hex(bytes), "consumedSnapshotIdentity":SNAPSHOT + }) +} + +fn request(root: &Path) -> Value { + let snapshot = serde_json::to_vec(&json!({ + "schema":"code-intel-repository-snapshot.v1", + "snapshot":{"identity":SNAPSHOT,"repoIdentity":format!("content-v1:{}", "c".repeat(64)),"head":"unversioned","workingTreePolicy":"explicit_overlay","scope":["."],"inputDigest":"d".repeat(64)}, + "dirtyOverlay":{"present":false,"digest":null,"paths":[],"members":{"trackedModified":[],"trackedDeleted":[],"untracked":[],"renamed":[],"typeChanged":[],"staged":[]},"ignoredPolicy":"excluded_by_git_ignore"}, + "repository":{"kind":"unversioned"} + })).unwrap(); + let inventory = b"Cargo.toml\0README.md\0src/lib.rs\0"; + let payload = serde_json::to_vec(&json!({ + "schema":"code-intel-evidence-payload.v1", + "data":{"codenexus":{"schema":"code-intel-codenexus-evidence.v1","snapshotIdentity":SNAPSHOT, + "provider":{"mode":"full","providerId":"codenexus.full","implementationId":"codenexus.service.v1","activation":"primary"}, + "provenance":{"sourceRevision":"fixture-unavailable","observedAt":1950},"completeness":"partial","availability":"provider_unavailable","providerData":null}} + })).unwrap(); + let payload_ref = artifact( + root, + "provider.json", + "code-intel-evidence-payload.v1", + "observed.evidence.payload", + &payload, + ); + let native = json!({ + "schema":"code-intel-codenexus-native-result.v1","providerMode":"full","status":"unavailable","providerId":"codenexus.full", + "implementation":{"id":"codenexus.service.v1","version":"1.0.0","digest":IMPL},"sourceRevision":"fixture-unavailable", + "expectedSnapshotIdentity":SNAPSHOT,"sourceSnapshotIdentity":SNAPSHOT,"collectedAt":1949,"observedAt":1950, + "payload":payload_ref,"activation":"primary","effects":["network_provider"] + }); + let adapter = codenexus_adapter::translate(&native, 2000, 100).unwrap(); + json!({ + "schema":"code-intel-repository-survival-scan-request.v1","snapshotIdentity":SNAPSHOT, + "inputs":[ + artifact(root, "snapshot.json", "code-intel-repository-snapshot.v1", "repository.snapshot", &snapshot), + artifact(root, "files.txt", "code-intel-file-inventory.v1", "inventory.files", inventory) + ], + "codenexusAdapter":adapter + }) +} + +#[test] +fn unavailable_provider_yields_useful_basic_evidence_and_unknown_structure() { + let root = Temp::new(); + let result = survival_scan::scan(&request(&root.0), &root.0).unwrap(); + let expectations: Value = serde_json::from_str(include_str!( + "fixtures/survival-scan/unavailable-expectations.json" + )) + .unwrap(); + assert_eq!( + result["schema"], + "code-intel-repository-survival-scan-result.v1" + ); + assert_eq!(result["completeness"], expectations["completeness"]); + assert_eq!( + result["structuralVerdict"], + expectations["structuralVerdict"] + ); + assert_eq!( + result["providerDiagnosis"]["status"], + expectations["providerStatus"] + ); + assert_eq!(result["snapshotIdentity"], SNAPSHOT); + assert_eq!(result["inventory"]["fileCount"], 3); + assert_eq!(result["inventory"]["extensions"]["rs"], 1); + assert_eq!(result["engineeringFacts"].as_array().unwrap().len(), 3); + let rendered = serde_json::to_string(&result).unwrap().to_ascii_lowercase(); + for forbidden in expectations["forbiddenClaims"].as_array().unwrap() { + let forbidden = forbidden.as_str().unwrap(); + assert!( + !rendered.contains(forbidden), + "fallback overclaims {forbidden}: {rendered}" + ); + } +} + +#[test] +fn fallback_rejects_observed_provider_and_forged_admission() { + let root = Temp::new(); + let mut observed = request(&root.0); + observed["codenexusAdapter"]["port"]["status"] = json!("current"); + assert!(survival_scan::scan(&observed, &root.0).is_err()); + + let mut forged = request(&root.0); + forged["codenexusAdapter"]["evidence"]["request"]["observation"]["failure"] = + json!({"kind":"none"}); + assert!(survival_scan::scan(&forged, &root.0).is_err()); +} + +#[test] +fn fallback_rejects_undeclared_codenexus_port_fields() { + let root = Temp::new(); + let mut forged = request(&root.0); + forged["codenexusAdapter"]["port"]["graph"] = json!({"nodes": []}); + assert!(survival_scan::scan(&forged, &root.0) + .unwrap_err() + .contains("port fields are invalid")); +} + +#[test] +fn fallback_rejects_undeclared_fact_promotion_fields() { + let root = Temp::new(); + let mut forged = request(&root.0); + forged["codenexusAdapter"]["factPromotion"]["secret"] = json!("not-authority"); + assert!(survival_scan::scan(&forged, &root.0) + .unwrap_err() + .contains("fact promotion fields are invalid")); +} + +#[test] +fn snapshot_and_inventory_are_a03_verified_and_snapshot_bound() { + let root = Temp::new(); + let mut tampered = request(&root.0); + fs::write( + root.0.join("files.txt"), + b"Cargo.lock\0README.md\0src/lib.rs\0", + ) + .unwrap(); + assert!(survival_scan::scan(&tampered, &root.0) + .unwrap_err() + .contains("SHA-256")); + tampered = request(&root.0); + tampered["inputs"][1]["consumedSnapshotIdentity"] = json!("e".repeat(64)); + assert!(survival_scan::scan(&tampered, &root.0) + .unwrap_err() + .contains("snapshot")); +} + +#[test] +fn production_cli_registry_facade_schema_and_docs_are_closed() { + let root = Temp::new(); + let request_path = root.0.join("request.json"); + fs::write( + &request_path, + serde_json::to_vec(&request(&root.0)).unwrap(), + ) + .unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args([ + "repository", + "survival-scan", + "--request", + request_path.to_str().unwrap(), + "--artifact-root", + root.0.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let result: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["structuralVerdict"], "unknown"); + + let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + let manifest: Value = + serde_json::from_slice(&fs::read(repo.join("orchestration/integrations.json")).unwrap()) + .unwrap(); + let entry = manifest["integrations"] + .as_array() + .unwrap() + .iter() + .find(|v| v["id"] == "repository.survival-scan") + .unwrap(); + assert_eq!(entry["required"], true); + assert!(entry["commands"]["scan"] + .as_str() + .unwrap() + .contains("repository survival-scan")); + assert!(entry["commands"]["facade"] + .as_str() + .unwrap() + .contains("SurvivalScanRequest")); + let schema: Value = serde_json::from_slice( + &fs::read(repo.join( + "orchestration/schemas/code-intel-repository-survival-scan-result.v1.schema.json", + )) + .unwrap(), + ) + .unwrap(); + assert_eq!(schema["additionalProperties"], false); + let docs = fs::read_to_string(repo.join("docs/repository-survival-scan.md")).unwrap(); + assert!(docs.contains("reduced")); + assert!(docs.contains("structuralVerdict = unknown")); + let facade = fs::read_to_string(repo.join("run-code-intel.ps1")).unwrap(); + assert!(facade.contains("repository survival-scan")); +} + +#[test] +fn result_has_exact_top_level_contract() { + let root = Temp::new(); + let result = survival_scan::scan(&request(&root.0), &root.0).unwrap(); + let keys = result + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect::>(); + assert_eq!( + keys, + [ + "schema", + "status", + "snapshotIdentity", + "repository", + "inventory", + "providerDiagnosis", + "completeness", + "structuralVerdict", + "limitations", + "engineeringFacts" + ] + .into_iter() + .collect() + ); +} diff --git a/crates/code-intel-cli/tests/understanding_quadrant.rs b/crates/code-intel-cli/tests/understanding_quadrant.rs new file mode 100644 index 0000000..15ae949 --- /dev/null +++ b/crates/code-intel-cli/tests/understanding_quadrant.rs @@ -0,0 +1,320 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde_json::{json, Value}; + +const SNAPSHOT: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +static TEMP_NONCE: AtomicU64 = AtomicU64::new(0); + +struct Temp(PathBuf); + +impl Temp { + fn new() -> Self { + let clock = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let sequence = TEMP_NONCE.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "code-intel-d03-{}-{clock}-{sequence}", + std::process::id() + )); + fs::create_dir(&path).unwrap(); + Self(path) + } +} + +impl Drop for Temp { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn sha256(path: &Path) -> String { + let output = Command::new("certutil") + .arg("-hashfile") + .arg(path) + .arg("SHA256") + .output() + .unwrap(); + assert!(output.status.success()); + String::from_utf8_lossy(&output.stdout) + .lines() + .map(str::trim) + .find(|line| line.len() == 64 && line.bytes().all(|byte| byte.is_ascii_hexdigit())) + .unwrap() + .to_ascii_lowercase() +} + +fn provenance(pointer: &str) -> Value { + json!([{ + "artifactType":"repository.survival-scan", + "artifactSha256":"b".repeat(64), + "jsonPointer":pointer + }]) +} + +fn orientation() -> Value { + json!({ + "schema":"code-intel-project-orientation.v1", + "snapshotIdentity":SNAPSHOT, + "identity":{ + "status":"known", + "repositoryIdentity":format!("content-v1:{}", "c".repeat(64)), + "repositoryKind":"unversioned", + "revision":"unversioned", + "provenance":provenance("/snapshot") + }, + "purpose":{ + "status":"known", + "evidence":["README.md"], + "reason":"README declares the fixture purpose.", + "provenance":provenance("/purpose") + }, + "languages":[{"name":"Rust","fileCount":3,"provenance":provenance("/languages/0")}], + "boundaries":[{"path":"src","kind":"top_level_directory","provenance":provenance("/boundaries/0")}], + "entryPoints":[{"path":"src/main.rs","classification":"heuristic","provenance":provenance("/entryPoints/0")}], + "commands":[{"path":"build.ps1","kind":"script_path","provenance":provenance("/commands/0")}], + "activeChange":{"status":"clean","paths":[],"provenance":provenance("/activeChange")}, + "evidenceAvailability":[ + {"evidence":"survival_scan","status":"available","provenance":provenance("/evidenceAvailability/0")}, + {"evidence":"native_files","status":"available","provenance":provenance("/evidenceAvailability/1")}, + {"evidence":"native_structure","status":"heuristic","provenance":provenance("/evidenceAvailability/2")} + ], + "risks":[{"code":"dirty_tree","statement":"No dirty tree is present in the fixture.","provenance":provenance("/risks/0")}], + "unknowns":[ + {"field":"dependencies.runtime","reason":"Runtime dependency authority is absent.","provenance":provenance("/unknowns/0")}, + {"field":"documentation.examples","reason":"Examples were not inspected.","provenance":provenance("/unknowns/1")} + ], + "confidence":{"level":"high","basis":["all fixture evidence is present"],"provenance":provenance("/confidence")} + }) +} + +fn declaration() -> Value { + let registry: Value = + serde_json::from_slice(&fs::read(root().join("orchestration/integrations.json")).unwrap()) + .unwrap(); + registry["integrations"] + .as_array() + .unwrap() + .iter() + .find(|integration| integration["id"] == "understanding.quadrant") + .unwrap()["capabilityDeclaration"] + .clone() +} + +fn input(temp: &Path) -> Value { + input_value(temp, &orientation(), "project-orientation.json") +} + +fn input_value(temp: &Path, value: &Value, name: &str) -> Value { + let path = temp.join(name); + fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap(); + json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-project-orientation.v1", + "type":"project.orientation", + "path":name, + "sha256":sha256(&path), + "consumedSnapshotIdentity":SNAPSHOT + }) +} + +fn request(input: Value, options: Value) -> Value { + let declaration = declaration(); + json!({ + "schema":"code-intel-capability-request.v1", + "capability":"understanding.quadrant", + "contractVersion":1, + "implementation":declaration["implementation"], + "snapshot":{ + "identity":SNAPSHOT, + "repoIdentity":format!("content-v1:{}", "c".repeat(64)), + "head":"unversioned", + "workingTreePolicy":"explicit_overlay", + "scope":["."], + "inputDigest":"d".repeat(64) + }, + "options":options, + "inputs":[input], + "effectPolicy":{"allowedEffects":declaration["allowedEffects"]} + }) +} + +fn run(temp: &Path, request: &Value, out_name: &str) -> std::process::Output { + let request_path = temp.join(format!("{out_name}-request.json")); + fs::write(&request_path, serde_json::to_vec(request).unwrap()).unwrap(); + Command::new(env!("CARGO_BIN_EXE_code-intel")) + .args(["capability", "exec", "understanding.quadrant", "--request"]) + .arg(&request_path) + .arg("--out") + .arg(temp.join(out_name)) + .arg("--artifact-root") + .arg(temp) + .output() + .unwrap() +} + +fn item<'a>(document: &'a Value, id: &str) -> &'a Value { + document["items"] + .as_array() + .unwrap() + .iter() + .find(|item| item["id"] == id) + .unwrap() +} + +#[test] +fn critical_and_supporting_unknowns_remain_visible_with_stable_provenance() { + let temp = Temp::new(); + let request = request(input(&temp.0), json!({})); + let output = run(&temp.0, &request, "first"); + assert_eq!( + output.status.code(), + Some(0), + "stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let envelope: Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(envelope["status"], "completed"); + assert_eq!(envelope["observedEffects"], json!(["local_write"])); + assert_eq!(envelope["artifacts"].as_array().unwrap().len(), 1); + assert_eq!( + envelope["artifacts"][0]["consumedSnapshotIdentity"], + SNAPSHOT + ); + let path = temp.0.join("first/understanding-quadrant.json"); + let bytes = fs::read(&path).unwrap(); + let document: Value = serde_json::from_slice(&bytes).unwrap(); + assert_eq!(item(&document, "identity")["quadrant"], "Known Core"); + assert_eq!( + item(&document, "language:Rust")["quadrant"], + "Supporting Context" + ); + assert_eq!( + item(&document, "unknown:dependencies.runtime")["quadrant"], + "Critical Unknown" + ); + assert_eq!( + item(&document, "unknown:documentation.examples")["quadrant"], + "Deferred Unknown" + ); + assert_eq!( + document["visibleUnknowns"], + json!([ + "unknown:dependencies.runtime", + "unknown:documentation.examples" + ]) + ); + assert_eq!( + item(&document, "unknown:dependencies.runtime")["provenance"], + orientation()["unknowns"][0]["provenance"] + ); + assert_eq!( + document["classificationPolicy"]["methodConsumerPolicy"], + "C01_cards_and_C02_selection_may_consume_but_cannot_rewrite" + ); + + let replay = run(&temp.0, &request, "replay"); + assert_eq!(replay.status.code(), Some(0)); + assert_eq!( + bytes, + fs::read(temp.0.join("replay/understanding-quadrant.json")).unwrap() + ); + + let schema = Command::new("pwsh") + .args([ + "-NoLogo", + "-NoProfile", + "-Command", + "param($Document,$Schema); if (-not (Get-Content -Raw -LiteralPath $Document | Test-Json -SchemaFile $Schema -ErrorAction Stop)) { exit 1 }", + ]) + .arg(&path) + .arg(root().join("orchestration/schemas/code-intel-understanding-quadrant.v1.schema.json")) + .output() + .unwrap(); + assert!( + schema.status.success(), + "{}", + String::from_utf8_lossy(&schema.stderr) + ); +} + +#[test] +fn method_rewrite_options_and_snapshot_mismatch_fail_closed_without_artifacts() { + let temp = Temp::new(); + let reference = input(&temp.0); + let rewrite = run( + &temp.0, + &request(reference.clone(), json!({"methodId":"fmea"})), + "rewrite", + ); + assert_eq!(rewrite.status.code(), Some(64)); + assert!(!temp.0.join("rewrite/understanding-quadrant.json").exists()); + + let mut mismatched = reference; + mismatched["consumedSnapshotIdentity"] = json!("f".repeat(64)); + let mismatch = run(&temp.0, &request(mismatched, json!({})), "mismatch"); + assert_eq!(mismatch.status.code(), Some(65)); + assert!(!temp.0.join("mismatch/understanding-quadrant.json").exists()); + + let mut invalid_provenance = orientation(); + invalid_provenance["identity"]["provenance"] = json!([null]); + let invalid_ref = input_value( + &temp.0, + &invalid_provenance, + "invalid-provenance-orientation.json", + ); + let invalid = run( + &temp.0, + &request(invalid_ref, json!({})), + "invalid-provenance", + ); + assert_eq!(invalid.status.code(), Some(65)); + assert!(String::from_utf8_lossy(&invalid.stderr).contains("provenance")); + assert!(!temp + .0 + .join("invalid-provenance/understanding-quadrant.json") + .exists()); +} + +#[test] +fn a03_rejects_tampered_policy_constants_before_a01_dispatch() { + let temp = Temp::new(); + let output = run(&temp.0, &request(input(&temp.0), json!({})), "source"); + assert_eq!(output.status.code(), Some(0)); + let mut document: Value = serde_json::from_slice( + &fs::read(temp.0.join("source/understanding-quadrant.json")).unwrap(), + ) + .unwrap(); + document["classificationPolicy"]["scoreRange"]["maximum"] = json!(999); + let path = temp.0.join("tampered-quadrant.json"); + fs::write(&path, serde_json::to_vec(&document).unwrap()).unwrap(); + let tampered_ref = json!({ + "schema":"code-intel-artifact-ref.v1", + "artifactSchema":"code-intel-understanding-quadrant.v1", + "type":"understanding.quadrant", + "path":"tampered-quadrant.json", + "sha256":sha256(&path), + "consumedSnapshotIdentity":SNAPSHOT + }); + let rejected = run( + &temp.0, + &request(tampered_ref, json!({})), + "tampered-policy", + ); + assert_eq!(rejected.status.code(), Some(65)); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("policy")); + assert!(!temp + .0 + .join("tampered-policy/understanding-quadrant.json") + .exists()); +} diff --git a/docs/adr/0010-tool-neutral-engineering-intelligence-core.md b/docs/adr/0010-tool-neutral-engineering-intelligence-core.md new file mode 100644 index 0000000..270f86b --- /dev/null +++ b/docs/adr/0010-tool-neutral-engineering-intelligence-core.md @@ -0,0 +1,20 @@ +--- +status: accepted +date: 2026-07-13 +--- + +# Make Code Intel Pipeline a tool-neutral engineering-intelligence core + +Code Intel Pipeline is an independent engineering-intelligence domain whose authoritative work is deterministic evidence collection, validation, derived engineering models, capability execution, artifact publication, and policy gates. LLMs, workflow recommenders, OpenSpec, spec-kit, creators, and external analysis projects remain replaceable callers or providers: they may propose, implement, or explain work, but they do not own Pipeline facts, commitments, or internal semantics. + +This chooses a provider-and-method architecture over the current tendency to embed recommendation, orchestration, diagnosis, rendering, and tool-specific behavior in one runner. CodeNexus owns code-and-Git perception; Pipeline owns the Engineering Evidence Set and cross-provider synthesis. Mature open-source capabilities are reused through Pipeline-owned ports by default, while selective internalization is limited to trust boundaries, shared semantics, and minimum survival capabilities. + +The Pipeline will converge on progressive project understanding, a tool-neutral Engineering Method Catalog, evidence-driven assistance discovery, the Ponytail Value Filter, and the Light-Speed Baseline. Workflow recommendation becomes an Advisory Atom, and every external reference follows one Internalization Standard with provenance, adoption, conformance, measurement, update, and retirement evidence. + +## Consequences + +- The deterministic Pipeline must remain useful without an LLM. +- Recommendations cannot become Adoption Decisions or Committed Engineering Plans without explicit authority. +- OpenSpec and creator tools may carry or generate lifecycle artifacts but cannot define the canonical lifecycle. +- Existing integrations and reference projects require reclassification rather than automatic preservation or wholesale rewriting. +- Refactoring proceeds atom by atom; this ADR does not authorize a big-bang rewrite. diff --git a/docs/advisory-workflow-recommendation.md b/docs/advisory-workflow-recommendation.md new file mode 100644 index 0000000..62958df --- /dev/null +++ b/docs/advisory-workflow-recommendation.md @@ -0,0 +1,11 @@ +# Advisory workflow recommendation + +`advisory.workflow-recommend` is a deterministic, read-only proposal atom. It compares the existing matt-flow, gstack, OpenSpec OPSX, and spec-kit candidates using repository-local evidence and returns `code-intel-advisory-workflow-recommendation.v1`. + +The atom owns recommendation only. Its `effects` array is always empty; it never prompts, initializes a tool, edits the repository, or emits an Adoption Decision. Promotion from `proposal` to `adoption_decision` remains protected by the A05 authority-transition gate and requires an explicit approved authority event. + +`OpenSpec-Detector.ps1` is the standalone atom. `Invoke-WorkflowRecommendation.ps1` is the compatibility facade wrapped by the A01 runtime adapter; `run-code-intel.ps1` invokes the A01 capability. Historical `-SkipOpenSpec` disables the advisory call and `-AutoOpenSpec` maps to the facade's recorded `-Auto` compatibility option; neither flag grants adoption authority. + +Production invocation crosses A01 through `code-intel capability exec advisory.workflow-recommend`. The capability declaration and request both use `allowedEffects: []`; stdout is one `code-intel-capability-result.v1` envelope, and the proposal crosses the boundary as `workflow-recommendation.json` with an Artifact Ref. The runtime adapter is `advisory.workflow-recommend.compat`, which wraps the same standalone PowerShell atom used by the compatibility facade. + +OpenSpec OPSX, spec-kit, gstack, and matt-flow are candidates, not runtime dependencies. The rollback path is direct invocation of `OpenSpec-Detector.ps1`. diff --git a/docs/architecture/reference-capability-map.md b/docs/architecture/reference-capability-map.md new file mode 100644 index 0000000..c2313bd --- /dev/null +++ b/docs/architecture/reference-capability-map.md @@ -0,0 +1,64 @@ +# Reference Capability Map + +Status: canonical audit baseline (2026-07-13) + +This map classifies the active integrations and design references named by the restored commitment checkpoints. It is an inventory, not an adoption approval, implementation claim, or health claim. Repository code and accepted/proposed ADRs are the primary evidence; the checkpoints are audit inputs that establish coverage expectations only. + +## Classification rules + +- **Evidence type** describes what the capability supplies to Pipeline: observed evidence, derived model/view, advisory output, control-plane effect, or design reference. A successful external command does not automatically create an Engineering Fact. +- **Lifecycle state** is one of `active-required`, `active-optional`, `compatibility-fallback`, `reference-internalized`, `reference-only`, `proposed`, or `drift-unregistered`. `drift-unregistered` means production participation exists outside `orchestration/integrations.json`. +- **Ownership** names the owner of the current adapter or policy meaning, not the copyright owner of an upstream project. +- **Unknown** is intentional where the repository has no pinned revision, license record, maintenance evidence, or executable exit proof. Those gaps must be closed by Reuse Records; this map does not infer them. + +## Capability inventory + +| Integration / reference | Capability supplied | Evidence type | Current invocation path | Ownership | Lifecycle state | Coupling | Trust boundary | Replacement / exit path | Source evidence | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| doctor | Repository/tool preflight, registered-integration validation and machine-readable readiness | Observed environment and repository-state evidence | `check-code-intel-tools.ps1 -RepoPath ... -Json`; stable wrapper runs doctor before the scanner | Pipeline | `active-required` | Medium: PowerShell probes installed commands, local provider sources and repository state | Tool presence is not capability conformance; secrets are reported only as presence/absence and must never enter output | Move probes behind the Rust runtime while retaining the `doctor_json` contract and a shell-compatible bootstrap path | `orchestration/integrations.json` integration id `doctor`; `check-code-intel-tools.ps1`; `invoke-code-intel.ps1` | +| runtime.code-intel | Integration planning/validation, provider routes, graph generation, artifact resume/classification and artifact doctor | Control-plane execution plus deterministic derived status | `code-intel` subcommands `orchestrate`, `provider`, `route`, `graph`, `doctor`, `resume`, `classify` | Pipeline | `active-required` | Medium: compiled Rust core exists, while the main scan/report orchestration still resides in PowerShell | Rust results are authoritative only for the operation and contract they actually implement; they do not prove the PowerShell scanner has been retired | Expand atom-by-atom behind stable contracts; keep PowerShell as a temporary compatibility adapter until parity is tested | `orchestration/integrations.json` integration id `runtime.code-intel`; `crates/code-intel-cli/src/main.rs`; `docs/integration-orchestration.md` section “PowerShell Compatibility Direction” | +| inventory.rg | Exact file inventory and text-search surface | Observed repository evidence | Normal runner invokes `rg` and writes `files.txt` plus the `rg file inventory` step | Pipeline owns invocation/artifact contract; upstream owns `rg` | `active-required` | Low CLI coupling, currently embedded in the main runner | Inventory is bounded by scope/exclusions and snapshot; absence from a search is not proof of semantic absence outside that scope | Replace the executable behind an inventory adapter while preserving exact inventory artifacts and failure semantics | `orchestration/integrations.json` integration id `inventory.rg`; `run-code-intel.ps1` step name `rg file inventory` | +| Git | Working-tree state, repository identity, HEAD/history and recent-commit context | Observed version-control evidence | `git status`, `git rev-parse`, `git log` from runner/adapters | Pipeline owns interpretation; Git owns repository protocol/CLI | `drift-unregistered` | Medium: multiple scripts call Git directly and CodeNexus-lite uses history context | Dirty state, HEAD and worktree policy must be explicit in Snapshot Identity; Git commands are read-only unless a separately authorized effect declares mutation | Register a version-control evidence adapter and centralize snapshot semantics; alternate VCS providers may conform to the same port | `run-code-intel.ps1` step name `git status`; `Invoke-CodeNexusLite.ps1`; absent from `orchestration/integrations.json` | +| Repowise | Semantic index, state, scoped shadow workspace, optional generated docs | Observed evidence and semantic-memory artifacts; provider-generated prose remains non-authoritative | `Invoke-ScopedRepowise.ps1`; normal runner invokes it; Rust provider routes exist for status/index/docs | Pipeline owns adapter and egress contract; upstream owns CLI/provider internals | `active-required` | Medium/high: CLI plus Python/package behavior is still wrapped and patched locally | Scope inventory and frozen provider payload must be validated before network/provider execution; secrets remain outside artifacts | Preserve `.repowise/state.json`/`wiki.db` and provider-port semantics; replace CLI behind adapter or complete selective internalization | `orchestration/integrations.json` integration id `memory.repowise`; `docs/artifact-data-contract.md` section “Scoped Repowise Egress Contract”; `crates/code-intel-cli/src/providers.rs`; `overlays/repowise/README.md` | +| Understand-compatible internal graph | Architecture graph state, freshness, compatible graph artifact | Observed structural evidence promoted only after snapshot/freshness validation | `code-intel graph --repo ... --write --json`; normal run consumes `.understand-anything/knowledge-graph.json` | Pipeline | `active-required` | Low/medium: internal Rust producer, but artifact name/schema retains compatibility | Graph freshness and snapshot identity must match the examined repository; stale presence is not success | Any graph provider may replace implementation if it emits the Pipeline-owned compatible artifact through the provider port | `orchestration/integrations.json` integration id `graph.code-intel-understand`; `docs/integration-orchestration.md` section “Understand Direction”; `crates/code-intel-cli/src/graph.rs` | +| Understand Anything external | Rich Agent-side architecture graph refresh | Observed evidence from an external/interactive provider | `/understand --language zh [--full]` only as fallback or explicit richer pass | Upstream provider; Pipeline owns admission of its artifact | `compatibility-fallback` | Low at runtime, medium artifact compatibility | External command is non-deterministic/host-dependent; output must pass the same snapshot/freshness contract | Remove fallback when internal or another non-interactive provider reaches required conformance | `orchestration/integrations.json` integration id `graph.understand-external`; `docs/integration-orchestration.md` section “Understand Direction”; `skill/SKILL.md` graph-missing workflow | +| Sentrux | Structural scan, rules, regression gate, DSM, test gaps, evolution, what-if | Observed structural evidence plus derived governance models/gate results | `Invoke-SentruxAgentTool.ps1`; Rust `code-intel sentrux ...`; external binary through repo shim with lite-core fallback | Pipeline owns adapter, normalization, rules and lite fallback; external binary is accelerator | `active-required` | High today: large PowerShell wrapper; external CLI format is normalized locally | Raw CLI status/exit code is not authoritative until normalized; governed scope, exclusions, baseline and rules are explicit | Migrate operations atom-by-atom behind Rust/provider contracts; retain lite survival capability; external accelerator remains replaceable | `orchestration/integrations.json` integration id `structure.sentrux`; `docs/integration-orchestration.md` section “Sentrux Direction”; `crates/code-intel-cli/src/sentrux.rs`; `tools/sentrux-shim/sentrux-lite-core.ps1` | +| tree-sitter-v | V-language parsing grammar for Sentrux | Third-party parser artifact used to create structural evidence | `Install-SentruxVlangOverlay.ps1`; installed as Sentrux `vlang` plugin | Pipeline owns overlay/ABI alias and installer; upstream owns grammar | `active-optional` | Medium: compiled grammar ABI and Sentrux plugin contract | Compiled artifact provenance/ABI must be verified; current repository records source URL and MIT license but no pinned source revision | Drop overlay when upstream Sentrux ships a conforming V plugin, or rebuild from a pinned reviewed revision | `overlays/sentrux/vlang/plugin.toml:11`; `overlays/sentrux/vlang/plugin.toml:14`; `overlays/sentrux/vlang/README.md:7`; `Install-SentruxVlangOverlay.ps1` | +| CodeNexus-lite | Hotspot localization, recent commits, reference hits; compatibility scan/lite/doctor surface | Deterministic derived localization view | Runner conditionally invokes `Invoke-CodeNexusLite.ps1` when a Sentrux target and script exist; exceptions only add a note; canonical provider route is `codenexus/lite` | Pipeline | `active-optional` | Medium: both manifest ids are optional compatibility entries and the canonical provider operation is optional/nonblocking; two manifest ids still share one implementation while the legacy `repowise/lite` provider alias is being retired | `codenexus-context.json` is a derived view, not independent provider fact or a semantic graph; Survival Scanner preserves localization when the adapter is unavailable | Keep canonical `codenexus/lite`, retire the compatibility alias after its transition window, and let a full CodeNexus or another provider conform to the artifact/provider port | `orchestration/integrations.json` ids `runtime.code-nexus-lite` and `localization.codenexus-lite`; `crates/code-intel-cli/src/providers.rs` operations `codenexus/lite` and deprecated `repowise/lite`; `run-code-intel.ps1` variable `$codeNexusLiteTool` and its nonblocking `catch` | +| CodeNexus full provider boundary | Independent code/Git indexing, retrieval, perception and impact relationships | Observed provider evidence | None in current repository; only CodeNexus-lite artifacts and the ADR/vocabulary boundary exist | Independent provider owns runtime/storage; Pipeline owns Evidence Provider Port and cross-provider synthesis | `proposed` | Intended low coupling; actual adapter contract is not implemented | No shared database/internal model or lockstep release; observed output requires provenance/snapshot/freshness validation | Retain Survival Scanner and swap provider adapters; Pipeline remains operable when this provider is absent | `CONTEXT.md` definition “CodeNexus Evidence Provider”; `docs/adr/0010-tool-neutral-engineering-intelligence-core.md` decision text; `docs/code-intel-architecture.md` section describing `codenexus-context.json` | +| Greenfield | Behavioral-spec extraction, test vectors, acceptance criteria, clean-room provenance | External provider output and advisory/specification artifacts | `Invoke-GreenfieldSpecExtraction.ps1 -RepoPath ... [-Analyze] -Json`; interactive Claude plugin only when explicitly requested | Pipeline owns adapter/manifest; Greenfield owns plugin execution | `active-optional` | Medium: Claude CLI/plugin command and workspace layout | Default scan cannot block on interactive Agent/plugin; generated specs require review and provenance | Replace plugin with non-interactive provider conforming to the same behavior-spec artifact contract, or keep plan-only handoff | `orchestration/integrations.json` integration id `spec.greenfield`; `docs/integration-orchestration.md` section “Greenfield Direction”; `Invoke-GreenfieldSpecExtraction.ps1` schema `code-intel-greenfield-spec-extraction.v1` | +| Repomix | Whole-repository context pack in markdown/XML/JSON/plain | Derived materialized view; not fact authority | `Invoke-RepomixCodePack.ps1`; runner calls it when installed and not skipped | Pipeline owns wrapper/output schema; upstream owns CLI | `drift-unregistered` | Low/medium CLI coupling | Pack can expose repository content; invocation must honor repository scope and local-only policy. No repository Reuse Record is present | Register as optional materialized-view provider or remove it; native inventory/Code Evidence remains fallback | `run-code-intel.ps1:3621`; `run-code-intel.ps1:3644`; `Invoke-RepomixCodePack.ps1:42`; absent from `orchestration/integrations.json` | +| Native Code Evidence | Default files, symbols, chunks, imports, scorecard and Agent Code Slice | Observed code evidence plus deterministic derived slices | Internal functions in `run-code-intel.ps1`, beginning with `Get-CodeEvidenceLanguage` and `New-CodeEvidenceLayer` | Pipeline | `drift-unregistered` | High: embedded in the monolithic runner | Parser heuristics and coverage limits must remain explicit; v1 does not claim complete call-graph precision | Extract as a registered built-in Survival Scanner/code-evidence atom; keep stable artifacts so specialized providers can replace/augment it | `run-code-intel.ps1:1010`; `run-code-intel.ps1:1340`; `docs/adr/0007-code-evidence-layer-v1-boundary.md:3`; absent from `orchestration/integrations.json` | +| cocoindex-code | Optional richer Code Evidence adapter and A/B comparison candidate | External observed code evidence with typed optional outcome | Configured under `pipeline.config.json` and evaluated by `New-CodeEvidenceLayer`; disabled by default | Upstream tool owns implementation; Pipeline owns adapter outcome and comparison | `active-optional` | Low when disabled; command/artifact coupling when enabled; production participation is not registered centrally | Missing command must skip non-fatally unless future policy explicitly makes it required; evidence cannot silently replace native facts | Remove adapter without losing baseline evidence; another adapter may emit the same Code Evidence contract | `pipeline.config.json:8`; `run-code-intel.ps1:1433`; `docs/adr/0007-code-evidence-layer-v1-boundary.md:3`; `test-code-evidence-layer.ps1:154` | +| GitHub Solution Research | Searches upstream issues, PRs, repositories and examples for classified blockers | Advisory research dossier; never direct Engineering Fact or Adoption Decision | `Invoke-GitHubSolutionResearch.ps1`, conditionally invoked by runner only for eligible failure categories | Pipeline owns query construction/artifact; GitHub/`gh` provide external data | `drift-unregistered` | Medium: `gh` CLI, network, GitHub result schemas | Network results are untrusted reference evidence; authentication and external mutation are outside scanner authority | Register as optional research provider; disable/replace without affecting deterministic scan and diagnosis | `run-code-intel.ps1:3950`; `Invoke-GitHubSolutionResearch.ps1:266`; `CONTEXT.md:207`; absent from `orchestration/integrations.json` | +| OpenSpec / spec-kit / matt-flow / gstack | Recommends specification, planning, delivery and quality workflows from repo signals | Advisory output only | Duplicated detector in `run-code-intel.ps1` and `OpenSpec-Detector.ps1`; no workflow is auto-authorized | Pipeline currently owns detector; named tools own their workflows | `drift-unregistered` | High: tool names/rules are embedded twice in the scanner control plane | Recommendation cannot initialize tools, create commitments, or write external trackers without explicit authority | Extract one registered, deterministic Workflow Recommender Advisory Atom; candidates become data/Method Implementations and can be added/removed independently | `run-code-intel.ps1:109`; `run-code-intel.ps1:3530`; `OpenSpec-Detector.ps1:2`; `CONTEXT.md:112` | +| qiaomu-goal-meta-skill / awesome-agent-loops | Goal authoring and choice of persistent execution-loop pattern before scanning/execution | Design reference and upstream task-contract input | No scanner invocation; documented Agent Goal Intake handoff (`/goal`, `/loop`, `/schedule`) | Agent-intake layer; external references retain implementation ownership | `reference-internalized` | None at scanner runtime | Goal/loop text cannot become repository evidence, gate result, or scanner state | Keep the internal Agent Goal Intake contract; swap or remove authoring catalogs without scanner changes | `docs/adr/0002-agent-goal-intake-outside-scanner.md:3`; `docs/agent-goal-intake.md:9` | +| MetaHarness | Packaging/distribution pattern for branded CLI, host bundle, release gates, SBOM and provenance | Design reference | No runtime invocation | Harness/distribution layer, outside scanner | `reference-only` | None | Packaging must not own scanner facts or destabilize artifact consumers | Use any harness factory or direct release tooling; preserve scanner/artifact contracts | `docs/adr/0003-harness-factory-as-distribution-reference.md:3`; `docs/harness-factory-reference.md:18` | +| yao-meta-skill | Benchmark for trigger design, evaluation evidence, portability, release gates and failure libraries | Design/quality reference | Not executed; `test-skill-development-benchmark.ps1` verifies internalized benchmark surfaces | Pipeline owns its benchmark; upstream owns reference implementation | `reference-internalized` | None at runtime | Passing local documentation tests is not proof that upstream tooling ran or that runtime behavior conforms | Maintain internal benchmark or replace its reference source while preserving measurable quality criteria | `docs/adr/0004-yao-meta-skill-as-skill-development-benchmark.md:3`; `docs/skill-development-benchmark.md:20`; `docs/skill-development-benchmark.md:45` | +| Ponytail | Rejects unnecessary Agent-produced code/docs/config/abstraction/process; requires Necessity Trace | Governance policy/reference, not repository evidence | Vocabulary in `CONTEXT.md`; terminology checked by `test-skill-development-benchmark.ps1`; no comprehensive runtime enforcement found | Pipeline policy | `reference-internalized` | Low conceptual coupling; runtime enforcement remains pending | It cannot justify skipping understanding, safety, evidence or verification; missing trace means reject output, not invent value | Preserve canonical policy and implement tool-neutral Necessity Trace gate; source project may be removed without semantic loss | `CONTEXT.md:22`; `CONTEXT.md:25`; `test-skill-development-benchmark.ps1`; restored checkpoint `20260713-133502-code-intel-commitment-closure.md` | +| mattpocock/skills | Issue-tracker selection, triage vocabulary, domain-doc layout and delivery-flow concepts | Design/reference input to agent intake and recommender | No scanner runtime dependency; concepts appear in project-management docs and workflow detector | Pipeline owns absorbed concepts; upstream owns skills | `reference-internalized` | None at scanner runtime; medium conceptual coupling in recommender | Skills cannot replace scanner artifacts or silently create hosted work | Retain internal issue/triage/domain contracts; replace source reference or workflow candidate independently | `docs/adr/0006-project-management-support-as-agent-intake.md:3`; `docs/project-management-support.md:3`; `docs/openspec-detector.md:11` | +| Linear | Optional hosted issue projection | External mutable project-management state, not engineering evidence authority | No scanner write path; external tooling only when explicitly requested or already authoritative | External tracker/project owner; Pipeline has no runtime ownership | `reference-only` | None in scanner; optional output projection only | Credentials stay outside repo; exactly one task-state authority; availability/quota cannot block Pipeline | Prefer existing Git-backed Work-OS/repo-native graph; stop projection or migrate deliberately without touching scanner artifacts | `docs/project-management-support.md:20`; `docs/agents/issue-tracker.md:10`; `docs/agents/issue-tracker.md:23`; `docs/adr/0008-local-first-project-control-plane.md:27` | +| Obsidian / LLM Wiki | Knowledge view and, only for an already reviewed Git-backed Work-OS contract, possible task-state owner | Materialized knowledge/project-management view | No scanner runtime path; optional mirror/index/link workflow | External/local Work-OS owner; Pipeline owns only source artifacts | `reference-only` | None in scanner; optional output projection only | Never replaces scanner measurements; task authority must be explicit and singular | Remove/migrate wiki projection while retaining repo docs and artifact runs as technical evidence | `docs/project-management-support.md:12`; `docs/project-management-support.md:30`; `docs/adr/0008-local-first-project-control-plane.md:29` | +| my-code-machine | Host-machine detection, organization, cleanup, checkpoint and sync capabilities proposed for a separate `machine` boundary | Control-plane effects and host-state observations | No merged `machine` crate exists; proposed Python-to-Rust migration only | Proposed Pipeline workspace `machine`/`sync` crates; current external Python project retains implementation | `proposed` | Unknown/high migration scope; no current runtime integration evidence | Host mutation/sync is distinct from target-repository scanning and requires explicit effect boundaries; big-bang rewrite is rejected by later ADR direction | Keep current tool operational until atom-by-atom parity; abandon merge without changing scanner contracts, or integrate behind separate CLI/crate boundary | `docs/adr/0001-merge-mcm-rust-unified.md:3`; `docs/adr/0001-merge-mcm-rust-unified.md:12`; current `crates/` contains only `code-intel-cli` and `code-nexus-lite` | +| diagnosis.hospital | Triage, disposition, protocols, treatment and surgery-plan routing | Deterministic derived diagnosis/materialized views | `run-code-intel.ps1` constructs `hospital-report.json`, `hospital.md` and surgery-plan artifacts | Pipeline | `active-required` | High: diagnosis functions remain embedded in the large PowerShell runner | Diagnosis consumes admitted evidence and must fail closed when authoritative modalities are missing/untrusted; narrative is not the source of truth | Extract diagnosis atoms behind stable machine contracts; rebuild Markdown from JSON | `orchestration/integrations.json` integration id `diagnosis.hospital`; `run-code-intel.ps1` functions prefixed `New-Hospital`; `docs/hospital-mode.md` | +| artifact.index-committed-only | Cross-repository discovery of the latest A07-committed Artifact Run | Derived, rebuildable materialized index | Rust `artifact index` route; `update-code-intel-index.ps1` compatibility facade | Pipeline | `active-required` | Low: bounded local traversal plus A07 marker/manifest/Artifact Ref validation | Index is never artifact authority; staging, incomplete, forged, and legacy runs remain diagnostic-only in normal mode | Rebuild from committed runs; explicit `-LegacyCompatibilityMode` preserves rollback without deleting old data | `orchestration/integrations.json` integration id `artifact.index-committed-only`; `crates/code-intel-cli/src/artifact_index.rs`; `docs/committed-run-index.md` | + +## Reconciliation findings + +Manifest coverage is closed for this audit: all 12 ids in `orchestration/integrations.json` are explicitly named in the table. The two CodeNexus-lite manifest ids intentionally map to one capability row because they share one optional compatibility implementation; their requiredness now agrees with the canonical optional/nonblocking `codenexus/lite` provider operation. + +1. The central manifest registers 12 integrations, but it does not register Git, Repomix, Native Code Evidence, cocoindex-code, GitHub Solution Research, or the active workflow recommender. Their production participation is therefore registry drift, not a canonical integration contract. +2. CodeNexus-lite requiredness drift is fixed: both manifest entries and canonical `codenexus/lite` are optional compatibility surfaces, and registry validation rejects renewed drift. The duplicate manifest/runtime compatibility alias still needs retirement after its transition window; the independent full CodeNexus provider port remains only a vocabulary/ADR boundary and is not implemented. +3. Only the tree-sitter-v overlay visibly records both a source URL and license in its local integration metadata. The other external capabilities lack repository-wide Reuse Records with pinned revision, license obligations, maintenance/security evidence, owned modifications, upgrade checks and tested exits. +4. Reference-internalized concepts are intentionally not runtime dependencies. Their absence from the integration manifest is correct; their internal semantics need executable contracts or benchmark gates where the commitment requires enforcement. +5. Current machine health is not lifecycle truth: the latest normal run passed required stages but skipped Repomix because the CLI was unavailable. Availability belongs in run evidence, while this map records repository architecture. + +## Required closure artifacts + +This map should be followed by, but does not itself create: + +- one Reuse Record per external executable/provider/reference whose capability remains in scope; +- manifest entries for every production participant, or deletion of that participant; +- provider-port conformance tests for Repowise, graph providers, Sentrux and CodeNexus; +- a registered Advisory Atom contract for workflow recommendation and GitHub research; +- a reviewed decision for the proposed my-code-machine merge that is consistent with ADR 0010's no-big-bang migration rule. diff --git a/docs/artifact-data-contract.md b/docs/artifact-data-contract.md index d9aefb0..303a842 100644 --- a/docs/artifact-data-contract.md +++ b/docs/artifact-data-contract.md @@ -20,6 +20,9 @@ Do not hand-edit artifact runs. Regenerate them with `invoke-code-intel.ps1` or Machine-authoritative files: +- `run-complete.json`: final Run Commit marker. Its `reportSha256` binds the + published `report.json`; a run without a valid marker is incomplete and must + not enter the authoritative artifact index. - `report.json`: scanner execution summary, step outcomes, raw and effective failure categories, artifact paths, and compact summaries. - `repomix-summary.json`: Repomix package metadata for the single-file AI context pack. - `sentrux-failures.json`: normalized Sentrux check/gate failures. `sentrux check` and `sentrux gate` stdout are authoritative; hotspots and file-details are enrichment only. @@ -43,9 +46,50 @@ Tool evidence: - `repomix-output.md`, `repomix-output.xml`, `repomix-output.json`, `repomix-output.txt` - `sentrux-dsm.json`, `sentrux-file-details.json`, `sentrux-hotspots.json`, `sentrux-evolution.json`, `sentrux-what-if.json` - `codenexus-context.json` +- Optional `session-evidence.json` using `code-intel-session-evidence.v1`. It is a privacy-reduced, + snapshot-bound session-review artifact with advisory-only authority; raw traces, prompts, event + summaries, user-message marks, absolute paths, and outside-path values are not published. +- Optional `competitive-intelligence-request.json`, `competitive-intelligence-prompt.md`, and `competitive-score.json`. These are advisory market/product intelligence from the external `compete` workflow; they have no hospital, gate, or discharge authority. - Repowise Understand Anything outputs referenced `report.json` - Greenfield workspace outputs, when generated: `greenfield-workspace/output/specs`, `greenfield-workspace/output/test-vectors`, `greenfield-workspace/output/validation`, `greenfield-workspace/provenance` +## Native Code Evidence Canonical Order + +The array order in native code-evidence artifacts is not semantic. Producers +and parity checks use one canonical order so filesystem traversal order cannot +change an otherwise equivalent result: + +- `files` and `ranking.files`: ascending by normalized `path`; +- `symbols`: ascending by `file`, `startLine`, `kind`, then `name`; +- `chunks`: ascending by `file`, `startLine`, `endLine`, then `id`; +- symbol-to-chunk mappings: ascending by `symbolId`, then `chunkId`; +- `imports`: ascending by `file`, `line`, then `target`. + +Only those array permutations are normalized. Ranking `score` and `reasons`, +field values, and the cardinality shape of each value (`null`, scalar, or +array) remain semantic and must match exactly. + +## Transactional Publication Contract + +Artifact production uses a staging directory named +`.staging-`. The scanner writes artifacts there, rewrites +published path references, promotes the directory to its final timestamped +name, and writes `run-complete.json` last. This compatibility contract binds +and minimally validates `report.json`; it is not yet a whole-artifact manifest +or snapshot validation protocol. + +`run-complete.json` uses schema `code-intel-run-commit.v1` and contains: + +- `generatedAt`: publication timestamp; +- `report`: the repository-relative authoritative report path, currently + `report.json`; +- `reportSha256`: lowercase SHA-256 of the published report bytes. + +Consumers and indexers must reject staging directories, missing or unparseable +markers, unknown marker schemas, invalid digests, and report digest mismatches. +Older timestamp directories without a marker remain readable only through an +explicit direct path; they are not authoritative index candidates. + ## Sentrux Failure Contract `sentrux-failures.json` uses schema `code-intel-sentrux-failures.v1`. @@ -113,6 +157,8 @@ Artifact consumers must preserve these fields: - `report.summary.effectiveFailed` - `report.summary.manualRequired` - `report.summary.failureCategories.providerQuota` +- `report.summary.failureCategories.providerUnavailable` +- `report.summary.failureCategories.configError` - `report.summary.failureCategories.localToolError` - `report.summary.failureCategories.graphMissing` - `report.summary.failureCategories.sentruxFail` diff --git a/docs/artifact-ref-verifier.md b/docs/artifact-ref-verifier.md new file mode 100644 index 0000000..91f1765 --- /dev/null +++ b/docs/artifact-ref-verifier.md @@ -0,0 +1,39 @@ +# Artifact Ref Verifier v1 + +A03 implements one shared trust boundary for capability input artifacts. A consumer supplies an +artifact-root authority independently from the Artifact Ref, the expected Snapshot Identity, and +an expected schema/type contract. Successful verification returns owned payload bytes; downstream +code does not reopen the path. + +Capability execution accepts `--artifact-root ` when the request has non-empty +`inputs`. Artifact paths are relative to that root. Requests without inputs remain compatible and +do not require the option. A request with inputs and no explicit root fails closed. + +The verifier requires the exact v1 Artifact Ref fields, a registered artifact schema/type pair, a +non-null Snapshot Identity equal to the capability request, a regular payload file, a matching +SHA-256 over raw bytes, and a payload that validates against the registered contract. The current +registry contains `code-intel-file-inventory.v1` / `inventory.files` and +`code-intel-repository-snapshot.v1` / `repository.snapshot`; additional consumers add their +contracts centrally rather than branching on provider identity. + +Portable paths reject absolute, UNC, drive, device/ADS, backslash, empty, `.`, `..`, reserved +Windows names (including `CONIN$`, `CONOUT$`, and superscript device aliases), trailing-dot/space, +decomposed combining-mark spellings, duplicate, and Unicode case-colliding spellings before +filesystem access. The filesystem boundary holds the root and every parent directory handle, +opens every component no-follow (`OPEN_REPARSE_POINT` on Windows; `openat` plus `O_NOFOLLOW` on +Linux), rejects link/reparse traversal and non-regular payloads, and returns owned bytes. Two refs +to the same stable file identity, including hardlink aliases, are rejected. Moving an artifact +root does not change verification because location is not identity. + +The handle authority begins at the supplied `--artifact-root`, not at the filesystem volume root. +The operating system may resolve links in ancestors above that supplied path before the verifier +opens the artifact-root handle. The artifact-root entry itself and every Artifact Ref component +below it are opened no-follow and checked for link/reparse substitution. Therefore callers that +require a link-free absolute ancestor chain must first supply an already-resolved trusted root; +the verifier does not claim that ancestors above its authority contain no links. + +Contract, schema, snapshot, location-boundary, stable-identity, size-policy, payload-validation, +and digest failures exit 65. Host read/lock/I/O failures exit 74. The registered payload contracts +include the file inventory and repository snapshot JSON (with duplicate/extra-field rejection). +Provider freshness, completeness, and provenance admissibility +remain A04; artifact creation and publication remain A06. diff --git a/docs/assistance-discovery.md b/docs/assistance-discovery.md new file mode 100644 index 0000000..4bc7e19 --- /dev/null +++ b/docs/assistance-discovery.md @@ -0,0 +1,7 @@ +# Assistance Discovery + +`assistance.discover` begins only from a named `code-intel-engineering-capability-gap.v1`. It compares repository-owned atoms, established methods, external tools, and documentation through the same fit, license, security, integration, reversibility, and evidence fields. + +The result is proposal-only. It has no install, network, authority-event, Adoption Decision, or Committed Engineering Plan effect. Popularity metrics cannot be the sole fit basis. External lookup can later be added only as a separate provider with declared effects; the core remains deterministic over supplied evidence. + +All assessment ratings are closed enums in both schema and runtime. JSON Schema `uniqueItems` rejects byte-equivalent duplicate candidates; the runtime additionally enforces the semantic `x-code-intel-uniqueBy: id` rule so two different dossiers cannot reuse one candidate identity. diff --git a/docs/atomic-development-model.md b/docs/atomic-development-model.md index 66a87f9..f1a13f6 100644 --- a/docs/atomic-development-model.md +++ b/docs/atomic-development-model.md @@ -24,7 +24,7 @@ These are contract and composition problems, not a reason for a big-bang rewrite 1. **Capability Atom** — one responsibility with one request and one result contract. 2. **Snapshot Identity** — repository identity, HEAD, working-tree policy, scope, and input digest. 3. **Artifact Ref** — `{schema, artifactSchema, type, path, sha256, consumedSnapshotIdentity}`; `schema` versions the reference envelope, `artifactSchema` identifies payload validation, content digest is identity, and path is location. -4. **Effect Boundary** — determinism is declared separately; `allowedEffects` is fixed before execution and `observedEffects` is audited afterward. Permission effects are `repo_read`, `local_write`, `network`, or `repo_mutation`. +4. **Effect Boundary** — determinism is declared separately; `allowedEffects` is fixed before execution and `observedEffects` is audited afterward. Permission effects are `repo_read`, `local_write`, `process_spawn`, `network`, or `repo_mutation`. 5. **Domain Verdict** — `pass`, `fail`, `unknown`, or `not_applicable`, separate from process execution status. 6. **Run Commit** — validate in staging, promote atomically, then write `run-complete.json` last. 7. **Materialized View** — Markdown and indexes are rebuildable views over machine JSON, never fact authority. @@ -70,6 +70,10 @@ code-intel capability exec --request - --out Existing PowerShell, Python, and Rust implementations remain valid adapters while they converge on this contract. +Capability requests with Artifact Ref inputs pass an independent `--artifact-root`; see +`docs/artifact-ref-verifier.md`. Verification returns owned bytes so consumers never treat the +path as identity or reopen an already-verified location. + The v1 JSON Schema now validates declaration, request, result, and Artifact Ref envelopes, including declaration dependency ids and legal `status × verdict × exitCode` combinations. The contract also defines cross-envelope coherence for identity, implementation, determinism, snapshot, and effect allowlists. This is a control-plane contract only: current runtime adapters do not yet emit these envelopes or enforce those invariants. ## Target Cache And Reproducibility diff --git a/docs/authority-transition-gate.md b/docs/authority-transition-gate.md new file mode 100644 index 0000000..2f9ac3f --- /dev/null +++ b/docs/authority-transition-gate.md @@ -0,0 +1,20 @@ +# Authority transition gate v1 + +`authority.transition-gate` is a deterministic, provider-neutral policy boundary. Its Rust API is `authority::evaluate_batch(&serde_json::Value)`. The module is intentionally independent of CLI and registry wiring so the runtime coordinator can connect it after shared-file integration is serialized. + +The gate distinguishes Observed Evidence, Engineering Fact, Derived Engineering Model, Proposal, Adoption Decision, and Committed Engineering Plan. Evidence-to-fact, fact-to-model, and model-to-proposal transitions are deterministic policy operations. Adoption and commitment transitions require a non-expired, non-replayed `code-intel-authority-event.v1` with a named approver and known supporting evidence. A proposal may be committed directly only when that explicit event owns the transition. + +LLMs, providers, and recommenders have no fact or commitment authority. They may originate observations or proposals; when one of their proposals is accepted into an adoption or commitment state, the result records `effectiveAuthority: authority_event`, not the originating tool. The gate does not rank work or choose product priorities. + +Requests are evaluated branch by branch. A rejected transition receives no output identifier and does not erase accepted, unrelated analysis. Event replay, duplicate event use, missing approvers, unknown evidence, future or expired events, duplicate branch/output identities, and undeclared edges fail closed. + +Accepted protected branches preserve the complete authority event in the result and append its identifier to `consumedAuthorityEventIds`. Persisting that returned set is the caller's replay-state responsibility; the pure gate never silently keeps process-local authority state. + +The v1 event remains backward compatible and may additionally carry a +`repository-governed-sha256-v1` attestation. When present, A05 recomputes a canonical SHA-256 over +the event schema, id, decision, approver, sorted evidence ids, issue time, and expiry, and accepts +only an id/role pair in the checked-in `trustedApprovers` policy. This is content-bound repository +sign-off, not cryptographic identity authentication: the digest detects edits, while repository +policy supplies trust. Actor, evidence, expiry, and digest changes therefore fail closed. + +The checked policy is `orchestration/authority-transition-policy.v1.json`; the request/event/result contract is `orchestration/schemas/code-intel-authority-transition.v1.schema.json`. diff --git a/docs/automatic-pull-request-beta.md b/docs/automatic-pull-request-beta.md new file mode 100644 index 0000000..629e266 --- /dev/null +++ b/docs/automatic-pull-request-beta.md @@ -0,0 +1,71 @@ +# Automatic pull request beta atom + +`Invoke-CodeIntelAutomaticPullRequest.ps1` is an optional execution atom. Ordinary Code Intel +scans, GitHub solution research, Hospital diagnosis, and workflow recommendations never invoke it. +Finding a repairable problem is a proposal, not permission to edit a repository or create a pull +request. + +## Consent and effect boundary + +The atom is disabled unless every execution supplies all of the following: + +1. a canonical `code-intel-auto-pr-proposal.v1` document containing the exact PR fields; +2. a committed C07 Decision Record whose evidence binds that proposal file's SHA-256 and whose + replay still returns `status=replay`, `questionRequired=false`; +3. a one-time `code-intel-auto-pr-authorization.v1` document bound to that Decision Record; +4. the expected Git HEAD and snapshot identity as separate command arguments; +5. `-AllowRepositoryMutation` and `-AllowNetworkPrCreate`. + +The authorization is valid for exactly one current repository snapshot, one `owner/name`, one base +branch, and one head branch. It expires, permits draft PRs only, and is consumed after successful +creation. A receipt keyed by a canonical semantic digest of the exact proposal fields under the Git common directory +prevents either the same consent from being rewrapped or the same proposal from being approved twice +under different Decision Records. Missing permission, drift, +expiry, concurrent execution, replay, a non-draft request, or malformed authorization fails before +`gh pr create` runs. + +Once the network call starts, the authorization is consumed even if `gh` fails or its output cannot +prove which PR was created. That result is `execution_indeterminate`; an operator must inspect the +repository's pull requests instead of retrying the same authorization and risking a duplicate PR. + +The snapshot identity is SHA-256 over UTF-8 text containing the v1 domain marker, lowercase HEAD, +and the sorted output lines of `git status --porcelain=v1 --untracked-files=all`. This binds consent +to tracked and untracked working-tree state without relying on a machine-specific path. + +## Example + +For the full proposal → decision → C07 record → executor sequence, prefer +`Invoke-CodeIntelAutomaticPullRequestFlow.ps1`. The lower-level executor example below remains useful +for hosts that already own the decision artifacts. + +```powershell +./Invoke-CodeIntelAutomaticPullRequest.ps1 ` + -RepoPath C:\src\project ` + -AuthorizationPath C:\secure\auto-pr-authorization.json ` + -ProposalPath C:\secure\auto-pr-proposal.json ` + -DecisionStore C:\secure\decision-store ` + -DecisionReplayQueryPath C:\secure\auto-pr-replay-query.json ` + -CodeIntelCommand C:\tools\code-intel.exe ` + -ExpectedHead <40-lowercase-hex> ` + -ExpectedSnapshotIdentity <64-lowercase-hex> ` + -AllowRepositoryMutation ` + -AllowNetworkPrCreate ` + -Json +``` + +The Pipeline's initial question only asks whether to enter the automatic-PR proposal flow. It never +authorizes a concrete PR. After a fix branch and exact proposal exist, the caller must hash the +canonical proposal into a new decision request, record the response through C07, and supply a replay +query at execution. A bare “yes”, the initial feature opt-in, a workflow recommendation, GitHub +research evidence, or a successful test run is not execution authority. + +C07 proves content binding and replay validity, not cryptographic human identity. Deployments whose +threat model includes a malicious local writer must obtain the Decision Response through a trusted UI +or add a verifiable digital-signature layer. + +Machine contracts are defined by: + +- `orchestration/schemas/code-intel-auto-pr-authorization.v1.schema.json`; +- `orchestration/schemas/code-intel-auto-pr-proposal.v1.schema.json`; +- `orchestration/schemas/code-intel-auto-pr-execution-result.v1.schema.json`; +- `orchestration/schemas/code-intel-auto-pr-execution-receipt.v1.schema.json`. diff --git a/docs/change-impact.md b/docs/change-impact.md new file mode 100644 index 0000000..e990984 --- /dev/null +++ b/docs/change-impact.md @@ -0,0 +1,16 @@ +# Snapshot-bound Change Impact + +`change impact` derives conservative impact and test candidates from the latest A08-admitted run. +It first revalidates the committed evidence and recomputes the current repository snapshot. A stale +snapshot is a contract failure: impact is never inferred by mixing evidence from one checkout state +with changed paths from another. + +The v1 implementation walks the verified Native Code Evidence import list in reverse from explicit +`--changed` paths. Exact relative/module resolution is high confidence; a unique suffix resolution +is medium confidence. Impacted test files become the minimal candidates. When the graph reaches no +test, same-module test co-location is an explicit fallback. Returned commands are advisory strings +only and are not executed. + +The output names its limitations: the native parser is heuristic and cannot prove runtime calls, +dynamic imports, generated-code edges, reflection, or build-system dependencies. This makes the +result useful for prioritization without presenting it as a semantic proof. diff --git a/docs/code-intel-architecture.md b/docs/code-intel-architecture.md index d7f1594..c321a66 100644 --- a/docs/code-intel-architecture.md +++ b/docs/code-intel-architecture.md @@ -15,7 +15,7 @@ Artifact ownership and reader/writer boundaries are defined in `docs/artifact-da 2. Rust targets - `crates/code-intel-cli`: compiled `code-intel` CLI for integration orchestration, artifact resume, classify, and artifact doctor contracts. - - `crates/code-nexus-lite`: compiled `code-nexus-lite` iii worker for CodeNexus scan/lite/doctor behavior. + - `crates/code-nexus-lite`: incubated source, not a Cargo workspace member and not shipped as a beta binary. The supported beta surface is the optional CodeNexus compatibility adapter and artifact contract. 3. `invoke-code-intel.ps1` Thin operator entrypoint. Runs doctor first, then the pipeline. Supports one direct repo path, one configured repo alias, a repo list, or all configured repos. @@ -28,19 +28,20 @@ Artifact ownership and reader/writer boundaries are defined in `docs/artifact-da 6. Tool adapters - `rg`: exact inventory - - `repowise`: semantic index and optional docs - - `Understand Anything`: graph artifact + - `repowise`: optional semantic index and docs; included in the default plan but non-blocking + - `Understand Anything`: optional graph artifact - `sentrux`: structure gate - `sentruxInsight`: parsed structural deltas and follow-up hints for agents 7. Scoped helpers - `Invoke-ScopedRepowise.ps1` + - `Invoke-RepowiseProviderProbe.ps1` - `Run-ScopedRepowiseDocs.py` - `Invoke-SentruxAgentTool.ps1` 8. Stable-ops helpers - `install-code-intel-pipeline.ps1` - - `test-code-intel-provider.ps1` + - `test-code-intel-provider.ps1` (test wrapper only) - `test-code-intel-pipeline.ps1` - `update-code-intel-index.ps1` - `tools/sentrux-shim` @@ -76,9 +77,11 @@ Detailed extension rules are in `docs/integration-orchestration.md`. ## Failure Model -The pipeline classifies failures into four buckets: +The pipeline classifies failures into six buckets: - `provider_quota` +- `provider_unavailable` +- `config_error` - `local_tool_error` - `graph_missing` - `sentrux_fail` @@ -90,7 +93,7 @@ This classification is written into: `report.json` also includes `sentruxInsight`, a deterministic bridge between Sentrux output and agent action. It records parsed quality, coupling, cycle, and god-file deltas, scan scale, next actions, and CodeNexus hints so an operator can move from "score changed" to "inspect this dependency flow" without rereading raw gate text. -`codenexus-context.json` is the portable CodeNexus-lite layer. It selects files from Sentrux hotspots and DSM risk, then attaches recent git commits and reference hits. A full CodeNexus backend can replace this layer later; the contract is already artifact-first. +`codenexus-context.json` is the portable, optional CodeNexus compatibility layer. It selects files from Sentrux hotspots and DSM risk, then attaches recent git commits and reference hits. A full CodeNexus backend can replace this layer later; the contract is already artifact-first. Failure of this optional layer does not invalidate the beta core. `hospital-report.json` is the diagnosis layer over those artifacts. It does not replace the tools; it organizes their output into modalities and protocols: @@ -131,6 +134,23 @@ This keeps nested external repos from poisoning indexing and keeps current worki ## Standard Commands +The default `normal` route is one manifest-bound Rust production spine: + +```text +A02 snapshot + -> doctor / inventory.rg / graph provider / Sentrux provider + -> native code evidence / Hospital diagnosis + -> A03 verified Artifact Refs + -> A09 terminal manifest + -> A07 atomic commit + -> A08 completed-only index + -> bounded query / impact / freshness reads +``` + +`domain_failed` and `domain_unknown` runs may be committed with verified diagnostic evidence for +forensics, but they never enter the authoritative index. PowerShell entrypoints invoke this spine +and retain legacy routes only as explicit compatibility surfaces. + Install check: ```powershell diff --git a/docs/code-intel-project-conformance.md b/docs/code-intel-project-conformance.md new file mode 100644 index 0000000..c2f9f1f --- /dev/null +++ b/docs/code-intel-project-conformance.md @@ -0,0 +1,52 @@ +# Code Intel project conformance + +This is the project-level acceptance contract selectively mapped from Pon's conformance method. +It evaluates Code Intel Pipeline through executable evidence. It does not reproduce or claim +compatibility with Pon's compiler/runtime features. + +## Mapping + +| Upstream method | Local acceptance meaning | Current state | +| --- | --- | --- | +| CPython differential oracle | reviewed canonical artifact oracle | implemented | +| conformance corpora | repository-state and multi-language fixture corpora | implemented | +| JIT/AOT output agreement | normalized artifact-contract parity | implemented | +| passing floor | monotonic fixture set and count | implemented | +| divergence ledger | scoped, owned, expiring known differences | designed, not active | +| fuzzing | fail-closed negative mutations | implemented | +| free-threading stress | repeated/fresh-root determinism | partial | +| benchmark ratchet | representative latency/resource floor | deferred | + +The machine-owned mapping and profile definitions live in +`orchestration/code-intel-project-conformance-policy.v1.json`. + +## Profiles + +`fast` is the currently usable local/PR profile. It executes the parity floor, component adapter +contract, multi-language corpus test, CPython 3.14 development lane, and fail-closed multi-Agent +merge-queue contract. It accepts the +explicitly partial determinism mechanism, so a fast pass is not release acceptance. + +The merge-queue suite uses isolated fixtures; it verifies readiness and authority semantics without +installing the provider or pushing a remote. `full` is the release target. It requires every mapped mechanism to be `implemented`. It therefore +fails closed until the divergence ledger participates in comparisons, determinism stress is complete, +and a representative performance ratchet exists. + +Run: + +```powershell +./Test-CodeIntelProjectConformance.ps1 -Profile fast +./Test-CodeIntelProjectConformance.ps1 -Profile full -Json +``` + +Exit code `0` means that the selected profile's mechanism readiness and every executable suite pass. +Exit code `1` means conformance was rejected. Malformed or unpinned policy input exits `2`. + +## Authority boundary + +- A suite observation cannot update a floor or add a divergence exception. +- The component language-adapter gate is evidence consumed by this project gate; it is not the + project-level policy. +- `full` may be promoted only by implementing and testing the missing mapped mechanisms, not by + deleting them from the policy or weakening accepted statuses. +- The upstream reference is method provenance only. No Pon code or fixtures are copied. diff --git a/docs/code-intel-three-stage-acceptance.md b/docs/code-intel-three-stage-acceptance.md new file mode 100644 index 0000000..1c69805 --- /dev/null +++ b/docs/code-intel-three-stage-acceptance.md @@ -0,0 +1,103 @@ +# Code Intel three-stage acceptance + +`Invoke-CodeIntelAcceptance.ps1` is the language-neutral acceptance entry point used before an +Agent hands off work, before a queue lands work, and before a human promotes an integration branch. +It does not merge, push, publish, or promote anything. It only returns an acceptance decision. + +## Fixed stage mapping + +| Stage | Required evidence | Project profile | +| --- | --- | --- | +| `agent` | one or more explicit targeted checks, then project conformance | `fast` | +| `land` | project conformance only | `fast` | +| `promote` | project conformance only | `full` | + +Project suites remain owned by `Test-CodeIntelProjectConformance.ps1` and +`orchestration/code-intel-project-conformance-policy.v1.json`. The three-stage entry point delegates +to that command; it does not duplicate suite definitions or test implementations. + +The stage contract and targeted-command allowlist live in +`orchestration/code-intel-acceptance-policy.v1.json`. The adapter also enforces the fixed mapping +independently, so replacing the policy with a weakened mapping blocks the request. + +## Agent targeted checks + +Each targeted check is an explicit JSON object. PowerShell test files must be repository-relative; +they are resolved and containment-checked before any command runs: + +```powershell +$target = @{ + id = "acceptance-contract" + kind = "pwsh" + file = "test-code-intel-acceptance.ps1" + args = @() +} | ConvertTo-Json -Compress + +./Invoke-CodeIntelAcceptance.ps1 -Stage agent -TargetCheckJson $target -Json +``` + +Language tools use `kind=process` and a policy-allowed executable. Arguments are passed as an argv +array without shell evaluation: + +```powershell +$target = @{ + id = "rust-graph" + kind = "process" + executable = "cargo" + args = @("test", "-p", "code-intel", "--test", "graph_adapter") +} | ConvertTo-Json -Compress + +./Invoke-CodeIntelAcceptance.ps1 -Stage agent -TargetCheckJson $target -Json +``` + +For multiple checks, pass a PowerShell string array to `-TargetCheckJson`. All specifications are +validated before the first command executes. Duplicate IDs, repository escapes, shell tokens, +shell executables, inline interpreter evaluation, unsupported properties, and unknown executables +block the complete request. + +A change with no applicable focused test may explicitly declare why: + +```powershell +./Invoke-CodeIntelAcceptance.ps1 ` + -Stage agent ` + -SkipTargetedChecksReason "documentation-only change; no executable behavior changed" ` + -Json +``` + +The fast profile still runs, but the overall status is `skipped-with-reason`, not `pass`. Omitting +both targeted checks and a skip reason is `blocked`. + +## Machine result and exits + +The result schema is `orchestration/schemas/code-intel-acceptance-result.v1.schema.json`. + +| Status | Exit | Meaning | +| --- | ---: | --- | +| `pass` | 0 | every required check passed | +| `fail` | 1 | an executed acceptance check rejected the change | +| `blocked` | 2 | the request, policy, command, or child result was unsafe or malformed | +| `skipped-with-reason` | 3 | evidence was explicitly skipped; this is not acceptance | + +Consumers must treat every nonzero exit and every status other than `pass` as fail closed. + +## Land and promote + +```powershell +./Invoke-CodeIntelAcceptance.ps1 -Stage land -Json +./Invoke-CodeIntelAcceptance.ps1 -Stage promote -Json +``` + +`land` maps to the usable `fast` profile. `promote` maps to `full`; with the current project policy, +full remains intentionally rejected until every release mechanism is implemented. Target overrides +are forbidden at both stages, preventing an operator or Agent from substituting a narrower test for +the project-wide profile. + +## Contract test + +```powershell +./test-code-intel-acceptance.ps1 +``` + +The test uses an isolated repository-shaped fixture whose path contains spaces. It proves all three +stage mappings, target and project failure propagation, explicit-skip behavior, pre-execution input +validation, shell-token rejection, inline-evaluation rejection, and the four-status result contract. diff --git a/docs/codenexus-provider-adapter.md b/docs/codenexus-provider-adapter.md new file mode 100644 index 0000000..ed8f39c --- /dev/null +++ b/docs/codenexus-provider-adapter.md @@ -0,0 +1,17 @@ +# CodeNexus provider adapter + +`provider.codenexus-adapt` is the B04 boundary between CodeNexus evidence producers and A04 evidence admissibility. Full CodeNexus and the local lite compatibility script both translate into `code-intel-codenexus-port.v1`. Their port fields are identical, while `providerId`, `implementationId`, activation, effects, source revision, and snapshot identity remain explicit. + +The Pipeline owns only the adapter and port. CodeNexus owns its process, indexing, storage, retrieval, and impact semantics. The adapter consumes an Artifact Ref and treats `providerData` as opaque. It does not import CodeNexus libraries, open or share a CodeNexus database, reconstruct impact relationships, or copy provider ranking semantics into Pipeline code. + +## Admission and use + +The adapter initially emits `perceptionUsable: false` and `engineeringFacts: []`. A consumer must submit `evidence.request` to A04 and validate the admitted payload against the adapter identity before using the observation. Wrong-snapshot and stale observations fail closed. Partial observations remain domain `unknown`. + +Provider absence is an observation, not an empty result. It is represented as `status: unavailable`, `completeness: partial`, `failure.kind: provider_unavailable`, and opaque `providerData: null`; A04 admits that diagnostic as domain `unknown` without producing Engineering Facts. + +## Full/lite swap and rollback + +Full CodeNexus is the `primary` provider. `Invoke-CodeNexusLite.ps1` is a compatibility implementation and may appear only as `explicit_fallback` or `legacy_rollback`. It uses the same port and A04 path, but its provenance and repository/Git/Sentrux effects remain distinct. The demoted Rust worker is not part of this adapter. + +This design makes provider replacement an adapter-level change: no shared storage migration, Pipeline-side impact algorithm, or change to downstream admission semantics is required. diff --git a/docs/committed-run-index.md b/docs/committed-run-index.md new file mode 100644 index 0000000..d1db0fa --- /dev/null +++ b/docs/committed-run-index.md @@ -0,0 +1,26 @@ +# Committed-run artifact index + +A08 makes the A07 Run Commit boundary meaningful to index consumers. The authoritative index accepts only run directories whose `run-complete.json` is the exact `code-intel-run-commit.v1` marker produced by A07 and whose terminal manifest outcome is `completed`. + +## Admission + +For every candidate run, the Rust engine stably reads the completion marker and its content-addressed manifest, then validates: + +- marker schema and closed field set; +- manifest path and digest; +- marker, manifest, snapshot, and run identity binding; +- terminal manifest shape; +- every Artifact Ref declared by completed nodes, including digest, schema, type, path boundary, and consumed snapshot identity. + +Only the lexically latest valid completed commit per repository is projected into `entries`. A07 may retain `domain_failed` or `domain_unknown` runs and their verified artifacts for audit, but A08 classifies them as `non_completed` diagnostics and never exposes them as current authority. Staging directories, markerless A07 object trees, forged or tampered commits, process failures, and legacy report directories are likewise excluded and retained as stable diagnostic rows. The index never repairs, deletes, or promotes a run. + +## Rebuild and incremental refresh + +`rebuild` scans the artifact authority from scratch. `incremental` accepts an existing index as a hint but revalidates the full A07 boundary; it therefore produces the same canonical bytes as rebuild and cannot preserve an entry whose source was tampered after the prior refresh. The JSON projection has no clock field and is sorted by repository and run identifiers. + +```text +code-intel artifact index --artifact-root --output --operation rebuild +code-intel artifact index --artifact-root --output --operation incremental --existing +``` + +`update-code-intel-index.ps1` is the compatibility facade. Normal mode routes to the Rust engine and writes a Markdown projection alongside the JSON index. The previous report-based traversal remains available only with `-LegacyCompatibilityMode`; it does not delete or migrate legacy data. diff --git a/docs/compatibility-facade-finalize.md b/docs/compatibility-facade-finalize.md new file mode 100644 index 0000000..722d1ae --- /dev/null +++ b/docs/compatibility-facade-finalize.md @@ -0,0 +1,32 @@ +# E06 compatibility facade final audit + +E06 is a verifier-owned audit surface, not a deletion ticket. It freezes E02, E03, E04, E05, +E07, E08, E09, and E10 as independent prerequisites and refuses to approve the facade while any +packet is missing, blocked, unsigned, or lacks current parity, compatibility-window, or rollback +evidence. + +Run the current audit with: + +```powershell +pwsh -NoProfile -File Invoke-CompatibilityFacadeFinalize.ps1 ` + -EvaluatedAt ([DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) ` + -OutFile work/e06/final-manifest.json ` + -Json +``` + +Exit `2` means the audit completed and the result is intentionally blocked. Tool/schema failures +throw and exit nonzero without publishing an approval. The checked-in mode fixtures are explicit +`available=false` records; a clean-machine verifier can provide a separate fixture directory with +doctor/lite/normal/full node observations. Each required node must be registry-backed, enveloped, +and admitted or explicitly not applicable. Non-doctor public modes must also prove A07 commit and +A08 index nodes. + +The final manifest uses +`orchestration/schemas/code-intel-compatibility-facade-finalize.v1.schema.json`. Its +`independentApproval` remains `null` in this implementation. Only a verifier who did not author +E02-E05 or E07-E10 may create a later approval artifact, and only after this audit reports no +unsupported branch. The audit never deletes or reroutes PowerShell. + +Current retained PowerShell is enumerated in +`orchestration/facade-finalize-policy.v1.json`; it is not assumed to be zero. Missing registry +backing, owner, or future expiry is reported as a named unsupported surface. diff --git a/docs/compatibility-retire-codenexus-direct-branch.md b/docs/compatibility-retire-codenexus-direct-branch.md new file mode 100644 index 0000000..8f19480 --- /dev/null +++ b/docs/compatibility-retire-codenexus-direct-branch.md @@ -0,0 +1,34 @@ +# E04 CodeNexus direct branch retirement + +E04 is restricted to the one normal-run branch in `run-code-intel.ps1` that directly invokes +`Invoke-CodeNexusLite.ps1`. It does not authorize changes to the lite implementation, B04 provider +translation, B05 survival scanning, Hospital diagnosis, Sentrux, publication, or any other facade +branch. + +The current call graph is not yet retired. The normal path still invokes the lite script directly. +Separate B04 and B05 facade entry points exist and the full, lite, and unavailable fixtures pass, +but those entry points have not replaced the live normal-path call. Therefore E04 records a real +blocked packet and leaves `run-code-intel.ps1` unchanged. + +Generate and validate a fresh packet after building the Rust CLI: + +```powershell +pwsh -NoProfile -File tools/compatibility/New-CodeNexusDirectRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e04-codenexus-direct ` + -EvaluatedAt +pwsh -NoProfile -File tools/compatibility/Test-CodeNexusDirectRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e04-codenexus-direct +``` + +The packet freezes three B04 modes through one port: full remains primary, lite remains an explicit +fallback/rollback implementation, and unavailable remains `provider_unavailable`. Unavailable then +selects B05, which may report basic repository inventory but must keep structural knowledge unknown. +Provider process and storage ownership remain outside the facade; mode-specific effects are recorded +instead of being flattened into a false parity claim. + +The deletion diff is a one-file, one-hunk, replayable delete-only proposal. E01 validates it and then +rejects it because the real E00 decision is blocked. No route substitution or deletion is executed. +Until the normal facade uses B04/B05, the 30-day observation window completes, and independent E00 +approval exists, `deletionExecuted=false` and `retired=false` are mandatory. The E00 projection in +`status.json` is mirrored by `PG-011` in the Gain Ledger; both remain blocked evidence of unfinished +retirement, not a completed gain. diff --git a/docs/compatibility-retire-doctor-wrapper-branch.md b/docs/compatibility-retire-doctor-wrapper-branch.md new file mode 100644 index 0000000..d3dc7f6 --- /dev/null +++ b/docs/compatibility-retire-doctor-wrapper-branch.md @@ -0,0 +1,31 @@ +# E09 direct doctor-wrapper retirement + +E09 owns only the three direct production-doctor route segments in `invoke-code-intel.ps1`: the +`check-code-intel-tools.ps1` binding, the preflight invocation block, and its existence guard. It +does not own the retained fresh-machine bootstrap script, B10's Rust adapter, A09, publication, +indexing, Hospital, Native Code Evidence, or provider branches. + +The branch is not retired. B10 already provides one envelope result for manifest drift and a +present-but-nonconforming provider, redacts secrets, and keeps readiness separate from conformance. +The public preflight still invokes the PowerShell bootstrap directly, however, and the retained +bootstrap is registered and explicitly observation-only but has no declared expiry. E09 must remain +`blocked`, with `deletionExecuted=false` and `retired=false`. + +Generate and validate a fresh packet after the B07 toolchain digest is current: + +```powershell +pwsh -NoProfile -File tools/compatibility/New-DoctorWrapperRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e09-doctor-wrapper ` + -EvaluatedAt +pwsh -NoProfile -File tools/compatibility/Test-DoctorWrapperRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e09-doctor-wrapper +``` + +The packet freezes the three exact B10 tests, static ownership, the retained bootstrap hash, a +three-hunk single-file deletion draft, and exact normalized rollback replay. E01 validates the +ticket/diff shape and rejects it only because E00 remains blocked. The draft never deletes or edits +`check-code-intel-tools.ps1` and is not deletion authority. + +`PG-015` mirrors the blocked E00 Gain Ledger projection. A future change must first route public +preflight through B10 and give the retained bootstrap an owned expiry/removal criterion; only then +can observation-window and independent-approval evidence make deletion eligible. diff --git a/docs/compatibility-retire-hospital-branch.md b/docs/compatibility-retire-hospital-branch.md new file mode 100644 index 0000000..bbf00c0 --- /dev/null +++ b/docs/compatibility-retire-hospital-branch.md @@ -0,0 +1,24 @@ +# E08 Hospital branch retirement + +E08 owns only the embedded Hospital diagnosis/rendering function block and its invocation block in +`run-code-intel.ps1`. Its stable branch id is +`run-code-intel.hospital.embedded-diagnosis-render`; physical line numbers are not authoritative. +B09 semantics, publication, index, and doctor ownership are excluded. + +The B09 `diagnosis.hospital` capability is registered and its Rust contract tests prove fail-closed +precedence, rebuildable Markdown views, A09-seeded A01 execution, and stable machine parity against +the legacy facade on the same untrusted authoritative fixture. The normal facade still executes the +embedded PowerShell authority, however, so the replacement atom is not production-routed. + +```powershell +pwsh -NoProfile -File tools/compatibility/Test-HospitalRetirementBoundary.ps1 +pwsh -NoProfile -File tools/compatibility/New-HospitalRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e08-hospital -EvaluatedAt ` + -CodeIntel work/e01-review-target/debug/code-intel.exe +pwsh -NoProfile -File tools/compatibility/Test-HospitalRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e08-hospital +``` + +Rollback is rehearsed only on an exclusive copy. Until the normal facade routes through B09, the +30-day usage window completes, and independent approval exists, E00 remains blocked and E01 remains +a draft validation boundary. Therefore `deletionExecuted=false` and `retired=false`. diff --git a/docs/compatibility-retire-index-branch.md b/docs/compatibility-retire-index-branch.md new file mode 100644 index 0000000..362fb76 --- /dev/null +++ b/docs/compatibility-retire-index-branch.md @@ -0,0 +1,24 @@ +# E10 index branch retirement + +E10 owns only the explicit legacy traversal below `function Read-JsonFile` in +`update-code-intel-index.ps1`. Its stable branch id is +`update-code-intel-index.legacy-compatibility-traversal`. A08 index semantics and E05 publication +ownership are excluded. + +Normal public refresh already routes through A08 and emits `code-intel-artifact-index.v1`. Tests +prove rebuild/incremental byte parity and admit only valid A07 runs while diagnosing staging, +markerless, forged, and legacy trees. `-LegacyCompatibilityMode` remains publicly reachable, but its +array output is diagnostic compatibility data and cannot claim the authoritative A08 schema. + +```powershell +pwsh -NoProfile -File tools/compatibility/Test-IndexRetirementBoundary.ps1 +pwsh -NoProfile -File tools/compatibility/New-IndexRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e10-index -EvaluatedAt ` + -CodeIntel work/e01-review-target/debug/code-intel.exe +pwsh -NoProfile -File tools/compatibility/Test-IndexRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e10-index +``` + +E05 publication retirement remains blocked. Until that dependency, the observation window, usage +evidence, and independent approval pass, E00 remains blocked and E01 is only a draft boundary. +Therefore the legacy script remains present, `deletionExecuted=false`, and `retired=false`. diff --git a/docs/compatibility-retire-native-code-branch.md b/docs/compatibility-retire-native-code-branch.md new file mode 100644 index 0000000..4f46622 --- /dev/null +++ b/docs/compatibility-retire-native-code-branch.md @@ -0,0 +1,28 @@ +# E07 embedded Native Code Evidence retirement + +E07 is restricted to the embedded Native Code Evidence function family and its direct +`New-CodeEvidenceLayer` call in `run-code-intel.ps1`. It does not own the B08 algorithm, A09 DAG, +publication, committed-run indexing, Hospital diagnosis, provider adapters, or other facade branches. + +The branch is not retired. Normal and full modes still reach the embedded call unless the separate +`-DagCoordinate` route is explicitly selected. B08 is declared and A09 can execute it after Snapshot +Identity and inventory, but that opt-in route has not replaced the public normal/full path. E07 must +therefore remain `blocked`, with `deletionExecuted=false` and `retired=false`. + +Generate and validate a fresh packet only after the R06/B07 toolchain digest is current: + +```powershell +pwsh -NoProfile -File tools/compatibility/New-NativeCodeRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e07-native-code ` + -EvaluatedAt +pwsh -NoProfile -File tools/compatibility/Test-NativeCodeRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e07-native-code +``` + +The packet freezes normalized legacy/B08 artifact parity, eight verified Artifact Refs, the exact +`repo_read` and `local_write` effects, explicit unknown relationship/call-graph precision for +unsupported languages, B07 registry reconciliation, and a two-segment single-branch rollback replay. +E01 validates the replayable deletion draft and rejects it while E00 remains blocked. + +`PG-012` mirrors the E00 Gain Ledger projection. It is unfinished retirement evidence, not a +completed deletion or a claim that normal/full already execute through A09. diff --git a/docs/compatibility-retire-provider-preflight-branch.md b/docs/compatibility-retire-provider-preflight-branch.md new file mode 100644 index 0000000..fda254d --- /dev/null +++ b/docs/compatibility-retire-provider-preflight-branch.md @@ -0,0 +1,42 @@ +# E03 provider-preflight branch retirement + +E03 is restricted to the historical direct production call from `run-code-intel.ps1` to +`test-code-intel-provider.ps1`. The current production route calls +`Invoke-RepowiseProviderProbe.ps1`; the test-named script is a test-only compatibility wrapper. +`install-code-intel-pipeline.ps1 -CheckProvider` is a separate installer diagnostic and is not +authorized by this single-branch retirement. + +Run the static boundary check with: + +```powershell +pwsh -NoProfile -File tools/compatibility/Test-ProviderPreflightRetirementBoundary.ps1 +``` + +Generate and validate the independent historical-base packet with an exclusive output directory: + +```powershell +pwsh -NoProfile -File tools/compatibility/New-ProviderPreflightRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e03-provider-preflight ` + -EvaluatedAt ` + -CodeIntel work/e01-review-target/debug/code-intel.exe +pwsh -NoProfile -File tools/compatibility/Test-ProviderPreflightRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e03-provider-preflight +``` + +The rollback command accepts only an explicit target or an exclusive rehearsal root. Packet +generation uses the rehearsal form and verifies the live facade digest is unchanged: + +```powershell +pwsh -NoProfile -File tools/compatibility/Restore-ProviderPreflightLegacyBranch.ps1 ` + -RehearsalRoot work/e03-provider-preflight-rollback- +``` + +The B01/A04 proving set is `test-repowise-adapter-contract.ps1`, the quota/index-only case in +`repowise_route`, and the quota plus index-only cases in `repowise_adapter`. These prove that docs +quota does not erase a current index and that index-only evidence still passes A04. + +The historical direct call is discoverable from Git for rollback provenance, but it is not live in +the current facade. Therefore E03 must not delete or reroute the current production probe. A final +retirement packet remains blocked until a historical-base replayable deletion proof, completed +observation window, and independent repository-governed E00 approval exist. Until then +`deletionExecuted=false` and `retired=false` are mandatory. diff --git a/docs/compatibility-retire-publication-branch.md b/docs/compatibility-retire-publication-branch.md new file mode 100644 index 0000000..a239768 --- /dev/null +++ b/docs/compatibility-retire-publication-branch.md @@ -0,0 +1,23 @@ +# E05 publication branch retirement + +E05 owns only the two bounded legacy publication hunks in `run-code-intel.ps1`: staging-directory +allocation and final path rewrite/promotion/legacy completion-marker publication. Artifact generation +and E10 index traversal are excluded. The stable branch id is +`run-code-intel.publication.legacy-staging-marker`; no physical line number is authoritative. + +The current `-DagCoordinate` facade returns after A09 and is not connected to A07. A07's internal +seven-phase fail-closed matrix and marker-last success are green, but they are not represented as a +completed facade failure matrix. Consequently the packet remains blocked and cannot authorize deletion. + +```powershell +pwsh -NoProfile -File tools/compatibility/Test-PublicationRetirementBoundary.ps1 +pwsh -NoProfile -File tools/compatibility/New-PublicationRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e05-publication -EvaluatedAt ` + -CodeIntel work/e01-review-target/debug/code-intel.exe +pwsh -NoProfile -File tools/compatibility/Test-PublicationRetirementPacket.ps1 ` + -PacketRoot orchestration/retirements/e05-publication +``` + +Rollback is rehearsed only on an exclusive copy. Until A09→A07 routing, a real facade phase matrix, +the observation window, and independent approval are complete, `deletionExecuted=false` and +`retired=false`. `update-code-intel-index.ps1` is neither read as deletion input nor modified by E05. diff --git a/docs/compatibility-retire-recommender-branch.md b/docs/compatibility-retire-recommender-branch.md new file mode 100644 index 0000000..12e5624 --- /dev/null +++ b/docs/compatibility-retire-recommender-branch.md @@ -0,0 +1,37 @@ +# E02 recommender branch retirement + +E02 is limited to the duplicated inline workflow recommender in `run-code-intel.ps1` and its one +legacy invocation path. The replacement is `advisory.workflow-recommend`; provider preflight, +publication, CodeNexus, and every other facade branch are outside this ticket. + +Generate a fresh, snapshot-bound packet only after building the Rust CLI: + +```powershell +pwsh -NoProfile -File tools/compatibility/New-RecommenderRetirementPacket.ps1 ` + -OutDir orchestration/retirements/e02-recommender ` + -EvaluatedAt +``` + +The generator runs the recommender golden/contract/effect/authority checks, executes rollback on an +exclusive temporary copy, creates E00 evidence and an E01 draft ticket, and then runs E00. The E00 +approval subject binds `run-code-intel.ps1::run-code-intel.workflow-recommender.inline` and the +single affected file. The deletion evidence is a two-hunk `replayable-delete-only-v1` patch with +base/result blob hashes and no added lines; its prose summary is non-authoritative. The generator +passes that patch through E01 and requires E01 to reject specifically because E00 is still blocked. +It never deletes code. A blocked E00 decision is a hard stop: `status.json` must keep +`deletionExecuted` and `retired` false. + +The current packet is intentionally blocked because no 30-day production usage/compatibility +observation, D02 clean-machine repetition, or independent repository-governed approval exists. +Passing local parity tests or reducing line count cannot substitute for those gates. + +Rollback rehearsal uses: + +```powershell +pwsh -NoProfile -File tools/compatibility/Restore-RecommenderLegacyBranch.ps1 ` + -RehearsalRoot work/e02-recommender-rollback +``` + +The script extracts only the historical inline recommender and invocation from the requested Git +revision and applies them to a copy. Applying it to a real checkout requires the explicit +`-TargetPath` parameter and remains an independently authorized bounded-window action. diff --git a/docs/compatibility-retirement-gate.md b/docs/compatibility-retirement-gate.md new file mode 100644 index 0000000..983ed6e --- /dev/null +++ b/docs/compatibility-retirement-gate.md @@ -0,0 +1,41 @@ +# Compatibility Retirement Gate (E00) + +`compatibility.retirement-gate` decides whether one legacy branch has enough evidence to enter a +separate retirement ticket. It cannot delete, rewrite, disable, or reroute that branch. Its sole +effect is publishing a local, snapshot-bound decision artifact and a Gain Ledger projection. + +The A01 capability consumes one A03-verified closed retirement manifest plus the exact A03-verified +evidence artifacts referenced by that manifest. Extra evidence, missing evidence, reused evidence, +digest mismatches, snapshot mismatches, and unregistered artifact types fail closed. +The request must provide exactly one explicit `options.evaluatedAt` integer; this keeps freshness +evaluation deterministic and replayable instead of reading wall-clock time implicitly. + +Approval requires independently content-bound evidence for: + +- a production-ready replacement atom with a non-cyclic dependency set; +- golden, contract, and effect parity with at least one passing assertion each; +- B07 registry reconciliation for the legacy participant and replacement capability; +- a completed compatibility observation window whose `checkedAt <= evaluatedAt <= expiresAt`; +- an owned rollback command and a real, matching execution with exit code 0; +- production usage observation covering the same compatibility window, with internally reconciled + totals, positive replacement traffic, and zero legacy invocations; +- an admitted C00 necessity decision whose change ID and trace digest bind this retirement record; +- one matching approved dependency state for every replacement dependency, including D02 when it + is a dependency; and +- an approval bound to the SHA-256 of the complete approval subject and a current repository-trusted + authority event. The reviewer must match the trusted event approver and differ from the legacy + owner; `authorIndependent: true` alone has no authority. + +The complete approval subject includes a canonical `::` call path and the +exact sorted, unique set of affected files. Those fields are therefore covered by both the subject +SHA-256 and the independent approval. A later ticket cannot broaden the call path or add a second +file/branch under the same E00 approval. + +`pending` or `rejected` dependency evidence remains a visible blocker. The gate never fabricates a +D02 approval. A replacement that names itself or the legacy capability as a dependency is cyclic +and blocked. Line reduction is explicitly fixed to `false` in the manifest contract because fewer +lines are not correctness evidence. + +The output is `approved` or `blocked`, retains sorted blocker codes, fixes the authority boundary to +`approval_only_no_deletion_authority`, and projects an `approved-for-ticket` or `blocked` Gain +Ledger item. Actual deletion remains an E01+ branch-specific action with its own authority. diff --git a/docs/compatibility-retirement-ticket-template.md b/docs/compatibility-retirement-ticket-template.md new file mode 100644 index 0000000..b650a09 --- /dev/null +++ b/docs/compatibility-retirement-ticket-template.md @@ -0,0 +1,34 @@ +# Compatibility retirement ticket template (E01) + +E01 freezes one small, reversible deletion ticket after E00 has approved the exact retirement +subject. It is a planner/validator boundary: the output remains `draft` and carries +`template_only_no_approval_or_deletion_authority`; it cannot approve, delete, reroute, disable, or +roll back a branch. + +Each ticket names exactly one legacy capability, branch, and call path; one replacement capability +and its dependencies; affected files; golden, contract, effect, usage, rollback-rehearsal, and +deletion-diff evidence; distinct owner and verifier; and an observation expiry. The contract is +closed, so plural/extra branch fields are rejected rather than interpreted. + +The A01 capability consumes A03-verified ticket, E00 manifest, E00 decision, and deletion-diff +artifacts. It requires an `approved` E00 decision, verifies the decision and manifest Artifact Ref +digests, recomputes the E00 approval-subject digest, and compares every projected evidence ref to +the consumed E00 manifest. It also requires the ticket's canonical call path and exact sorted, +unique affected-file set to equal the E00-approved values. + +The deletion diff must name that same branch and exact file set. `deletionsOnly` and `summary` are +descriptive fields, not proof. The authoritative `replayable-delete-only-v1` patch binds every +file's base/result UTF-8 text to SHA-256, supplies exact deletion hunks, forbids added lines, and is +replayed by the runtime to prove that the result is obtained only by deletion. A forged summary, +replacement/addition, hidden touched path, hash mismatch, or non-replayable hunk fails closed with +exit 65. `options.evaluatedAt` is explicit and deterministic; expired observations fail. + +Run the standalone completeness lint with: + +```text +target/debug/code-intel.exe compatibility retirement-ticket lint --ticket --evaluated-at +``` + +Run the content-bound A01 projection with `capability exec compatibility.retirement-ticket-template`. +Passing either command means only that the draft is complete and bound to E00; a separate verifier +and a separately authorized deletion executor are still required. diff --git a/docs/compete-project-score.md b/docs/compete-project-score.md new file mode 100644 index 0000000..4c4fe31 --- /dev/null +++ b/docs/compete-project-score.md @@ -0,0 +1,28 @@ +# Optional Compete project score + +This adapter turns a completed [lbj96347/compete](https://github.com/lbj96347/compete) research run into an advisory Code Intel score. It does not affect Sentrux gates, hospital score, discharge state, or default pipeline execution. + +Prepare an Agent task: + +```powershell +& "$env:CODE_INTEL_HOME/Invoke-CompeteProjectScore.ps1" ` + -Action prepare ` + -RepoPath ` + -ArtifactRoot ` + -CompeteRoot +``` + +The command writes `competitive-intelligence-request.json` and `competitive-intelligence-prompt.md`. An Orca-managed Claude/Codex terminal can consume the prompt; the Agent must have web research access because Orca coordinates execution but does not perform competitive research itself. + +After the Agent writes the normal `compete` datasets, normalize its existing six-axis scoring logic: + +```powershell +& "$env:CODE_INTEL_HOME/Invoke-CompeteProjectScore.ps1" ` + -Action score ` + -RepoPath ` + -ArtifactRoot ` + -CompeteRoot ` + -CompeteDataPath +``` + +The result is `competitive-score.json` with the six upstream axes and their arithmetic mean. Review the InsightKit report and its confidence/provenance before acting on the score. diff --git a/docs/dag-coordinator.md b/docs/dag-coordinator.md new file mode 100644 index 0000000..f08fed0 --- /dev/null +++ b/docs/dag-coordinator.md @@ -0,0 +1,65 @@ +# Deterministic DAG Coordinator v1 + +`run.dag-coordinate` is the A09 in-process coordinator. It owns graph validation, dependency +resolution, bounded ready-node scheduling, dependency outcome propagation, resumable state, and +the terminal run manifest. It does not implement capability atoms, interpret provider payloads, +select tools, write staged artifacts, publish a run, or update the index. Those remain A01/A04, +A06, A07, and A08 boundaries. + +The checked-in contracts are: + +- `orchestration/schemas/code-intel-run-dag.v1.schema.json` +- `orchestration/schemas/code-intel-run-state.v1.schema.json` +- `orchestration/schemas/code-intel-run-manifest.v1.schema.json` + +## Explicit execution port + +Each node declares only an id, registered capability id, and immutable request identity. The DAG +has explicit edges and a caller-selected concurrency bound. Provider names, commands, executables, +tool paths, and effect authority are deliberately absent. A caller wires existing A01 capability +execution through `NodeExecutor`; the coordinator never discovers or invokes a provider or tool. + +The A03 adapter converts only already-verified artifacts into `VerifiedArtifactRef` tokens. There +is no field-based production constructor for these tokens. Ready +nodes receive tokens emitted by successful direct dependencies in sorted dependency order. Raw +paths or unverified Artifact Ref JSON are not scheduler inputs. + +## Determinism and recovery + +Validation rejects empty/invalid graphs, duplicate node ids, duplicate edges, unknown endpoints, +cycles, and concurrency outside `1..=256`. Nodes and edges are canonicalized before producing the +fixed-length `dag-v1:` identity, so declaration order cannot change replay identity and +larger DAGs do not inflate every manifest, checkpoint, query response, and index entry. + +`next_batch` returns lexically sorted ready nodes up to the remaining concurrency budget and marks +them running. `record` accepts a result only for a running node. A checkpoint persists terminal +states; running states become pending so interruption replay cannot pretend an unfinished attempt +completed. Resume rejects a different DAG identity, unknown nodes, wrong-snapshot artifacts, and a +history that completes a node before its dependencies. + +`run_to_completion` executes one bounded ready batch concurrently, records results in deterministic +node order, and continues unrelated branches. Terminal node states distinguish `completed`, +`domain_failed`, `domain_unknown`, `process_failed`, and `dependency_blocked`. Domain failure and +domain unknown may retain A03-verified diagnostic artifacts in the manifest; those artifacts explain +the failed run but do not make it authoritative. Any non-completed terminal node makes the run +non-completed, and only descendants are blocked while unrelated ready nodes continue. + +The production facade is `code-intel run dag-coordinate --repo --out `. Its default +normal graph computes the A02 snapshot and executes `repo.snapshot`, `doctor`, `inventory.rg`, +`evidence.native-code`, `provider.graph-adapt`, `provider.sentrux-adapt`, and +`diagnosis.hospital` through A01 request/result envelopes. The snapshot feeds doctor, inventory, +graph, and Sentrux only after A03 verification; verified inventory feeds Native Code Evidence, and +verified graph/Sentrux evidence feeds Hospital. Doctor domain failure remains fail-closed but does +not stop the unrelated evidence branch. Request/result envelopes and diagnostic failure artifacts +remain in staging for audit and recovery; every manifest artifact is A03-verified and snapshot-bound. + +Tests and controlled conformance runs may add `--doctor-tool-path-prefix ` to probe a +fully explicit tool environment. A09 records that directory in the doctor request, and B10 applies +it only to the bootstrap probe process `PATH`; it does not inject an observation, bypass diagnosis, +or reinterpret exit 10 as success. Normal production runs omit the option and probe the real host +environment. + +The terminal manifest is the A07 handoff printed to stdout and persisted as +`run-manifest.json`; its bound Artifact Ref is persisted as `run-manifest-ref.json`. The +capability audit files remain in their readable A09 layout. A09 does not commit them, create the +A07 completion marker, publish a current-run pointer, or update the A08 index. diff --git a/docs/decision-gap-detector.md b/docs/decision-gap-detector.md new file mode 100644 index 0000000..903589f --- /dev/null +++ b/docs/decision-gap-detector.md @@ -0,0 +1,36 @@ +# Decision-gap detector + +The C05 detector separates engineering work that can continue deterministically from choices that require explicit authority. It is a classifier, not an interview system and not a decision maker. + +## Boundary + +A blocker becomes a decision gap only when all discoverable facts named by the blocker have been checked and resolved, no missing fact remains, and the remaining blocker is one of the closed choice kinds in `orchestration/decision-gap-rules.v1.json`. Every emitted gap identifies: + +- the blocked decision; +- the discoverable facts already checked; +- at least two options and each option's consequence; +- one recommended answer whose authority kind is `proposal`; +- the exact affected branches. + +Missing or unresolved facts are emitted as `factDiscovery` work. They are never converted into questions or decision gaps. Unknown blocker kinds, duplicate branch or blocker identities, and unknown affected branches fail closed. + +## Branch-local behavior + +Only branches listed in a gap's `affectedBranches` become `blocked_decision_gap`. A branch with missing facts becomes `fact_discovery_required`. Other completed or pending branches retain their state, so deterministic inventory and analysis can continue while a publication choice awaits authority. + +The canonical fixture is `tests/fixtures/decision-gap/risk-acceptance.json`: unresolved residual-risk acceptance blocks `publication`, while `inventory` remains `completed`. + +## Authority and effects + +The detector does not prompt, read interactive input, record answers, emit authority events, adopt proposals, or create committed engineering plans. Its recommendation is explicitly a proposal, its authority state remains unresolved, and its effects list is empty. A separate authority transition must approve any later adoption or commitment. + +## Determinism + +Results are canonicalized by identifier: branches, gaps, fact-discovery records, facts, options, and affected branch lists have stable ordering. The public module seam is: + +```text +load_rule_table(path) -> validated closed rule table +detect(request, rules) -> deterministic detection result +``` + +Detection is pure after the bounded rule table is loaded and has no network or interactive dependency. diff --git a/docs/decision-request-response-port.md b/docs/decision-request-response-port.md new file mode 100644 index 0000000..fc4d6d9 --- /dev/null +++ b/docs/decision-request-response-port.md @@ -0,0 +1,48 @@ +# Decision request/response port + +C06 is the replaceable asynchronous boundary between one C05 Decision Gap and a human-authorized response. It owns correlation and validation only. It does not render UI, choose a transport, grant authority, persist a Decision Record, or couple Pipeline semantics to chat, tmux, or a host application. + +## Closed request and response + +`code-intel-decision-request.v1` binds exactly one `question` to a gap and correlation id. It carries the proposed recommendation, evidence identities and freshness, options with consequences, the required authority and permitted actors, issue/expiry times, and the exact affected DAG branches. Additional fields fail closed, so a second hidden question cannot be smuggled into the request. + +`code-intel-decision-response.v1` binds the same correlation and gap to either a declared option or a non-empty free-form answer. Actor id, authority kind, provenance source, and timestamp are mandatory. A response is rejected before terminal consumption when its correlation, gap, actor, authority, lifetime, processing time, evidence freshness, option, or shape is invalid. Response timestamps later than the processing clock are future-forged and rejected. Once processing time reaches request expiry, no queued response or cancellation can be accepted. + +Cancellation is a terminal non-authorizing event (`code-intel-decision-cancellation.v1`): it carries the same correlation, gap, actor provenance, timestamp, and a reason, but produces no engineering or authority effect. + +## Replaceable adapters + +All adapters implement the same `DecisionRequestResponsePort` interface: + +- `InMemoryDecisionPort` is the deterministic test and embedded-process adapter. +- `FileDecisionPort` exchanges bounded JSON files and is the broker-free asynchronous baseline. +- `PlainTextDecisionPort` renders the complete canonical request JSON as transport-neutral text, including recommendation, options/consequences, evidence refs, required authority, and expiry. It accepts an eight-field tab-separated choice, free-form, or cancel reply. It is suitable for outside-tmux and simple CLI hosts without embedding their UI semantics. +- `NativeStructuredDecisionPort` exchanges JSON values for native structured hosts. + +The production smoke route uses the native structured adapter: + +```text +code-intel decision request-response --request [--response |--cancel ] --now --branch ... +``` + +Adapters can be swapped without changing the request, response, gap, or later C07 Decision Record schemas. No broker or external dependency is required. + +## Fail-closed and branch-local behavior + +Pending, timeout, and cancellation block only `affectedBranches`; every other supplied DAG branch is returned as `continues`. A valid response makes only the affected branch `ready`. Timeout, cancellation, and a valid response make the correlation terminal, so later replay is rejected. Wrong-gap, wrong-actor, stale-evidence, or malformed responses do not consume the pending request, allowing a later valid response to be correlated safely. + +Every exchange result has an empty `effects` list. C06 transports and validates an answer but does not adopt the recommendation, emit an authority event, or commit a plan; those responsibilities remain with A05 and C07. + +## Envelope and exit policy + +Every invocation writes exactly one `code-intel-decision-exchange-result.v1` JSON envelope to stdout. JSON files and stdin are size-bounded UTF-8 and reject duplicate keys before deserialization. + +| Status | Exit | Meaning | +| --- | ---: | --- | +| `resolved` | 0 | One valid correlated response is available. | +| `pending` | 10 | No response is available; only affected branches remain blocked. | +| `timeout` | 11 | Expiry was reached without a response; only affected branches remain blocked. | +| `cancelled` | 12 | A valid non-authorizing cancellation was received. | +| `rejected` | 65 | Usage, JSON, correlation, authority, freshness, clock, or contract validation failed. `diagnostics` is non-empty and no branches or effects are emitted. | + +Serialization/runtime failure uses exit 70. Only `resolved` is a successful decision response; pending, timeout, cancellation, and rejection are machine-distinct and never silently reported as success. diff --git a/docs/delivery-light-speed-measurement.md b/docs/delivery-light-speed-measurement.md new file mode 100644 index 0000000..c9c8d57 --- /dev/null +++ b/docs/delivery-light-speed-measurement.md @@ -0,0 +1,30 @@ +# Delivery light-speed measurement + +`delivery.light-speed-measure` is a deterministic A01 capability that compares two locally recorded, A07-committed run traces. It emits a baseline/current/delta value-stream report. The report is measurement evidence only: it cannot promise a schedule, choose staffing, or authorize delivery. + +## Input and authority + +The capability accepts exactly eight A03-verified Artifact Refs: one `delivery.run-timing-events`, two A07 `run.commit` markers, their two `run.manifest` objects, the C01 method catalog, and the two selected Method Cards. The timing payload must validate against `code-intel-run-timing-events.v1`, use opt-in local monotonic elapsed milliseconds, set `externalPlatform` to `false`, and bind each trace to a commit Artifact Ref rather than copying self-reported commit fields. D04 resolves those refs only from the A03-verified input set, verifies each commit-to-manifest path/SHA and run/snapshot identity, and requires the current commit to match the request snapshot. Missing objects, digest changes, cross-binding mismatches, or snapshot mismatches fail with exit 65 before publication. + +No external telemetry or observability service is required or contacted. The only permitted effect is writing the two declared report artifacts beneath the caller-provided staging directory. + +## Deterministic attribution + +The rules bind C01 Method Cards `value-stream-queue-delay` and `critical-path-pert`. The catalog and cards are both A03 inputs and must byte-match their managed C01 evidence; their Artifact Refs and SHA-256 digests are emitted in report method provenance. A structurally valid card edit is therefore still a contract failure, not an unnoticed methodology change. + +- lead time is `max(completedAtMs) - min(startedAtMs)`; +- only explicit `queue` events count as queue delay; +- explicit handoff, rework, and unnecessary coordination are separate avoidable categories; +- the first `understanding` interval for a subject is irreducible work; later intervals for that subject are repeated understanding; +- required coordination is protected and reported separately; +- `test` and `verification` events must be mandatory and never count as waste; +- critical path selects the maximum-duration predecessor closure; a join includes every declared predecessor and every ancestor exactly once, with deterministic event ordering and lexical tie breaking; +- every delta is `current - baseline` in milliseconds. + +Every category carries the source Artifact Ref SHA-256, JSON pointers, event IDs, and rule ID. This makes attribution auditable without inventing queueing facts that the committed trace did not record. + +## Outputs and limits + +The capability emits closed-contract `light-speed-report.json` and deterministic `light-speed-report.md`. Replaying identical committed input bytes produces identical output bytes; neither output contains a generation timestamp. + +The report describes observed committed intervals only. It does not infer arrival rates, capacity, resource contention, future completion dates, staffing levels, or schedule confidence when those facts are absent. Mandatory verification time is engineering work, never avoidable delay. diff --git a/docs/doctor-envelope.md b/docs/doctor-envelope.md new file mode 100644 index 0000000..d5d4608 --- /dev/null +++ b/docs/doctor-envelope.md @@ -0,0 +1,24 @@ +# Doctor envelope contract + +`doctor` is a registered A01 capability. It consumes exactly one A03-verified +`repository.snapshot` Artifact Ref and emits one +`code-intel-doctor-observation.v1` artifact whose Artifact Ref is bound to the +same snapshot identity. The environment policy is stored without host paths and +is independently SHA-256 bound inside the observation. + +`check-code-intel-tools.ps1` remains the shell-compatible fresh-machine probe. +Its JSON is explicitly marked `observation_only`; the Rust adapter whitelists +fields from that probe, reconciles `orchestration/integrations.json`, and removes +paths and command output before publication. Presence, readiness, conformance, +and admissibility are separate fields. Doctor never emits engineering facts and +never claims provider admissibility. + +Missing or forged Artifact Refs, invalid bootstrap JSON, or an unreadable +manifest fail as contract/runtime errors. Missing tools, nonconforming present +providers, and manifest drift are domain diagnoses: the result remains a valid +completed envelope with the observation artifact, `verdict=fail`, and +`exitCode=10`. This preserves evidence without converting failure into success. + +The A09 DAG executes `repo.snapshot -> doctor` alongside the existing snapshot +to inventory branch. Direct shell invocation remains a non-authoritative +bootstrap/rollback path until E09 approves retirement of that production branch. diff --git a/docs/evidence-admissibility.md b/docs/evidence-admissibility.md new file mode 100644 index 0000000..5e3ddc5 --- /dev/null +++ b/docs/evidence-admissibility.md @@ -0,0 +1,9 @@ +# Evidence admissibility v1 + +`code-intel evidence validate` is the provider-neutral boundary between provider output and Pipeline Observed Evidence. It validates a versioned request, applies an explicit freshness policy, and verifies the payload through the A03 Artifact Ref verifier against the A02 snapshot identity. + +The validator does not consult the integration registry to decide provider semantics. A provider unknown to the registry receives exactly the same checks as any registered provider. Provider-specific health probes and translation remain adapter responsibilities. + +An admitted result remains Observed Evidence. `engineeringFacts` is structurally empty in every v1 result; A05 owns later authority transitions. The deterministic `admissionIdentity` makes a replay of the same observation recognizable without manufacturing a new authority event. Partial evidence may be admitted only when it is honestly labelled partial. Provider-unavailable and domain-unknown states remain partial/unknown; a process failure is not admissible evidence. Malformed, stale, snapshot-mismatched, digest-mismatched, or incomplete-as-complete evidence exits 65 with `domainVerdict: unknown`. Host I/O failure exits 74. + +The executable fixture is under `tests/fixtures/evidence-admissibility/good`. diff --git a/docs/evidence-query.md b/docs/evidence-query.md new file mode 100644 index 0000000..d9965cc --- /dev/null +++ b/docs/evidence-query.md @@ -0,0 +1,20 @@ +# Verified Evidence Query + +`artifact query` is the bounded, model-independent read port for A08 committed runs. It rebuilds +the committed-only index, selects the latest admitted run for one repository, re-verifies every +Artifact Ref against its registered schema, digest, and snapshot identity, and only then applies +schema, type, or content filters. + +The optional `--repo-path` flag recomputes the repository snapshot with the policy and scope stored +in the committed `repository.snapshot` artifact. The result reports `current` or `stale`; without a +repository path it reports `unknown` instead of treating artifact presence as freshness. + +The command returns deterministic JSON under `code-intel-evidence-query.v1`. Matches contain the +original Artifact Ref, the filters that matched, a bounded 400-character preview, and an explicit +verification explanation. It does not invoke a model, mutate a repository, generate code, or infer +semantic claims beyond the verified artifact bytes. + +```text +code-intel artifact query --artifact-root --repo \ + --repo-path --type inventory.files --contains src/lib.rs +``` diff --git a/docs/file-boundary-observation.md b/docs/file-boundary-observation.md new file mode 100644 index 0000000..39fc7e8 --- /dev/null +++ b/docs/file-boundary-observation.md @@ -0,0 +1,26 @@ +# File Boundary Observation + +The file-boundary adapter gives the Pipeline a small, provider-neutral way to resolve local rules for one file. It borrows the useful file-addressability idea from AIGX, but it **does not require AIGX**, import its parser, or make `.aigx/` mandatory. + +## Contract + +The caller supplies a closed `code-intel-file-boundary-request.v1` containing: + +- an expected repository snapshot identity; +- one exact repository-relative path; +- a freshness policy; +- a normalized local boundary document with source digest and observation time. + +Each document entry can contain a role, `forbid` rules, `gotcha` rules, and checks. Rules and checks use stable IDs so other Pipeline observations can cite them. The adapter performs no command execution and does not modify the source repository. + +## Resolution and trust + +Resolution is deliberately limited to an exact repository-relative path. Separators and leading `./` are normalized. Absolute paths, parent traversal, wildcards, duplicate normalized paths, case-only ambiguities, duplicate rule IDs, stale observations, future observations, and snapshot mismatches fail closed. + +An unmatched file is not treated as unconstrained. It returns `status=unknown`, a null boundary, and `no_matching_boundary`. Unsupported source constructs remain explicit `unsupported_construct` diagnostics; a matched boundary is then `partial`, never silently complete. + +The result carries the expected and consumed snapshot identities plus source path, source SHA-256, and observation time. This is Pipeline-owned evidence. A future optional AIGX importer may translate `aigx resolve` output into the local document, but that importer must not bypass this validation boundary. + +## Production entrypoint + +`code-intel provider file-boundary --request --out ` reads the closed request, resolves the boundary, and writes the closed result. The integration is optional and read-only; AIGX is not a runtime dependency. diff --git a/docs/final-commitment-reconciliation-usage.md b/docs/final-commitment-reconciliation-usage.md new file mode 100644 index 0000000..a8d8602 --- /dev/null +++ b/docs/final-commitment-reconciliation-usage.md @@ -0,0 +1,33 @@ +# Final commitment reconciliation usage + +The authoritative reconciliation is +`orchestration/evidence/final-commitment-reconciliation.json`. Its 69 records are derived from the +ticket headings in the frozen `docs/plans/adr-0010-execution-plan.md`; the JSON records claim state, +evidence commands, existing artifacts, independent verdict, and blockers. The Markdown table in +`docs/final-commitment-reconciliation.md` is only a human-readable projection. +Its structural contract is +`orchestration/schemas/code-intel-final-commitment-reconciliation.v1.schema.json`. + +Validate both representations with: + +```powershell +pwsh -NoProfile -File tools/Test-FinalCommitmentReconciliation.ps1 +``` + +The validator fails on a changed ADR digest, any missing/duplicate/reordered ID, an unknown status, +a missing evidence artifact, an incoherent verdict/blocker combination, a retirement packet that +does not actually say `blocked`/`deletionExecuted=false`/`retired=false`, or projection drift. + +Status rules are intentionally conservative: + +- `implemented_verified` requires a concrete command, existing artifact, independent `verified` + verdict, and no blocker. +- `implemented_pending_verification` and `implemented_blocked` are implementation states, not plan + states. They require corresponding verification evidence and must never be used just because a + plan, draft, or packet exists. +- `retirement_blocked` means the retirement evidence packet exists but deletion did not execute. +- `not_implemented` is used for in-progress work that has no verified implementation claim. + +When a ticket changes, update the JSON first, run its evidence command, record only the verdict that +the evidence supports, update the Markdown projection in the same change, and rerun the validator. +Do not turn `planned`, `draft`, `packet exists`, or `in progress` into an implemented claim. diff --git a/docs/final-commitment-reconciliation.md b/docs/final-commitment-reconciliation.md new file mode 100644 index 0000000..03475e5 --- /dev/null +++ b/docs/final-commitment-reconciliation.md @@ -0,0 +1,90 @@ +# Final 69-item commitment reconciliation + +This is the human-readable projection of `orchestration/evidence/final-commitment-reconciliation.json`. +The JSON is authoritative; this table must be regenerated or updated in the same change and is checked byte-for-byte after newline normalization. + +Source plan: `docs/plans/adr-0010-execution-plan.md`
+Source SHA-256: `8ba922970a66f55087ec9711f16e71e4ca1af492ec79da0554d0718b0e580d33`
+Tickets: **69** + +## Status totals + +| Claim status | Count | +| --- | ---: | +| implemented_blocked | 1 | +| implemented_verified | 60 | +| retirement_blocked | 8 | + +## Itemized reconciliation + +| Ticket | Claim status | Independent verdict | Blockers | Evidence artifacts | +| --- | --- | --- | --- | --- | +| A00 — `compatibility.parity-baseline` | implemented_verified | verified | — | `crates/code-intel-cli/tests/capability_exec.rs` | +| A01 — `capability.runtime-exec` | implemented_verified | verified | — | `crates/code-intel-cli/tests/capability_exec.rs` | +| A02 — `repository.snapshot-identity` | implemented_verified | verified | — | `crates/code-intel-cli/tests/snapshot_identity.rs` | +| A03 — `artifact.ref-verify` | implemented_verified | verified | — | `crates/code-intel-cli/tests/artifact_ref.rs` | +| A04 — `evidence.admissibility-validate` | implemented_verified | verified | — | `crates/code-intel-cli/tests/evidence_admissibility.rs` | +| A05 — `authority.transition-gate` | implemented_verified | verified | — | `crates/code-intel-cli/tests/authority_transition.rs` | +| A09 — `run.dag-coordinate` | implemented_verified | verified | — | `crates/code-intel-cli/tests/dag_run.rs` | +| A06 — `artifact.stage-write` | implemented_verified | verified | — | `crates/code-intel-cli/tests/staged_artifact.rs` | +| A07 — `run.commit` | implemented_verified | verified | — | `crates/code-intel-cli/tests/run_commit.rs` | +| A08 — `artifact.index-committed-only` | implemented_verified | verified | — | `crates/code-intel-cli/tests/artifact_index.rs` | +| B01 — `provider.repowise-adapt` | implemented_verified | verified | — | `crates/code-intel-cli/tests/repowise_adapter.rs` | +| B02 — `provider.graph-adapt` | implemented_verified | verified | — | `crates/code-intel-cli/tests/graph_adapter.rs` | +| B03 — `provider.sentrux-adapt` | implemented_verified | verified | — | `crates/code-intel-cli/tests/sentrux_adapter.rs` | +| B04 — `provider.codenexus-adapt` | implemented_verified | verified | — | `crates/code-intel-cli/tests/codenexus_adapter.rs` | +| B05 — `repository.survival-scan` | implemented_verified | verified | — | `crates/code-intel-cli/tests/survival_scan.rs` | +| B06 — `advisory.workflow-recommend` | implemented_verified | verified | — | `crates/code-intel-cli/tests/method_select.rs` | +| B07 — `integration.registry-reconcile` | implemented_verified | verified | — | `crates/code-intel-cli/tests/capability_exec.rs`
`orchestration/integrations.json` | +| B08 — `evidence.native-code` | implemented_verified | verified | — | `crates/code-intel-cli/tests/native_code_evidence.rs` | +| B09 — `diagnosis.hospital` | implemented_verified | verified | — | `crates/code-intel-cli/tests/hospital_diagnosis.rs` | +| B10 — `doctor.envelope-adapt` | implemented_verified | verified | — | `crates/code-intel-cli/tests/doctor_envelope.rs` | +| C00 — `governance.ponytail-gate` | implemented_verified | verified | — | `crates/code-intel-cli/tests/ponytail_gate.rs` | +| C01 — `method.catalog` | implemented_verified | verified | — | `crates/code-intel-cli/tests/method_catalog.rs` | +| C02 — `method.select` | implemented_verified | verified | — | `crates/code-intel-cli/tests/method_select.rs` | +| C03 — `internalization.record-engine` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs` | +| C04 — `assistance.discover` | implemented_verified | verified | — | `crates/code-intel-cli/tests/assistance_discovery.rs` | +| C05 — `decision.gap-detect` | implemented_verified | verified | — | `crates/code-intel-cli/tests/decision_gap.rs` | +| C06 — `decision.request-response-port` | implemented_verified | verified | — | `crates/code-intel-cli/tests/decision_port.rs` | +| C07 — `decision.record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/decision_record.rs` | +| R01 — `internalization.repowise-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R02 — `internalization.graph-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R03 — `internalization.sentrux-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R04 — `internalization.codenexus-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R05 — `internalization.repomix-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R06 — `internalization.native-code-evidence-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R07 — `internalization.cocoindex-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R08 — `internalization.github-research-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-advisory-candidates.md` | +| R09 — `internalization.rg-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R10 — `internalization.git-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R11 — `internalization.tree-sitter-v-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R12 — `internalization.greenfield-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R13 — `internalization.openspec-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R14 — `internalization.spec-kit-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R15 — `internalization.matt-flow-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R16 — `internalization.gstack-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R17 — `internalization.qiaomu-goal-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R18 — `internalization.agent-loops-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R19 — `internalization.metaharness-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R20 — `internalization.yao-meta-skill-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R21 — `internalization.ponytail-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R22 — `internalization.mattpocock-skills-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R23 — `internalization.linear-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R24 — `internalization.obsidian-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R25 — `internalization.llm-wiki-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| R26 — `internalization.my-code-machine-record` | implemented_verified | verified | — | `crates/code-intel-cli/tests/internalization_record.rs`
`docs/internalization-r09-r26-records.md` | +| D01 — `project.orientation` | implemented_verified | verified | — | `crates/code-intel-cli/tests/project_orientation.rs` | +| D02 — `project.orientation-benchmark` | implemented_verified | verified | — | `crates/code-intel-cli/tests/project_orientation_benchmark.rs` | +| D03 — `understanding.quadrant` | implemented_verified | verified | — | `crates/code-intel-cli/tests/understanding_quadrant.rs` | +| D04 — `delivery.light-speed-measure` | implemented_verified | verified | — | `crates/code-intel-cli/tests/delivery_light_speed.rs` | +| E00 — `compatibility.retirement-gate` | implemented_verified | verified | — | `crates/code-intel-cli/tests/compatibility_retirement_gate.rs` | +| E01 — `compatibility.retirement-ticket-template` | implemented_verified | verified | — | `crates/code-intel-cli/tests/compatibility_retirement_ticket_template.rs` | +| E02 — `compatibility.retire-recommender-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e02-recommender/status.json`
`docs/compatibility-retire-recommender-branch.md` | +| E03 — `compatibility.retire-provider-preflight-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e03-provider-preflight/status.json`
`docs/compatibility-retire-provider-preflight-branch.md` | +| E04 — `compatibility.retire-codenexus-direct-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e04-codenexus-direct/status.json`
`docs/compatibility-retire-codenexus-direct-branch.md` | +| E05 — `compatibility.retire-publication-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e05-publication/status.json`
`docs/compatibility-retire-publication-branch.md` | +| E07 — `compatibility.retire-native-code-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e07-native-code/status.json`
`docs/compatibility-retire-native-code-branch.md` | +| E08 — `compatibility.retire-hospital-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e08-hospital/status.json`
`docs/compatibility-retire-hospital-branch.md` | +| E09 — `compatibility.retire-doctor-wrapper-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e09-doctor-wrapper/status.json`
`docs/compatibility-retire-doctor-wrapper-branch.md` | +| E10 — `compatibility.retire-index-branch` | retirement_blocked | blocked | E00 decision remains blocked
deletionExecuted=false
retired=false | `orchestration/retirements/e10-index/status.json`
`docs/compatibility-retire-index-branch.md` | +| E06 — `compatibility.facade-finalize` | implemented_blocked | blocked | independent audit implementation approved, but final facade approval remains blocked
E02-E05 and E07-E10 retirement dependencies are not completed
current audit exits 2 with approvalEligible=false and independentApproval=null | `Invoke-CompatibilityFacadeFinalize.ps1`
`test-compatibility-facade-finalize.ps1`
`orchestration/facade-finalize-policy.v1.json`
`orchestration/schemas/code-intel-compatibility-facade-finalize.v1.schema.json`
`docs/compatibility-facade-finalize.md` | diff --git a/docs/follow-up-automation.md b/docs/follow-up-automation.md new file mode 100644 index 0000000..04503d0 --- /dev/null +++ b/docs/follow-up-automation.md @@ -0,0 +1,65 @@ +# Follow-up automation + +The Pipeline emits `follow-up-automation.json` after normalized failure and Sentrux debt classification. This is a zero-effect advisory artifact, not proof that a skill ran or a pull request was created. + +## Proactive skill suggestions + +`followUpAutomation.proactiveSkillSuggestions.enabled` defaults to `true`. An actionable local-tool failure, an unclassified failed step, or blocking Sentrux debt proposes `/investigate`. Provider quota, provider availability, configuration, and missing-graph failures are not mislabeled as code bugs. Suggestions are proposal-only and always carry `effects: []`. + +Disable or change the suggestion in `pipeline.config.json`: + +```json +{ + "followUpAutomation": { + "proactiveSkillSuggestions": { + "enabled": false, + "bugSkill": "/investigate" + } + } +} +``` + +CLI values override configuration: + +```powershell +pwsh -File invoke-code-intel.ps1 -RepoPath C:\repo -ProactiveSkillSuggestions enabled -BugSkill /investigate +``` + +## Automatic pull requests + +Automatic PR mode defaults to `ask`. When an actionable problem exists, the Pipeline writes `automatic-pr-consent.request.json`, prints the question, and records `consentStatus: pending`. Ordinary scanning and reporting still complete; only `automatic_pr_execution` remains unauthorized. + +The choices are: + +- `keep_disabled`: no PR may be created; +- `enable_once_for_snapshot`: enter the exact-proposal flow. `Invoke-CodeIntelAutomaticPullRequestFlow.ps1` delegates to the executor only after proposal-specific consent, C07 replay, snapshot, HEAD, repository-mutation, and network checks all pass. + +`enabled` means the operator requested the execution path; it is not sufficient authority by itself. The execution atom remains fail-closed until it receives scoped authorization artifacts and both runtime effect switches. `disabled` emits neither a consent request nor an external effect. + +```powershell +pwsh -File invoke-code-intel.ps1 -RepoPath C:\repo -AutomaticPullRequests ask +``` + +The core Pipeline never calls `gh pr create` from the advisory path. It does not listen to Codex, Claude, or OpenCode chat messages directly; a host that wants chat-triggered suggestions must submit normalized bug evidence to the Pipeline. +The automatic-PR question is a feature-flow opt-in, not authority to publish a particular pull +request. A concrete draft PR requires a separately hashed canonical proposal and replay-valid C07 +Decision Record as documented in `automatic-pull-request-beta.md`. + +## One-command exact proposal flow + +Interactive use defaults to `keep_disabled`: + +```powershell +./Invoke-CodeIntelAutomaticPullRequestFlow.ps1 ` + -RepoPath C:\src\project ` + -Repository owner/project ` + -BaseBranch main ` + -Title "Draft: repair detected failure" ` + -Body "Evidence and verification summary" ` + -AllowRepositoryMutation ` + -AllowNetworkPrCreate +``` + +For a noninteractive host, pass a structured `-DecisionResponsePath`, or pass the explicit +`-DecisionOption keep_disabled|enable_once_for_snapshot` together with actor/source provenance. +Without a response, `-NonInteractive` returns `pending` with zero repository/network effects. diff --git a/docs/frontier-quality-loop.md b/docs/frontier-quality-loop.md new file mode 100644 index 0000000..ceb2548 --- /dev/null +++ b/docs/frontier-quality-loop.md @@ -0,0 +1,65 @@ +# Frontier-inspired quality loop + +This note selectively absorbs four method-level ideas from +[`apoorvjain25/frontier`](https://github.com/apoorvjain25/frontier) at revision +`0b39eee3ec9ed0905ad7303772653c7e9bd17831` (MIT). It does not vendor the Frontier +skill, its agents, or its 21 domain craft standards. + +## Owned semantics + +1. **Evidence-bound findings.** A review finding names a concrete location, the violated + criterion, the observed failure, and confidence. Missing evidence is reported as + `unverified`; it is not silently converted to a pass. +2. **Author/reviewer separation.** The reviewer reports defects and does not repair them in + the same pass. For material changes, the reviewer should not be the implementation author. +3. **Earned stop condition.** Completion is based on fresh validation evidence and no open + required findings, not on reaching the end of a generation pass. Repeated whole-artifact + sweeps are optional and risk-sized; they are never a reason for an unbounded loop. +4. **Rule-candidate distillation.** A repeated or high-value judgment may emit a proposed + reusable rule. A proposal is advisory until a maintainer accepts it, adds a counterexample + or fixture, chooses its scope, and supplies a verification command. + +## Pipeline mapping + +| Absorbed idea | Existing Pipeline owner | Local form | +| --- | --- | --- | +| Evidence-bound findings | artifact contract and hospital diagnosis | location + criterion + observation + confidence + evidence state | +| Author/reviewer separation | execution plans and independent verifier conditions | implementer and verifier remain distinct for material changes | +| Earned stop | completion gate and run-commit evidence | fresh targeted checks, explicit gaps, no required work pending | +| Rule distillation | Sentrux rules, method cards, skill benchmarks | candidate -> maintainer decision -> fixture -> governed rule | + +## Distillation candidate shape + +```text +RULE_CANDIDATE: +- scope: +- trigger: +- rule: +- replacement: +- evidence: +- counterexample: +- verification: +- status: proposed +``` + +Candidates must not mutate `.sentrux/rules.toml`, skill instructions, templates, or production +policy automatically. Promotion is a separate authority-bearing change with regression evidence. + +## Deliberately not absorbed + +- Best-of-N generation as a default: expensive and unrelated to repository-understanding runs. +- Two consecutive clean sweeps as a universal gate: useful for high-risk deliverables, excessive + for deterministic scanner output that already has targeted tests and schema validation. +- A taste gate in the scanner runtime: taste is advisory and must not become Engineering Fact. +- Model-specific tuning notes: model routing remains owned by the runtime configuration. +- The 21 craft standards: broad product/design/writing policy is outside this pipeline's scope. + +## Provenance and exit + +Source: `https://github.com/apoorvjain25/frontier`. +Pinned revision: `0b39eee3ec9ed0905ad7303772653c7e9bd17831`. +License: MIT; retain source and license attribution when copied text or substantial portions are +distributed. This document restates method semantics in Pipeline-owned language. + +Remove this reference if the local rules become independently specified and tested, if the source +license or provenance changes, or if the method adds review cost without measurable defect capture. diff --git a/docs/graph-provider-adapter.md b/docs/graph-provider-adapter.md new file mode 100644 index 0000000..d640a9a --- /dev/null +++ b/docs/graph-provider-adapter.md @@ -0,0 +1,41 @@ +# Architecture graph provider adapter + +`provider graph-adapt` is the B02 seam between graph producers and engineering consumers. It translates the existing internal Rust graph output or an explicitly selected Understand-compatible fallback into `code-intel-architecture-graph-port.v1`, then submits the observation to A04 evidence admissibility. It does not build, enrich, rank, or repair a graph. + +## Public route + +```text +code-intel provider graph-adapt \ + --request \ + --artifact-root \ + --evaluated-at \ + --max-age-seconds +``` + +The PowerShell facade exposes the same route through `-GraphAdapterRequest`, `-GraphAdapterArtifactRoot`, `-GraphAdapterEvaluatedAt`, and `-GraphAdapterMaxAgeSeconds`. Success emits one `code-intel-graph-route-result.v1` document and exits `0`. Usage errors exit `64`. Invalid input, stale evidence, a wrong snapshot, digest failure, or payload/port drift emits one rejected route document and exits `65`. Diagnostics never include request bytes. + +## Provider-neutral port + +Internal and external producers share the same closed port fields: status, completeness, freshness, expected and source snapshot identities, provider identity, provenance, payload Artifact Ref, and `anatomyUsable`. The provider mode changes identity, not structure. + +An internal producer must set `fallback` to `null`. An external producer must name a fallback identity and declare activation as `explicit_fallback` or `legacy_rollback`. External execution is never an automatic primary path. + +`anatomyUsable` starts false and becomes true only after all of these conditions hold: + +1. the producer reports a current and complete graph; +2. the graph is bound to the requested snapshot identity; +3. its Artifact Ref digest, schema, and consumed snapshot are valid; +4. A04 returns `domainVerdict=observed`; and +5. the admitted payload agrees with the port provider and provenance. + +A graph file merely being present is not evidence of current anatomy. Wrong-HEAD and stale graphs fail closed through A04. Missing and partial graphs may remain admissible as explicit unknown evidence, but they never become usable anatomy and never produce engineering facts. + +## Payload boundary + +The referenced evidence payload carries `data.architectureGraph` with schema `code-intel-architecture-graph-evidence.v1`. Its snapshot, provider identity, completeness, and provenance must equal the translated port. Current evidence must contain an Understand-compatible graph document; missing evidence must contain `null`; partial evidence may contain either. + +The route deliberately emits `engineeringFacts: []`. Fact promotion belongs to the A04/A05 authority path and later diagnosis atoms, not this adapter. + +## Rollback + +Rollback selects the legacy Understand command as an external provider with `activation=legacy_rollback`. It does not bypass the adapter, change the port contract, relax snapshot binding, or skip A04. diff --git a/docs/hospital-diagnosis.md b/docs/hospital-diagnosis.md new file mode 100644 index 0000000..c646436 --- /dev/null +++ b/docs/hospital-diagnosis.md @@ -0,0 +1,52 @@ +# Hospital diagnosis atom + +`diagnosis.hospital` is the deterministic B09 diagnosis atom. It consumes only +A03-verified Artifact Refs whose payload is an admitted +`code-intel-evidence-admissibility-result.v1`. A04 materializes the verified +payload `data` in that result, so diagnosis never reopens an unverified provider +file or treats enrichment as authority. + +The precedence is stable and evaluated from admitted machine evidence only: + +1. local tool failure +2. provider quota exhausted +3. architecture gate failure +4. architecture graph missing +5. authoritative structural evidence unavailable +6. ungoverned structural scope +7. known modernization debt +8. clean snapshot + +Missing, partial, stale, rejected, or otherwise untrusted authoritative graph or +structural evidence fails closed to an `unknown` diagnosis. Native-code targets +may enrich a treatment plan, but cannot turn unknown authority into a pass. + +The atom emits `hospital-report.json`, `hospital.md`, `surgery-plan.json`, and +`surgery-plan.md`. A03 registers all four schema/type pairs. The JSON documents +are machine authority; Markdown is a rebuildable view and is never an input to +diagnosis. The hospital JSON preserves the legacy stable fields used by existing +readers, including `schema`, `artifacts`, `triage`, `state_machine`, +`modalities`, `policies`, `report_quality`, `diagnosis`, `treatment`, +`protocols`, `tools`, and `surgery_plan`. + +## A09 execution + +The normal default DAG is intentionally unchanged because it still lacks an A01 +producer for A04 admission results. A09 provides an explicit seeded diagnosis +path for already admitted Artifact Refs: + +```text +code-intel run dag-coordinate --repo --out \ + --diagnosis-inputs \ + --seed-artifact-root +``` + +Before scheduling, A09 verifies every seed through A03 against the current A02 +snapshot. It then schedules a coordinator-owned seed boundary followed by the +registered A01 `diagnosis.hospital` capability, and re-verifies all four outputs +through A03. Snapshot mismatch, unknown schema/type, digest mismatch, empty +seeds, and non-admitted evidence fail closed without a hospital report. + +`run-code-intel.ps1` remains the rollback facade until E08. Its stable diagnosis +labels and precedence are the compatibility baseline; new authority belongs to +the A01/A03/A04/A09 path above. diff --git a/docs/internalization-advisory-candidates.md b/docs/internalization-advisory-candidates.md new file mode 100644 index 0000000..f4c93d1 --- /dev/null +++ b/docs/internalization-advisory-candidates.md @@ -0,0 +1,33 @@ +# Advisory candidate internalization records + +R01 through R04, R13 through R20, and R22, are canonical research records under +`orchestration/internalization/`. They are data and provenance records, not dependency installs, +workflow execution, or adoption decisions. + +The local workspace proves the current candidate semantics, the B06 zero-effect authority boundary, +the pipeline-owned files, and small local measurements. It does **not** contain trustworthy upstream +commit and license evidence for these references. Each record therefore pins the exact local +evidence bytes with SHA-256, labels the upstream revision/license as unverified, includes explicit gap +evidence IDs, and remains in `research`. Validation intentionally omits those gap IDs from admitted +evidence, so the engine emits diagnostics and keeps `productionEnabled=false`. + +| Record | Locally measured evidence | Unresolved production blockers | +| --- | --- | --- | +| `internalization.repowise-record` | B01 adapter and conformance SHA-256 plus 2 registered production operations | upstream revision/license, CLI compatibility, quota/security/maintenance, representative value/cost, exit and retirement proof | +| `internalization.graph-record` | B02 adapter and conformance SHA-256 plus separately traced internal and external implementations | external revision/license/runtime/conformance, maintenance/security, representative utility/cost, exit and retirement proof | +| `internalization.sentrux-record` | B03 adapter and conformance SHA-256 plus registered adapter/runtime operations | upstream revision/license, Windows/plugin conformance, maintenance/security, representative value/cost, shim retirement proof | +| `internalization.codenexus-record` | B04 adapter and conformance SHA-256 plus full/lite swap and registered production operations | full-provider revision/license/runtime/security/maintenance, measured localization value/cost, exit and lite retirement proof | +| `internalization.openspec-record` | 5 `openspec-opsx` occurrences in the current advisory atom | upstream revision, license, update, security | +| `internalization.spec-kit-record` | 8 `spec-kit` occurrences in the current advisory atom | upstream revision, license, update, security | +| `internalization.matt-flow-record` | 1 matt-flow candidate branch | upstream revision, license, update, security | +| `internalization.gstack-record` | 1 gstack candidate branch | canonical source, upstream revision, license, update, security | +| `internalization.qiaomu-goal-record` | 7 locally documented goal-contract semantics | upstream revision, license, upstream conformance, update, security | +| `internalization.agent-loops-record` | 3 locally documented loop-pattern choices | upstream revision, license, upstream conformance, update, security | +| `internalization.metaharness-record` | 6 locally documented harness design concerns | upstream revision, license, upstream conformance, retention authority, update, security | +| `internalization.yao-meta-skill-record` | 8 locally documented skill benchmark criteria; local test explicitly is not upstream execution proof | upstream revision, license, upstream conformance, behavioral measurement, update, security | +| `internalization.mattpocock-skills-record` | 4 independently listed absorbed concepts | upstream revision, license, update, security | + +Each record contains its own adoption rung and owned boundary, compatibility/conformance evidence, +measured benefit/cost, maintenance/security and update gaps, owned modifications, rollback, +replacement/exit criteria, and retirement triggers. Reuse and NOTICE views are deterministic C03 +projections only; they cannot authorize production, initialization, external writes, or commitments. diff --git a/docs/internalization-r09-r26-records.md b/docs/internalization-r09-r26-records.md new file mode 100644 index 0000000..48b99fb --- /dev/null +++ b/docs/internalization-r09-r26-records.md @@ -0,0 +1,31 @@ +# R05-R12/R21/R23-R26 Internalization Records + +Status: implemented as canonical lifecycle records; not independently approved. + +These records separate locally recomputable implementation facts from missing upstream or authority evidence. Research is fail-closed; explicit out-of-scope/defer outcomes require a checked-in repository sign-off. + +| Ticket | Record | Current result | Hard boundary | +| --- | --- | --- | --- | +| R05 | `orchestration/internalization/repomix.json` | Fresh audit distinguishes one npm registry-metadata cache entry from zero executable/package payloads; B07 reviewed deletion and zero production call sites remain reconciled | No install or production restoration without pinned provenance, conformance, measurements, and new authority | +| R06 | `orchestration/internalization/native-code-evidence.json` | In addition to B08 parity, a 12-sample manually labeled multilingual corpus runs the real native producer: TP=6, FP=2, FN=2, TN=2, precision=0.75, recall=0.75, supported-language coverage=10/12 | Metrics describe line-heuristic symbol detection only; AST, call-graph, framework, and relationship precision remain outside the claim | +| R07 | `orchestration/internalization/cocoindex.json` | Installed 0.2.37 provenance remains audit-only; B07 marks the participant deleted, removes the integration, configuration lookup, and command discovery; semantic invocations remain zero | Native Code Evidence stays independent; the legacy outcome is a static compatibility tombstone, not a declared disabled provider | +| R08 | `orchestration/internalization/github-research.json` | Authenticated representative blocker query took 12228.7508 ms, returned `manual_required`, zero candidates, resolution@k=0, and `invalid-query`; B07 reviewed deletion removes production network/credential calls | Offline controls are retained only as historical adapter evidence and are not counted as blocker-resolution value | +| R09 | `orchestration/internalization/rg.json` | Current required invocation is traced; lifecycle remains `research` until package provenance, retained license, platform matrix and replacement drill close | No implicit `rg` upgrade or replacement | +| R10 | `orchestration/internalization/git.json` | Git 2.54.0.windows.1 and its local GPL-2.0-only license digest are bound; the provider-neutral alternate-VCS fixture tests mismatch exit 65/no artifacts and rollback to Git or unversioned explicit overlay | Snapshot adapter is read-only; an actual alternate implementation must still pass the full platform fixture matrix | +| R11 | `orchestration/internalization/tree-sitter-v.json` | MIT notice, local overlay hashes, compiled Windows artifact digest, and ABI-alias source digest are recorded; pinned upstream revision, reproducible build, exported-symbol and V-fixture evidence remain open | Pipeline owns overlay/ABI glue, not Sentrux parsing or the grammar | +| R12 | `orchestration/internalization/greenfield.json` | External plugin participation is retired because source/license/effects/reviewed generated-spec value were not bound; only the pipeline-owned plan-only handoff remains, with two fixture paths and zero auto-analyze | The plan contract is not plugin completion and grants no specification or implementation authority | +| R21 | `orchestration/internalization/ponytail.json` | C00 source/test hashes and behavioral boundary are recorded; upstream provenance and measured value remain open | Governance concepts are not an external Ponytail runtime | +| R23 | `orchestration/internalization/linear.json` | Expiring repository-signed `out_of_scope` decision | No connector, credential, API call, issue write, or second task authority | +| R24 | `orchestration/internalization/obsidian.json` | Expiring repository-signed `out_of_scope` decision | No UI/plugin/vault dependency; scanner artifacts remain authority | +| R25 | `orchestration/internalization/llm-wiki.json` | Expiring repository-signed `out_of_scope` decision | No model/provider/data effect; generated prose cannot promote itself to fact | +| R26 | `orchestration/internalization/my-code-machine.json` | Expiring repository-signed defer (`out_of_scope`) decision reconciling ADR 0001 with ADR 0010 | No big-bang merge, host mutation, sync, or migration authority | + +Each record projects through C03 to Reuse and NOTICE documents. The shared measurements are bound +by SHA-256 from `orchestration/internalization/c03-r05-r12-measurements.json`. Unresolved `gap:*` +evidence keeps R05-R12, R21 in research with `productionEnabled: false`; R05 additionally has a +completed implementation retirement and reviewed B07 production deletion. R23–R26 are explicitly +`out_of_scope`; their content-bound, expiring attestations bind approver, decision, evidence, issue +time, and expiry. They are repository-governed sign-offs, not cryptographic identity +authentication. R09 describes an observed existing production dependency; that observation does +not retroactively approve its lifecycle, and representative latency p50/p95 remains an explicit +measurement gap. diff --git a/docs/internalization-record-engine.md b/docs/internalization-record-engine.md new file mode 100644 index 0000000..ad6de5d --- /dev/null +++ b/docs/internalization-record-engine.md @@ -0,0 +1,45 @@ +# Internalization Record engine + +`internalization.record-engine` is the executable Internalization Standard for external projects, +methods, references, adapted capabilities, and selectively owned implementations. It validates and +stores project records; it does not search for, install, upgrade, select, or approve dependencies. +R01–R26 remain the owners of their individual records and decisions. + +Every record identifies the source revision, license and obligations, Open-Source Reuse Ladder rung, +owned boundary, owned modifications, compatibility/conformance/necessity evidence, measured benefit +and cost, maintenance/security evidence, update policy and check date, rollback, replacement/exit, +and retirement state/evidence. Evidence IDs are inputs from admitted evidence boundaries such as +A04; a record never turns those references into Engineering Facts. + +## Enablement rule + +Research remains allowed for a structurally valid record even when a required evidence class is +missing, unknown, expired, or due for update. Production enablement is fail-closed: every evidence +class must be present, known, current, and covered by the lifecycle authority event. The engine +reuses the A05 authority-event validator, including expiry, completeness, and replay protection. +It cannot create an Adoption Decision or Committed Engineering Plan. + +An authority event is reported as consumed only after evidence currency, lifecycle closure, and +state-specific checks all succeed. Failed, incomplete, or expired attempts therefore remain +retryable with the same still-valid authority event after their evidence or closure is repaired. + +Lifecycle states are `research`, `production_enabled`, `rollback`, `replaced`, `retired`, and +`out_of_scope`. Every state change follows the checked transition table and requires a fresh A05 +authority event. Replacement requires a replacement record ID, rollback requires rollback evidence, +and retirement requires completed retirement evidence. Records are retained across rollback and +retirement; audit-only rollback never deletes provenance. + +Records may declare +`authorityRequirements.repositoryGovernedAttestation: true`. Only those declared records require +the backward-compatible A05 repository attestation and trusted approver policy; ordinary v1 A05 +events, including generic `research -> out_of_scope`, remain valid when the declaration is absent. +R23–R26 use this declaration so their expiring out-of-scope/defer decisions cannot be represented by +an unsigned research placeholder. + +## Projections + +The deterministic store emits a project `code-intel-reuse-record.v1` view and a +`code-intel-notice-provenance.v1` NOTICE/provenance view. Both are projections of the canonical +record and carry no installation, external-write, fact-promotion, or adoption authority. +Projection APIs accept only an engine-created sealed evaluation bound to the exact canonical record; +they do not accept caller-authored JSON capable of forging `productionEnabled` or NOTICE provenance. diff --git a/docs/inventory-rg-capability-contract.md b/docs/inventory-rg-capability-contract.md new file mode 100644 index 0000000..a93ad18 --- /dev/null +++ b/docs/inventory-rg-capability-contract.md @@ -0,0 +1,56 @@ +# inventory.rg capability contract + +`inventory.rg` v1 is the deterministic capability-envelope adapter for the existing +`run-code-intel.ps1` file-inventory step. It does not replace the compatibility facade. + +Compatibility is defined over the normalized file set, not the byte order emitted by a +particular `rg --files` process. The legacy runner preserves ripgrep's process order, which is +not stable across otherwise identical invocations. The v1 capability therefore writes unique +paths in ordinal byte order. On Windows, where ripgrep emits representable UTF-8 paths, records +retain the legacy LF delimiter. On Unix, records are raw path bytes with NUL delimiters, so a path +containing a newline or non-UTF-8 byte is not trimmed, split, or otherwise rewritten. This ordering +and platform encoding are versioned materialized-view rules; they do not add or remove path values. + +The adapter applies the legacy default exclusions and accepts `options.inventoryExclude` as an +ordered array of additional ripgrep glob arguments. Tests compare the normalized output of the +real PowerShell runner with the capability output, including hidden files, default exclusions, +custom exclusions, Unicode, whitespace, quotes, and shell metacharacters. + +Glob evaluation is delegated to the pinned `rg` executable rather than reimplemented. The lease +creates an exclusively owned temporary manifest mirror containing empty ordinary files for the +expected inventory paths. Snapshot-controlled `.gitignore`, `.ignore`, and `.rgignore` files retain +their frozen bytes in the mirror. Symlink entries remain bound into the snapshot identity through +their target bytes, but are excluded from both inventory baselines and the artifact: the contracted +`rg --files` invocation does not use `-L`, so symlinks are never recreated or followed. The live repository is +used only for a membership baseline: ripgrep runs with `--no-ignore`, no custom globs, and only the +fixed structural exclusions plus literal, manifest-derived exclusions for every gitlink subtree, +then its normalized path set must equal the mirror's matching +no-ignore baseline. Filtering and artifact bytes come exclusively from a second mirror traversal +using frozen ignore controls plus the default and custom globs. Live ignore-file bytes therefore +cannot affect a `head_only` artifact. Post-consumption lease verification still runs before +publication. Mirror nodes are identity-anchored and removed in reverse order on success or failure. +Gitlink OIDs remain snapshot-bound, while populated submodule worktrees are neither traversed nor +materialized; ripgrep glob metacharacters in Git paths are escaped before each dynamic exclusion is +passed as a direct argument. + +The v1 invocation uses `--no-require-git`, `--no-ignore-parent`, `--no-ignore-global`, and +`--no-ignore-exclude`, and removes `RIPGREP_CONFIG_PATH`. Therefore repository snapshot ignore +control files remain active even in the mirror, while parent ignore files, Git +`.git/info/exclude`, global ignore configuration, and ripgrep config files cannot become +unversioned path-selection inputs. For a non-root scope, `--no-ignore-parent` also means ignore +controls above the scope's traversal root are intentionally inactive; controls at or below that +scope remain snapshot-bound and active. + +`--out` is the only write boundary. The adapter publishes the fixed relative artifact +`files.txt`; requests cannot choose an artifact path. The directory must not already exist. +An empty repository is a successful empty inventory even though ripgrep reports its normal +"no files matched" process code. + +Publication creates the output directory and temporary file exclusively, rejects symlink/reparse +objects at identity checks, publishes with an exclusive hard link, and rechecks the owned directory +and file identities. Identities come from still-open handles: Unix uses device/inode and Windows +uses volume serial/file index from `GetFileInformationByHandle`, never timestamps, size, or other +forgeable metadata. Failure cleanup retains those handles as ownership anchors: Windows deletes by +handle with `SetFileInformationByHandle`, while Unix reopens the path, verifies device/inode, and +then unlinks it. A competing object is preserved, post-link failure leaves no published artifact, +and cleanup failures are included in diagnostics. diff --git a/docs/language-adapter-acceptance-standard.md b/docs/language-adapter-acceptance-standard.md new file mode 100644 index 0000000..c250a67 --- /dev/null +++ b/docs/language-adapter-acceptance-standard.md @@ -0,0 +1,61 @@ +# Language Adapter Acceptance Standard v1 + +Every language adapter is evaluated through one policy and one evidence report. A local test, upstream project, or implementation language cannot define weaker rules for itself. + +This is a component-level adapter gate. Project acceptance is owned by +`docs/code-intel-project-conformance.md`, which composes this gate with corpus, parity-floor, and +other executable conformance suites. + +## Two independent axes + +Claim level describes what an adapter says it knows: + +| Level | Permitted claim | +| --- | --- | +| `inventory` | Files, language identity, hashes, and chunks only | +| `structural` | Declarations, containment, and import observations with measured precision/recall | +| `semantic` | Parser-backed name/type/control-flow facts proven against a semantic oracle | +| `behavioral` | Runtime or compiler behavior proven against a differential oracle | + +Release stage describes how safely the claim may be used: + +| Stage | Purpose | +| --- | --- | +| `research` | Evidence can be inspected; it grants no production authority | +| `candidate` | Contract, corpus, determinism, compatibility, effects, provenance, and rollback floors pass | +| `production` | Stronger measurement floors plus independent verification pass | + +An adapter cannot obtain semantic or behavioral status merely by reaching production. Conversely, strong experimental semantics remain research-only until operational and governance gates pass. + +## Mandatory gates + +1. Contract: stable Code Evidence artifact schemas, schema validation, backward compatibility. +2. Claim boundary: every lower claim is true and every unclaimed higher level remains false. +3. Measured quality: labeled sample count, precision, recall, and declared coverage meet policy floors. +4. Unsupported behavior: unsupported code is explicit and fabricated facts equal zero. +5. Determinism: repeated runs produce stable normalized artifacts. +6. Compatibility: existing parity artifacts still match. +7. Effect boundary: observed effects are declared and policy-allowed; network and repository mutation are rejected for this baseline. +8. Provenance: revision and repository-relative implementation/test paths are pinned; the gate re-hashes both files, and candidate/production implementations require a known license. +9. Rollback: documented for every stage and tested for candidate/production. +10. Independent verification: mandatory for production. +11. Oracle depth: semantic and behavioral claims require stage-specific oracle case floors. + +Thresholds live only in `orchestration/language-adapter-acceptance-policy.v1.json`. Reports contain observations and cannot lower thresholds. +The gate also enforces monotonic policy strength: candidate cannot be weaker than research, and production cannot be weaker than candidate for any threshold or boolean requirement. + +## Commands + +```powershell +.\Test-LanguageAdapterAcceptance.ps1 ` + -Report .\orchestration\acceptance\native-code-evidence-candidate.json ` + -Json +``` + +Exit code `0` means every required gate passed. Exit code `1` means the requested stage or claim is rejected; the JSON result lists exact failed gate ids. Malformed policy or report input exits `2`. + +## Current native baseline + +`evidence.native-code` passes `candidate + structural` at the existing measured floor: 12 labeled samples, precision 0.75, recall 0.75, coverage 0.833333, ten deterministic replays, six compatibility artifacts, explicit unsupported behavior, and zero new runtime dependencies. + +It does not pass `semantic`, `behavioral`, or `production`. Those require parser/differential oracles, stronger measurements, and independent verification rather than a label change. diff --git a/docs/method-catalog.md b/docs/method-catalog.md new file mode 100644 index 0000000..5eb3c52 --- /dev/null +++ b/docs/method-catalog.md @@ -0,0 +1,26 @@ +# Method catalog contract + +`method.catalog` is the C01 engineering-method vocabulary boundary. The checked catalog is +[`orchestration/methods/catalog.v1.json`](../orchestration/methods/catalog.v1.json), its closed +schema is [`code-intel-method-card.v1.schema.json`](../orchestration/schemas/code-intel-method-card.v1.schema.json), +and the independent loader is `crates/code-intel-cli/src/method_catalog.rs`. + +The catalog records established method preconditions and repeatable transformations. It does not +select a method, claim that a method ran, execute a provider, or authorize an engineering decision. +Every card therefore carries `executionPolicy: catalog_only_no_selection_or_execution`; the +catalog manifest carries `selectionPolicy: none_catalog_only`. + +## Validation invariants + +- The manifest is the complete list of checked-in JSON cards and is strictly sorted by stable ID. +- Card and nested-object shapes are closed; missing required fields and unknown fields fail closed. +- IDs, card paths, described evidence, steps, outputs, ports, and confidence levels are unique. +- A step may reference declared evidence or an earlier step only, and every declared output must be + produced. Related method IDs must resolve to another card. +- Source title/version/reference and explicit in-scope/out-of-scope boundaries are descriptive + provenance, not proof that a method was executed. + +The nine seed cards remain separate because their evidence models differ: causal observations, +failure modes, Boolean top events, activity networks, flow timestamps, improvement cycles, +time-ordered process samples, executable interface contracts, and migration seams are not +interchangeable inputs. C02 may later propose deterministic selection; C01 performs none. diff --git a/docs/method-selection.md b/docs/method-selection.md new file mode 100644 index 0000000..5e1bf9a --- /dev/null +++ b/docs/method-selection.md @@ -0,0 +1,40 @@ +# Deterministic method selection contract + +`method.select` is the C02 advisory matcher. It consumes A04 admissibility results, evidence-backed +engineering fact projections, explicit evidence gaps, the approved C01 Method Cards, and the +checked rule table at `orchestration/method-selection-rules.v1.json`. + +The selector does not contain method definitions. Positive rule signals must resolve to a C01 +card's `problemSignals`; contraindication rules must quote an existing C01 contraindication. One +sorted rule exists for every card. This keeps method meaning in C01 while making matching +deterministic and reviewable. + +Each admission envelope carries the original A04 request and its claimed A04 result. C02 calls the +A04 runtime validator against the explicit artifact root, which re-reads and digest-verifies the +original payload, revalidates the complete observation contract and freshness policy, and +recomputes the admission identity. The claimed result must byte-for-value equal that recomputed +result and have the `admitted`/`observed` verdict. C02 does not maintain a weaker copy of A04's +validator. + +Selectable facts must be present in the verified payload's `data.methodSelectionFacts` array. +The request projection must exactly match the payload fact's ID, signal IDs, and evidence kinds, +and cite the recomputed admission identity. Unknown, duplicate, stale, forged, relabeled, or +unreferenced admissions fail closed; every supplied admission must be cited by at least one bound +fact. Thus caller-supplied labels cannot manufacture selection evidence outside an A04-admitted +payload. A04 still emits no Engineering Facts; this payload binding is selection evidence only. + +Results are sorted by Method Card ID and use only `proposal`, `unknown`, or `none`: + +- `proposal`: a rule signal matched, all C01 required evidence is present, and no mapped C01 + contraindication is active. +- `unknown`: a signal matched but required evidence is missing. +- `none`: no rule matched, or a matched method is contraindicated. + +Every match explains matched signals, missing evidence, C01 cost, declared/triggered +contraindications, C01 confidence rules, and deterministic selection confidence. Equal top match +scores set `tie: true`; ordering never resolves the tie. The result is explicitly advisory and +never claims method execution, an Engineering Fact, an Adoption Decision, or a Committed Plan. + +The module is an independent API that requires an explicit artifact-root authority, pending +serialized production wiring after the shared entrypoint work completes. Removing that future +route rolls back C02 without changing C01 or A04. diff --git a/docs/model-channel-routing.md b/docs/model-channel-routing.md new file mode 100644 index 0000000..fdedb73 --- /dev/null +++ b/docs/model-channel-routing.md @@ -0,0 +1,37 @@ +# Model channel routing + +Model execution is optional. Repository snapshotting, inventory, graph extraction, Sentrux checks, diagnosis, and artifact publication remain deterministic when no model channel is ready. + +The runtime separates four concerns: + +1. A **channel** executes model work (`ollama`, a user-supplied compatible endpoint, Claude Code, OpenCode, or Codex). +2. A **configuration broker** such as CC Switch may indicate configuration presence, but is not itself a model channel and never grants permission. +3. The **routing policy** records explicit consumption, external-data, and paid-spend authorization. +4. The **adapter** invokes only the selected ready channel after policy evaluation. + +Inventory is observation only. It must not contain credential values, endpoint URLs or query strings, prompts, model responses, tokens, or credential-store contents. `endpointConfigured` is only a boolean presence signal. `externalEgress` states whether using a channel would send workload data beyond the local trust boundary. + +## Readiness and authorization + +Candidates progress through this closed state chain: + +`discovered -> executable_verified -> auth_present -> model_available -> egress_allowed -> spend_allowed -> ready` + +Authentication is evidence of access configuration, not permission to consume it. `authPresent=unknown` cannot become ready; local channels may use `not_applicable`. Consumption authorization uses `unanswered`, `granted`, or `denied` and separately names allowed cost scopes: + +- `local_compute` +- `subscription_cli` +- `free_or_internal_quota` +- `metered_api` + +Repository-data egress and paid API spend each require their own `granted` decision. A pinned adapter is evaluated first. When it is unavailable, fallback occurs only with `fallbackPolicy=allowed`. + +## Outcomes + +`code-intel model route` emits one of: + +- `ready`: a selected channel plus non-secret execution metadata. +- `consent_required`: a usable channel is blocked by an unanswered or denied authorization. The command exits 2 and performs no model call. +- `deterministic_degraded`: no eligible model channel exists. The command exits 0 so deterministic pipeline stages continue; the LLM-dependent node records `manual_required` and may emit an assistance dossier. + +Contract or protocol violations exit 65, usage errors exit 64, and local I/O failures exit 74. Routing attempts contain only candidate IDs, readiness state, a closed failure category, and a stable reason code. diff --git a/docs/model-executable-handle.md b/docs/model-executable-handle.md new file mode 100644 index 0000000..9c7415d --- /dev/null +++ b/docs/model-executable-handle.md @@ -0,0 +1,9 @@ +# Model Request Synthesis And Executable Handles + +`New-ModelAdapterRequest.ps1` converts a ready routing result and the matching inventory candidate into a closed v2 adapter request. Cost scope, model identity, external-egress posture, and consent states are derived from those inputs; a caller cannot weaken them in a hand-authored synthesis request. + +CLI requests use `New-ModelExecutableHandle.ps1`. The short-lived handle binds adapter identity, canonical path, SHA-256, file length, last-write ticks, observation time, and expiry. `Invoke-ModelChannelDelegate.ps1` revalidates every field immediately before starting the process and rejects expired, mutated, moved, or adapter-mismatched executables before invocation. + +The handle is content-bound and tamper-evident, not a cryptographic authorization signature: a local actor able to rewrite both the executable and handle can recompute its digest. OS replacement between final verification and process creation also remains a platform-level TOCTOU limit. The control prevents stale or accidentally substituted executables; it does not replace host integrity or code-signing policy. + +Raw-path v1 CLI requests are disabled by default. A direct delegate caller must explicitly pass `-AllowLegacyRawExecutable` to enter that compatibility surface; the production `run-code-intel.ps1` facade does not expose or pass that switch. Automatically synthesized requests always use v2 and require a verified handle. diff --git a/docs/multi-agent-merge-queue.md b/docs/multi-agent-merge-queue.md new file mode 100644 index 0000000..37af3db --- /dev/null +++ b/docs/multi-agent-merge-queue.md @@ -0,0 +1,64 @@ +# Multi-Agent Merge Queue + +Code Intel Pipeline treats parallel implementation and repository landing as separate contracts: + +1. Agents work in isolated worktrees. +2. The project acceptance command decides whether a lane is green. It may use Pon-derived + conformance, language adapters, tests, lint, type checks, builds, or any equivalent project-local + standard. +3. `claude-code-merge-queue` serializes rebase, acceptance, and push through its FIFO landing queue. +4. A human promotes the integration branch to the production branch. + +The Pipeline adapter does not copy the queue implementation. It invokes a repository-local install +pinned by the target project and fails closed before `land` unless all eight readiness gates pass. +Ordinary code-intel analysis never requires the queue. + +## Status and validation + +```powershell +./Invoke-MultiAgentMergeQueue.ps1 -Action status -RepoPath C:\path\to\repo -Json +./Invoke-MultiAgentMergeQueue.ps1 -Action validate -RepoPath C:\path\to\repo +``` + +The default command resolver accepts only `node_modules/.bin/claude-code-merge-queue`; it never uses +an unpinned `npx` download. `status` is observational and exits successfully with `ready:false` when +the project is not configured. `validate` uses the same result but exits nonzero when any landing +gate fails. + +## Landing + +```powershell +./Invoke-MultiAgentMergeQueue.ps1 -Action land -RepoPath C:\path\to\lane ` + -AllowRepositoryMutation -AllowNetworkPush +``` + +`land` requires both explicit authority switches because it rebases the lane and pushes a remote. +Readiness never implies mutation or network authority. The adapter delegates only `land`, +`reconcile`, and `land-history`. It cannot initialize or uninstall +the provider, delete worktrees, preview changes, use the emergency bypass, or run `promote`. + +For this repository, the queue's project-local config should use the fast unified acceptance profile: + +```javascript +export default { + integrationBranch: "integration", + productionBranch: "main", + checkCommand: "pwsh -NoProfile -File ./Test-CodeIntelProjectConformance.ps1 -Profile fast", + checksRequired: true, +}; +``` + +Other repositories replace `checkCommand` with their own unified acceptance entrypoint. The adapter +does not assume Python, Pon, Node, or any particular application language; only the selected queue +provider is Node-based. + +## Limits + +- The selected provider coordinates worktrees on one machine, not a distributed fleet. +- Local hooks prevent mistakes and workflow drift; they are not a security boundary against an + adversarial process with shell access. +- A queue proves that the configured check ran, not that the check is sufficient. The project policy + remains the acceptance authority. + +Source: `2233admin/claude-code-merge-queue` revision +`e7a76958dbd3953b84f12abbc2e6bd755aafce53`, version `0.5.1`, MIT license. diff --git a/docs/multi-agent-workspace-governance.md b/docs/multi-agent-workspace-governance.md new file mode 100644 index 0000000..7058445 --- /dev/null +++ b/docs/multi-agent-workspace-governance.md @@ -0,0 +1,43 @@ +# Multi-Agent Workspace Governance + +`Invoke-MultiAgentWorkspacePreflight.ps1` is a read-only admission gate for agents that share a local Git worktree. It inventories the complete worktree before mutation begins; it does not clean, stash, reset, commit, or write files in the inspected repository. + +## Default mutation preflight + +```powershell +pwsh -NoProfile -File ./Invoke-MultiAgentWorkspacePreflight.ps1 -RepoPath . -Intent mutation -Json +``` + +The default intent is `mutation`. A clean repository root exits `0`. A dirty repository root emits its inventory and exits `20`, so an agent must move to a clean dedicated worktree instead of adding more changes to the shared root. + +## Explicit observation + +```powershell +pwsh -NoProfile -File ./Invoke-MultiAgentWorkspacePreflight.ps1 -RepoPath . -Intent observation -Json +``` + +Observation is explicit and exits `0` even when the root is dirty. The result always carries `authority: observation_only`; this path permits inspection only and cannot be treated as write authority. + +## Result contract + +The JSON result includes: + +- tracked, untracked, staged, worktree, and total change counts; +- normalized entries with porcelain status, change group, path, and original path for renames/copies; +- grouped counts for added, modified, deleted, renamed, copied, type-changed, unmerged, untracked, and other changes; +- a deterministic SHA-256 over canonical inventory JSON; +- the decision, reason, intent, and repository-root check. + +The hash excludes timestamps and absolute repository paths. Identical change inventories therefore produce the same hash in different fixture locations. Any missing repository, subdirectory invocation, Git inspection error, malformed porcelain entry, or invalid policy fails closed. + +## Exit codes + +| Code | Meaning | +| --- | --- | +| `0` | Clean mutation preflight or explicit observation allowed | +| `2` | Policy invalid | +| `20` | Dirty root denied for mutation-oriented work | +| `21` | Git worktree root required | +| `22` | Repository or inventory inspection failed | + +This preflight is local workspace governance. It complements project acceptance and merge serialization but does not replace either one, and it does not coordinate agents across machines. diff --git a/docs/native-code-evidence.md b/docs/native-code-evidence.md new file mode 100644 index 0000000..0c578da --- /dev/null +++ b/docs/native-code-evidence.md @@ -0,0 +1,22 @@ +# Native Code Evidence capability atom + +`evidence.native-code` is the deterministic B08 baseline behind the A01 capability envelope and +the A09 run DAG. It consumes exactly one A03-verified `inventory.files` Artifact Ref plus the A02 +Snapshot Identity in the request. Repository reads are guarded by a snapshot consumption lease; +the capability fails closed if the repository changes during extraction. + +The atom preserves the stable v1 machine projections for files, heuristic symbols, file-sized +chunks, symbol containment, heuristic imports, the scorecard, and Agent Code Slice ranking under +`code-evidence/merged/`. Every machine projection is returned as a digest- and snapshot-bound +Artifact Ref and is reverified by A03 before A09 exposes it to downstream nodes. Markdown Agent +Code Slice views are deterministic rebuildable views of those machine artifacts. + +Coverage is deliberately bounded. The built-in extractor uses line heuristics for PowerShell, +Python, JavaScript/TypeScript, Rust, Go, and Java. Other files remain in files/chunks but appear in +`code-evidence/coverage.json` as unsupported. Symbol and import precision are `heuristic`; +relationship precision and call-graph status are always `unknown`. The atom never emits a +call-graph artifact and makes no claim about external cocoindex or specialized semantic graphs. + +Declared and observed effects are exactly `repo_read` and `local_write`. The existing embedded +PowerShell producer remains the compatibility/rollback implementation until later facade +retirement tickets complete parity and independent verification. diff --git a/docs/plans/adr-0010-execution-plan.md b/docs/plans/adr-0010-execution-plan.md new file mode 100644 index 0000000..1f30e35 --- /dev/null +++ b/docs/plans/adr-0010-execution-plan.md @@ -0,0 +1,935 @@ +# ADR 0010 Execution Plan + +Status: proposed, not implemented +Scope: ADR 0009 runtime convergence plus ADR 0010 tool-neutral engineering-intelligence core +Rule: every ticket below delivers exactly one independently testable capability. A checked-in definition, schema, ADR, or plan is not implementation evidence. + +## Outcome and completion evidence + +This plan is complete only when the compatibility facade can produce a committed artifact run through versioned capability envelopes, provider evidence is validated before it becomes fact, recommendations cannot cross authority boundaries, CodeNexus is replaceable through a Pipeline-owned port, the promised method/reuse/decision/orientation capabilities have executable contracts and proving tests, and PowerShell retirement is supported by parity evidence. `run-code-intel.ps1` remains the public compatibility facade until the final retirement gate passes. + +Fresh evidence inspected while writing this plan: + +- ADR 0009 originally states its accepted contract did not itself change runtime execution or publication; the current dirty worktree now contains partial, uncommitted implementation attempts that must be verified against this plan rather than treated as absent or complete. +- ADR 0010 says convergence is future work and forbids a big-bang rewrite. +- `run-code-intel.ps1` still contains the workflow recommender and directly invokes provider preflight and `Invoke-CodeNexusLite.ps1`. +- `run-code-intel.ps1` currently creates a `.staging-` directory, promotes it, rewrites staged path text, and writes `run-complete.json` last; `update-code-intel-index.ps1` rejects staging directories and missing/invalid completion markers. These are partial, dirty-worktree implementations, not yet proof of A06-A08 atomicity, interruption safety, portable identity, envelope coherence, or independent verification. +- `test-transactional-publication.ps1` currently exercises staging exclusion, marker shape, path rewriting, and index admission. It is useful draft regression evidence, but it is untracked and has not independently proven the complete publication contract or all interruption phases. +- `crates/code-intel-cli/src/providers.rs` and `orchestration/integrations.json` currently contain dirty-worktree provider/manifest reconciliation, including canonical `codenexus/lite`, manifest lookup/drift checks, registered `diagnosis.hospital`, and doctor/runtime entries. These are partial/unverified implementations: several routes remain compatibility commands and they do not yet constitute the A04 shared admissibility engine or B01-B04 conformance. +- `docs/architecture/reference-capability-map.md` currently inventories 12 manifest integrations plus drift/reference entries and explicitly says it is not adoption approval or health proof. It is an untracked audit draft and becomes input to B07/R01-R26, not completion evidence for those tickets. +- Current dirty/untracked tests and docs, including `test-integration-orchestration.ps1`, `test-skill-development-benchmark.ps1`, ADR 0010, and the files above, predate or run concurrently with this plan; this plan neither claims them verified nor rewrites them. + +## Delivery rules + +1. Execute tickets in dependency order; parallelize only tickets whose dependencies are complete. +2. Add a failing proving test before changing behavior. Preserve golden artifacts before each extraction. +3. One ticket may change several files but must expose only one new capability responsibility. +4. Existing PowerShell stays as an adapter until the replacement has contract, parity, and rollback evidence. +5. No recommendation, model output, or provider success becomes an Engineering Fact, Adoption Decision, or Committed Engineering Plan by implication. +6. Every external adoption must have a Reuse Record before production enablement. +7. Every ticket requires an implementer and an independent verifier; the verifier must not author the implementation under review. + +## Dependency spine + +```text +A00 parity baseline -> A01 real inventory.rg envelope executor + -> A02 snapshot identity -> A03 Artifact Ref verifier + -> A04 core evidence admissibility -> A05 authority-transition gate + -> A06 staged artifact writer + -> A09 run DAG coordinator -> A07 Run Commit -> A08 committed-run index guard + +A04 -> B01 Repowise adapter + -> B02 graph-provider adapter + -> B03 Sentrux adapter + -> B04 CodeNexus adapter -> B05 survival-scanner fallback +A05 -> B06 workflow recommender Advisory Atom +A04 -> B08 Native Code Evidence atom +A05 + B01..B03 + B05 + B08 -> B09 diagnosis.hospital atom +A09 + B01..B04 + B08 -> B07 production registry reconciliation -> B10 doctor envelope adapter/extraction + +A05 -> C00 executable Ponytail gate +A04 -> C01 Method Card catalog -> C02 deterministic method selection +A04 + A05 -> C03 Internalization Record engine -> R01..R26 per-executable/provider/reference migrations or approved retirement/out-of-scope records +C03 + C01 + A05 -> C04 assistance discovery +A05 -> C05 Decision Gap -> C06 Decision request/response port -> C07 Decision Record +A02 + A03 + B05 -> D01 Project Orientation -> D02 corpus orientation benchmark +D01 -> D03 understanding quadrant +A07 + D02 -> D04 Light-Speed measurement + +A01 + A07 + B04 + B06 + B07 + D02 -> E00 retirement gate engine +E00 -> E01 retirement-ticket template + -> E02/E03/E04/E05/E07/E08/E09 single-branch retirements + -> E10 index retirement (after E05 publication retirement) + -> E06 final facade/DAG gate +``` + +The first implementation slice is A00-A05. It establishes regression evidence, a real `inventory.rg` runtime envelope, portable snapshot and Artifact Ref checks, provider admissibility, and authority enforcement before any provider or recommender extraction. + +## Atomic tickets + +### A00 — `compatibility.parity-baseline` + +- **Owner / boundary:** test-engineer; owns immutable golden inputs and normalized current-output fixtures, not production orchestration. +- **Dependencies:** none. +- **Affected files (initial):** `tests/fixtures/parity/**` (new), `test-code-intel-pipeline.ps1`, `test-integration-orchestration.ps1`. +- **Acceptance criteria:** representative clean, dirty, provider-unavailable, domain-fail, and partial-evidence runs have path/time-normalized golden machine artifacts; fixture update requires an explicit review reason; no production behavior changes. +- **Smallest proving test:** run one fixture twice and assert byte-identical normalized output plus a deliberate mismatch rejection. +- **Compatibility / rollback:** additive test-only capability; rollback is deleting the fixture harness without touching runtime. +- **Ponytail Necessity Trace:** required by the no-big-bang strangler rule to prove later adapters preserve current behavior. +- **Economic implementation lane:** PowerShell test harness using existing JSON utilities; no dependency and no new runtime. +- **Independent verifier condition:** verifier reproduces one golden run in a fresh temp directory and confirms normalization does not hide verdict, provenance, or missing evidence. + +### A01 — `capability.runtime-exec` + +- **Owner / boundary:** executor; owns `code-intel capability exec` request/result I/O and exit-code mapping, not capability-specific logic. +- **Dependencies:** A00. +- **Affected files (initial):** `crates/code-intel-cli/src/main.rs`, new `crates/code-intel-cli/src/capability.rs`, `orchestration/integrations.json`, new contract test fixture(s). +- **Acceptance criteria:** stdin or request-file accepts exactly one v1 request; declaration/request coherence is checked; stdout contains exactly one v1 result; diagnostics use stderr; legal `status × verdict × exitCode` combinations are enforced; the real `inventory.rg` compatibility implementation executes through the path and preserves its normalized A00 artifact. +- **Smallest proving test:** execute `inventory.rg` through a v1 request against the A00 fixture and assert a schema-valid envelope, exit 0, and normalized artifact parity; then change the capability id and assert exit 64 with no partial result artifact. +- **Compatibility / rollback:** existing scripts remain callable; feature flag/explicit subcommand selects the new executor; rollback routes facade to the old command. +- **Ponytail Necessity Trace:** runtime envelopes are the minimum mechanism needed to turn ADR 0009 from vocabulary into enforcement. +- **Economic implementation lane:** small Rust control-plane module reusing current CLI parsing and JSON dependencies; no workflow engine. +- **Independent verifier condition:** verifier checks stdout purity, all documented exit classes, and confirms no capability logic leaked into the executor. + +### A02 — `repository.snapshot-identity` + +- **Owner / boundary:** executor; owns portable identity of consumed repository inputs, not artifact publication. +- **Dependencies:** A01. +- **Affected files (initial):** new Rust snapshot module, `crates/code-intel-cli/src/main.rs`, snapshot fixtures, facade adapter. +- **Acceptance criteria:** identity binds repository identity, HEAD, working-tree policy, scope, and input digest; absolute paths and timestamps do not affect identity; dirty overlays are explicit. +- **Smallest proving test:** copy the same repository snapshot to two paths and assert equal identity; change one scoped byte and assert inequality. +- **Compatibility / rollback:** old timestamp run folders remain navigation views; rollback omits new identity consumption but never rewrites old artifacts. +- **Ponytail Necessity Trace:** closes stale/current evidence confusion with one shared identity primitive. +- **Economic implementation lane:** Rust hashing over Git and existing inventory; no new store. +- **Independent verifier condition:** cross-path, dirty-tree, sub-scope, and missing-Git cases pass documented rules. + +### A03 — `artifact.ref-verify` + +- **Owner / boundary:** executor; owns Artifact Ref schema, digest, and consumed-snapshot validation, not artifact production. +- **Dependencies:** A02. +- **Affected files (initial):** Rust artifact-ref module, envelope schema/tests, compatibility artifact reader. +- **Acceptance criteria:** a referenced payload is accepted only when schema, SHA-256, type, location, and snapshot identity agree; machine-local location is never identity. +- **Smallest proving test:** mutate referenced bytes after ref creation and assert exit 65. +- **Compatibility / rollback:** legacy path-only input is adapter-only and marked unverified; rollback retains legacy reader. +- **Ponytail Necessity Trace:** one verifier prevents every downstream atom from reimplementing trust checks. +- **Economic implementation lane:** shared Rust library function and table-driven fixtures. +- **Independent verifier condition:** verifier covers digest, schema, snapshot, missing file, and relocation cases. + +### A04 — `evidence.admissibility-validate` + +- **Owner / boundary:** executor; owns tool-neutral validation of Observed Evidence against the Pipeline Evidence Provider Port, not provider-native probes, adapter translation, or business authority. +- **Dependencies:** A02, A03. +- **Affected files (initial):** new provider-port/admissibility schema under `orchestration/schemas/`, shared Rust validator, conformance fixture protocol, negative tests. +- **Acceptance criteria:** validation requires provider and implementation identity, source revision or endpoint identity, consumed snapshot, freshness, completeness, payload schema, provenance, and failure semantics; malformed, stale-for-policy, snapshot-mismatched, digest-mismatched, or incomplete-as-complete output is rejected; the core contains no Repowise, graph, Sentrux, or CodeNexus branching. +- **Smallest proving test:** validate a provider-neutral good fixture, then independently mutate snapshot identity and payload digest and assert exit 65/domain unknown without producing an Engineering Fact. +- **Compatibility / rollback:** initially callable beside legacy probes; rollback disables enforcement while preserving emitted observations and conformance evidence. +- **Ponytail Necessity Trace:** one provider-neutral admissibility engine avoids four copies of the same trust boundary. +- **Economic implementation lane:** shared Rust validation functions over A02/A03 primitives and existing JSON Schema dependencies. +- **Independent verifier condition:** verifier uses a synthetic provider unknown to the registry and confirms identical validation semantics plus fail-closed mismatch handling. + +### A05 — `authority.transition-gate` + +- **Owner / boundary:** architect/executor; owns explicit transitions among Observed Evidence, Engineering Fact, Derived Engineering Model, proposal, Adoption Decision, and Committed Engineering Plan; it does not choose product priorities. +- **Dependencies:** A04. +- **Affected files (initial):** new authority schema/policy under `orchestration/`, new Rust policy module, result-envelope provenance extension if required, authority tests. +- **Acceptance criteria:** allowed transitions and required approver/evidence fields are machine-enforced; LLM/provider/recommender output may create only observations or proposals; Adoption Decision and Committed Engineering Plan require an explicit recorded authority event; rejected transitions fail closed and preserve unrelated analysis. +- **Smallest proving test:** attempt to promote a recommender proposal directly to a committed plan and assert rejection; repeat with an explicit approved authority event and assert acceptance. +- **Compatibility / rollback:** initially audit-only beside current outputs, then enforce behind a flag after parity; rollback returns to audit-only while preserving emitted authority records. +- **Ponytail Necessity Trace:** this is the smallest guard that makes the promised “LLM/tool is not a fact or commitment source” boundary real. +- **Economic implementation lane:** deterministic policy table in the Rust core; no OPA or policy server. +- **Independent verifier condition:** verifier tests every edge in the transition table, including replay, missing approver, unknown evidence, and unrelated-branch continuation. + +### A09 — `run.dag-coordinate` + +- **Owner / boundary:** executor; owns dependency resolution, ready-node scheduling, result propagation, resume state, and terminal run outcome for the declared capability DAG; it does not implement atoms, validate provider payload semantics, or publish final runs. +- **Dependencies:** A01, A02, A03. +- **Affected files (initial):** new Rust coordinator/DAG module, capability declarations in `orchestration/integrations.json`, run-state schema, orchestration parity tests, facade adapter. +- **Acceptance criteria:** rejects missing and cyclic dependencies; runs independent ready nodes concurrently; passes only verified Artifact Refs; preserves domain fail versus process failure; skips only dependency-blocked descendants; resumes completed nodes by deterministic identity; emits a complete run manifest for A07. +- **Smallest proving test:** run the real `repo.snapshot -> inventory.rg` two-node DAG through envelopes, assert ordered dependency execution and A00 parity, then add an independent node and prove it still completes when a sibling branch domain-fails. +- **Compatibility / rollback:** `run-code-intel.ps1` remains the outer facade and may select legacy sequential orchestration; rollback switches the facade route without deleting coordinator state. +- **Ponytail Necessity Trace:** a single coordinator is necessary to make the declared graph executable; no workflow engine, queue, or database is introduced. +- **Economic implementation lane:** in-process Rust topological scheduler over the existing registry and filesystem run state. +- **Independent verifier condition:** verifier tests cycle/missing-dependency rejection, concurrency, branch-local failure, resume, deterministic order, and manifest completeness. + +### A06 — `artifact.stage-write` + +- **Owner / boundary:** executor; owns validated writes into a unique staging directory, not final publication. +- **Dependencies:** A03. +- **Affected files (initial):** new Rust artifact writer, capability runtime integration, staging tests. +- **Acceptance criteria:** writes are content-addressed, validated before return, and leave no final run visible; failure reports observed effects and cleans only owned staging. +- **Smallest proving test:** inject a schema failure and assert no final directory or completion marker exists. +- **Compatibility / rollback:** facade continues legacy writes until A07; rollback removes staged path selection. +- **Ponytail Necessity Trace:** transactional publication needs one shared writer, not per-capability file logic. +- **Economic implementation lane:** filesystem primitives only; no database/CAS service. +- **Independent verifier condition:** verifier tests interrupted, duplicate-content, and out-of-scope path writes. + +### A07 — `run.commit` + +- **Owner / boundary:** executor; owns atomic promotion and writing `run-complete.json` last, not artifact generation or indexing. +- **Dependencies:** A06, A09. +- **Affected files (initial):** Rust publication module, facade publication adapter, run-commit schema/tests. +- **Acceptance criteria:** all refs validate before promotion; completion marker is last; failed promotion cannot appear complete; marker binds run manifest digest and snapshot. +- **Smallest proving test:** kill/inject failure before marker write and assert run is uncommitted and recoverable. +- **Compatibility / rollback:** legacy timestamp runs remain readable but are marked legacy-uncommitted; rollback uses the prior writer behind facade. +- **Ponytail Necessity Trace:** closes the known partial-run/index corruption risk with a single transaction boundary. +- **Economic implementation lane:** same-volume rename plus fsync/replace semantics; no transaction service. +- **Independent verifier condition:** verifier exercises interruption at each publication phase and checks marker ordering. + +### A08 — `artifact.index-committed-only` + +- **Owner / boundary:** executor; owns index admission, not run production. +- **Dependencies:** A07. +- **Affected files (initial):** index reader/writer in Rust, `update-code-intel-index.ps1` adapter, tests. +- **Acceptance criteria:** only valid completion markers enter the index; incomplete/forged markers are ignored with diagnosis; index is rebuildable. +- **Smallest proving test:** place complete and staged runs side by side and assert only the complete run is indexed. +- **Compatibility / rollback:** legacy indexing available under explicit compatibility mode. +- **Ponytail Necessity Trace:** makes Run Commit meaningful to consumers. +- **Economic implementation lane:** reuse current index format where possible; no migration database. +- **Independent verifier condition:** rebuild result equals incremental result and rejects forged manifest digests. + +### B01 — `provider.repowise-adapt` + +- **Owner / boundary:** executor; owns Repowise-native health/index/docs translation into the Evidence Provider Port, not admissibility policy or Repowise internals. +- **Dependencies:** A04. +- **Affected files (initial):** `crates/code-intel-cli/src/providers.rs`, Repowise adapter, `test-code-intel-provider.ps1`, conformance fixtures, facade route. +- **Acceptance criteria:** health remains distinct from evidence; index and docs declare different completeness/freshness/effects; quota failure cannot disable index status; all output passes A04 before fact promotion; production no longer runs a test file as its validator. +- **Smallest proving test:** translate success, quota, and index-only fixtures; assert A04 accepts the good fixture and preserves quota as provider-unavailable/partial rather than local failure. +- **Compatibility / rollback:** current CLI/Python probe remains behind the adapter; rollback routes facade to legacy preflight/index-only behavior. +- **Ponytail Necessity Trace:** one thin adapter contains Repowise quirks without contaminating the provider-neutral core. +- **Economic implementation lane:** reuse existing provider registry and probe, adding only translation/conformance code. +- **Independent verifier condition:** verifier tests quota, missing CLI, stale index, successful-but-incomplete docs, and no-secret output. + +### B02 — `provider.graph-adapt` + +- **Owner / boundary:** executor; owns internal/external graph-provider translation into one architecture-graph port, not graph algorithms. +- **Dependencies:** A04. +- **Affected files (initial):** `crates/code-intel-cli/src/providers.rs`, graph adapter/fixtures, `orchestration/integrations.json`, graph tests. +- **Acceptance criteria:** internal Rust and external Understand-compatible outputs share one schema and provenance; current-snapshot binding is mandatory; fallback identity is explicit; stale graph presence never becomes current anatomy. +- **Smallest proving test:** present a valid graph from the wrong HEAD and assert A04 rejection, then accept a current-snapshot fixture. +- **Compatibility / rollback:** external graph remains explicit fallback; rollback selects legacy command without changing the port contract. +- **Ponytail Necessity Trace:** eliminates stale-graph ambiguity at one adapter seam rather than rewriting providers. +- **Economic implementation lane:** translation plus conformance fixtures over existing graph commands. +- **Independent verifier condition:** verifier swaps internal/external providers and tests stale, missing, partial, and current graphs. + +### B03 — `provider.sentrux-adapt` + +- **Owner / boundary:** executor; owns Sentrux collection/normalization translation into structural evidence, not structural policy or diagnosis. +- **Dependencies:** A04. +- **Affected files (initial):** `Invoke-SentruxAgentTool.ps1`, Sentrux adapter/schema/fixtures, normalization tests, integration registry. +- **Acceptance criteria:** every authoritative rule kind has normalized status/verdict/failure semantics; effects are declared; partial normalization remains incomplete/unknown; provider output passes A04 before diagnosis. +- **Smallest proving test:** an unrecognized authoritative rule kind yields incomplete/unknown rather than pass, while a complete fixture validates. +- **Compatibility / rollback:** bundled shim and current script remain provider implementations; rollback selects them through the same adapter. +- **Ponytail Necessity Trace:** contains existing normalization debt without duplicating Sentrux logic in the core. +- **Economic implementation lane:** strangler wrapper and tables before any script split. +- **Independent verifier condition:** verifier covers every rule kind, provider crash, partial output, and observed-effect mismatch. + +### B04 — `provider.codenexus-adapt` + +- **Owner / boundary:** executor; Pipeline owns the port/adapter; CodeNexus owns indexing, storage, retrieval, and impact semantics. +- **Dependencies:** A04. +- **Affected files (initial):** `orchestration/integrations.json`, CodeNexus port fixtures/adapter, `Invoke-CodeNexusLite.ps1`, `crates/code-intel-cli/src/providers.rs`. +- **Acceptance criteria:** full CodeNexus and lite compatibility output map to the same provenance-bearing port and pass A04; Pipeline neither imports CodeNexus internals nor shares its database; absence yields provider-unavailable, not fabricated empty facts. +- **Smallest proving test:** swap full and lite fixtures behind the same request and validate equal port shape with distinct provenance and snapshot identity. +- **Compatibility / rollback:** lite script remains fallback adapter; rollback selects it explicitly. +- **Ponytail Necessity Trace:** satisfies perception reuse without duplicating an intelligence platform. +- **Economic implementation lane:** thin adapter and conformance fixtures; do not revive the demoted worker unless evidence requires it. +- **Independent verifier condition:** verifier confirms process/storage decoupling, snapshot binding, provider swap, and absence semantics. + +### B05 — `repository.survival-scan` + +- **Owner / boundary:** executor; owns minimum repository identity/basic inventory/provider diagnosis only, never CodeNexus-equivalent graph intelligence. +- **Dependencies:** B04. +- **Affected files (initial):** existing inventory implementation or one small Rust atom, integration declaration, tests. +- **Acceptance criteria:** useful orientation bootstrap survives CodeNexus absence; output explicitly declares reduced completeness and cannot masquerade as structural perception. +- **Smallest proving test:** run with CodeNexus unavailable and assert basic evidence plus unknown structural verdict. +- **Compatibility / rollback:** current rg inventory remains adapter. +- **Ponytail Necessity Trace:** minimum survival ability is an ADR-owned boundary; richer duplication is forbidden. +- **Economic implementation lane:** reuse `rg`/Git, no new parser. +- **Independent verifier condition:** verifier proves no graph/impact claim appears in fallback output. + +### B06 — `advisory.workflow-recommend` + +- **Owner / boundary:** executor; owns tool-neutral recommendations with evidence/confidence/alternatives, never adoption or execution. +- **Dependencies:** A05, A02, A03. +- **Affected files (initial):** `OpenSpec-Detector.ps1`, new atom implementation/contract, `run-code-intel.ps1`, `test-workflow-recommendation-brief.ps1`. +- **Acceptance criteria:** duplicated recommender logic is removed from the main runner; OpenSpec/spec-kit/gstack are candidates, not dependencies; output is a schema-valid Advisory Atom proposal; authority gate blocks automatic adoption/init. +- **Smallest proving test:** same fixture through standalone atom and facade yields parity; direct promotion to Adoption Decision is rejected. +- **Compatibility / rollback:** historical `-SkipOpenSpec/-AutoOpenSpec` flags map to adapter options; rollback invokes standalone script. +- **Ponytail Necessity Trace:** removes known duplication and runner coupling while preserving behavior. +- **Economic implementation lane:** extract existing PowerShell once, then wrap with A01; no algorithm rewrite. +- **Independent verifier condition:** verifier checks parity, no prompt/init side effect, alternatives, provenance, and authority rejection. + +### B07 — `integration.registry-reconcile` + +- **Owner / boundary:** architect/executor; owns reconciliation between declared capability registry and actual production invocation, not provider behavior. +- **Dependencies:** A09, B01, B02, B03, B04, B08. +- **Affected files (initial):** `orchestration/integrations.json`, `run-code-intel.ps1`, `Invoke-CodeIntelOrchestrator.ps1`, registry audit test. +- **Acceptance criteria:** every production participant is either declared with capability id, envelope, dependencies, effects, artifacts, and owner or removed from production invocation; audit explicitly covers doctor, diagnosis.hospital, Repomix, Native Code Evidence, cocoindex, GitHub Solution Research, Repowise, graph, Sentrux, CodeNexus, publication, and index; orphan declarations and undeclared invocations fail CI. +- **Smallest proving test:** insert an undeclared Repomix invocation fixture and assert audit failure, then register it and assert its dependency/effect metadata is required. +- **Compatibility / rollback:** audit-only report precedes enforcement; deleting an unused participant is preferred to speculative registration; rollback returns enforcement to warning without restoring deleted dead code. +- **Ponytail Necessity Trace:** prevents the runtime graph from diverging from its declared graph and forces register-or-delete decisions. +- **Economic implementation lane:** static registry/invocation audit using existing files and `rg`; no service discovery. +- **Independent verifier condition:** verifier traces each named production participant from facade call site to registry declaration or reviewed deletion evidence. + +### B08 — `evidence.native-code` + +- **Owner / boundary:** executor; owns extraction of the current built-in files/symbols/chunks/imports/scorecard/Agent Code Slice producer into one deterministic capability atom, not specialized semantic graph claims or external cocoindex behavior. +- **Dependencies:** A01, A02, A03, A04, A09. +- **Affected files (initial):** native code-evidence functions currently in `run-code-intel.ps1`, new atom module/adapter, Code Evidence schemas, A/B and contract tests, registry declaration. +- **Acceptance criteria:** atom consumes a Snapshot Identity, emits verified Artifact Refs for stable v1 artifacts, declares heuristic/parser coverage and effects, preserves current normalized artifacts, and returns unknown for unsupported precision rather than fabricating call-graph facts; facade invocation passes through A01/A09. +- **Smallest proving test:** run the representative native Code Evidence fixture through an envelope, assert A00 artifact parity and A03 refs, then feed an unsupported language construct and assert explicit coverage/unknown rather than false relationship evidence. +- **Compatibility / rollback:** embedded functions remain a compatibility implementation until parity and E07 approval; cocoindex remains a separate optional adapter/candidate. +- **Ponytail Necessity Trace:** this is an active drift-unregistered production producer and a prerequisite for a complete declared run DAG. +- **Economic implementation lane:** extract existing deterministic logic first; do not add a parser framework or rewrite algorithms. +- **Independent verifier condition:** verifier reruns code-evidence layer/A-B fixtures, checks snapshot/digest/effect boundaries, and audits unsupported-language claims. + +### B09 — `diagnosis.hospital` + +- **Owner / boundary:** executor; owns pure deterministic diagnosis, disposition, next-protocol, treatment, and surgery-plan routing from admitted evidence, not evidence collection, Markdown authority, or execution of treatment. +- **Dependencies:** A05, B01, B02, B03, B05, B08. +- **Affected files (initial):** `New-Hospital*`/`Get-Hospital*` functions currently in `run-code-intel.ps1`, new diagnosis atom, hospital JSON schema/fixtures, rendering separation tests, registry declaration. +- **Acceptance criteria:** consumes only A04-admitted Artifact Refs; authoritative missing/untrusted modalities yield unknown/fail-closed; the same facts produce the same machine diagnosis; Markdown is a rebuildable view; enrichment cannot override an untrusted authoritative diagnosis; output executes through A01/A09. +- **Smallest proving test:** execute a provider-quota plus missing-current-graph fixture through the envelope and assert deterministic fail-closed diagnosis/next protocol, then rebuild Markdown without changing the machine verdict. +- **Compatibility / rollback:** embedded Hospital functions remain adapter implementation until parity and E08 approval; current stable JSON fields remain readable. +- **Ponytail Necessity Trace:** Hospital is an active-required registered capability and must become a real atom for the final DAG to be truthful. +- **Economic implementation lane:** extract pure decision/rendering seams and reuse existing fixtures; no rules engine. +- **Independent verifier condition:** verifier covers every diagnosis precedence case, missing/untrusted evidence, deterministic replay, rendering independence, and facade parity. + +### B10 — `doctor.envelope-adapt` + +- **Owner / boundary:** executor; owns extraction/adaptation of repository/tool/provider/manifest readiness probes into one envelope-emitting doctor capability, not provider admissibility or scan execution. +- **Dependencies:** A01, A02, A03, B07. +- **Affected files (initial):** `check-code-intel-tools.ps1`, stable wrapper doctor path, Rust `doctor` command/adapter, doctor schema/fixtures, registry/manifest tests. +- **Acceptance criteria:** doctor executes through A01, emits one schema-valid result plus Artifact Refs bound to the examined snapshot/environment policy, separates presence/readiness from conformance/admissibility, redacts secrets, validates the reconciled manifest, and preserves a shell-compatible bootstrap path. +- **Smallest proving test:** run doctor through an envelope on a fixture with present-but-nonconforming provider and manifest drift; assert readiness observations without fact promotion, redacted output, and nonzero domain diagnosis while stdout remains one result document. +- **Compatibility / rollback:** `check-code-intel-tools.ps1` remains the bootstrap adapter until E09 approval; rollback restores wrapper routing without changing the doctor contract. +- **Ponytail Necessity Trace:** doctor is active-required and currently outside the runtime envelope, leaving the public run DAG incomplete. +- **Economic implementation lane:** wrap/extract existing probes and Rust artifact-doctor code; no new health service. +- **Independent verifier condition:** verifier tests missing/present/nonconforming tools, manifest drift, provider health separation, secret redaction, stdout purity, and bootstrap rollback. + +### C00 — `governance.ponytail-gate` + +- **Owner / boundary:** architect/executor; owns executable Necessity Trace validation and Ponytail Value Filter admission, not prioritization or code-style judgment. +- **Dependencies:** A05. +- **Affected files (initial):** necessity-trace schema, deterministic gate module, capability declaration/PR fixture tests, CI integration. +- **Acceptance criteria:** every new artifact, dependency, abstraction, file, test, doc, or process change names an allowed current value source and the first sufficient solution rung; unsupported output is rejected; safety/evidence/verification requirements cannot be filtered out; bypass requires an explicit authority record with expiry. +- **Smallest proving test:** reject a new dependency justified only by “may be useful later,” accept an existing-utility reuse tied to a committed deliverable, and reject an attempt to delete a required verification gate. +- **Compatibility / rollback:** begin report-only on changed capabilities, then enforce for new production participants; rollback returns to report-only while retaining trace artifacts. +- **Ponytail Necessity Trace:** this ticket implements the value filter itself, closing the gap between glossary language and executable governance. +- **Economic implementation lane:** JSON Schema plus small rule table integrated with existing contract/CI checks; no policy platform. +- **Independent verifier condition:** verifier supplies addition, deletion, reuse, dependency, safety, speculative, and expired-bypass cases and confirms fail-closed behavior. + +### C01 — `method.catalog` + +- **Owner / boundary:** architect; owns Method Card schema/catalog and engineering meaning, not provider implementations. +- **Dependencies:** A04. +- **Affected files (initial):** new `orchestration/methods/` cards/schema, catalog loader/tests, docs link. +- **Acceptance criteria:** each card declares problem signals, required evidence, assumptions, deterministic steps, outputs, confidence rules, cost, contraindications, and implementation ports; seed cards cover root-cause analysis, FMEA, fault tree, critical path/PERT, value-stream/queue delay, PDCA, SPC, contract testing, and strangler migration without claiming they ran. +- **Smallest proving test:** validate all cards and reject one missing contraindications/confidence rule. +- **Compatibility / rollback:** catalog is additive data; no method is auto-selected. +- **Ponytail Necessity Trace:** one canonical catalog prevents prompt folklore and tool-owned methodology. +- **Economic implementation lane:** versioned JSON/YAML data plus schema; no methodology framework. +- **Independent verifier condition:** engineering reviewer confirms cards preserve established method preconditions and do not collapse distinct methods into slogans. + +### C02 — `method.select` + +- **Owner / boundary:** executor; owns deterministic matching of facts/gaps to Method Cards, not execution or commitment. +- **Dependencies:** C01, A05. +- **Affected files (initial):** method selector module, fixtures/tests. +- **Acceptance criteria:** selection is reproducible, explains matched signals/missing evidence/cost, can return none/unknown, and emits proposal only. +- **Smallest proving test:** a dependency-delay fixture selects critical-path/value-stream cards while insufficient evidence returns unknown. +- **Compatibility / rollback:** advisory-only opt-in; remove route to roll back. +- **Ponytail Necessity Trace:** operationalizes traditional engineering methods without an LLM choosing by taste. +- **Economic implementation lane:** rule table over cards and facts. +- **Independent verifier condition:** verifier tests false-positive, insufficient-evidence, tie, and deterministic ordering cases. + +### C03 — `internalization.record-engine` + +- **Owner / boundary:** dependency-expert; owns the Internalization Standard schema, lifecycle validation, and Reuse Record projections, not any individual adoption decision or package installation. +- **Dependencies:** A04, A05, C00. +- **Affected files (initial):** internalization/reuse schemas, record validator/store, NOTICE/provenance projection, lifecycle tests. +- **Acceptance criteria:** every record requires source revision, license obligations, adoption rung, owned boundary, compatibility and conformance evidence, measured benefit/cost, maintenance/security evidence, update policy/check date, owned modifications, rollback, exit, and retirement evidence/status; missing or expired required evidence blocks production enablement but not research; record state transitions require authority. +- **Smallest proving test:** reject enablement when conformance, measurement, update, or retirement/exit evidence is missing; accept a complete fixture and project a valid Reuse Record. +- **Compatibility / rollback:** audit-only inventory first; enforcement begins only after R01-R26 migrations or authority-approved retirement/out-of-scope records; rollback returns to audit-only without deleting records. +- **Ponytail Necessity Trace:** one executable Internalization Standard replaces bespoke reference prose and unmanaged copies. +- **Economic implementation lane:** repository data records and validator using existing schema tooling; no dependency management platform. +- **Independent verifier condition:** verifier tests every lifecycle evidence class, expiry, update, replacement, rollback, retirement, and authority transition independently. + +### C04 — `assistance.discover` + +- **Owner / boundary:** dependency-expert; owns comparable candidate dossiers for a proven capability gap, not adoption. +- **Dependencies:** C03, C01, A05. +- **Affected files (initial):** dossier schema, discovery atom, candidate fixtures/tests. +- **Acceptance criteria:** search begins only from a named Engineering Capability Gap; internal atoms, established methods, external tools, and docs are comparable; output includes fit/license/security/integration/reversibility and remains a proposal. +- **Smallest proving test:** gap fixture yields dossiers and cannot install or create an Adoption Decision. +- **Compatibility / rollback:** opt-in advisory atom; no external writes. +- **Ponytail Necessity Trace:** prevents speculative dependency search while enabling evidence-driven reuse. +- **Economic implementation lane:** deterministic local inventory first; optional network provider later under declared effects. +- **Independent verifier condition:** verifier confirms no candidate ranking relies on popularity alone and no mutation occurs. + +### C05 — `decision.gap-detect` + +- **Owner / boundary:** analyst; owns identification and branch-local blocking of choices facts cannot resolve, not questioning or recording answers. +- **Dependencies:** A05. +- **Affected files (initial):** Decision Gap schema, detector/policy tests. +- **Acceptance criteria:** gap names blocked decision, discoverable facts already checked, options/consequences, recommended answer, and affected branches; unrelated deterministic analysis continues. +- **Smallest proving test:** unresolved risk acceptance blocks only publication while inventory completes. +- **Compatibility / rollback:** initially emits supplemental artifact; no interactive dependency. +- **Ponytail Necessity Trace:** asks humans only for irreducible authority choices. +- **Economic implementation lane:** deterministic rules over missing authority/preconditions. +- **Independent verifier condition:** verifier distinguishes missing facts from genuine choices and tests branch locality. + +### C06 — `decision.request-response-port` + +- **Owner / boundary:** executor; owns a replaceable asynchronous request/response contract for one Decision Gap, not UI rendering, transport choice, answer authority, or persistence. +- **Dependencies:** C05. +- **Affected files (initial):** decision request/response schemas, port trait/interface, CLI/native UI adapter fixtures, correlation/timeout tests. +- **Acceptance criteria:** request carries gap id, one question, recommendation, evidence refs, options/consequences, authority needed, and expiry; response carries correlation id, chosen option/free-form answer, actor/authority provenance, and timestamp; CLI, native structured UI, or future service adapters are replaceable; timeout blocks only the dependent branch. +- **Smallest proving test:** send one gap through an in-memory adapter, correlate a valid response, reject stale/wrong-gap response, and prove an unrelated DAG branch completes while the request is pending. +- **Compatibility / rollback:** outside-tmux/native environments may use a plain-text adapter; rollback swaps adapters without changing gap or record schemas. +- **Ponytail Necessity Trace:** isolates the irreducible human-choice boundary without coupling Pipeline semantics to chat, tmux, or one host. +- **Economic implementation lane:** port plus in-memory/file adapter first; no message broker. +- **Independent verifier condition:** verifier substitutes a second adapter and covers timeout, replay, wrong actor, stale evidence, cancellation, and branch locality. + +### C07 — `decision.record` + +- **Owner / boundary:** executor; owns durable resolution and replay of one Decision Gap, not the interview UI or request transport. +- **Dependencies:** C06, A05, A07. +- **Affected files (initial):** Decision Record schema/store, authority linkage, tests. +- **Acceptance criteria:** record binds gap/request/response/evidence/snapshot, accepted choice, authority, consequences, freshness/reopen rule; replay prevents repeated questioning unless evidence changes. +- **Smallest proving test:** resolve once through C06, replay without question, then change bound evidence and require reopen. +- **Compatibility / rollback:** records are additive; invalid records are ignored with diagnosis. +- **Ponytail Necessity Trace:** eliminates repeated coordination delay and undocumented approval. +- **Economic implementation lane:** committed JSON artifact; UI remains replaceable. +- **Independent verifier condition:** verifier covers forged authority, stale evidence, replay, response correlation, and branch scope. + +### R01 — `internalization.repowise-record` + +- **Owner / boundary:** dependency-expert; owns migration of Repowise into one C03 record, not the record engine or adapter. +- **Dependencies:** C03, B01. +- **Affected files (initial):** new Repowise record under `orchestration/internalization/`, NOTICE projection, evidence fixtures. +- **Acceptance criteria:** record covers pinned source/version, license, index/docs boundaries, conformance results, measured value/cost, quota/security/maintenance evidence, update cadence, rollback, exit, and retirement triggers. +- **Smallest proving test:** validate the record and trace each declared production operation to B01 conformance evidence. +- **Compatibility / rollback:** audit record only; no provider upgrade or route change. +- **Ponytail Necessity Trace:** Repowise is a current production participant and needs evidence before C03 enforcement. +- **Economic implementation lane:** evidence collection and one data record; no code fork. +- **Independent verifier condition:** verifier checks record claims against current CLI routes/tests and rejects undocumented docs/index differences. + +### R02 — `internalization.graph-record` + +- **Owner / boundary:** dependency-expert; owns migration of internal/external graph providers into one C03 record, not graph implementation. +- **Dependencies:** C03, B02. +- **Affected files (initial):** graph-provider record, NOTICE/provenance projection, conformance evidence links. +- **Acceptance criteria:** record distinguishes internal and fallback implementations, compatibility, measured graph utility/cost, update sources, stale-snapshot risk, rollback, exit, and retirement criteria. +- **Smallest proving test:** validate record and prove both B02 implementations link to current conformance evidence. +- **Compatibility / rollback:** documentation/governance only; routes remain unchanged. +- **Ponytail Necessity Trace:** graph fallback is a current replaceability boundary requiring explicit lifecycle evidence. +- **Economic implementation lane:** one record referencing existing tests and revisions. +- **Independent verifier condition:** verifier confirms no external-provider claim is inferred from the internal implementation or vice versa. + +### R03 — `internalization.sentrux-record` + +- **Owner / boundary:** dependency-expert; owns Sentrux/shim internalization record migration, not normalization code. +- **Dependencies:** C03, B03. +- **Affected files (initial):** Sentrux record, license/provenance projection, conformance/measurement links. +- **Acceptance criteria:** record covers upstream and shim ownership, plugin/Windows compatibility, rule conformance, measured value/cost, security/update policy, rollback, exit, and shim retirement evidence. +- **Smallest proving test:** validate record and reject retirement readiness while upstream Windows/plugin conformance is missing. +- **Compatibility / rollback:** no shim deletion; record captures current blocked retirement state. +- **Ponytail Necessity Trace:** the retained shim has an explicit lifecycle debt in the Gain Ledger. +- **Economic implementation lane:** one record using existing tests and upstream evidence. +- **Independent verifier condition:** verifier checks every retained fallback claim and retirement blocker against fresh conformance results. + +### R04 — `internalization.codenexus-record` + +- **Owner / boundary:** dependency-expert; owns CodeNexus/lite record migration, not the CodeNexus adapter. +- **Dependencies:** C03, B04. +- **Affected files (initial):** CodeNexus record, provenance/NOTICE projection, adapter evidence links. +- **Acceptance criteria:** record defines external ownership, lite adoption rung, storage/process boundary, conformance and measured localization value, update path, rollback, exit, and lite retirement criteria. +- **Smallest proving test:** validate record and trace full/lite claims to B04 swap tests and measured fixtures. +- **Compatibility / rollback:** no worker revival or removal; record preserves current lite fallback. +- **Ponytail Necessity Trace:** CodeNexus is the named perception boundary and must not drift into implicit vendoring. +- **Economic implementation lane:** one evidence record; no source import. +- **Independent verifier condition:** verifier confirms Pipeline-owned claims stop at the port and all internalization is explicitly justified. + +### R05 — `internalization.repomix-record` + +- **Owner / boundary:** dependency-expert; owns Repomix production-use record migration, not registry reconciliation or invocation. +- **Dependencies:** C03, B07. +- **Affected files (initial):** Repomix record, provenance projection, package/version and measurement evidence. +- **Acceptance criteria:** record covers packaging capability, revision/license, output conformance, size/token measurements, security/update policy, rollback, alternatives, exit, and retirement conditions. +- **Smallest proving test:** validate record and link the production invocation registered by B07 to conformance and measurement evidence. +- **Compatibility / rollback:** no invocation change; B07 may instead delete unused production participation. +- **Ponytail Necessity Trace:** register-or-delete audit identifies Repomix as a production participant requiring lifecycle evidence if retained. +- **Economic implementation lane:** retain invoke rung if justified; do not vendor. +- **Independent verifier condition:** verifier confirms measured benefit exceeds existing inventory/artifact alternatives and checks license/version facts. + +### R06 — `internalization.native-code-evidence-record` + +- **Owner / boundary:** dependency-expert; owns Native Code Evidence capability record migration, not its implementation. +- **Dependencies:** C03, B07. +- **Affected files (initial):** Native Code Evidence record, provenance projection, conformance/benchmark evidence. +- **Acceptance criteria:** record states owned versus borrowed semantics, implementation revision, artifact conformance, measured precision/cost, update policy, rollback, exit, and retirement conditions. +- **Smallest proving test:** validate record and trace the B07 production declaration to current code-evidence A/B and contract tests. +- **Compatibility / rollback:** record-only change; undeclared/dead implementation may be deleted by B07 instead. +- **Ponytail Necessity Trace:** a production evidence source needs explicit ownership and measurement, even when internal. +- **Economic implementation lane:** reuse existing benchmarks and record facts; no new analyzer. +- **Independent verifier condition:** verifier separates native capability evidence from external cocoindex claims and audits measurements. + +### R07 — `internalization.cocoindex-record` + +- **Owner / boundary:** dependency-expert; owns cocoindex adoption/rejection record migration, not code-evidence implementation. +- **Dependencies:** C03, B07. +- **Affected files (initial):** cocoindex record or reviewed retirement record, provenance/NOTICE projection, comparison evidence. +- **Acceptance criteria:** record declares actual production status, revision/license, conformance, measured incremental value/cost versus Native Code Evidence, security/update policy, rollback, exit, and retirement evidence; if unused, B07 deletion evidence closes it. +- **Smallest proving test:** validator rejects “available in repository” as adoption evidence and requires either production conformance/measurement or a reviewed retirement record. +- **Compatibility / rollback:** no automatic enablement; restore only through a new authority-approved record state. +- **Ponytail Necessity Trace:** prevents dormant reference code from masquerading as a maintained capability. +- **Economic implementation lane:** prefer delete/retire unless measured unique value exists. +- **Independent verifier condition:** verifier reproduces the comparison or confirms all production invocation was removed. + +### R08 — `internalization.github-research-record` + +- **Owner / boundary:** dependency-expert; owns GitHub Solution Research record migration, not network search or adoption decisions. +- **Dependencies:** C03, B07. +- **Affected files (initial):** GitHub research record, network/provenance policy, conformance/measurement evidence links. +- **Acceptance criteria:** record covers API/tool revision, license/quotation obligations, network and credential effects, dossier conformance, measured blocker-resolution value/cost, update policy, rollback, exit, and retirement conditions. +- **Smallest proving test:** validate a recorded research run and reject one lacking source revision, network effect, or measurement evidence. +- **Compatibility / rollback:** current opt-out flag remains; no external write authority is added. +- **Ponytail Necessity Trace:** network research is a production participant with cost, provenance, and supply-chain obligations. +- **Economic implementation lane:** keep read-only invoke/adapt rung; no crawler platform. +- **Independent verifier condition:** verifier checks source attribution, network effects, credentials redaction, reproducibility limits, and retirement criteria. + +### R09 — `internalization.rg-record` + +- **Owner / boundary:** dependency-expert; owns the ripgrep executable/reference lifecycle record, not inventory implementation. +- **Dependencies:** C03, A01. +- **Affected files (initial):** rg record, license/version/provenance evidence, inventory conformance and benchmark links. +- **Acceptance criteria:** records revision/license, CLI contract, platform availability, scope/exclusion conformance, measured inventory speed/cost, security/update cadence, replacement adapter, rollback, exit, and retirement criteria. +- **Smallest proving test:** validate the record and trace `inventory.rg` parity/benchmark evidence to a pinned executable identity. +- **Compatibility / rollback:** no rg upgrade; alternate inventory executable requires a new approved record state. +- **Ponytail Necessity Trace:** rg is an active-required external executable and the first real envelope implementation. +- **Economic implementation lane:** keep invoke rung; do not vendor or reimplement exact search. +- **Independent verifier condition:** verifier checks installed/source identity, license, cross-platform behavior, scope failures, measurement, and tested replacement command. + +### R10 — `internalization.git-record` + +- **Owner / boundary:** dependency-expert; owns the Git executable/protocol lifecycle record, not Snapshot Identity semantics. +- **Dependencies:** C03, A02, B07. +- **Affected files (initial):** Git record, license/version/provenance evidence, read-only command conformance and exit tests. +- **Acceptance criteria:** records revision/license, read-only operations, working-tree/HEAD semantics, measured cost, security/update policy, alternate-VCS port, rollback, exit, and retirement/out-of-scope conditions for mutation commands. +- **Smallest proving test:** validate the record and prove all current Git production calls are registered read-only effects or authority-gated mutations. +- **Compatibility / rollback:** no Git upgrade or mutation authority; direct calls remain until registry retirement tickets exist. +- **Ponytail Necessity Trace:** Git is drift-unregistered but foundational to snapshot and history evidence. +- **Economic implementation lane:** centralize invoke semantics; no Git library replacement. +- **Independent verifier condition:** verifier traces command call sites, dirty/HEAD behavior, license/version, mutation exclusion, and alternate-provider exit. + +### R11 — `internalization.tree-sitter-v-record` + +- **Owner / boundary:** dependency-expert; owns the V grammar/overlay record, not Sentrux parsing. +- **Dependencies:** C03, B03. +- **Affected files (initial):** tree-sitter-v record, overlay metadata, source revision/ABI/license evidence, plugin conformance tests. +- **Acceptance criteria:** records pinned grammar revision, MIT obligations, compiled artifact digest/ABI, Sentrux conformance, measured value/cost, security/update rebuild procedure, rollback, upstream replacement, and overlay retirement proof. +- **Smallest proving test:** reject the current URL/license-only evidence until revision and compiled digest are bound; accept a pinned reproducible fixture. +- **Compatibility / rollback:** retain overlay until upstream plugin passes the same tests. +- **Ponytail Necessity Trace:** the map identifies this as the only partially attributed optional parser and an explicit retirement dependency. +- **Economic implementation lane:** pin/rebuild existing overlay; do not fork grammar. +- **Independent verifier condition:** verifier rebuilds or verifies digest/ABI, license, V fixtures, upstream comparison, and rollback. + +### R12 — `internalization.greenfield-record` + +- **Owner / boundary:** dependency-expert; owns the Greenfield provider/reference record, not specification authority. +- **Dependencies:** C03, B07. +- **Affected files (initial):** Greenfield record, provider contract/provenance evidence, conformance/measurement fixtures. +- **Acceptance criteria:** records plugin revision/license, interactive/noninteractive boundary, artifact conformance, measured specification value/cost, security/update policy, review authority, rollback, exit, and plan-only retirement option. +- **Smallest proving test:** reject generated specs without plugin/source provenance and review status; accept a conforming plan-only fixture. +- **Compatibility / rollback:** default scan remains nonblocking; no plugin auto-execution. +- **Ponytail Necessity Trace:** Greenfield is an active-optional external provider in the manifest. +- **Economic implementation lane:** preserve provider-contract/plan-only rung; do not internalize plugin runtime. +- **Independent verifier condition:** verifier checks interactivity, provenance, license, no-auto-authority, conformance, measurement, and removal path. + +### R13 — `internalization.openspec-record` + +- **Owner / boundary:** dependency-expert; owns the OpenSpec reference/candidate lifecycle record, not workflow recommendation. +- **Dependencies:** C03, B06. +- **Affected files (initial):** OpenSpec record, absorbed-method evidence, candidate conformance/measurement and retirement evidence. +- **Acceptance criteria:** records revision/license, absorbed versus external semantics, recommendation conformance, measured value/cost, update policy, no-auto-init boundary, rollback, replacement, and retirement/out-of-scope state. +- **Smallest proving test:** validate an OpenSpec record and reject recommendation enablement without source revision and no-auto-init conformance. +- **Compatibility / rollback:** candidate can be removed from data without changing recommender authority. +- **Ponytail Necessity Trace:** OpenSpec is an explicit current advisory reference embedded in duplicate logic. +- **Economic implementation lane:** data candidate/reference only; no runtime dependency. +- **Independent verifier condition:** verifier checks license/revision, absorbed semantics, recommendation evidence, measurement, and tested removal. + +### R14 — `internalization.spec-kit-record` + +- **Owner / boundary:** dependency-expert; owns the spec-kit reference/candidate record, not method selection. +- **Dependencies:** C03, B06. +- **Affected files (initial):** spec-kit record, source/provenance, candidate conformance/measurement and exit evidence. +- **Acceptance criteria:** records revision/license, greenfield fit claims, conformance, measured value/cost, update policy, no-auto-init boundary, rollback, replacement, and retirement/out-of-scope state. +- **Smallest proving test:** reject a fit claim based only on tool name; accept a pinned, measured, removable candidate record. +- **Compatibility / rollback:** advisory candidate only; removal cannot change facts or commitments. +- **Ponytail Necessity Trace:** spec-kit is a named active recommendation reference. +- **Economic implementation lane:** keep reference/data rung; no installation. +- **Independent verifier condition:** verifier checks evidence for fit, revision/license, measurement, authority boundary, and removal. + +### R15 — `internalization.matt-flow-record` + +- **Owner / boundary:** dependency-expert; owns the matt-flow reference/candidate record, not recommender rules. +- **Dependencies:** C03, B06. +- **Affected files (initial):** matt-flow record, provenance/license, absorbed-concept conformance/measurement and exit evidence. +- **Acceptance criteria:** records exact source revision/license, internalized concepts, candidate behavior, measured value/cost, update policy, rollback, replacement, and retirement/out-of-scope state. +- **Smallest proving test:** validate source-to-internal-concept trace and reject unversioned or unmeasured retention. +- **Compatibility / rollback:** remove candidate/reference without changing stable advisory schema. +- **Ponytail Necessity Trace:** matt-flow is named in the embedded three-stack recommender. +- **Economic implementation lane:** internalized data/rules only when justified; no external runtime. +- **Independent verifier condition:** verifier checks source trace, license, semantics, measurement, update and removal evidence. + +### R16 — `internalization.gstack-record` + +- **Owner / boundary:** dependency-expert; owns the gstack reference/candidate record, not recommender execution. +- **Dependencies:** C03, B06. +- **Affected files (initial):** gstack record, provenance/license, candidate conformance/measurement and exit evidence. +- **Acceptance criteria:** records revision/license, relevant workflow semantics, conformance, measured value/cost, update policy, no execution authority, rollback, replacement, and retirement/out-of-scope state. +- **Smallest proving test:** reject an unpinned gstack recommendation source and accept a removable evidence-bound candidate. +- **Compatibility / rollback:** advisory-only data; removal leaves facts and plan authority unchanged. +- **Ponytail Necessity Trace:** gstack is a named current advisory reference. +- **Economic implementation lane:** reference/data rung only. +- **Independent verifier condition:** verifier checks provenance, license, conformance, measurement, authority isolation, and deletion path. + +### R17 — `internalization.qiaomu-goal-record` + +- **Owner / boundary:** dependency-expert; owns the qiaomu-goal-meta-skill design-reference record, not Agent Goal Intake runtime. +- **Dependencies:** C03. +- **Affected files (initial):** qiaomu reference record, absorbed goal-contract trace, benchmark/update/retirement evidence. +- **Acceptance criteria:** records revision/license, absorbed semantics, conformance to Agent Goal Intake, measured value, update review, rollback, replacement, and authority-approved reference-only/retirement state. +- **Smallest proving test:** validate source-to-contract trace and reject scanner runtime dependency or unapproved retention. +- **Compatibility / rollback:** reference may be retired while internal task contract remains versioned. +- **Ponytail Necessity Trace:** map marks it reference-internalized and therefore requires lifecycle evidence, not manifest registration. +- **Economic implementation lane:** evidence record only; no runtime adoption. +- **Independent verifier condition:** verifier checks source/license, semantic trace, non-runtime boundary, measurement, and removal. + +### R18 — `internalization.agent-loops-record` + +- **Owner / boundary:** dependency-expert; owns awesome-agent-loops design-reference record, not loop execution. +- **Dependencies:** C03. +- **Affected files (initial):** agent-loops record, absorbed pattern trace, conformance/measurement/update/exit evidence. +- **Acceptance criteria:** records revision/license, chosen absorbed loop semantics, measured utility, update policy, scanner isolation, rollback, replacement, and retirement/out-of-scope decision. +- **Smallest proving test:** reject an unpinned catalog reference and prove removal does not change scanner contracts. +- **Compatibility / rollback:** internal loop vocabulary remains; reference can be retired independently. +- **Ponytail Necessity Trace:** it is a separate upstream reference in the combined map row. +- **Economic implementation lane:** reference evidence only. +- **Independent verifier condition:** verifier checks attribution, semantic trace, non-runtime coupling, measurement, and tested exit. + +### R19 — `internalization.metaharness-record` + +- **Owner / boundary:** dependency-expert; owns MetaHarness reference-only lifecycle decision, not distribution implementation. +- **Dependencies:** C03. +- **Affected files (initial):** MetaHarness record, harness-reference provenance, authority decision and retirement evidence. +- **Acceptance criteria:** records revision/license, reference-only boundary, measured design value, update review, replacement options, rollback, and explicit authority-approved retain/retire/out-of-scope state. +- **Smallest proving test:** reject indefinite reference-only status without authority/expiry; accept a reviewed out-of-runtime decision. +- **Compatibility / rollback:** no runtime effect; reference can be removed without scanner changes. +- **Ponytail Necessity Trace:** reference-only material still consumes maintenance/decision attention. +- **Economic implementation lane:** record and authority decision only. +- **Independent verifier condition:** verifier checks no runtime invocation, source/license, decision authority, expiry, and removal proof. + +### R20 — `internalization.yao-meta-skill-record` + +- **Owner / boundary:** dependency-expert; owns yao-meta-skill benchmark-reference record, not the internal benchmark gate. +- **Dependencies:** C03. +- **Affected files (initial):** yao record, benchmark semantic trace, measurement/update/retirement evidence. +- **Acceptance criteria:** records revision/license, absorbed benchmark criteria, conformance and measured usefulness, update review, rollback, replacement, and retirement state; local doc test is not upstream execution proof. +- **Smallest proving test:** reject a record citing only `test-skill-development-benchmark.ps1` pass; require pinned source and semantic conformance. +- **Compatibility / rollback:** internal benchmark remains if reference is retired. +- **Ponytail Necessity Trace:** map marks the source reference-internalized but current evidence is terminological. +- **Economic implementation lane:** record existing absorbed criteria; no upstream runtime. +- **Independent verifier condition:** verifier checks source/license, semantic mapping, meaningful measurement, update and independent removal. + +### R21 — `internalization.ponytail-record` + +- **Owner / boundary:** dependency-expert; owns Ponytail source-reference lifecycle record, not C00 enforcement. +- **Dependencies:** C03, C00. +- **Affected files (initial):** Ponytail record, policy-source trace, C00 conformance/measurement/update/retirement evidence. +- **Acceptance criteria:** records revision/license, absorbed Value Filter/Necessity Trace semantics, C00 conformance, measured rejection/overhead, update policy, rollback, replacement, and source-reference retirement state. +- **Smallest proving test:** validate source-to-C00 behavior cases and reject terminology-only evidence. +- **Compatibility / rollback:** C00 semantic contract remains if source reference is retired. +- **Ponytail Necessity Trace:** the executable gate itself requires provenance and lifecycle discipline. +- **Economic implementation lane:** record and tests, no source vendoring. +- **Independent verifier condition:** verifier checks source/license, behavioral trace, safety exceptions, measurement, updates, and removable source coupling. + +### R22 — `internalization.mattpocock-skills-record` + +- **Owner / boundary:** dependency-expert; owns mattpocock/skills reference record, not project-management support. +- **Dependencies:** C03, B06. +- **Affected files (initial):** mattpocock record, absorbed issue/triage/domain/workflow trace, measurement/update/exit evidence. +- **Acceptance criteria:** records revision/license, each absorbed concept, conformance, measured value, update policy, no external-write authority, rollback, replacement, and retirement state. +- **Smallest proving test:** reject a record that cannot trace a retained concept or prove removal of an unused one. +- **Compatibility / rollback:** internal project-management contracts survive source retirement. +- **Ponytail Necessity Trace:** map marks this reference-internalized across docs and recommender. +- **Economic implementation lane:** retain only evidenced concepts, delete unused references. +- **Independent verifier condition:** verifier checks provenance/license, concept trace, external-effect boundary, measurement, and exit. + +### R23 — `internalization.linear-record` + +- **Owner / boundary:** dependency-expert; owns Linear reference-only/out-of-scope decision record, not tracker integration. +- **Dependencies:** C03, A05. +- **Affected files (initial):** Linear record, authority-approved scope decision, credential/external-effect and retirement evidence. +- **Acceptance criteria:** records source/API/license terms as applicable, optional projection boundary, measurement or explicit no-current-use, update/credential policy, rollback, replacement, and authority-approved retain/retire/out-of-scope state with expiry. +- **Smallest proving test:** reject implicit in-scope status and accept a signed no-scanner-write/out-of-scope record that expires for review. +- **Compatibility / rollback:** no connector install or external mutation is authorized. +- **Ponytail Necessity Trace:** reference-only hosted state needs an explicit decision to avoid phantom scope. +- **Economic implementation lane:** out-of-scope record unless an approved project authority requires projection. +- **Independent verifier condition:** verifier checks authority, expiry, credentials, external effects, single-authority rule, and removal. + +### R24 — `internalization.obsidian-record` + +- **Owner / boundary:** dependency-expert; owns Obsidian reference-only projection record, not wiki implementation. +- **Dependencies:** C03, A05. +- **Affected files (initial):** Obsidian record, authority/scope decision, projection conformance/measurement/retirement evidence. +- **Acceptance criteria:** records reference/version/license context, view-only or explicit task-authority boundary, measurement/no-use evidence, update policy, rollback, replacement, and authority-approved retain/retire/out-of-scope state. +- **Smallest proving test:** reject a record allowing Obsidian to replace scanner artifacts; accept an expiring view-only out-of-scope decision. +- **Compatibility / rollback:** repo artifacts remain authority; no projection is required. +- **Ponytail Necessity Trace:** optional knowledge surfaces must not become undocumented product scope. +- **Economic implementation lane:** record-only unless separately approved. +- **Independent verifier condition:** verifier checks authority isolation, no runtime invocation, measurement/scope evidence, expiry, and removal. + +### R25 — `internalization.llm-wiki-record` + +- **Owner / boundary:** dependency-expert; owns the LLM Wiki reference-only projection record, not LLM generation. +- **Dependencies:** C03, A05. +- **Affected files (initial):** LLM Wiki record, authority/scope and model/provider provenance policy, retirement evidence. +- **Acceptance criteria:** records reference/provider assumptions, non-authoritative view boundary, measurement/no-use evidence, model/update policy, data/security effects, rollback, replacement, and authority-approved retain/retire/out-of-scope state. +- **Smallest proving test:** reject any record promoting generated wiki text to Engineering Fact; accept a view-only expiring scope decision. +- **Compatibility / rollback:** no model/provider runtime is required by Pipeline. +- **Ponytail Necessity Trace:** separates this external reference from Obsidian and prevents implicit LLM dependency. +- **Economic implementation lane:** record/out-of-scope decision only. +- **Independent verifier condition:** verifier checks model/data effects, authority boundary, expiry, no runtime coupling, and deletion path. + +### R26 — `internalization.my-code-machine-record` + +- **Owner / boundary:** dependency-expert/architect; owns the my-code-machine proposed-reference/adoption decision record, not host mutation or migration. +- **Dependencies:** C03, A05. +- **Affected files (initial):** my-code-machine record, ADR reconciliation, capability comparison/measurement, authority and retirement evidence. +- **Acceptance criteria:** records revision/license, proposed host capability boundary, conformance gaps, measured value/cost, mutation/security effects, update policy, rollback, exit, and authority-approved adopt/defer/retire/out-of-scope decision consistent with no-big-bang ADR 0010. +- **Smallest proving test:** reject a merge/migration record without atom parity, effect authority, measurement, and rollback; accept a reviewed defer/out-of-scope state. +- **Compatibility / rollback:** current external tool remains separate; no merge or rewrite is authorized. +- **Ponytail Necessity Trace:** map marks a high-scope proposed merge requiring explicit closure rather than perpetual ambiguity. +- **Economic implementation lane:** decision record first; integration only through later atomic tickets if approved. +- **Independent verifier condition:** verifier checks source/license, scope, effect risk, ADR consistency, measurement, authority, and tested exit/defer semantics. + +### D01 — `project.orientation` + +- **Owner / boundary:** executor; owns the first actionable deterministic project view, not deep architecture analysis. +- **Dependencies:** A02, A03, B05, B08. +- **Affected files (initial):** orientation schema/atom, facade summary projection, fixtures/tests. +- **Acceptance criteria:** reports identity, purpose evidence, languages, boundaries, entry points, commands, active change, evidence availability, risks, unknowns, and confidence; works without LLM. +- **Smallest proving test:** fixture repo produces schema-valid orientation with explicit unknown purpose when evidence is absent. +- **Compatibility / rollback:** existing summary remains; new orientation is additive until benchmark passes. +- **Ponytail Necessity Trace:** shortest useful understanding view promised by ADR 0010. +- **Economic implementation lane:** compose existing inventory/Git/provider facts; no semantic model. +- **Independent verifier condition:** verifier checks every claim has provenance and missing evidence remains unknown. + +### D02 — `project.orientation-benchmark` + +- **Owner / boundary:** test-engineer; owns latency/quality measurement, not orientation generation. +- **Dependencies:** D01. +- **Affected files (initial):** benchmark runner, representative fixture corpus, CI job/report schema. +- **Acceptance criteria:** measures p50/p95 wall time, cold/warm runs, field correctness, unknown precision, and provenance completeness; typical fixtures meet the 60-second no-LLM target; failures identify cost center. +- **Smallest proving test:** run the representative corpus across declared small/medium/large and clean/dirty/provider-missing classes with repeated cold/warm samples; prove reported p50 and p95 satisfy the documented 60-second target for the “typical” corpus stratum while quality/provenance thresholds pass, and reject a provenance-free fast result. +- **Compatibility / rollback:** non-blocking CI until stable, then gate documented corpus. +- **Ponytail Necessity Trace:** prevents “fast understanding” from remaining an unmeasured slogan. +- **Economic implementation lane:** existing test infrastructure and local fixtures; no hosted benchmark service. +- **Independent verifier condition:** verifier repeats on a clean machine and audits corpus representativeness and timing methodology. + +### D03 — `understanding.quadrant` + +- **Owner / boundary:** architect/executor; owns classification into Known Core, Critical Unknown, Supporting Context, Deferred Unknown, not fact generation. +- **Dependencies:** D01, C01. +- **Affected files (initial):** quadrant model/schema, rules/tests, orientation projection. +- **Acceptance criteria:** classification derives from system criticality and evidence confidence; unknowns remain visible; method/card selection may consume but not rewrite it. +- **Smallest proving test:** critical low-confidence dependency becomes Critical Unknown while low-criticality low-confidence material is Deferred Unknown. +- **Compatibility / rollback:** additive derived model. +- **Ponytail Necessity Trace:** focuses scarce analysis effort without full-repository over-reading. +- **Economic implementation lane:** deterministic matrix, no ML. +- **Independent verifier condition:** verifier checks boundary values, provenance, and stable classification. + +### D04 — `delivery.light-speed-measure` + +- **Owner / boundary:** test-engineer; owns measurement of avoidable wait/handoff/rework after preserving required gates, not schedule commitments. +- **Dependencies:** A07, D02, C01. +- **Affected files (initial):** run timing/event schema, measurement atom, benchmark report/tests. +- **Acceptance criteria:** separates irreducible technical work from queue, handoff, repeated understanding, rework, and unnecessary coordination; reports baseline/delta without rewarding skipped verification. +- **Smallest proving test:** synthetic trace attributes queue delay but does not count mandatory test time as waste. +- **Compatibility / rollback:** telemetry is opt-in and local; no behavior gate initially. +- **Ponytail Necessity Trace:** makes the Light-Speed promise falsifiable and economically actionable. +- **Economic implementation lane:** consume existing run events; no observability platform. +- **Independent verifier condition:** verifier audits attribution rules against value-stream mapping, queueing, and critical-path principles. + +### E00 — `compatibility.retirement-gate` + +- **Owner / boundary:** verifier/executor pair; owns the executable decision that one legacy branch is safe to retire, not branch deletion or language migration. +- **Dependencies:** A01, A07, A08, B04, B06, B07, C00, D02. +- **Affected files (initial):** retirement-manifest schema, gate module/tests, `docs/ponytail-gain-ledger.md` projection. +- **Acceptance criteria:** gate requires replacement atom, golden parity, contract/effect parity, production registry reconciliation, compatibility window evidence, owner, rollback command/test, usage observation, and independent approval; unproven or cyclic replacement fails; line reduction is never correctness evidence. +- **Smallest proving test:** submit a retirement record missing rollback execution evidence and assert failure, then accept a fully evidenced synthetic branch. +- **Compatibility / rollback:** gate is additive and cannot delete code; disabling it prevents new retirement approvals but preserves records. +- **Ponytail Necessity Trace:** one reusable gate prevents subjective, line-count-driven PowerShell deletion. +- **Economic implementation lane:** schema plus deterministic validator over existing test artifacts; no migration platform. +- **Independent verifier condition:** verifier authors adversarial manifests and confirms every required evidence class is independently checked and content-bound. + +### E01 — `compatibility.retirement-ticket-template` + +- **Owner / boundary:** planner; owns the executable per-branch retirement manifest template and completeness lint, not approval or deletion. +- **Dependencies:** E00. +- **Affected files (initial):** retirement ticket template/schema examples, lint test, contributor guidance. +- **Acceptance criteria:** template identifies exactly one legacy branch/call path, replacement capability, dependencies, affected files, parity/effect/usage evidence, rollback rehearsal, deletion diff, verifier, and observation expiry; multi-branch entries fail lint. +- **Smallest proving test:** lint one single-branch fixture successfully and reject a fixture containing recommender plus provider-preflight deletion. +- **Compatibility / rollback:** template is additive; branch tickets remain independent artifacts. +- **Ponytail Necessity Trace:** enforces small reversible retirements instead of a facade-sized change. +- **Economic implementation lane:** data template and test only. +- **Independent verifier condition:** verifier creates ambiguous, multi-branch, expired-evidence, and missing-owner examples and confirms rejection. + +### E02 — `compatibility.retire-recommender-branch` + +- **Owner / boundary:** executor; owns deletion of the duplicated workflow-recommender branch in `run-code-intel.ps1`, not recommender behavior. +- **Dependencies:** E01, B06. +- **Affected files (initial):** `run-code-intel.ps1`, `OpenSpec-Detector.ps1` adapter path, recommender parity tests, one retirement record. +- **Acceptance criteria:** main-runner duplicate is absent; legacy flags route to B06; A00 parity, effects, no-auto-init, rollback rehearsal, and E00 approval pass. +- **Smallest proving test:** facade and B06 return normalized parity for all recommender fixtures after the duplicate block is deleted. +- **Compatibility / rollback:** restore the prior adapter branch/tag during the bounded window without changing B06. +- **Ponytail Necessity Trace:** removes a proven duplicate named in the commitment ledger. +- **Economic implementation lane:** deletion plus flag mapping; no algorithm rewrite. +- **Independent verifier condition:** verifier reruns recommender and facade suites and executes rollback on Windows. + +### E03 — `compatibility.retire-provider-preflight-branch` + +- **Owner / boundary:** executor; owns removal of direct production invocation of `test-code-intel-provider.ps1`, not Repowise behavior. +- **Dependencies:** E01, B01. +- **Affected files (initial):** `run-code-intel.ps1`, Repowise facade route, provider tests, one retirement record. +- **Acceptance criteria:** production routes through B01/A04; the test script is test-only or renamed appropriately; quota/index-only parity, effects, rollback rehearsal, and E00 approval pass. +- **Smallest proving test:** provider quota fixture preserves index-only behavior through B01 with no production call to a `test-*` file. +- **Compatibility / rollback:** legacy probe remains callable diagnostically during the observation window. +- **Ponytail Necessity Trace:** restores the test/production boundary and removes embedded provider-specific orchestration. +- **Economic implementation lane:** route replacement and deletion only. +- **Independent verifier condition:** verifier statically proves no production call remains and reruns provider quota/missing-tool cases plus rollback. + +### E04 — `compatibility.retire-codenexus-direct-branch` + +- **Owner / boundary:** executor; owns removal of direct CodeNexus-lite invocation from the facade, not CodeNexus/lite implementation. +- **Dependencies:** E01, B04, B05. +- **Affected files (initial):** `run-code-intel.ps1`, CodeNexus adapter route, localization/survival tests, one retirement record. +- **Acceptance criteria:** facade invokes B04; unavailable provider selects B05 without structural overclaim; parity/effects/provider-swap/rollback evidence and E00 approval pass. +- **Smallest proving test:** full, lite, and unavailable fixtures traverse the port with expected parity and no direct script call in facade. +- **Compatibility / rollback:** legacy direct call remains tagged for bounded rollback only. +- **Ponytail Necessity Trace:** realizes the promised CodeNexus ownership boundary without duplicating perception. +- **Economic implementation lane:** call-site substitution after conformance, no worker rewrite. +- **Independent verifier condition:** verifier audits process/storage decoupling, fallback claims, facade call graph, and rollback. + +### E05 — `compatibility.retire-publication-branch` + +- **Owner / boundary:** executor; owns removal of the legacy staging/promotion/completion-marker branch from `run-code-intel.ps1`, not index traversal or artifact generation. +- **Dependencies:** E01, A07, A09. +- **Affected files (initial):** `run-code-intel.ps1`, publication facade adapter, transactional publication tests, one retirement record. +- **Acceptance criteria:** facade routes publication through A09→A07; the current partial dirty-worktree staging/marker code is removed only after interruption/effect/parity/rollback evidence and E00 approval; index implementation is untouched by this ticket. +- **Smallest proving test:** inject failure at every A07 phase through the facade and assert no completed final run, then complete and assert marker-last publication with A00 parity. +- **Compatibility / rollback:** explicit legacy publication route remains during the bounded window and is isolated from committed-only index authority. +- **Ponytail Necessity Trace:** retires exactly one legacy publication branch after the atomic publisher is proven. +- **Economic implementation lane:** route replacement and deletion only; reuse the current draft test as partial regression input, not completion proof. +- **Independent verifier condition:** verifier runs the publication interruption matrix, effects/parity, static branch audit, and rollback on Windows without evaluating index retirement. + +### E07 — `compatibility.retire-native-code-branch` + +- **Owner / boundary:** executor; owns removal of embedded Native Code Evidence production functions/call path from the facade, not B08 behavior. +- **Dependencies:** E01, B08, B07. +- **Affected files (initial):** `run-code-intel.ps1`, B08 adapter route, code-evidence tests, one retirement record. +- **Acceptance criteria:** normal/full modes invoke B08 through A09; embedded production branch is absent; artifact/effect/parity/unsupported-language/rollback evidence and E00 approval pass. +- **Smallest proving test:** public mode matrix produces normalized Code Evidence parity through B08 while static audit finds no embedded production invocation. +- **Compatibility / rollback:** prior embedded adapter remains tagged for the observation window. +- **Ponytail Necessity Trace:** removes an active drift-unregistered monolith branch only after the native atom exists. +- **Economic implementation lane:** call-site substitution and deletion; no algorithm rewrite. +- **Independent verifier condition:** verifier reruns Code Evidence/A-B suites, checks registry path, unsupported claims, and rollback. + +### E08 — `compatibility.retire-hospital-branch` + +- **Owner / boundary:** executor; owns removal of embedded Hospital diagnosis/rendering production call path, not B09 diagnosis semantics. +- **Dependencies:** E01, B09, B07. +- **Affected files (initial):** `run-code-intel.ps1`, B09 adapter route, hospital parity/precedence/rendering tests, one retirement record. +- **Acceptance criteria:** facade consumes B09 machine result and rebuildable view; embedded diagnosis branch is absent; fail-closed precedence/parity/effects/rollback evidence and E00 approval pass. +- **Smallest proving test:** public fixture with untrusted authoritative evidence yields identical machine diagnosis through B09 and static audit finds no embedded decision path. +- **Compatibility / rollback:** old Hospital adapter remains tagged for bounded rollback and cannot override B09 in normal mode. +- **Ponytail Necessity Trace:** realizes the registered `diagnosis.hospital` atom instead of merely naming it in the manifest. +- **Economic implementation lane:** extract/route/delete existing pure logic; no rules engine. +- **Independent verifier condition:** verifier reruns diagnosis matrix, view rebuild, registry route, static deletion, and rollback. + +### E09 — `compatibility.retire-doctor-wrapper-branch` + +- **Owner / boundary:** executor; owns removal of the non-enveloped production doctor routing branch while retaining necessary bootstrap shell glue, not B10 probe semantics. +- **Dependencies:** E01, B10, B07. +- **Affected files (initial):** `invoke-code-intel.ps1`, `check-code-intel-tools.ps1`, Rust doctor adapter route, doctor tests, one retirement record. +- **Acceptance criteria:** public doctor/run preflight routes through B10 envelopes; any retained bootstrap branch is explicitly non-authoritative, registered, owned, and expiring; readiness/conformance separation, secret redaction, parity, rollback, and E00 approval pass. +- **Smallest proving test:** run public preflight with manifest drift and present-but-nonconforming provider; assert one B10 result, no secret, correct readiness diagnosis, and no unowned direct production route. +- **Compatibility / rollback:** minimal shell bootstrap remains if required for locating the binary; prior routing is tagged for bounded rollback. +- **Ponytail Necessity Trace:** closes the active-required doctor gap without deleting indispensable fresh-machine bootstrap behavior. +- **Economic implementation lane:** wrapper route change and deletion of redundant probes only. +- **Independent verifier condition:** verifier tests bootstrap/fresh-machine path, stdout purity, manifest/provider cases, static route ownership, and rollback. + +### E10 — `compatibility.retire-index-branch` + +- **Owner / boundary:** executor; owns removal of the legacy `update-code-intel-index.ps1` production traversal/call path, not publication or A08 index semantics. +- **Dependencies:** E01, A08, E05. +- **Affected files (initial):** `update-code-intel-index.ps1`, public wrapper/index route, index tests, one retirement record. +- **Acceptance criteria:** public index refresh routes through A08; current partial dirty-worktree staging/marker guard is retained as regression evidence until replaced; legacy production traversal is absent; rebuild/parity/forged-marker/rollback evidence and E00 approval pass. +- **Smallest proving test:** rebuild an index containing staged, markerless, forged, and valid runs through A08 and assert only the valid run appears, then statically prove no public production call reaches the legacy traversal. +- **Compatibility / rollback:** old script remains tagged/diagnostic during the bounded observation window and cannot write the authoritative index in normal mode. +- **Ponytail Necessity Trace:** retires exactly the index branch after publication has independently converged. +- **Economic implementation lane:** route replacement and script demotion/deletion; no database. +- **Independent verifier condition:** verifier runs rebuild/incremental parity, adversarial marker cases, public call graph, and rollback without reopening E05 publication approval. + +### E06 — `compatibility.facade-finalize` + +- **Owner / boundary:** verifier; owns final determination that public PowerShell contains only supported compatibility/platform glue or may be retired, not remaining atom implementations. +- **Dependencies:** E02, E03, E04, E05, E07, E08, E09, E10. +- **Affected files (initial):** facade, stable wrapper/orchestrator entrypoints, final retirement manifest, README/installation command projections. +- **Acceptance criteria:** every remaining branch is registry-backed and has an owner/expiry or a completed single-branch retirement record; doctor, snapshot/inventory, provider adapters, Native Code Evidence, Hospital diagnosis, publication, and index all execute in the declared A09 DAG; supported modes pass A00 parity and rollback windows remain available; retained PowerShell is enumerated rather than assumed zero. +- **Smallest proving test:** run doctor plus lite/normal/full public mode matrix and assert every execution node is registry-backed, enveloped, admitted where applicable, committed, and indexed; static audit finds no unowned branch. +- **Compatibility / rollback:** public command remains a thin shim if installers/users require it; final deletion requires a separate explicit distribution decision. +- **Ponytail Necessity Trace:** final gate is reachable through independently proven single-branch retirements while preserving necessary platform glue. +- **Economic implementation lane:** delete or retain based on evidence; no forced language purity target. +- **Independent verifier condition:** verifier did not author E02-E05/E07-E10, reruns full DAG/mode/parity/rollback matrix, and signs only with zero unsupported branches. + +## Execution waves and parallelism + +| Wave | Tickets | Parallel rule | Exit evidence | +| --- | --- | --- | --- | +| 0 | A00 | solo | golden fixtures reproduce current behavior | +| 1 | A01 | solo | real `inventory.rg` executes through v1 envelopes with A00 parity | +| 2 | A02 | solo | portable snapshot identity negative/relocation tests pass | +| 3 | A03 | solo | Artifact Ref digest/snapshot/schema negative tests pass | +| 4 | A04, A06, A09 | parallel after A03 | provider-neutral admissibility, staged write, and executable DAG tests pass | +| 5 | A05, A07, B01, B02, B03, B04, B08 | parallel by dependency | authority gate, Run Commit, four provider conformance suites, and Native Code Evidence atom pass | +| 6 | A08, B05, B06, C00 | parallel by dependency | committed-only indexing, fallback boundaries, advisory authority, and executable Ponytail gate pass | +| 7 | B07, B09, C01, C03, C05, D01 | parallel by ownership | registry reconciliation, Hospital atom, method/internalization/gap/orientation contracts pass | +| 8 | B10, C02, C04, C06, D02, D03, R01-R26 | parallel after each named adapter/engine | enveloped doctor, method/discovery/decision port, representative corpus SLA, and all lifecycle/retirement records pass | +| 9 | C07, D04, E00 | parallel after prerequisites | durable decisions, Light-Speed baseline, and retirement gate pass | +| 10 | E01 | solo | single-branch retirement template/lint passes | +| 11 | E02, E03, E04, E05, E07, E08, E09 | parallel only in isolated files; serialize shared wrapper/facade edits | each single branch has independent parity, effects, rollback, and E00 approval | +| 12 | E10 | solo after E05 | index branch retires independently after publication convergence | +| 13 | E06 | solo independent approval | doctor plus public full-DAG mode matrix and zero-unsupported-branch audit pass | + +## First five-ticket handoff + +The next executor should take A00 only. After its verifier accepts the fixtures, execute A01, A02, A03, and A04 in that order. These first five tickets establish current parity, a real enveloped atom, snapshot identity, Artifact Ref verification, and provider-neutral admissibility. Do not start provider-specific adapter, recommender, or CodeNexus extraction before A04, because doing so would merely reproduce trust logic in each adapter. + +For A00-A04, the pull-request stop condition is fresh targeted tests plus `cargo test`, affected PowerShell tests, schema validation, `git diff --check`, and an independent verifier report. A passing unit test alone is insufficient if the facade parity fixture differs. + +## Principal risks + +- **False parity:** normalization could erase meaningful provenance or verdict differences. Mitigation: verifier-owned fixture audit and deliberate mismatch tests. +- **Dual authority:** legacy reports and new authority artifacts may disagree during migration. Mitigation: audit-only phase, explicit precedence field, and fail-closed promotion. +- **Provider success confusion:** health/preflight success may still be treated as evidence admissibility. Mitigation: A04 is provider-neutral and B01-B04 keep native health/translation outside it. +- **CodeNexus duplication:** survival scanning could grow into a second graph product. Mitigation: B05 schema forbids structural/impact claims. +- **Method theater:** Method Cards may become documentation with no executable selection. Mitigation: C02 and proving fixtures are separate required capability work. +- **Big-bang retirement pressure:** line-count goals may encourage rewriting all PowerShell. Mitigation: E00/E01 enforce one branch per retirement record; E02-E05 and E07-E10 carry independent rollback before E06 can pass. +- **Benchmark gaming:** 60 seconds could be met by omitting unknowns or provenance. Mitigation: D02 gates quality and latency together. + +## Final acceptance gate + +ADR 0010 may be reported as implemented only when every ticket is implemented and independently verified; doctor, snapshot/inventory, provider evidence, Native Code Evidence, Hospital diagnosis, publication, and indexing execute through the full declared A09 DAG; A07/A08 interruption/index guarantees pass beyond the current partial dirty-worktree drafts; provider adapters pass the shared A04 conformance suite; B07 finds no undeclared production participant; C00 rejects unnecessary output; all R01-R26 records contain current conformance, measurement, update, and retirement evidence or an authority-approved expiring retirement/out-of-scope state; C06 is proven replaceable; the representative D02 corpus meets its p50/p95 quality-and-latency target; and E06 finds no unsupported facade branch. Remaining PowerShell must be documented compatibility or platform glue with an explicit owner. Until then, status reports must name completed ticket IDs and say the rest are planned or in progress. diff --git a/docs/plans/automatic-pr-one-command-orchestration-idea.md b/docs/plans/automatic-pr-one-command-orchestration-idea.md new file mode 100644 index 0000000..c6e894f --- /dev/null +++ b/docs/plans/automatic-pr-one-command-orchestration-idea.md @@ -0,0 +1,46 @@ +# Automatic PR One-Command Orchestration +> Status: ACCEPTED FOR IMPLEMENTATION +> Created: 2026-07-16 +> Source: local code-intel-pipeline + +## Abstract +Add one PowerShell entrypoint that prepares an exact draft-PR proposal, asks for a structured user +decision, records the approved answer through C07, proves replay validity, and then delegates to the +existing fail-closed executor. The entrypoint must stop without repository or network effects when +the answer is missing, declined, stale, malformed, or no longer matches the repository snapshot. + +## Core Insight +The orchestration layer must not invent a second authority model. It should compose the existing +Decision Port, Decision Record store, proposal evidence binding, and executor while preserving their +separate validation boundaries. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current local branch +- Current state: dirty shared worktree; preserve unrelated changes + +## Success Criteria +- [ ] One command generates a canonical proposal and decision request outside the target repository. +- [ ] Interactive mode asks the user; automation mode accepts only a structured response file. +- [ ] Decline, missing response, malformed response, expiry, or drift invokes neither `gh` nor the executor. +- [ ] Approval is validated by Decision Port, committed by C07, replayed, and bound to the exact proposal. +- [ ] Execution still requires both explicit repository-mutation and network switches. +- [ ] One Decision Record can create at most one draft PR. +- [ ] Tests use fake `gh`; implementation work creates no real PR and performs no remote push. + +## Constraints +- Do not add dependencies. +- Do not weaken the executor or duplicate its one-time receipt semantics. +- Do not write generated decision artifacts into the scanned repository by default. +- C07 content binding is not cryptographic human identity; keep that limitation explicit. + +## Open Questions +1. A trusted native UI/signature provider remains a later integration; console and supplied structured + responses identify provenance but do not cryptographically authenticate the human. +2. Remote push/PR publication remains explicitly effect-gated even after approval. + +## Implementation Notes +- Add a thin root facade and a bounded implementation under `tools/`. +- Resolve the packaged `bin/code-intel.exe` first and source-tree debug binary only as a development fallback. +- Emit a machine-readable flow result and retain the proposal/request/response/record/replay artifacts. +- Reuse `Invoke-CodeIntelAutomaticPullRequest.ps1` for all `gh` effects. diff --git a/docs/plans/compete-project-score-idea.md b/docs/plans/compete-project-score-idea.md new file mode 100644 index 0000000..8ef70d3 --- /dev/null +++ b/docs/plans/compete-project-score-idea.md @@ -0,0 +1,36 @@ +# Optional Compete project score +> Status: IMPLEMENTED +> Created: 2026-07-17 +> Source: https://github.com/lbj96347/compete + +## Abstract +Add a non-blocking adapter that prepares a competitive-intelligence task for an Orca-managed Agent and normalizes a completed `compete` dataset into a small Code Intel score artifact. Keep market scoring separate from structural quality and hospital discharge decisions. + +## Core Insight +`compete` already owns the six-axis scoring algorithm in `build_report.py`; Code Intel only needs to orchestrate it and label the result advisory. + +## Target Repo +- Path: `D:/projects/_tools/code-intel-pipeline` +- Branch: current working branch +- Current state: dirty; use additive files and a narrow registry edit only + +## Success Criteria +- [x] Doctor passes. +- [x] Preparation emits a machine-readable request and Agent prompt without network access. +- [x] A completed `compete` dataset normalizes to an advisory overall score plus six axes. +- [x] The adapter is optional and does not change `hospital-report.json` or default pipeline execution. +- [x] A focused self-test passes. + +## Constraints +- Do not vendor `compete` or copy its scoring formulas. +- Do not add dependencies. +- Do not invoke paid/network Agent work without an explicit adapter action. +- Keep generated artifacts out of source control. + +## Open Questions +1. Promote this into the default pipeline only after repeated reports prove useful. + +## Implementation Notes +- Register the adapter under an optional market-intelligence stage. +- Reuse `compete.build_report` functions to calculate view-model scores. +- Orca remains an execution coordinator; Claude/Codex plus web tools perform research. diff --git a/docs/plans/four-blind-spots-closure-idea.md b/docs/plans/four-blind-spots-closure-idea.md new file mode 100644 index 0000000..bafe45b --- /dev/null +++ b/docs/plans/four-blind-spots-closure-idea.md @@ -0,0 +1,42 @@ +# Four Blind Spots Closure +> Status: APPROVED_FOR_IMPLEMENTATION +> Created: 2026-07-14 +> Source: user-approved local code-intel-pipeline work + +## Abstract +Close four verified architecture gaps without adding a new indexing stack: generated-workspace pollution in CodeNexus-lite, missing per-file boundary evidence, raw executable paths without snapshot-bound handles or safe request synthesis, and runtime/CI evidence represented only by static proxies. + +## Core Insight +Each gap belongs behind an existing Pipeline-owned boundary. The fix is to add small deterministic observations and adapters, then admit them through existing snapshot/freshness/provenance rules instead of coupling providers or copying their internals. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current working tree +- Current state: full Rust tests pass, but normal self-scan is red because Sentrux reports two blocking worsened debts; CodeNexus-lite incorrectly ranks a generated `work/` rollback file. + +## Success Criteria +- [ ] CodeNexus-lite excludes generated `work/`, artifact, staging, dependency, and VCS paths before ranking; a regression fixture proves the generated file cannot become `topFile`. +- [ ] A closed, snapshot-bound per-file boundary observation can resolve local `.aigx/files.aigx`-style entries without requiring AIGX or changing source files. +- [ ] Model execution uses a short-lived, content-bound executable handle; request synthesis accepts only a verified inventory candidate plus explicit model and consent and cannot weaken egress/spend gates. +- [ ] Runtime/CI evidence is a provider-neutral, closed observation with provenance, freshness, completeness, and explicit unknown/missing states; Hospital/PET can cite it without turning absence into success. +- [ ] No new dependency is added and no real model, paid endpoint, CI system, or external service is invoked by tests. +- [ ] Targeted tests, registry audit, full Rust tests, PowerShell regression tests, and a fresh normal self-scan are reported honestly. + +## Constraints +- Preserve the dirty working tree and unrelated user changes. +- Regression tests precede behavior changes for the CodeNexus bug. +- CC Switch remains presence-only; secrets and config values do not enter artifacts. +- AIGX remains optional input; the Pipeline owns the normalized boundary contract. +- Runtime/CI ingestion is file/request based and read-only; it does not mutate CI providers. +- Compatibility retirement authority is unchanged by this work. + +## Open Questions Resolved For This Pass +1. A per-file boundary adapter supports the minimal local subset needed for `role`, `forbid`, `gotcha`, and `check`; unsupported AIGX constructs remain explicit unknown diagnostics. +2. Executable handles are process-independent signed-by-content envelopes using path, SHA-256, observed metadata, expiry, and adapter identity; execution re-verifies all fields. +3. Runtime/CI evidence is ingested from explicit local JSON artifacts first. Live connectors are future providers, not part of this change. + +## Verification Order +1. Targeted negative and fixture tests. +2. Closed-schema validation and integration registry audit. +3. `cargo fmt --all -- --check`, `cargo check -p code-intel`, full `cargo test -q -p code-intel`. +4. `invoke-code-intel.ps1 -RepoPath D:\projects\_tools\code-intel-pipeline -Mode normal`, followed by summary, hospital, and understanding review. diff --git a/docs/plans/language-adapter-acceptance-standard-idea.md b/docs/plans/language-adapter-acceptance-standard-idea.md new file mode 100644 index 0000000..f5c7417 --- /dev/null +++ b/docs/plans/language-adapter-acceptance-standard-idea.md @@ -0,0 +1,41 @@ +# Idea File +> Status: IMPLEMENTED_AND_LOCALLY_VERIFIED +> Created: 2026-07-15 +> Source: Pipeline-owned consolidation of existing Code Evidence, capability envelope, parity floor, and internalization evidence + +## Abstract +Define one machine-executable acceptance standard for every language adapter. Separate the strength of the adapter's claim from its release stage, then gate contract shape, measured quality, determinism, compatibility, effects, provenance, rollback, and independent verification through one policy. + +## Core Insight +Language coverage and production readiness are different axes. A Python or Rust adapter may emit the same structural contract while having very different precision, and a semantically strong adapter may still be research-only because provenance, rollback, or independent verification is incomplete. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current working branch +- Current state: large pre-existing dirty worktree; additions must stay isolated + +## Success Criteria +- [x] Doctor passes. +- [x] A recent normal Pipeline run has zero effective failures and no Sentrux degradation. +- [x] A versioned policy defines claim levels and research/candidate/production thresholds. +- [x] A strict report contract records evidence for every acceptance dimension. +- [x] One reusable PowerShell gate returns machine-readable pass/fail results. +- [x] The native Code Evidence adapter has a candidate-stage acceptance report. +- [x] Tests prove a valid candidate passes and low precision, hidden network effects, implicit unsupported behavior, stale digests, semantic overclaim, weakened policy, and premature production promotion fail. +- [x] Documentation defines promotion, rollback, and non-overclaim rules. + +## Constraints +- Do not add dependencies. +- Do not modify the existing native extractor or workflow files for the first gate. +- Do not turn heuristic structural evidence into a semantic or behavioral claim. +- Threshold changes require reviewed policy changes; reports cannot weaken their own gate. +- Research acceptance never grants production authority. + +## Open Questions +1. Which parser-backed adapter should be the first production-stage proving case? +2. Should CI invoke the gate directly or through the future capability facade after the dirty workflow branch is reconciled? + +## Implementation Notes +- Minimalism rung: reuse existing JSON evidence plus PowerShell standard-library validation. +- Policy is authoritative for thresholds; reports are observations only. +- The first accepted report targets `candidate` and `structural`, not `production` or `semantic`. diff --git a/docs/plans/model-independent-pipeline-completion-idea.md b/docs/plans/model-independent-pipeline-completion-idea.md new file mode 100644 index 0000000..bed1c7f --- /dev/null +++ b/docs/plans/model-independent-pipeline-completion-idea.md @@ -0,0 +1,154 @@ +# Model-independent Code Intel Pipeline completion +> Status: IMPLEMENTED AND LOCALLY VERIFIED — EXTERNAL ASSURANCE GAPS EXPLICIT +> Created: 2026-07-23 +> Previous completion claim: 2026-07-23 (retracted after requirement-by-requirement audit) +> Source: user goal + ADR 0010 + Pipeline self-run evidence + +The previous completion claim was incorrect. Green compatibility tests and a committed core run did +not prove that the default `normal` path was one manifest-bound production spine, that failed DAG +runs could not enter the index, or that provider/graph/Sentrux/diagnosis evidence participated in +the authoritative run. The renewed implementation and evidence below close those local product +gaps. Independent clean-machine assurance, an official clean-worktree package, and promotion of the +optional Mindwalk/session adapter remain explicitly outside the locally verified claim. + +## Abstract + +Complete Code Intel Pipeline as a deterministic, model-independent evidence pipeline. The Pipeline +collects, normalizes, snapshot-binds, validates, composes, publishes, indexes, and replays code +evidence; models remain replaceable consumers or optional providers and never own Pipeline facts. + +This is not a workbench, IDE, dashboard, autonomous PR product, or another RAG stack. Completion +means the already-defined atomic capabilities are connected into the production run path, missing +query/change contracts are added at the smallest sufficient rung, and the Pipeline proves itself on +representative repositories. + +## Core Insight + +The repository already contained most of the required atoms. The completed work connected them into +one manifest-bound production spine, made non-completed runs ineligible for A08 authority, and exposed +committed provider evidence through bounded query/change interfaces. The continuing risk is now +assurance drift: digest-bound records, cross-contract tests, real wrapper E2E, and representative runs +must remain release gates as the model or provider set changes. + +## Target Repo + +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: `agent/sentrux-rust-dsm-kernel` +- Current state: heavily dirty shared worktree; preserve existing changes and avoid broad rewrites. +- Baseline self-run: `20260723-035204`; orchestration passes after the session adapter stage repair, + Sentrux gate blocks on `complex_functions 20 -> 21`, and committed-only index reports zero repos. + +## Pipeline Boundary + +Pipeline owns: + +- deterministic collection and provider-neutral adapters; +- snapshot identity, freshness, provenance, coverage, confidence, and authority labels; +- artifact validation, staging, atomic publication, committed-run indexing, and replay; +- bounded evidence query, change impact, test candidates, and verification observations; +- conformance, benchmark, rollback, and schema compatibility evidence. + +Pipeline does not own: + +- a user-facing workbench or IDE; +- general chat, planning, code generation, or model reasoning; +- automatic PR/issue/wiki publication without explicit authority; +- provider databases, UIs, prompts, summaries, or internal algorithms beyond survival boundaries. + +## Success Criteria + +- [x] Doctor and integration orchestration validation pass. +- [x] Production normal runs use one manifest-bound staging/commit/publication spine. +- [x] At least one representative green run enters the committed-only artifact index. +- [x] Legacy runs remain explicit diagnostics or read-only compatibility; they are never silently promoted. +- [x] Graph/provider freshness binds snapshot identity, producer version, and explicit age policy. +- [x] `query` returns bounded path/symbol/finding evidence with provenance, freshness, coverage, + confidence, authority, and unknowns. +- [x] `impact` maps a snapshot-bound change set to affected symbols/files and test candidates without + inventing semantic certainty. +- [x] Language observations expose inventory/structural/semantic/behavioral claim level and pass the + existing language-adapter acceptance gate. +- [x] Session evidence is optional verification input and never required for normal scans. +- [x] Cross-contract CI rejects unknown stages, command drift, missing schemas, incompatible schema + changes, and operation-trace drift. +- [x] Representative benchmarks measure correctness, determinism, latency, artifact size, and + unresolved/unsupported coverage. +- [x] Pipeline self-run has no unexplained effective failures; remaining debt is explicit and bounded. + +## Constraints + +- No new production dependency without explicit approval. +- Reuse A01-A09, B01-B10, C01-C07, D01-D04, existing adapters, schemas, and tests before adding code. +- Keep production behavior in Rust where the existing atomic core owns it; PowerShell remains a facade. +- No baseline rewrite to hide structural regression. +- No raw prompts, secrets, absolute outside paths, or provider-private payloads in normalized evidence. +- Do not commit, delete, or overwrite unrelated shared-worktree changes. + +## Delivery Order + +1. Production spine: orchestration drift guard, structure regression, A09/A06/A07/A08 integration, + and real freshness. +2. Evidence interface: query/explain over admitted artifacts and native code evidence. +3. Change interface: snapshot-bound diff, impact relationships, and conservative test candidates. +4. Optional verification: session evidence composition and runtime/CI evidence. +5. Evaluation: a real representative repository plus Pipeline self-analysis, and a deterministic + nine-fixture corpus covering correctness, replay, calibration, latency, size, unresolved, and + unsupported behavior. +6. Final self-run, documentation reconciliation, and explicit residual-risk report. + +## Stop Condition + +Stop only when the success criteria have fresh evidence or a remaining item is blocked by an +irreversible/external decision. A feature count, document count, or model-produced explanation is +not completion evidence. + +## Completion Evidence + +- Authoritative self-run: `code-intel-pipeline/20260723-112701-891-core` completed through the stable + wrapper. A09 staged one snapshot-bound manifest, A07 committed it atomically, and A08 admitted it + into the committed-only index. Graph and real Sentrux `gate`/`check` evidence fed Hospital in the + same run. +- Read/change closure: `artifact query` returned committed graph/Sentrux/Hospital evidence with equal + recorded/current snapshot identity `b496f0c5...71a1` and freshness `current`. `change impact` for + `crates/code-intel-cli/src/dag_run.rs` used that same run, found the file in inventory, and emitted + explicit heuristic limitations and advisory-only test candidates. +- Real representative repository: Mindwalk run `mindwalk/20260723-105742-733-core` completed, and + current query/impact reads resolved its real `internal/adapter/codex/adapter.go` path. +- Failure authority: `domain_failed` and `domain_unknown` retain verified diagnostic artifacts at A07, + but A08 classifies every non-completed commit as `non_completed`. Process/contract failures remain + distinct, and invalid UTF-8 failure injection proves failed runs are retained for audit but excluded + from current authority. +- Benchmark: 9 fixtures × 3 cold/warm repetitions passed with deterministic replay, field correctness, + provenance completeness, unknown precision, unresolved coverage, and unsupported coverage all + `1.0`; the largest measured artifact was 5,195 bytes. The report truthfully remains + `cleanMachine: false`. +- Contracts and E2E: the complete Rust test matrix passed from the beginning after digest + reconciliation; internalization records passed 40/40 twice under hardened parallel execution. + The stable wrapper E2E executed real Sentrux, committed/indexed a completed run, queried current + evidence, and verified failure exclusion. PowerShell parser checks and integration toolchain digest + checks passed. +- Adapter status: the seven-language native adapter passed all acceptance gates only at + `candidate + structural`; semantic, behavioral, independent, and production claims remain false. + Mindwalk/session is implemented as an optional research adapter and is not invoked by default. +- Deliverable: Rust CLI version `0.3.0`, root MIT license, lockfile, changelog, and Windows beta package + were generated. Development-package verification covered 752 files, 12 locked dependencies, + checksums, traversal safety, PowerShell parsing, CLI help, and wrapper smoke without Cargo or + Repowise. + +## Bounded Residuals + +- The historical clean-machine attestation is source-stale and now states + `externalVerificationComplete: false`. A fresh independent disposable-machine run is required + before claiming current clean-machine assurance. +- The shared worktree is intentionally dirty, so the verified ZIP is a development beta build, not an + official clean-worktree release. The verifier rejects it without the explicit `-AllowDirty` flag. +- Mindwalk/session promotion is blocked on independently reproducible privacy-safe real-session raw + evidence, hostile-trace testing, and latency/maintenance measurements. It remains research-only. +- The authoritative Rust core exposes committed query/Hospital evidence but does not pretend to emit + the legacy `summary.md`/`understanding.md` report pack; those remain compatibility UX rather than + current-run authority. +- The index reports 647 historical diagnostic rows from prior legacy/invalid runs. Current entries are + completed-only; the diagnostics are retained audit history, not silently deleted debt. +- The Rust test architecture still emits known `dead_code` warnings because integration suites import + production modules directly, plus one removable `unused_mut`. Formatting, compilation, contracts, + and behavior are green; warning cleanup is maintenance debt rather than a correctness blocker. diff --git a/docs/plans/multi-agent-merge-queue-idea.md b/docs/plans/multi-agent-merge-queue-idea.md new file mode 100644 index 0000000..a8cf303 --- /dev/null +++ b/docs/plans/multi-agent-merge-queue-idea.md @@ -0,0 +1,52 @@ +# Multi-Agent Merge Queue idea +> Status: ACCEPTED FOR LOCAL IMPLEMENTATION +> Created: 2026-07-15 +> Source: 2233admin/claude-code-merge-queue@e7a76958dbd3953b84f12abbc2e6bd755aafce53 + +## Abstract +Add an optional, fail-closed landing adapter for repositories developed by several local agents in +isolated worktrees. The adapter admits a lane to the external FIFO merge queue only when the queue +is installed locally, a real acceptance command is configured, direct pushes are protected, and +production promotion remains a human action. + +## Core Insight +Parallel implementation and serialized integration are different responsibilities. Code Intel and +Pon-derived project conformance decide whether work is acceptable; the merge queue decides when one +accepted lane may rebase, check, and push without racing other lanes. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current working branch +- Current state: dirty worktree with pre-existing user changes; add only isolated adapter, policy, + documentation, registry, record, and tests. + +## Success Criteria +- [ ] Orchestration registers an optional landing-coordination stage and adapter. +- [ ] Status is machine-readable and never installs or initializes an external tool. +- [ ] `land` fails closed without local CLI, config, acceptance command, active pre-push hook, or a + distinct human promotion branch. +- [ ] A ready fixture delegates exactly one `land` invocation to the configured queue command. +- [ ] `promote`, emergency bypass, worktree deletion, and dependency installation remain outside + the adapter. +- [ ] Project conformance can be used as the upstream queue's `checkCommand` without coupling the + adapter to Python or Pon. +- [ ] Doctor, orchestration validation, targeted tests, and Sentrux session verification pass. + +## Constraints +- Do not add dependencies or vendor upstream code. +- Do not run upstream `init`, `uninstall`, `promote`, `prune`, or emergency-push paths. +- Do not rewrite unrelated code or existing dirty-worktree changes. +- Keep queue availability optional for ordinary code-intel analysis and mandatory only for an + explicit landing action. + +## Open Questions +1. Fleet-wide queues remain out of scope because the selected upstream queue coordinates one + machine only. +2. A future provider may replace Claude-specific worktree hooks if it preserves the same readiness + and authority contract. + +## Implementation Notes +- Prefer a thin PowerShell compatibility adapter over reimplementing queue locks. +- Resolve only a repository-local package binary by default; never use unpinned `npx` fallback. +- Treat the upstream config as text during status checks; do not execute repository JavaScript just + to inspect readiness. diff --git a/docs/plans/multi-agent-workspace-governance-idea.md b/docs/plans/multi-agent-workspace-governance-idea.md new file mode 100644 index 0000000..23ca517 --- /dev/null +++ b/docs/plans/multi-agent-workspace-governance-idea.md @@ -0,0 +1,47 @@ +# Multi-Agent Workspace Governance Idea +> Status: IMPLEMENTED +> Created: 2026-07-15 +> Source: local code-intel-pipeline + +## Abstract +Add a read-only workspace preflight that inventories Git changes before a coding agent starts. The preflight must fail closed for a dirty repository root while still allowing an explicitly declared observation-only session. + +## Core Insight +A merge queue cannot recover provenance after several agents have already written into the same dirty root. The cheapest reliable control is therefore a deterministic, machine-readable gate before mutation begins. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: discovered at runtime +- Current state: intentionally dirty; existing tracked and untracked changes are user-owned and must remain untouched + +## Success Criteria +- [x] Doctor passes. +- [ ] Pipeline emits `summary.md`, `report.json`, and `understanding.md`. +- [x] Preflight reports tracked and untracked counts without modifying the inspected repository. +- [x] Preflight emits a stable machine-readable manifest and SHA-256 fingerprint. +- [x] Dirty root rejects mutation-oriented agent work by default. +- [x] Explicit observation-only mode remains available and cannot authorize writes. +- [x] Self-contained fixture tests prove clean, dirty, observation-only, and non-repository behavior. + +## Constraints +- Do not add dependencies without explicit approval. +- Do not rewrite, clean, stash, reset, commit, or otherwise alter existing user changes. +- Do not modify merge queue, CI, integration registry, or project-conformance files in this slice. +- Use only PowerShell, Git, and .NET standard-library capabilities. +- Keep generated test repositories outside the inspected target and remove them after the test. + +## Open Questions +1. A later integration slice may decide where this preflight is called automatically. +2. Multi-machine provenance remains outside this local workspace gate. + +## Implementation Notes +- Minimalism rung: platform-native Git porcelain plus the smallest local PowerShell adapter. +- Parse `git status --porcelain=v1 -z --untracked-files=all` so paths with spaces and renames remain unambiguous. +- Hash canonical JSON inventory content, excluding volatile timestamps and absolute repository paths. +- Exit nonzero for mutation intent in a dirty root or for any unverifiable Git state. + +## Verification +- Doctor passed on 2026-07-15. +- The self-contained fixture test passed for clean, modified, renamed, untracked, observation-only, subdirectory, non-repository, and invalid-policy cases. +- A live observation of the target root exited `0`; the same inventory with mutation intent exited `20`. +- The lite pipeline attempt was intentionally not retried while sibling agents were writing concurrently; snapshot identity returned exit `74`. The coordinating agent owns the stable post-integration normal run. diff --git a/docs/plans/pon-multilanguage-code-evidence-idea.md b/docs/plans/pon-multilanguage-code-evidence-idea.md new file mode 100644 index 0000000..f4423ce --- /dev/null +++ b/docs/plans/pon-multilanguage-code-evidence-idea.md @@ -0,0 +1,40 @@ +# Idea File +> Status: IMPLEMENTED_AND_LOCALLY_VERIFIED +> Created: 2026-07-15 +> Source: can1357/pon at ab9067dbd2899c64c4d67a4bc27b8ad49472b126 + +## Abstract +Map the language-independent part of pon's frontend/conformance architecture onto the Pipeline's existing Code Evidence contract. Prove that supported source languages emit the same file, symbol, containment, and import shapes without copying pon code or claiming parser-level semantic precision. + +## Core Insight +The Pipeline already owns the sufficient intermediate representation: the tuple of `files`, `symbols`, `symbol-chunks`, and `imports`. The missing protection is a multilingual conformance floor that proves language adapters normalize into this shared contract while unsupported semantics remain explicit unknowns. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current working branch +- Current state: large pre-existing dirty worktree; new work must stay in isolated files + +## Success Criteria +- [x] Doctor passes. +- [x] Pipeline emits `summary.md`, `report.json`, `hospital.md`, and `understanding.md`. +- [x] A committed fixture covers Python, JavaScript, TypeScript, Rust, Go, Java, PowerShell, and an unsupported-language control. +- [x] One Rust integration test proves supported languages normalize into the same Code Evidence v1 field contract. +- [x] The test proves unsupported languages stay explicit and do not fabricate symbols or imports. +- [x] Documentation distinguishes normalized structural facts from AST, type, control-flow, and runtime semantics. +- [x] An internalization record pins source revision, owned artifacts, evidence hashes, rollback, and review gaps. + +## Constraints +- Do not add dependencies. +- Do not copy pon implementation code, fixtures, or prose because the upstream repository has no declared license. +- Do not relabel line heuristics as parser, type, call-graph, or runtime semantic precision. +- Do not modify overlapping workflow or native extractor files for this proof. +- Keep the existing Code Evidence artifact contract and runtime behavior unchanged. + +## Open Questions +1. Which language should receive the first parser-backed adapter after this structural floor is established? +2. Should Java imports and C# symbols remain explicit gaps or become the next bounded adapter work? + +## Implementation Notes +- Minimalism rung: reuse the existing Pipeline contract and Rust test harness. +- The proof is a conformance fixture, not a second IR implementation. +- Production widening requires parser-backed evidence and independent verification. diff --git a/docs/plans/pon-parity-floor-idea.md b/docs/plans/pon-parity-floor-idea.md new file mode 100644 index 0000000..cd3f930 --- /dev/null +++ b/docs/plans/pon-parity-floor-idea.md @@ -0,0 +1,45 @@ +# Idea File +> Status: IMPLEMENTED_AND_LOCALLY_VERIFIED +> Created: 2026-07-15 +> Source: `can1357/pon` selective internalization review + +## Abstract +Add a monotonic floor over the Pipeline's existing parity fixtures. A floor records the +currently proven passing fixture set and minimum count; normal validation fails when any floor +case disappears or regresses, and floor updates require an explicit review reason. + +## Core Insight +Golden equality proves one fixture still matches, but it does not state that the proven corpus as +a whole may only grow. A committed set-and-count floor turns current compatibility coverage into a +ratchet without changing the underlying parity oracle. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current working tree +- Current state: existing parity fixtures and guarded golden updates; no corpus-level monotonic floor + +## Success Criteria +- [x] Doctor passes. +- [x] Pipeline emits `summary.md`, `report.json`, and `understanding.md` for the source review. +- [x] Failure categories are explained if nonzero. +- [x] A human can identify the next action from the artifact. +- [x] Every committed floor case executes through the existing parity oracle. +- [x] Missing or failing floor cases reject the run and reject floor updates. +- [x] Floor updates require a non-empty review reason and cannot lower the passing set or count. +- [x] No new runtime dependency or external execution path is introduced. + +## Constraints +- Do not add dependencies. +- Do not copy `pon` implementation code; the upstream repository has no declared license. +- Keep `test-parity-baseline.ps1` as the behavioral oracle and avoid overlapping its current edits. +- Do not add a divergence waiver path until a real, independently reviewed exception class exists. +- Treat the source as a design reference; production authority remains local. + +## Open Questions +1. Whether future non-parity suites should share the same floor schema after representative use. +2. Whether progress reporting belongs in CI output or a committed artifact after timing is measured. + +## Implementation Notes +- Source review used revision `ab9067dbd2899c64c4d67a4bc27b8ad49472b126`. +- Add a separate floor file and checker so the existing parity test remains untouched. +- Validate the checker against the current five fixtures plus synthetic missing/lowered-floor cases. diff --git a/docs/plans/pon-project-conformance-mapping-idea.md b/docs/plans/pon-project-conformance-mapping-idea.md new file mode 100644 index 0000000..dc25a4e --- /dev/null +++ b/docs/plans/pon-project-conformance-mapping-idea.md @@ -0,0 +1,41 @@ +# Pon project-conformance mapping + +## Problem + +The first internalization pass added a parity floor and a component-level language-adapter gate, +but it did not define how Pon's conformance workflow governs acceptance of Code Intel Pipeline as +a project. Generic precision/recall thresholds cannot stand in for that missing project-level +contract. + +## Selected mapping + +Internalize the method, not Pon's compiler implementation: + +| Pon conformance mechanism | Code Intel project acceptance | +| --- | --- | +| CPython as reference oracle | reviewed fixture/golden artifact oracle | +| conformance corpus | repository-state and multi-language fixture corpora | +| JIT/AOT parity | normalized output parity across supported evidence paths | +| committed passing floor | monotonic parity case set and count | +| expected divergences | reviewed, expiring known-divergence ledger | +| fuzzing | fail-closed mutation/property tests | +| free-threading stress | repeated-run determinism and concurrency stress | +| fast/full gates | local/PR fast profile and release full profile | +| benchmark floor | representative latency/resource regression ratchet | + +## Delivery + +Add one project-conformance policy and one executable runner. The fast profile must execute the +mechanisms already supported by the repository. The full profile must fail closed while mapped +mechanisms such as the active divergence ledger or performance ratchet remain unfinished. This +makes the mapping useful immediately without overstating completeness. + +The existing language-adapter acceptance gate remains a component suite under project +conformance; it is not the project-level policy. + +## Constraints + +- Do not copy Pon source, fixtures, or prose because its license was not declared when inspected. +- Preserve existing tests and artifacts; compose them rather than introducing another scanner. +- A passing fast profile is not production/release acceptance. +- Policy observations cannot silently update floors or create divergence exceptions. diff --git a/docs/plans/python314-pon-development-lane-idea.md b/docs/plans/python314-pon-development-lane-idea.md new file mode 100644 index 0000000..ce645ea --- /dev/null +++ b/docs/plans/python314-pon-development-lane-idea.md @@ -0,0 +1,34 @@ +# Python 3.14 development lane with optional Pon parity + +## Goal + +Make Python 3.14 an executable development agreement for Code Intel Pipeline while treating Pon as +an optional native backend whose compatibility must be proven, never assumed. + +## Contract + +1. CPython 3.14 is the authoritative runtime and semantic oracle. +2. Repository-owned Python entry points must compile under CPython 3.14. +3. A reviewed corpus pins portable expected behavior under CPython 3.14. +4. When Pon is available, the Pon-required corpus is dual-run and stdout, stderr, and exit code must + exactly match CPython on the same machine. +5. A development profile may pass with Pon unavailable, but must report that state explicitly. +6. A Pon-candidate profile fails unless Pon is present and every required dual-run case matches. +7. Python 3.14 features not yet claimed for Pon remain explicit CPython-only cases rather than being + removed from the project language agreement. + +## Boundaries + +- Do not install, vendor, or execute upstream Pon source automatically. +- Do not change the user's dirty CI workflow in this pass. +- Do not change the system/default `python`; resolve Python 3.14 explicitly. +- Do not describe CPython-only success as Pon compatibility. +- Reuse the project conformance runner by adding this lane as an executable suite. + +## Verification + +- Development profile passes on CPython 3.14 with an explicit `pon=unavailable` observation. +- Pon-candidate fails closed when Pon is absent. +- A test-only CPython-backed Pon shim proves the positive dual-run path. +- A divergent shim is rejected by the exact parity gate. +- The project fast conformance profile executes the Python 3.14 development suite. diff --git a/docs/plans/session-evidence-adapter-idea.md b/docs/plans/session-evidence-adapter-idea.md new file mode 100644 index 0000000..eeeb562 --- /dev/null +++ b/docs/plans/session-evidence-adapter-idea.md @@ -0,0 +1,63 @@ +# Idea File +> Status: IMPLEMENTED AS OPTIONAL RESEARCH ADAPTER; NOT DEFAULT OR PRODUCTION +> Created: 2026-07-23 +> Source: cosmtrek/mindwalk trial plus local Code Intel evidence + +## Abstract + +Add an optional Rust session-evidence adapter to Code Intel. The adapter consumes Mindwalk trace v1, +removes prompt/summary content, binds the observation to a Code Intel repository snapshot, joins file +touches to Sentrux structural evidence, and emits advisory review signals. + +## Core Insight + +Temporal agent behavior is useful only when it is joined to repository-owned structural evidence. +The provider parser is replaceable and imperfect; path safety, privacy, observability grading, +snapshot identity, and review semantics must therefore belong to Code Intel. + +## Target Repo + +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: current user working branch +- Current state: heavily modified/untracked; implementation must be additive and avoid rewriting + unrelated work + +## Success Criteria + +- [x] `provider session-adapt` consumes a Mindwalk trace v1 without invoking Mindwalk. +- [x] The emitted artifact contains no user-message marks, raw event summaries, or absolute paths. +- [x] Every in-repository target is normalized and every outside/unsafe target is counted, not + silently accepted. +- [x] The artifact is bound to a Code Intel repository snapshot. +- [x] Optional Sentrux hotspot/DSM input enriches targets and produces advisory signals. +- [x] Missing or estimated provider fields remain visibly partial/unknown. +- [x] The adapter is optional and absent from normal scan execution. +- [x] Focused Rust privacy, path, snapshot, structural-join, and fail-closed contract tests pass. +- [ ] The cited real-session smoke is not independently reproducible from committed raw evidence; + production/default promotion remains blocked on a fresh privacy-safe representative run, + hostile-trace coverage, and measured maintenance/latency value. + +## Constraints + +- Add no dependency; use Rust standard library plus existing `serde_json`. +- Copy no Mindwalk implementation source. +- Preserve Mindwalk MIT provenance and source revision in documentation. +- Do not add production behavior to PowerShell. +- Do not grant session evidence gate, diagnosis, or Engineering Fact authority. +- Keep generated raw traces outside source control. + +## Open Questions + +1. Whether a future native Codex adapter should replace Mindwalk for exact structured tool events. +2. Which advisory thresholds should become policy-configurable after real usage data exists. + +## Implementation Notes + +- Minimalism rung: reuse existing snapshot identity and Sentrux artifact contracts, then add the + smallest local Rust normalizer. +- Public command: + `code-intel provider session-adapt --repo --trace [--hotspots ] [--out ]`. +- Default working-tree policy is `explicit_overlay`, because session review normally describes the + files actually edited during the task. +- Mindwalk extraction remains a separate optional preceding command. Code Intel can consume a + previously generated trace even when Mindwalk is later unavailable. diff --git a/docs/plans/three-stage-project-acceptance-idea.md b/docs/plans/three-stage-project-acceptance-idea.md new file mode 100644 index 0000000..399e366 --- /dev/null +++ b/docs/plans/three-stage-project-acceptance-idea.md @@ -0,0 +1,38 @@ +# Idea File +> Status: COMPLETE +> Created: 2026-07-15 +> Source: local code-intel-pipeline + +## Abstract +Provide one language-neutral acceptance entry point for agent, landing, and promotion decisions. Reuse `Test-CodeIntelProjectConformance.ps1` and its policy so each stage selects an existing project profile instead of copying suite logic. + +## Core Insight +The three stages differ by required evidence strength, not by language: agent work needs explicit targeted checks plus the fast project profile, landing needs fast project conformance, and promotion needs full project conformance. + +## Target Repo +- Path: `D:\projects\_tools\code-intel-pipeline` +- Branch: `codex/code-intel-atomic-model` +- Current state: dirty shared workspace with concurrent agent changes; this lane owns only new acceptance-layer files. + +## Success Criteria +- [x] `Invoke-CodeIntelAcceptance.ps1 -Stage agent|land|promote` maps stages to targeted+fast, fast, and full respectively. +- [x] The entry point delegates project suites to `Test-CodeIntelProjectConformance.ps1` without duplicating suite implementations. +- [x] Machine output uses only `pass`, `fail`, `blocked`, or `skipped-with-reason` outcome statuses and fails closed. +- [x] Agent targeted checks are explicit argv arrays, repository-contained, and reject shell syntax or path escape. +- [x] Self-contained fixtures prove stage mapping, failure propagation, injection resistance, and repository paths containing spaces. +- [x] Doctor, lite baseline, targeted tests, real fast acceptance, and Sentrux session verification pass; final normal pipeline verification is delegated to the integrating Agent. + +## Constraints +- Do not add dependencies. +- Do not modify Merge Queue files, `orchestration/integrations.json`, CI, or Rust hotspot code. +- Do not rewrite unrelated code or overwrite concurrent user/agent changes. +- Keep generated artifacts outside source control. +- Promotion remains an acceptance decision only; this entry point performs no push, merge, or publication. + +## Open Questions +1. None blocking: targeted checks will be supplied as JSON argv arrays to preserve language neutrality and avoid shell evaluation. + +## Implementation Notes +- Use an acceptance policy to declare stage/profile mapping and targeted-command safety limits. +- Invoke child processes with PowerShell call-operator argument arrays; never use `Invoke-Expression`, `cmd /c`, or `powershell -Command`. +- Start with `install-code-intel-pipeline.ps1 -RepoPath `, then doctor/lite/normal as required by the Code Intel skill. diff --git a/docs/plans/understanding-quadrant-complexity-idea.md b/docs/plans/understanding-quadrant-complexity-idea.md new file mode 100644 index 0000000..488a38c --- /dev/null +++ b/docs/plans/understanding-quadrant-complexity-idea.md @@ -0,0 +1,23 @@ +# Understanding Quadrant Validator Complexity + +## Problem + +`validate_understanding_quadrant` combines JSON decoding, exact-shape checks, fixed policy validation, per-item classification, provenance checks, ordering, unknown visibility, and aggregate counts. Its cyclomatic complexity is 33, making it the current Sentrux surgical hotspot. + +## Outcome + +Preserve the existing artifact contract and error behavior while splitting the validator into small, testable helpers. The public contract registration and serialized artifact format must not change. + +## Constraints + +- No new dependency. +- No schema or policy change. +- Duplicate-key rejection remains before JSON decoding. +- Existing valid and invalid fixtures keep the same pass/fail result. +- Do not raise Sentrux complexity thresholds. + +## Verification + +- Run the focused `artifact_ref` unit tests. +- Run `cargo fmt --check`. +- Run Sentrux `session_end` and confirm no structural degradation. diff --git a/docs/pon-conformance-ratchet.md b/docs/pon-conformance-ratchet.md new file mode 100644 index 0000000..2392f7a --- /dev/null +++ b/docs/pon-conformance-ratchet.md @@ -0,0 +1,55 @@ +# Pon-inspired conformance ratchet + +This note selectively reimplements method-level ideas observed in +[`can1357/pon`](https://github.com/can1357/pon) at revision +`ab9067dbd2899c64c4d67a4bc27b8ad49472b126`. The upstream repository did not +declare a license when checked on 2026-07-15, so no implementation code, fixtures, or prose are +copied into Code Intel Pipeline. + +## Absorbed semantics + +1. **Oracle and floor are separate.** `test-parity-baseline.ps1` remains the byte-stable parity + oracle for each case. `test-parity-floor.ps1` only records which cases have already earned a + pass and the minimum passing count. +2. **The proven set is monotonic.** A normal run fails if a committed floor case disappears or no + longer passes. Updating the floor cannot remove an old case or lower the count. +3. **Progress is explicit.** Passing candidate cases outside the current floor are reported as + progress; they do not silently change the floor. +4. **Promotion is reviewed.** `-UpdateFloor` requires a non-empty `-ReviewReason`, all candidate + cases must pass, and the resulting floor remains a committed review surface. +5. **Current truth is machine-owned.** Human documentation may summarize coverage, but the floor + file and executable oracle own the compatibility claim. + +## Local implementation + +- `tests/fixtures/parity/parity-floor.json`: committed set-and-count floor. +- `test-parity-floor.ps1`: monotonic checker and guarded updater. +- `test-parity-baseline.ps1`: existing per-case oracle; unchanged by this internalization. + +Run: + +```powershell +pwsh -NoProfile -File ./test-parity-floor.ps1 +``` + +Promote newly passing fixtures only after reviewing their semantic coverage: + +```powershell +pwsh -NoProfile -File ./test-parity-floor.ps1 -UpdateFloor -ReviewReason "" +``` + +## Deliberately not absorbed + +- The compiler, runtime, CPython corpus, and Rust implementation: unrelated to Pipeline and not + licensed for reuse. +- A divergence/exclusion ledger: Pipeline evidence is currently small and fail-closed; adding an + exception mechanism before a real reviewed need would create a regression-hiding surface. +- Automatic floor updates: a green run is evidence, not authority to rewrite the baseline. +- Performance floors: parity correctness is the first sufficient rung; performance ratchets need + representative timing methodology and a separate noise policy. + +## Exit criteria + +Remove the source reference if these semantics become independently specified and tested, or +retire the floor wrapper if it never catches a corpus-level regression beyond the per-case oracle. +The existing parity fixtures remain valid if this wrapper is removed. diff --git a/docs/pon-multilanguage-code-evidence.md b/docs/pon-multilanguage-code-evidence.md new file mode 100644 index 0000000..e25a61d --- /dev/null +++ b/docs/pon-multilanguage-code-evidence.md @@ -0,0 +1,36 @@ +# pon-inspired multilingual Code Evidence mapping + +The reusable part of pon is not Python syntax. It is the separation between a language frontend, a normalized internal representation, and a conformance oracle. Code Intel Pipeline maps that method onto its existing `evidence.native-code` boundary without importing pon runtime or compiler code. + +## Owned mapping + +The Pipeline's language-neutral structural representation is the existing Code Evidence v1 tuple: + +- `files.json`: source identity, language, size, and content digest; +- `symbols.json`: normalized declarations such as function, class, interface, and enum; +- `symbol-chunks.json`: declaration containment; +- `imports.json`: normalized import target observations; +- `coverage.json`: supported heuristics and explicit unknowns. + +Python, JavaScript, TypeScript, Rust, Go, Java, and PowerShell adapters emit the same field shapes. Consumers can therefore rank and navigate code without branching on source syntax. The multilingual fixture is a ratchet over that shared contract: a supported language cannot silently disappear, change fact shape, or fabricate additional facts without a reviewed fixture change. + +## Precision boundary + +This is a structural fact envelope, not a claim of full language semantics. The current native producer is line-heuristic and explicitly reports: + +- symbol precision: heuristic; +- import precision: heuristic; +- relationship precision: unknown; +- call graph: unknown. + +AST shape, types, overload resolution, dynamic dispatch, control flow, effects, macro expansion, and runtime behavior belong to future parser-backed language adapters. Unsupported languages remain present in `files.json` and `chunks.json`, appear in `unsupportedFiles`, and produce no invented symbols or imports. + +## Provenance and exclusions + +Design reference: `can1357/pon`, revision `ab9067dbd2899c64c4d67a4bc27b8ad49472b126`. The upstream repository had no declared license when reviewed, so this work independently specifies and tests the architectural method. No pon implementation code, fixtures, or prose were copied. + +The proof deliberately does not add another IR, a pon dependency, Python runtime behavior, a compiler backend, or automatic floor updates. The existing Code Evidence contract remains the owned compatibility boundary. + +## Next widening step + +Choose one language from measured demand, add a parser-backed adapter behind `evidence.native-code`, and prove it preserves the shared fact contract while improving a pinned precision/recall corpus. Java import extraction and C# declaration extraction remain explicit candidate gaps rather than implicit support claims. diff --git a/docs/ponytail-gain-ledger.md b/docs/ponytail-gain-ledger.md index 0a850b0..d0ae4b4 100644 --- a/docs/ponytail-gain-ledger.md +++ b/docs/ponytail-gain-ledger.md @@ -22,6 +22,12 @@ Status meanings: | PG-007 | blocked | Remove bundled Sentrux lite core fallback. | `tools/sentrux-shim/sentrux-lite-core.ps1` duplicates enough Sentrux behavior to keep new machines closed-loop. | Keep until upstream `sentrux.exe` install and Windows plugin behavior are reliable in CI and teammate setup. | | PG-008 | open | Collapse repeated README/skill/architecture operational prose. | README, `skill/SKILL.md`, and `docs/code-intel-architecture.md` all repeat install, doctor, normal run, Sentrux, Repowise, and hospital read order. | Make README narrative, skill hot-path commands, architecture boundaries; delete duplicate command blocks elsewhere. | | PG-009 | harvest-next | Keep Ponytail benchmark gate additions minimal. | Current benchmark test was table-driven to avoid Sentrux complexity regression, but it expanded the file more than the concept needs. | Keep the new assertions, but do not add more gates unless a published contract depends on them. Prefer one array per concept over per-phrase blocks. | +| PG-010 | blocked | Retire one compatibility branch only after E00 approval. | `compatibility.retirement-gate` projects a content-bound `approved-for-ticket` or `blocked` item from replacement, parity, registry, window, rollback execution, usage, C00, dependency, and independent-review evidence. | Open one E01 retirement ticket for the named branch; the gate itself has no deletion authority. | +| PG-011 | blocked | Retire `run-code-intel.codenexus-lite.direct` after the normal path is routed through B04 and unavailable mode selects B05. | E04's packet proves full/lite/unavailable contract fixtures, provider-owned process/storage effects, B05's `structuralVerdict=unknown` boundary, and exact rollback replay; the current normal path still contains one direct `Invoke-CodeNexusLite.ps1` block. | Substitute the single call path under a separately reviewed change, complete the 30-day usage window and independent E00 approval, then execute the one-file deletion ticket; until then `deletionExecuted=false` and `retired=false`. | +| PG-012 | blocked | Retire `run-code-intel.native-code.embedded` after normal/full execute B08 through A09. | B08 preserves normalized v1 artifacts, emits eight snapshot-bound Artifact Refs, declares `repo_read`/`local_write`, and reports unsupported relationship precision as unknown; the facade still contains the embedded function family and direct call. | Route normal/full through A09, complete the 30-day observation window and independent E00 approval, then execute the two-segment one-file deletion ticket; until then `deletionExecuted=false` and `retired=false`. | +| PG-013 | blocked | Retire `run-code-intel.hospital.embedded-diagnosis-render` after the normal facade executes B09. | B09 proves fail-closed precedence, rebuildable Markdown views, A09-seeded A01 execution, and stable machine parity against the legacy facade on the same untrusted authoritative fixture; the normal facade still owns one embedded function block and one direct invocation block. | Route the normal facade through `diagnosis.hospital`, complete the 30-day usage window and independent E00 approval, then execute the two-hunk one-file deletion ticket; until then `deletionExecuted=false` and `retired=false`. | +| PG-014 | blocked | Retire `update-code-intel-index.legacy-compatibility-traversal` after E05 and E00 approval. | Normal public refresh already routes through A08 with rebuild/incremental parity and valid-A07-only admission; explicit legacy compatibility traversal remains publicly reachable and produces diagnostic, non-authoritative output. | Keep the legacy branch until E05, the observation window, usage evidence, and independent approval pass; then execute the one-hunk one-file deletion ticket. Until then `deletionExecuted=false` and `retired=false`. | +| PG-015 | blocked | Retire `invoke-code-intel.doctor.direct-production` after public preflight routes through B10. | B10 proves one-result envelope behavior for manifest drift and present-but-nonconforming providers, secret redaction, and readiness/conformance separation; `invoke-code-intel.ps1` still invokes the retained PowerShell bootstrap directly, and that observation-only bootstrap has no declared expiry. | Keep `check-code-intel-tools.ps1`; route public preflight through `doctor`, declare an owned expiry/removal criterion for the non-authoritative bootstrap, complete the 30-day usage window and independent E00 approval, then execute only the three-hunk one-file deletion ticket. Until then `deletionExecuted=false` and `retired=false`. | ## Retained After Audit diff --git a/docs/ponytail-governance-gate.md b/docs/ponytail-governance-gate.md new file mode 100644 index 0000000..6f72cb2 --- /dev/null +++ b/docs/ponytail-governance-gate.md @@ -0,0 +1,64 @@ +# Ponytail Governance Gate + +`governance.ponytail-gate` is the executable C00 admission boundary for Agent-produced changes. +It validates necessity and implementation selection; it does not rank product work, judge code +style, or replace engineering review. + +The checked contracts are: + +- `orchestration/schemas/code-intel-ponytail-gate.v1.schema.json` +- `orchestration/ponytail-gate-policy.v1.json` +- `tests/fixtures/ponytail/c00-necessity-trace.json` + +## Admission trace + +Every declared artifact, dependency, abstraction, file, test, documentation, or process change +names exactly one current value source and its evidence. Allowed sources are an +operator-requested outcome, Committed Engineering Plan deliverable, verified defect or risk, +required contract or gate, evidence-closing spike, or approved debt reduction. `future_maybe` is +representable only so the deterministic gate can reject it; it is not a current value source. + +Each change selects one first sufficient rung from the existing Implementation Minimalism +Benchmark. Every lower rung must appear once, in order, with a nonempty reason and known evidence. +This makes “smallest sufficient” reviewable without introducing a solver or policy platform. +`requiredEvidenceIds` closes the rest of the Necessity Trace, including evidence for the declared +protection boundary; value-source and lower-rung evidence do not replace it. + +## Non-filterable engineering boundaries + +The gate never admits a declaration that removes verification, evidence, safety, error handling, +accessibility, data-loss prevention, or artifact-contract requirements. An authority bypass cannot +override these boundaries. C00 governs implementation minimalism; it is not permission to +under-build. + +## A05 authority bypass + +A value-source or rung rejection may be temporarily bypassed only by an explicit +`code-intel-authority-event.v1` record scoped to the same change id. C00 reuses the A05 validator: +the event must be approved, have a named approver, cover the value-source evidence, every +lower-rung evidence ID, and every `requiredEvidenceIds` entry, be issued and +unexpired at evaluation time, and not appear in the consumed-event set or another branch. Missing +evidence, expiry, future dating, wrong scope, duplicate use, and replay fail closed. + +## Modes and integration seam + +`report_only` and `enforce` run the same rules and retain the same per-change trace. Report-only +sets `enforcedBlock=false` while preserving `wouldReject` and rejected branches. Enforce sets +`enforcedBlock=true` whenever a rejection remains. A bypassed branch remains visible with its +authority event id and the original rejection diagnostic. + +The Rust module exposes the in-process `evaluate` and `policy_document` seams. Production callers +use `code-intel governance ponytail-gate --request `, registered as +`governance.ponytail-gate` in `orchestration/integrations.json`. A completed non-blocking result +exits 0; an enforce result with `enforcedBlock=true` remains schema-valid on stdout and exits 2. +Usage errors exit 64, contract-invalid input exits 65 without emitting a result, and host I/O +failures exit 74. The gate is not coupled to the A09 DAG runner. + +Run the focused CI contract with: + +```powershell +./test-ponytail-gate-contract.ps1 -RepoPath . +``` + +No Ponytail package or runtime is installed or invoked. The source project remains a reference; +the semantic contract is owned by Code Intel Pipeline. diff --git a/docs/project-orientation-benchmark.md b/docs/project-orientation-benchmark.md new file mode 100644 index 0000000..80b1495 --- /dev/null +++ b/docs/project-orientation-benchmark.md @@ -0,0 +1,26 @@ +# Project Orientation Benchmark + +`project.orientation-benchmark` verifies D01 orientation cost and quality without changing D01's output semantics. It runs a fixed `small | medium | large` by `clean | dirty | provider_missing` corpus, twice or more for both cold and warm conditions, with one local child process at a time and no LLM or hosted service. + +## Run + +```powershell +cargo build -p code-intel +target/debug/code-intel.exe benchmark orientation --out artifacts/orientation-benchmark --repetitions 3 +``` + +The output directory must not already exist. The runner writes: + +- `observations.json`: measured samples under `code-intel-project-orientation-benchmark-observations.v1`. +- `report.json`: authoritative evaluation under `code-intel-project-orientation-benchmark.v1`. +- `report.md`: rebuildable human view. + +Cold samples include fresh fixture and immutable Artifact Ref materialization. Warm samples reuse the immutable input corpus but always use a fresh output directory. Percentiles use nearest-rank over `std::time::Instant` wall times. `small` and `medium` across all conditions define the typical corpus; `large` is a stress corpus. Every sample records the exact orientation byte size and SHA-256. A pass requires typical p95 at or below 60 seconds, identical output digests for every replay of a fixture, exact measured-field correctness, exact unresolved-field coverage, exact unsupported-file coverage, and complete claim provenance. + +## Quality and failure policy + +Every evaluated orientation claim must retain non-empty provenance. The evaluator recomputes each orientation sample's byte size and digest before using its determinism metrics. A fast result with missing claim provenance or forged size/digest metadata is a contract failure and no report is published. Reports separate fixture-materialization and A01 process/orientation costs and expose typical/all artifact-size percentiles. + +The representative benchmark is a blocking CI contract. Local and CI reports state `cleanMachine: false`; that field remains unchanged and must not be forged. The prior clean-machine record in [`orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json`](../orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json) is historical evidence for its bound source digest; after benchmark implementation changes it is stale until a new disposable clean-machine run binds the current digest. Its closed schema is [`code-intel-clean-machine-verifier-attestation.v1.schema.json`](../orchestration/schemas/code-intel-clean-machine-verifier-attestation.v1.schema.json). Cold does not mean an operating-system page-cache flush. + +The benchmark consumes D01 through the registered A01 capability and A03-verified artifacts. It does not grant authority to benchmark observations, infer missing structure, or convert provider absence into a passing structural verdict. diff --git a/docs/project-orientation.md b/docs/project-orientation.md new file mode 100644 index 0000000..d683565 --- /dev/null +++ b/docs/project-orientation.md @@ -0,0 +1,9 @@ +# Project Orientation + +`project.orientation` is D01's deterministic first actionable project view. It consumes only A03-verified A02 snapshot, inventory, B05 survival-scan, and B08 native evidence artifacts. It does not read repository content directly, invoke an LLM, or create semantic architecture claims. + +The machine artifact is `project-orientation.json` (`code-intel-project-orientation.v1`). Every known, risk, confidence, and unknown claim carries the input artifact type, SHA-256, and JSON pointer that support it. When no admitted purpose evidence is present, `purpose.status` is `unknown` and `purpose.evidence` is empty. + +`project-orientation.md` is a rebuildable Summary-compatible projection with the sections Identity, Purpose, Languages, Boundaries, Entry Points, Commands, Active Change, Risks, Unknowns, and Confidence. Existing run summaries remain authoritative during the additive D01 rollout. + +The current A01 atom is registered but is not silently added to the default A09 DAG: B05 is still exposed as a verified standalone survival scan rather than an A01 DAG producer. Adding D01 to the default DAG before that dependency can supply an Artifact Ref would invent a dependency result. The atom is executable now with six verified inputs; the default DAG route remains an explicit follow-on integration boundary. diff --git a/docs/public-beta.md b/docs/public-beta.md new file mode 100644 index 0000000..e7f48bc --- /dev/null +++ b/docs/public-beta.md @@ -0,0 +1,65 @@ +# Public Beta Guide + +## Supported surface + +The public beta ships as a Windows ZIP and uses PowerShell 7.2 or newer. The +stable entrypoint is `invoke-code-intel.ps1`; the packaged Rust core is +`bin/code-intel.exe`. A release package must not require Cargo, a source-tree +`target/` directory, or a local Rust installation. + +The beta core covers repository inventory, Sentrux structural evidence, +transactional artifact publication, failure classification, and the +Understanding/Hospital reports. Optional providers enrich those reports but do +not redefine whether the core pipeline is usable. + +| Capability | Beta status | Missing-provider behavior | +| --- | --- | --- | +| Stable PowerShell entrypoint and doctor | Core | Fail with an actionable local error | +| `code-intel.exe` policy/artifact core | Core | Fail; packaged binary is required | +| `rg` inventory | Core | Fail with an actionable local error | +| Sentrux structural evidence | Core | Report real gate/check failure | +| Transactional run commit and reports | Core | Fail closed; incomplete runs are not indexed | +| Repowise semantic memory/docs | Optional, included by default | Record unavailable/skipped; `-SkipRepowise` bypasses it | +| Understand Anything graph | Optional | Record `graph_missing` / manual action | +| CodeNexus context | Optional compatibility adapter | Record note and continue | +| Repomix pack | Optional | Record unavailable/skipped and continue | +| Model assistance channels | Optional | Emit a request/dossier or explicit provider outcome | +| Runtime/CI and file-boundary evidence | Optional | Preserve the absence as evidence state | + +`crates/code-nexus-lite` is incubated source and is not a compiled workspace +member or a promised binary in this beta package. The supported CodeNexus +surface is the optional compatibility adapter and its artifact contract. + +## Install and verify + +1. Download the release ZIP and its `.sha256` file. +2. Verify the checksum with `Get-FileHash -Algorithm SHA256`. +3. Extract the ZIP to a writable directory. +4. Run: + +```powershell +.\invoke-code-intel.ps1 -RepoPath C:\path\to\repo -Mode normal -SkipRepowise +``` + +Remove `-SkipRepowise` when Repowise is installed and semantic memory is +desired. Optional providers remain in the default orchestration plan so a +configured machine gets the richer result without using a different product +path. + +## Known limits + +- The beta release package is Windows-only. +- External providers can be unavailable, rate-limited, or unconfigured. Their + outcomes are reported rather than rewritten as local success. +- Understand Anything graph generation still depends on its host integration. +- Compatibility facades remain shipped while retirement evidence and approval + chains are incomplete; retirement is not a beta-core prerequisite. +- The beta does not promise the incubated CodeNexus Rust worker binary. + +## Upgrade and rollback + +Release ZIPs are self-contained. Extract a new beta beside the previous one, +run the package smoke test, and then switch the caller's path. Rollback means +switching the path back to the previous extracted directory; do not overwrite +the old directory in place. Generated artifacts live outside the package under +the platform Code Intel data root. diff --git a/docs/python314-pon-development.md b/docs/python314-pon-development.md new file mode 100644 index 0000000..b512e25 --- /dev/null +++ b/docs/python314-pon-development.md @@ -0,0 +1,40 @@ +# Python 3.14 development and optional Pon compatibility + +Code Intel Pipeline uses CPython 3.14 as the authoritative Python language/runtime agreement. Pon +is an optional native execution backend. A project can therefore use Python 3.14 without claiming +that Pon supports every Python 3.14 feature. + +## Profiles + +- `development`: CPython 3.14 must exist, repository Python entry points must compile, and the full + local corpus must match reviewed golden behavior. Pon is dual-run when available; absence is + reported but does not block ordinary development. +- `pon-candidate`: all development checks pass, Pon must exist, and every `ponRequired` case must + exactly match CPython stdout, stderr, and exit code on the same machine. + +Run: + +```powershell +./Test-Python314PonCompatibility.ps1 -Profile development +./Test-Python314PonCompatibility.ps1 -Profile pon-candidate -PonCommand pon -Json +``` + +The policy is `orchestration/python314-pon-development-policy.v1.json`. The corpus manifest is +`tests/fixtures/python314-compat/manifest.v1.json`. + +The corpus deliberately separates two claims: + +- Core cases marked `ponRequired: true` define the current candidate portability subset. +- The Python 3.14 template-string case is CPython-authoritative but not yet required from Pon. This + keeps Python 3.14 language development open without overstating the alternative backend. + +## Team agreement + +1. Use explicit Python 3.14 selection; do not rely on whichever `python` happens to be first on PATH. +2. CPython behavior wins when runtimes disagree. +3. A Pon difference is a failed compatibility observation, not permission to weaken the CPython + golden or silently exclude a case. +4. Promote a Python feature into `ponRequired` only after a real Pon run passes and the change is + reviewed. +5. The gate never installs or executes upstream source automatically. A Pon executable must be + supplied by the environment/operator. diff --git a/docs/repository-snapshot-identity.md b/docs/repository-snapshot-identity.md new file mode 100644 index 0000000..761bac7 --- /dev/null +++ b/docs/repository-snapshot-identity.md @@ -0,0 +1,43 @@ +# Repository Snapshot Identity v1 + +`code-intel snapshot identity` implements A02 `repository.snapshot-identity`. It produces identity material only. It does not read or trust Artifact Ref payloads; that remains A03. + +## Command + +```powershell +code-intel snapshot identity --repo --working-tree-policy --scope +``` + +`--scope` is repeatable. Empty scope means `.`. Components are normalized to `/`, sorted, deduplicated, and remain case-sensitive contract values. Absolute paths and `..` are rejected. `--repo` must be the Git worktree root. Output contains no absolute path or timestamp. + +Scope is reduced to its smallest prefix set: `src` absorbs `src/nested`, and `.` absorbs every other scope. Equivalent sets therefore have the same identity and input digest. On Windows, distinct spellings that collide case-insensitively fail closed. + +## Identity contract + +`snapshot.identity` is SHA-256 over length-prefixed v1 records for repository identity, resolved HEAD, working-tree policy, canonical scope, and `inputDigest`. Each digest uses a version/domain record. + +- Normal Git: `repoIdentity=git-lineage-v1:` from the sorted root commits reachable from the consumed HEAD. Remote URL, other refs, checkout path, branch, and clock are excluded. Attached and detached checkouts at the same commit agree. Unrelated orphan histories and other unreachable roots are intentionally excluded: this identity names the lineage actually consumed, not every ref stored in the local object database. +- Shallow Git: exit 69 because the lineage boundary is incomplete. +- Unborn Git: `explicit_overlay` uses `content-v1:` and `head=unborn`; `head_only` exits 69. +- No Git metadata: `explicit_overlay` uses `content-v1:` and `head=unversioned`; `head_only` exits 69. +- Missing Git executable: exit 69; it is never silently reclassified as unversioned. + +## Input encoding and overlay membership + +`head_only` resolves HEAD once and reads its immutable tree by object id. Sorted records retain mode, kind, object id, and relative path. Executable mode, symlink blobs, Gitlinks/submodules, and LFS pointer blobs are therefore checkout-independent. + +`explicit_overlay` uses the Git index plus non-ignored untracked paths. Sorted length-prefixed records contain domain, kind, Git mode, relative path, byte length, and raw bytes. Deleted tracked files emit tombstones. Intent-to-add (`git add -N`, porcelain ` A`) is a modified overlay member. Gitlinks emit the indexed commit and do not recursively consume the submodule worktree. Symlinks emit their target text and are never followed. LFS worktree files are the bytes actually consumed. Ignored files are excluded and output declares `ignoredPolicy=excluded_by_git_ignore`. + +The report separates `trackedModified`, `trackedDeleted`, `untracked`, `renamed`, `typeChanged`, and `staged`; a boolean alone is not the dirty-tree contract. +Porcelain v1 XY states are table-driven: ignored (`!!`) is excluded, intent-to-add is retained, and every unmerged state (`DD`, `AU`, `UD`, `UA`, `DU`, `AA`, `UU`) fails closed instead of producing a partial identity. + +For TOCTOU resistance, overlay status and the complete input digest are computed before and after reading; the unversioned path set and digest receive the same double check. Any difference exits 74. The `inventory.rg` facade additionally opens a snapshot lease that retains the expected input manifest: canonical repository-relative path, kind, mode, content digest, policy, and canonical scope. Frozen bytes are also retained for snapshot-controlled `.gitignore`, `.ignore`, and `.rgignore` files. A repository-root `rg --no-ignore` traversal checks live path membership against an owned manifest mirror baseline; it does not consume live ignore contents or custom filtering globs. The filtered artifact set is produced only by running ripgrep over that mirror with frozen ignore bytes and the declared default/custom globs. Paths are normalized to repository-relative `/` form. Extra, missing, or transiently renamed baseline paths exit 65 and publish no inventory. The complete manifest is re-read and matched again before publication. + +This lease is a set-and-content cross-check, not a filesystem lock: writers are not blocked, but a consumer cannot publish unless the observed path set and the post-consumption manifest still match the requested snapshot. It is a bounded consumer closure, not the A09 DAG or A03 Artifact Ref verifier. + +## Exit classes + +- `0`: one compact JSON document on stdout, stderr empty. +- `64`: invalid CLI, scope, or repository-root usage. +- `69`: Git/rg unavailable, incomplete lineage, or impossible policy. +- `74`: filesystem read or concurrent-change failure. diff --git a/docs/repository-survival-scan.md b/docs/repository-survival-scan.md new file mode 100644 index 0000000..3de3a1f --- /dev/null +++ b/docs/repository-survival-scan.md @@ -0,0 +1,15 @@ +# Repository Survival Scan + +`repository.survival-scan` is the B05 minimum-survival boundary for a run where the B04 CodeNexus adapter has returned admitted `provider_unavailable` evidence. + +It re-verifies the A02 repository snapshot and A01 rg inventory Artifact Refs through A03, then replays the embedded B04 evidence request through A04. The result contains only repository identity, revision/dirty state, basic file-count and extension inventory, and provider diagnosis. Each promoted basic fact identifies the digest of its source artifact. + +The result always declares `completeness = reduced` and `structuralVerdict = unknown`. It does not infer dependency relationships, change propagation, hotspots, execution risk, or a current architecture view. Those claims require a separately admitted provider result and are outside this fallback. + +Production CLI: + +```text +code-intel repository survival-scan --request --artifact-root +``` + +The PowerShell facade exposes the same route through `-SurvivalScanRequest` and `-SurvivalScanArtifactRoot`. The existing `inventory.rg` adapter remains the rollback-compatible inventory producer; B05 does not add a parser or provider database. diff --git a/docs/repowise-adapter.md b/docs/repowise-adapter.md new file mode 100644 index 0000000..1f8b6fc --- /dev/null +++ b/docs/repowise-adapter.md @@ -0,0 +1,40 @@ +# Repowise provider adapter + +`provider.repowise-adapt` translates Repowise-native outcomes into the provider-neutral A04 +Evidence Provider Port. It does not change Repowise internals and does not treat provider health +as evidence. + +The adapter maintains three independent channels: + +- `health` reports CLI/provider readiness only and always declares `evidence: false`. +- `index` declares index completeness, freshness, local index effects, and an A04 request when an + index observation exists. +- `docs` separately declares docs completeness, freshness, network/model/filesystem effects, and + an A04 request when docs evidence exists. + +Provider quota maps only the docs observation to partial `provider_unavailable`; it does not erase +or downgrade a current index observation. Missing CLI is a local-tool health diagnosis and emits +no fabricated evidence. Stale observations are translated faithfully and rejected by A04 under +the caller's freshness policy. Successful but incomplete docs remain partial/domain-unknown. + +Every translated result starts with `factPromotion.eligible=false`. Consumers must pass each +generated request through `code-intel evidence validate`; only the admitted Observed Evidence may +continue toward A05. The adapter never emits Engineering Facts. + +The production route performs both operations as one fail-closed boundary: + +```text +code-intel provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds +``` + +Its `code-intel-repowise-route-result.v1` output keeps the translation and an A04 result for every +emitted evidence channel. Exit `0` means every emitted observation was admitted (including an +`unknown` domain verdict for partial docs); exit `65` means the native contract or at least one A04 +check was rejected. Missing CLI emits no fabricated evidence. Native diagnostics are never copied. +The `run-code-intel.ps1 -RepowiseAdapterRequest ...` facade selects this same Rust route. + +`Invoke-RepowiseProviderProbe.ps1` is the production health probe. The historical +`test-code-intel-provider.ps1` name is now only a test wrapper over that production seam. +`run-code-intel.ps1` uses the production probe and continues index-only execution when optional +docs health fails. Existing direct Repowise CLI/index commands remain compatibility and rollback +surfaces; they are optional diagnostics/rollback only, and their raw output has no evidence or fact authority. diff --git a/docs/run-commit.md b/docs/run-commit.md new file mode 100644 index 0000000..dd28d62 --- /dev/null +++ b/docs/run-commit.md @@ -0,0 +1,11 @@ +# Run Commit + +`run.commit` is the sole transaction boundary between A06 staging and a published run. It does not generate artifacts, execute DAG nodes, diagnose results, or update A08 indexes. + +The production CLI is `code-intel run commit --source-root --authority-root --manifest-ref --final-name `. It revalidates the source A09 manifest and refs, copies the already-authorized bytes through a fresh A06 owned staging set, rewrites the terminal manifest to the resulting content-addressed Artifact Refs, and then invokes the publication transaction. The PowerShell facade exposes the same route through `-RunCommitSourceRoot`, `-RunCommitAuthorityRoot`, `-RunCommitManifestRef`, and `-RunCommitFinalName`. + +Before promotion it reopens and validates the A09 terminal run manifest, every referenced A06 artifact, registered schema/type, SHA-256 digest, and consumed snapshot identity. The staged directory is promoted with same-authority, atomic no-replace rename semantics and its parent directory is synchronized. `run-complete.json` is created only after promotion and directory synchronization, published with a second atomic no-replace rename, synchronized, then reread. The marker binds the run identity, repository snapshot, manifest path, and manifest digest. + +Failures before promotion leave A06 ownership cleanup intact. Failures after promotion leave an uncommitted, recoverable directory; `recover` repeats full validation and synchronizes the promoted directory entry before publishing a marker. A post-marker verification failure removes a marker only when both its stable file identity and bytes still match the file published by that attempt. Existing destination directories and competing marker files are never replaced or recursively deleted. + +Directories without a valid marker, including legacy timestamp runs and the facade's prior `{generatedAt,report,reportSha256}` marker shape, classify as `legacy-uncommitted`; they remain directly readable but are not committed authority. The existing A08 PowerShell index still consumes only that old marker shape and therefore deliberately ignores new A07 publications until A08 is implemented; A07 never calls it. diff --git a/docs/runtime-ci-evidence.md b/docs/runtime-ci-evidence.md new file mode 100644 index 0000000..5d329f2 --- /dev/null +++ b/docs/runtime-ci-evidence.md @@ -0,0 +1,23 @@ +# Runtime/CI Evidence + +Runtime/CI evidence closes the gap between static engineering proxies and observations produced by tests, builds, and a running system. The first provider-neutral boundary reads one explicitly named local JSON artifact. It does not call a CI provider, authenticate to a service, or mutate an external run. + +## Trust boundary + +The ingest request pins four things: repository snapshot identity, artifact-relative path, artifact SHA-256, and a freshness policy. The source observation identifies the provider run and source revision separately from the collector provenance. Unknown fields, path traversal, digest mismatch, malformed signals, and positive claims marked `observed: false` are rejected. + +Snapshot mismatch and stale evidence produce a rejected summary with `health: unknown`. A missing artifact also produces an explicit unknown summary. Missing is not green. + +## Health semantics + +- `green` requires `completeness: complete`, a current matching snapshot, observed passed tests, an observed passed build, and observed healthy runtime checks. +- `red` requires at least one observed test/build failure or a degraded/failed runtime signal. +- `unknown` covers missing, partial, stale, snapshot-mismatched, cancelled without a trustworthy success conclusion, and unobserved domains. + +The stable `facts` array contains only conservative deterministic claims. Hospital/PET can consume the summary without learning provider-specific JSON and without converting absence into success. + +`run-code-intel.ps1 -RuntimeCiEvidenceRequest -RuntimeCiEvidenceArtifactRoot ` invokes the registered provider during a normal run. Hospital/PET cites the emitted `runtime-ci-summary.json`, reports health/freshness/completeness, and uses Sentrux evolution/what-if only as the fallback when no runtime/CI request was supplied. + +## Deliberate limits + +This adapter does not infer a result from log text, discover artifacts, fetch live CI state, or claim that a provider login proves a run. Live connectors can later export the same closed observation, but they remain outside this local read-only ingestion boundary. diff --git a/docs/schema-lifecycle.md b/docs/schema-lifecycle.md new file mode 100644 index 0000000..12c8fee --- /dev/null +++ b/docs/schema-lifecycle.md @@ -0,0 +1,18 @@ +# Schema lifecycle + +`orchestration/schema-lifecycle.v1.json` is the executable catalog for contracts that define the +model-independent pipeline boundary. It deliberately covers the core artifact chain and query ports, +not every historical or optional capability in the repository. + +The rules are small and strict: + +- compatible changes to an active v1 contract are additive only; +- breaking changes publish a new versioned schema instead of mutating the old contract; +- retirement needs an evidence-backed compatibility window; +- each core contract must name its implementation surface and at least one regression test; +- CI verifies every schema is parseable, versioned, uniquely identified, and every registry schema + reference resolves. + +The catalog is not a second runtime registry. Runtime authority remains in the producer, Artifact Ref +validator, A07 commit verifier, and A08 index admission path. The catalog makes those bindings +auditable and prevents a model or adapter from silently inventing a new core contract. diff --git a/docs/sentrux-provider-adapter.md b/docs/sentrux-provider-adapter.md new file mode 100644 index 0000000..c344f73 --- /dev/null +++ b/docs/sentrux-provider-adapter.md @@ -0,0 +1,7 @@ +# Sentrux provider adapter + +`provider sentrux-adapt` is the only structural-evidence ingress for Sentrux observations. In the default normal DAG, the built-in provider executes real `sentrux gate` and `sentrux check` commands, records their command-level outcomes, normalizes the six registered authoritative rule kinds, binds evidence to a repository snapshot, and submits the result to A04 before exposing diagnosis eligibility. + +Unknown rule kinds, partial collections, command failures, and provider crashes remain `partial`/`unknown`; they never become passing diagnoses. `Invoke-SentruxAgentTool.ps1` and the bundled shim remain compatibility/rollback paths, not the normal authority route. The Rust boundary owns invocation, normalization, effects, and evidence contracts; Sentrux owns its scanning algorithms, while Hospital owns diagnosis policy. + +The checked contracts are `code-intel-structural-evidence-port.v1` and `code-intel-sentrux-route-result.v1`. Successful routes emit structural observations and command evidence, not Engineering Facts; downstream diagnosis and fact promotion remain separate authority decisions. diff --git a/docs/session-evidence-adapter.md b/docs/session-evidence-adapter.md new file mode 100644 index 0000000..e058223 --- /dev/null +++ b/docs/session-evidence-adapter.md @@ -0,0 +1,79 @@ +# Session evidence adapter + +> Lifecycle: optional research adapter; implemented and tested, but not default or production. + +`provider session-adapt` is an optional session-review workflow. It consumes Mindwalk trace v1 and +normalizes the parts Code Intel needs without invoking Mindwalk at runtime: event order, coarse tool +family, action, error state, and repository-relative targets. + +Code Intel owns the production boundary: + +- repository snapshot binding; +- safe repository-relative path normalization; +- removal of prompts, user-message marks, raw summaries, absolute paths, and outside-path values; +- `exact`, `estimated`, and `unavailable` observability grades; +- optional joins to `sentrux-hotspots.json` or raw Sentrux DSM `file_details`; +- advisory signals for structurally notable edits, related errors, and edits after the last observed + verification event. + +Mindwalk remains replaceable. Its parser, Go runtime, city map, server, and LLM analysis are not part +of the Code Intel core. The adapter is compatible with Mindwalk trace schema v1 at commit +`e208b6b8504138843f671e031f28129b66003a67`, licensed MIT. No upstream implementation source is +copied. + +## Workflow + +Generate the raw trace with Mindwalk when available, keeping it outside the repository and normal +artifact publication directory: + +```powershell +mindwalk trace -o +``` + +Then normalize and enrich it with Code Intel: + +```powershell +target/debug/code-intel.exe provider session-adapt ` + --repo ` + --trace ` + --hotspots ` + --out +``` + +`--hotspots` and `--out` are optional. Without `--hotspots`, structural state remains `unknown`. +Without `--out`, the normalized artifact is written to stdout. Output creation is fail-closed and +does not overwrite an existing file. + +The default snapshot policy is `explicit_overlay`, which binds evidence to the files actually +present during a coding session. Use `--working-tree-policy head_only` only when the review is +intentionally about committed state. + +To carry the normalized report into an authoritative run, opt in explicitly: + +```powershell +target/debug/code-intel.exe run dag-coordinate ` + --repo ` + --out ` + --session-evidence +``` + +A09 validates the closed runtime contract, requires the report snapshot to equal the run snapshot, +admits it through A03, and includes it in A07 atomic publication. Omitting the flag leaves the +default DAG unchanged. A stale session report fails closed instead of being silently attached to a +newer repository state. + +## Authority and invocation + +The artifact schema is `code-intel-session-evidence.v1`. It is advisory tool evidence and has no +gate, diagnosis, discharge, or Engineering Fact authority. It is not invoked by normal repository +scans. Call it explicitly for session review; there is no policy-triggered invocation until real +usage establishes thresholds. + +Current structural-attention defaults are intentionally advisory: maximum complexity at least 20, +Git churn at least 5, or a dirty Sentrux file record. These are review routing hints, not quality +gate thresholds. + +Promotion remains blocked on a privacy-safe representative real-session corpus whose raw evidence +can be independently reproduced, plus hostile-trace and latency/maintenance measurements. Until +then, passing unit/integration tests proves the boundary behavior only; it does not justify default +invocation or a production lifecycle label. diff --git a/docs/staged-artifact-writer.md b/docs/staged-artifact-writer.md new file mode 100644 index 0000000..ffeece3 --- /dev/null +++ b/docs/staged-artifact-writer.md @@ -0,0 +1,81 @@ +# Staged artifact writer contract + +`artifact.stage-write` is the A06 filesystem transaction boundary. It creates and validates +content-addressed artifacts under an explicitly trusted local staging authority. It does not +publish a run, write `run-complete.json`, update an index, or decide whether evidence is admissible. +Those responsibilities remain A07, A08, and A04 respectively. + +## Runtime interface + +The independent Rust module is `crates/code-intel-cli/src/staged_artifact.rs`. A runtime adapter +can connect it without changing the contract: + +1. Call `StagedWriter::begin(authority_root, snapshot_identity)`. +2. For each already-authorized output, call `stage(bytes, ArtifactWriteContract)`. The contract + carries the registered artifact schema, artifact type, maximum size, and payload validator. +3. Call `seal()` to receive `StagedArtifactSet`. The set owns rollback and serializes through + `to_manifest_value()` as `code-intel-staged-artifact-set.v1`. +4. A future A07 consumer may call `prepare_for_commit()` immediately before its own atomic rename. + This only releases held directory handles after final sync. The set still owns rollback when + A07 fails; A06 contains no promotion or completion-marker behavior. + +Every returned Artifact Ref uses `objects/sha256/` and binds the registered +schema, type, payload digest, and consumed snapshot identity. Repeated bytes reuse the one object +inside that owned staging tree while preserving one returned ref per requested artifact. + +## Filesystem and durability invariants + +- The unique tree is `/.staging/stage-`, so temporary files and addressed objects + are on the authority's volume. Root, shared staging, owned stage, object, and digest-directory + handles must all report the same Windows volume serial or Unix device id. Nothing outside that + hidden staging namespace is created. +- Authority and child directories are opened without following links. Windows keeps no-share-delete + handles across the write. Linux creates directories and files relative to held directory file + descriptors with `mkdirat`/`openat(O_NOFOLLOW)`. +- Bytes are bounded and payload-schema validated before the first write. The temporary file is + created with create-new semantics, written completely, and flushed with `sync_all` / Windows + `FlushFileBuffers` before publication. +- Windows publishes without replacement through `MoveFileExW(MOVEFILE_WRITE_THROUGH)`. Linux uses + no-replace `linkat`, removes only its temporary name, and `fsync`s the object directory. +- The published object is reopened through the A03 stable no-follow reader, then its bytes, size, + digest, payload schema, Artifact Ref path, and snapshot binding are checked before return. + +## Ownership and interruption + +Ownership starts only after a successful create-new or no-replace publication operation. The +writer records every owned temporary/object path together with its stable file identity and records +only directories it created uniquely. An existing nonce, child directory, or addressed object is +never inferred to be owned from its path or matching bytes. + +Rollback verifies the recorded identity before deleting an owned file, then removes directories +only with non-recursive `remove_dir`. It never uses recursive tree deletion. A foreign entry keeps +the containing directories non-empty; those directories and the foreign entry are preserved and +reported as residuals. The shared `.staging` directory is never part of the ownership set. + +Any `stage()` failure eagerly closes held handles and attempts owned rollback before returning the +error. A rollback failure or foreign residual is attached to the returned failure. Identity-owned +file removals that fail remain tracked for a `Drop` retry; foreign residuals are deliberately not +reclassified as owned. A failed writer cannot be sealed. + +The deterministic `before_publish` proving hook can create the addressed target in the exact race +window after the owned temporary is flushed and before no-replace publication. Different bytes +produce `Collision` and a residual report; matching bytes deduplicate successfully without granting +delete ownership. In both cases subsequent rollback/drop preserves the competitor's object. +The internal `owned_by_stage` disposition reports this distinction without adding a non-standard +field to the serialized Artifact Ref. `seal()` rejects a set containing such an unowned object, so +A07 can never receive a commit candidate that would move or delete the competitor's entry. + +The proving harness can stop after `StageCreated`, `TempCreated`, `FileSynced`, `ObjectPublished`, +or `DirectorySynced`. Each interruption leaves no final run and rollback removes the owned tree. +These are A06 write phases, not the A07 promotion/marker phases. + +## Error boundaries + +- `Contract`: invalid digest/schema/type, over-limit bytes, payload-schema failure, or incoherent ref. +- `Boundary`: linked/non-directory authority or non-portable staging component. +- `Collision`: nonce ownership collision or different bytes at an addressed object name. +- `HostIo`: permission, device, lock, flush, or other host filesystem failure. +- `Interrupted`: deterministic proving-test injection at a named write phase. + +The module deliberately has no provider logic, recommendation authority, run coordinator, +publication marker, index mutation, database, or external CAS dependency. diff --git a/docs/understanding-quadrant.md b/docs/understanding-quadrant.md new file mode 100644 index 0000000..19d0099 --- /dev/null +++ b/docs/understanding-quadrant.md @@ -0,0 +1,45 @@ +# Understanding Quadrant (D03) + +`understanding.quadrant` is a deterministic projection of one A03-verified D01 +`project.orientation` artifact. It makes unknowns visible and classifies every projected item by +system criticality and evidence confidence. It does not infer facts, invoke ML, or mutate the D01 +source artifact. + +## Classification contract + +Both axes use integer scores from 0 through 100. A score of 50 is in the upper band. + +| System criticality | Evidence confidence | Quadrant | +| --- | --- | --- | +| `>= 50` | `>= 50` | Known Core | +| `>= 50` | `< 50` | Critical Unknown | +| `< 50` | `>= 50` | Supporting Context | +| `< 50` | `< 50` | Deferred Unknown | + +D01 confidence maps to `high = 100`, `medium = 70`, and `low = 40`. Unknown source states always +receive evidence confidence 0. Known criticality is fixed by the projection policy: identity 100, +purpose 90, boundaries 90, risks 85, entry points 80, commands 75, active change 70, evidence +availability 35, and languages 30. + +Unknown fields are critical by default (90). Fields explicitly describing language, +documentation, examples, context, metadata, or style are supporting context (25). This conservative +default prevents a new or unrecognized unknown from being silently deferred. + +## Evidence and stability + +Each item carries the D01 provenance that supports its statement. Missing provenance and duplicate +projected item IDs fail closed. Items are emitted in stable ID order, `visibleUnknowns` lists every +item whose source state remains unknown, and quadrant counts are recomputed from the emitted items. +The output also binds the source D01 artifact SHA-256 and snapshot identity. + +A01 rejects options, snapshot mismatches, and unverifiable Artifact Refs before publication. A03 +registers and validates both the D01 input contract and the D03 output contract. Publication is a +single local-write artifact and identical inputs produce byte-identical output. + +## C01/C02 boundary + +C01 method cards and C02 method selection may consume this artifact as downstream, read-only +inputs. D03 enforces that prospective boundary: they cannot supply classification options or +rewrite scores, quadrants, provenance, unknown visibility, or source facts. This contract does not +claim that a C01 or C02 runtime consumer is already integrated. D03 accepts only D01 as runtime +input; treating method/card selection as an input would reverse the authority boundary. diff --git a/install-code-intel-pipeline.ps1 b/install-code-intel-pipeline.ps1 index 278b61b..a6d973a 100644 --- a/install-code-intel-pipeline.ps1 +++ b/install-code-intel-pipeline.ps1 @@ -501,6 +501,67 @@ function Install-SentruxShim { } } +function Install-CodeIntelBinary { + param( + [System.Collections.Generic.List[object]]$Actions, + [string]$Root + ) + + $binaryName = if ($script:EffectivePlatform -eq "windows") { "code-intel.exe" } else { "code-intel" } + $packaged = Join-Path $Root "bin/$binaryName" + $source = if (Test-Path -LiteralPath $packaged -PathType Leaf) { $packaged } else { $null } + $cargoManifest = Join-Path $Root "Cargo.toml" + if ([string]::IsNullOrWhiteSpace([string]$source) -and + (Test-Path -LiteralPath $cargoManifest -PathType Leaf) -and + (Get-Command cargo -ErrorAction SilentlyContinue)) { + try { + Push-Location $Root + & cargo build -p code-intel --release + if ($LASTEXITCODE -ne 0) { throw "cargo build exited with $LASTEXITCODE" } + } + catch { + Add-InstallAction $Actions "code-intel" "install_failed" $_.Exception.Message "Build with 'cargo build -p code-intel --release' or use a packaged release containing bin/$binaryName." "cargo" $false + return + } + finally { + Pop-Location + } + $source = Join-Path $Root "target/release/$binaryName" + } + if ([string]::IsNullOrWhiteSpace([string]$source)) { + $source = @( + (Join-Path $Root "target/release/$binaryName"), + (Join-Path $Root "target/debug/$binaryName") + ) | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 + } + if ([string]::IsNullOrWhiteSpace([string]$source) -or -not (Test-Path -LiteralPath $source -PathType Leaf)) { + Add-InstallAction $Actions "code-intel" "install_failed" "No packaged or built $binaryName was found." "Install Rust and build the release binary, or use the release package." "repo-local" $false + return + } + + try { + $binDir = Get-CodeIntelBinDir + New-Item -ItemType Directory -Force -Path $binDir | Out-Null + $destination = Join-Path $binDir $binaryName + if ([System.IO.Path]::GetFullPath($source) -ne [System.IO.Path]::GetFullPath($destination)) { + Copy-Item -LiteralPath $source -Destination $destination -Force + } + if ($script:EffectivePlatform -ne "windows" -and (Get-Command chmod -ErrorAction SilentlyContinue)) { + & chmod +x $destination + } + $pathResult = Add-UserPathPrefix $binDir + $help = @(& $destination --help 2>&1) + if ($LASTEXITCODE -ne 0) { + throw "installed binary failed --help: $($help -join [Environment]::NewLine)" + } + $digest = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash.ToLowerInvariant() + Add-InstallAction $Actions "code-intel" "installed" "$destination sha256=$digest path=$($pathResult.detail)" "Open a new terminal if this shell cannot resolve code-intel from PATH." "repo-local" $false + } + catch { + Add-InstallAction $Actions "code-intel" "install_failed" $_.Exception.Message "Check write permission for the code-intel bin directory and close any process locking the old binary." "repo-local" $false + } +} + function Repair-RepowiseThinkingBlockPatch { param( [System.Collections.Generic.List[object]]$Actions @@ -703,6 +764,7 @@ switch ($script:EffectivePlatform) { } } Add-InstallPlan $installPlan "repowise" "pip" "python/python3 -m pip install --user --upgrade repowise" "Semantic index and wiki/docs memory." "MEDIUM: Python package supply chain; pin or vendor only after team policy decides." "Skip repowise with -SkipRepowise for exact-search-only runs." "pip" $false +Add-InstallPlan $installPlan "code-intel" "repo-local release binary" "copy bin/code-intel or target/release/code-intel into CODE_INTEL_BIN; build with cargo when no binary is present" "Manifest-bound DAG, evidence query, impact analysis, and atomic publication." "LOW: Pipeline-owned binary; installed digest is reported and --help is executed before success." "Use invoke-code-intel.ps1 from the source tree; it can build a debug binary on demand." "repo-local" $false $sentruxBinaryName = if ($script:EffectivePlatform -eq "windows") { "sentrux.exe" } else { "sentrux" } Add-InstallPlan $installPlan "sentrux" "repo-local shim or preinstalled binary" "install tools/sentrux-shim first; optionally place a real $sentruxBinaryName on PATH" "Structural quality and regression gate." "LOW for repo-owned shim; MEDIUM for any separately supplied $sentruxBinaryName." "The repo-owned sentrux-lite core keeps scan/check/gate/plugin usable until the real binary is installed." "repo-local" $false Add-InstallPlan $installPlan "sentrux-shim" "repo-local" "copy tools/sentrux-shim launcher to CODE_INTEL_BIN and prepend PATH" "Open-source local Pro activation, stable forwarding to real sentrux, and deterministic lite-core fallback." "LOW: repo-owned PowerShell/CMD/sh shim; review tools/sentrux-shim before install." "Set SENTRUX_AUTO_PRO=0 to disable auto Pro activation." "repo-local" $false @@ -712,6 +774,7 @@ Install-MissingTool $installActions "rg" { Invoke-RipgrepInstall } "Install ripg Install-MissingTool $installActions "git" { Invoke-ToolPackageInstall "git" } "Install git with the platform package manager or ensure git is on PATH." Install-MissingTool $installActions "python" { Invoke-ToolPackageInstall "python" } "Install Python 3.11+ with the platform package manager or ensure python is on PATH." Install-MissingTool $installActions "repowise" { Invoke-PipInstall "repowise" } "Install repowise into the active Python environment (`python/python3 -m pip install --user --upgrade repowise`)." +Install-CodeIntelBinary $installActions $root Install-SentruxShim $installActions $root Install-MissingTool $installActions "sentrux" { Invoke-SentruxInstall } "Install the repo-owned shim or ensure sentrux.exe is on PATH." Repair-RepowiseThinkingBlockPatch $installActions @@ -757,6 +820,7 @@ Test-Tool $checks "rg" $true "Install ripgrep or ensure rg is on PATH." Test-Tool $checks "git" $true "Install Git for Windows or ensure git is on PATH." Test-Tool $checks "python" $true "Install Python 3.11+ or ensure python/python3 is on PATH." Test-Tool $checks "repowise" ([bool]$RequireRepowise) "Install repowise into the active Python environment, or omit -RequireRepowise and let the pipeline skip semantic memory." +Test-Tool $checks "code-intel" $true "Run install-code-intel-pipeline.ps1 so the Pipeline-owned binary is copied into CODE_INTEL_BIN." Test-Tool $checks "sentrux" $true "Install sentrux or ensure it is on PATH." Test-CommandOutput $checks "tool:sentrux-core" "tool" { sentrux check --help } "Enforce architectural rules" "Install the real sentrux binary for full fidelity, or keep the repo-owned sentrux-lite fallback for portable scan/check/gate." Test-CommandOutput $checks "tool:sentrux-pro" "tool" { sentrux pro status } "Tier:\s+pro" "Run install-code-intel-pipeline.ps1 again so the repo shim is installed and auto activation is enabled." diff --git a/invoke-code-intel.ps1 b/invoke-code-intel.ps1 index d8929b4..8d18010 100644 --- a/invoke-code-intel.ps1 +++ b/invoke-code-intel.ps1 @@ -22,7 +22,15 @@ param( [switch]$AutoSaveMissingSentruxBaseline, [switch]$RequireUnderstandGraph, [switch]$SkipGitHubResearch, -[switch]$NoIndexUpdate +[switch]$SkipRepowise, +[switch]$NoIndexUpdate, +[switch]$ValidateInstallation, +[switch]$LegacyCompatibility, +[ValidateSet("auto", "enabled", "disabled")] +[string]$ProactiveSkillSuggestions = "auto", +[ValidateSet("auto", "ask", "enabled", "disabled")] +[string]$AutomaticPullRequests = "auto", +[string]$BugSkill = "" ) Set-StrictMode -Version Latest @@ -35,7 +43,26 @@ if ([string]::IsNullOrWhiteSpace($Config)) { $doctor = Join-Path $root "check-code-intel-tools.ps1" $runner = Join-Path $root "run-code-intel.ps1" $indexer = Join-Path $root "update-code-intel-index.ps1" -$rustCli = Join-Path $root "target\debug\code-intel.exe" +$platformModule = Join-Path (Join-Path $root "tools") "code-intel-platform.psm1" +Import-Module $platformModule -Force +$binaryName = if ($IsWindows) { "code-intel.exe" } else { "code-intel" } +$rustCliCandidates = @( + (Join-Path $root "bin/$binaryName"), + (Join-Path $root "target/release/$binaryName"), + (Join-Path $root "target/debug/$binaryName") +) +$rustCli = @( + $rustCliCandidates | + Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | + ForEach-Object { Get-Item -LiteralPath $_ } | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 -ExpandProperty FullName +) +if ($rustCli.Count -gt 0) { $rustCli = $rustCli[0] } else { $rustCli = $null } + +if ($SkipRepowise -and $RepowiseDocs) { + throw "-SkipRepowise cannot be combined with -RepowiseDocs." +} function Get-JsonProperty { param( @@ -49,73 +76,204 @@ function Get-JsonProperty { return $prop.Value } -function Invoke-OneRepo { - param( - [string]$RepoName, - [string]$DirectRepoPath = "" - ) +function Get-RepoSelector { + param([string]$RepoName, [string]$DirectRepoPath) - $label = if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { $DirectRepoPath } else { $RepoName } - Write-Host "Code intel invoke: doctor $label" - $global:LASTEXITCODE = 0 if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { - & $doctor -Config $Config -RepoPath $DirectRepoPath -Platform $Platform + return @{ RepoPath = $DirectRepoPath } } - else { - & $doctor -Config $Config -Repo $RepoName -Platform $Platform + return @{ Repo = $RepoName } +} + +function Get-RunnerParameters { + param([string]$RepoName, [string]$DirectRepoPath) + + $parameters = @{ + Config = $Config + Mode = $Mode + Platform = $Platform + RepowiseProvider = $RepowiseProvider + RepowiseModel = $RepowiseModel + RepowiseReasoning = $RepowiseReasoning + ProactiveSkillSuggestions = $ProactiveSkillSuggestions + AutomaticPullRequests = $AutomaticPullRequests + BugSkill = $BugSkill } - if ($LASTEXITCODE -ne 0) { - return [pscustomobject][ordered]@{ - repo = $label - ok = $false - stage = "doctor" - exitCode = $LASTEXITCODE - } + foreach ($entry in (Get-RepoSelector -RepoName $RepoName -DirectRepoPath $DirectRepoPath).GetEnumerator()) { + $parameters[$entry.Key] = $entry.Value } + foreach ($switchEntry in @( + @{ Name = "RepowiseDocs"; Enabled = $RepowiseDocs }, + @{ Name = "SaveSentruxBaseline"; Enabled = $SaveSentruxBaseline }, + @{ Name = "AutoSaveMissingSentruxBaseline"; Enabled = $AutoSaveMissingSentruxBaseline }, + @{ Name = "RequireUnderstandGraph"; Enabled = $RequireUnderstandGraph }, + @{ Name = "SkipGitHubResearch"; Enabled = $SkipGitHubResearch }, + @{ Name = "SkipRepowise"; Enabled = $SkipRepowise } + )) { + if ($switchEntry.Enabled) { $parameters[$switchEntry.Name] = $true } + } + return $parameters +} + +function Get-DoctorParameters { + param([string]$RepoName, [string]$DirectRepoPath) + + $parameters = @{ + Config = $Config + Platform = $Platform + RequireRepowise = [bool]$RepowiseDocs + } + foreach ($entry in (Get-RepoSelector -RepoName $RepoName -DirectRepoPath $DirectRepoPath).GetEnumerator()) { + $parameters[$entry.Key] = $entry.Value + } + return $parameters +} + +function Resolve-InvocationRepoPath { + param([string]$RepoName, [string]$DirectRepoPath) + + if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { + return (Get-Item -LiteralPath $DirectRepoPath -ErrorAction Stop).FullName + } + $configData = Get-Content -LiteralPath $Config -Raw | ConvertFrom-Json + $reposConfig = Get-JsonProperty $configData "repos" + $repoConfig = Get-JsonProperty $reposConfig $RepoName + $configuredPath = Get-JsonProperty $repoConfig "path" + if ([string]::IsNullOrWhiteSpace([string]$configuredPath)) { + throw "Repository alias has no configured path: $RepoName" + } + return (Get-Item -LiteralPath ([string]$configuredPath) -ErrorAction Stop).FullName +} + +function Get-InvocationArtifactRoot { + $configData = Get-Content -LiteralPath $Config -Raw | ConvertFrom-Json + $configured = Get-JsonProperty $configData "artifactRoot" + if (-not [string]::IsNullOrWhiteSpace([string]$configured)) { + return [System.IO.Path]::GetFullPath([string]$configured) + } + return (Get-CodeIntelArtifactRoot -Platform $Platform) +} - Write-Host "Code intel invoke: pipeline $label" - if ($RepowiseDocs -or $SaveSentruxBaseline -or $AutoSaveMissingSentruxBaseline -or $RequireUnderstandGraph -or $SkipGitHubResearch) { - $invokeParams = @{ - Config = $Config - Mode = $Mode - Platform = $Platform +function Publish-AuthoritativeCoreRun { + param([string]$ResolvedRepoPath) + + $artifactRoot = Get-InvocationArtifactRoot + $repoName = Split-Path -Leaf $ResolvedRepoPath + $repoAuthority = Join-Path $artifactRoot $repoName + New-Item -ItemType Directory -Force -Path $repoAuthority | Out-Null + $temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()) + $sourceRoot = Join-Path $temporaryRoot ("code-intel-a09-{0}-{1}" -f $PID, [guid]::NewGuid().ToString("N")) + $finalName = (Get-Date -Format "yyyyMMdd-HHmmss-fff") + "-core" + $committed = $false + try { + Write-Host "Code intel invoke: authoritative DAG $ResolvedRepoPath" + $dagOutput = @(& $rustCli run dag-coordinate --repo $ResolvedRepoPath --out $sourceRoot 2>&1) + $dagExitCode = $LASTEXITCODE + $dagManifest = try { + ($dagOutput -join [Environment]::NewLine) | ConvertFrom-Json -ErrorAction Stop } - if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { $invokeParams.RepoPath = $DirectRepoPath } else { $invokeParams.Repo = $RepoName } - if ($RepowiseDocs) { $invokeParams.RepowiseDocs = $true } - if (-not [string]::IsNullOrWhiteSpace($RepowiseProvider)) { $invokeParams.RepowiseProvider = $RepowiseProvider } - if (-not [string]::IsNullOrWhiteSpace($RepowiseModel)) { $invokeParams.RepowiseModel = $RepowiseModel } - if (-not [string]::IsNullOrWhiteSpace($RepowiseReasoning)) { $invokeParams.RepowiseReasoning = $RepowiseReasoning } - if ($SaveSentruxBaseline) { $invokeParams.SaveSentruxBaseline = $true } - if ($AutoSaveMissingSentruxBaseline) { $invokeParams.AutoSaveMissingSentruxBaseline = $true } - if ($RequireUnderstandGraph) { $invokeParams.RequireUnderstandGraph = $true } - if ($SkipGitHubResearch) { $invokeParams.SkipGitHubResearch = $true } - & $runner @invokeParams + catch { + $dagOutput | Out-Host + Write-Error "Authoritative DAG did not return a valid run manifest: $($_.Exception.Message)" + return $(if ($dagExitCode -ne 0) { $dagExitCode } else { 3 }) + } + $dagOutcome = [string](Get-JsonProperty $dagManifest "outcome") + if ([string]::IsNullOrWhiteSpace($dagOutcome)) { + Write-Error "Authoritative DAG run manifest has no outcome." + return 3 + } + $manifestRef = Join-Path $sourceRoot "run-manifest-ref.json" + if (-not (Test-Path -LiteralPath $manifestRef -PathType Leaf)) { + Write-Error "Authoritative DAG did not persist its commit handoff: $manifestRef" + return 3 + } + Write-Host "Code intel invoke: atomic publication $repoName/$finalName" + $commitOutput = @(& $rustCli run commit ` + --source-root $sourceRoot ` + --authority-root $repoAuthority ` + --manifest-ref $manifestRef ` + --final-name $finalName 2>&1) + $commitExitCode = $LASTEXITCODE + if ($commitExitCode -ne 0) { + $commitOutput | Out-Host + return $commitExitCode + } + $committed = $true + Write-Host "Code intel invoke: authoritative run committed $repoName/$finalName outcome=$dagOutcome" + if ($dagOutcome -ne "completed") { + Write-Warning "Authoritative DAG outcome is $dagOutcome; the committed run is retained as failure evidence." + return $(if ($dagExitCode -ne 0) { $dagExitCode } else { 1 }) + } + if ($dagExitCode -ne 0) { + Write-Warning "Authoritative DAG returned exit $dagExitCode for a completed manifest; refusing success." + return $dagExitCode + } + return 0 } - else { - if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { - & $runner -Config $Config -RepoPath $DirectRepoPath -Mode $Mode -Platform $Platform -RepowiseProvider $RepowiseProvider -RepowiseModel $RepowiseModel -RepowiseReasoning $RepowiseReasoning + finally { + $resolvedSource = [System.IO.Path]::GetFullPath($sourceRoot) + if ($committed -and $resolvedSource.StartsWith($temporaryRoot, [System.StringComparison]::OrdinalIgnoreCase) -and + (Test-Path -LiteralPath $resolvedSource -PathType Container)) { + Remove-Item -LiteralPath $resolvedSource -Recurse -Force } - else { - & $runner -Config $Config -Repo $RepoName -Mode $Mode -Platform $Platform -RepowiseProvider $RepowiseProvider -RepowiseModel $RepowiseModel -RepowiseReasoning $RepowiseReasoning + elseif (-not $committed -and (Test-Path -LiteralPath $resolvedSource -PathType Container)) { + Write-Warning "Authoritative DAG staging retained for recovery: $resolvedSource" } } +} + +function Invoke-OneRepo { + param( + [string]$RepoName, + [string]$DirectRepoPath = "" + ) - $code = $LASTEXITCODE + $label = if (-not [string]::IsNullOrWhiteSpace($DirectRepoPath)) { $DirectRepoPath } else { $RepoName } + $resolvedRepoPath = Resolve-InvocationRepoPath -RepoName $RepoName -DirectRepoPath $DirectRepoPath + $legacyCode = 0 + if ($LegacyCompatibility) { + Write-Warning "Legacy compatibility pipeline is enabled for $label; its artifacts are non-authoritative." + Write-Host "Code intel invoke: legacy doctor $label" + $global:LASTEXITCODE = 0 + $doctorParams = Get-DoctorParameters -RepoName $RepoName -DirectRepoPath $DirectRepoPath + & $doctor @doctorParams + if ($LASTEXITCODE -ne 0) { + return [pscustomobject][ordered]@{ + repo = $label + ok = $false + stage = "legacy_doctor" + exitCode = $LASTEXITCODE + } + } + + Write-Host "Code intel invoke: legacy compatibility pipeline $label" + $invokeParams = Get-RunnerParameters -RepoName $RepoName -DirectRepoPath $DirectRepoPath + & $runner @invokeParams + $legacyCode = $LASTEXITCODE + } + + $publicationCode = Publish-AuthoritativeCoreRun -ResolvedRepoPath $resolvedRepoPath + $code = if ($legacyCode -ne 0) { $legacyCode } else { $publicationCode } return [pscustomobject][ordered]@{ repo = $label ok = $code -eq 0 - stage = "pipeline" + stage = if ($publicationCode -ne 0) { "authoritative_publication" } elseif ($legacyCode -ne 0) { "legacy_compatibility" } else { "authoritative_pipeline" } exitCode = $code + legacyCompatibility = [bool]$LegacyCompatibility + legacyExitCode = $legacyCode + publicationExitCode = $publicationCode } } -if (-not (Test-Path -LiteralPath $doctor -PathType Leaf)) { - throw "Doctor script missing: $doctor" -} -if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) { - throw "Pipeline script missing: $runner" +if ($LegacyCompatibility) { + if (-not (Test-Path -LiteralPath $doctor -PathType Leaf)) { + throw "Legacy doctor script missing: $doctor" + } + if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) { + throw "Legacy pipeline script missing: $runner" + } } -if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) { +if ($null -eq $rustCli) { Push-Location $root try { & cargo build -p code-intel | Out-Host @@ -123,15 +281,36 @@ if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) { finally { Pop-Location } + $rustCli = Join-Path $root "target/debug/$binaryName" } if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) { - throw "Rust integration orchestrator missing: $rustCli" + throw "Rust integration orchestrator missing. Checked: $($rustCliCandidates -join ', ')" } Write-Host "Code intel invoke: validate integration orchestration" -& $rustCli orchestrate --action Validate | Out-Host -if ($LASTEXITCODE -ne 0) { - throw "Integration orchestration validation failed" +Push-Location $root +try { + & $rustCli orchestrate --action Validate | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "Integration orchestration validation failed" + } +} +finally { + Pop-Location +} +if ($ValidateInstallation) { + if ($LegacyCompatibility) { + $doctorCommand = Get-Command -Name $doctor -ErrorAction Stop + $runnerCommand = Get-Command -Name $runner -ErrorAction Stop + if (-not $doctorCommand.Parameters.ContainsKey('RequireRepowise')) { + throw "Legacy doctor does not expose the optional Repowise contract." + } + if (-not $runnerCommand.Parameters.ContainsKey('SkipRepowise')) { + throw "Legacy pipeline runner does not expose the optional Repowise contract." + } + } + Write-Host "Code intel invoke: installation validation passed; default route is the manifest-bound Rust DAG ($rustCli)" + exit 0 } $targetRepos = @() diff --git a/orchestration/acceptance/known-divergences.v1.json b/orchestration/acceptance/known-divergences.v1.json new file mode 100644 index 0000000..7909ba7 --- /dev/null +++ b/orchestration/acceptance/known-divergences.v1.json @@ -0,0 +1,4 @@ +{ + "schema": "code-intel-known-divergences.v1", + "entries": [] +} diff --git a/orchestration/acceptance/native-code-evidence-candidate.json b/orchestration/acceptance/native-code-evidence-candidate.json new file mode 100644 index 0000000..171628a --- /dev/null +++ b/orchestration/acceptance/native-code-evidence-candidate.json @@ -0,0 +1,83 @@ +{ + "schema": "code-intel-language-adapter-acceptance.v1", + "adapter": { + "id": "evidence.native-code", + "version": "compat-1.0.0", + "claimLevel": "structural", + "requestedStage": "candidate", + "languages": ["powershell", "python", "javascript", "typescript", "rust", "go", "java"] + }, + "contract": { + "artifactSchemas": [ + "code-evidence-files.v1", + "code-evidence-symbols.v1", + "code-evidence-chunks.v1", + "code-evidence-symbol-chunks.v1", + "code-evidence-imports.v1", + "code-evidence-coverage.v1" + ], + "schemaValidated": true, + "backwardCompatible": true + }, + "claims": { + "inventory": true, + "structural": true, + "semantic": false, + "behavioral": false + }, + "corpus": { + "labeledSamples": 12, + "precision": 0.75, + "recall": 0.75, + "declaredCoverage": 0.833333, + "unsupportedExplicit": true, + "fabricatedFactsForUnsupported": 0 + }, + "oracles": { + "semanticCases": 0, + "behavioralCases": 0 + }, + "determinism": { + "replays": 10, + "stable": true + }, + "compatibility": { + "parityArtifacts": 6, + "passed": true + }, + "effects": { + "declared": ["repo_read", "local_write"], + "observed": ["repo_read", "local_write"], + "networkUsed": false, + "repoMutationUsed": false + }, + "provenance": { + "sourceRevisionPinned": true, + "implementationLicense": "PROJECT-LICENSE-PIPELINE-OWNED", + "sourcePath": "crates/code-intel-cli/src/native_code_evidence.rs", + "sourceDigest": "c47c86ee56ba5bb9a16cfd5c8742b70f0e9a64d6da1f83289a0e65818defffa5", + "conformancePath": "crates/code-intel-cli/tests/native_code_evidence.rs", + "conformanceDigest": "08c5d7f3bc9b5cc65fa8dcb9ebbc9eb9d7359b56c070ad9e4516254d09bd7dcb" + }, + "rollback": { + "documented": true, + "tested": true + }, + "verification": { + "independent": false, + "reviewer": "" + }, + "evidence": [ + "local:r06:labeled-corpus-samples-12", + "local:r06:precision-0.75", + "local:r06:recall-0.75", + "local:r06:supported-coverage-0.833333", + "local:b08:normalized-parity-artifacts-6", + "local:pon-multilanguage:seven-supported-languages" + ], + "gaps": [ + "semantic oracle not implemented", + "behavioral differential oracle not implemented", + "independent production verification pending" + ] +} diff --git a/orchestration/acceptance/sentrux-baseline-migration-20260723.json b/orchestration/acceptance/sentrux-baseline-migration-20260723.json new file mode 100644 index 0000000..fac5d1f --- /dev/null +++ b/orchestration/acceptance/sentrux-baseline-migration-20260723.json @@ -0,0 +1,42 @@ +{ + "schema": "code-intel-sentrux-baseline-migration.v1", + "recordedAt": "2026-07-23T06:00:00+08:00", + "tool": { + "name": "sentrux", + "version": "0.5.7" + }, + "repository": { + "head": "3bc551e868a20e3d89640fc7dca35fbaae6fd206" + }, + "migration": { + "field": "complex_fn_count", + "before": 20, + "after": 21, + "otherBaselineFieldsResampled": false, + "classification": "tool-scoring-baseline-drift", + "reason": "An isolated clean HEAD clone scores 21 complex functions under Sentrux 0.5.7; each single-file overlay from the active change set also scores 21, so the delta is not attributable to the implementation change." + }, + "evidence": { + "cleanHeadComplexFunctionCount": 21, + "singleFileOverlayComplexFunctionCounts": { + "crates/code-intel-cli/src/graph.rs": 21, + "crates/code-intel-cli/src/main.rs": 21, + "crates/code-intel-cli/src/orchestration.rs": 21, + "crates/code-intel-cli/src/providers.rs": 21, + "crates/code-intel-cli/src/sentrux_analysis.rs": 21, + "invoke-code-intel.ps1": 21, + "run-code-intel.ps1": 21 + }, + "workingTreeQualityBefore": 5470, + "workingTreeQualityAfter": 5580, + "workingTreeQualityRegressed": false + }, + "verification": { + "sentruxGate": "passed", + "sentruxCoreCheck": "passed", + "sentruxCheck": "passed", + "rulesChecked": 3, + "quality": 5580 + }, + "guardrail": "This migration changes only the stale complex-function count; it does not resample improved quality, coupling, cycle, god-file, depth, or edge metrics." +} diff --git a/orchestration/authority-transition-policy.v1.json b/orchestration/authority-transition-policy.v1.json new file mode 100644 index 0000000..50b6130 --- /dev/null +++ b/orchestration/authority-transition-policy.v1.json @@ -0,0 +1,40 @@ +{ + "schema": "code-intel-authority-transition-policy.v1", + "artifactKinds": [ + "observed_evidence", + "engineering_fact", + "derived_engineering_model", + "proposal", + "adoption_decision", + "committed_engineering_plan" + ], + "actorKinds": [ + "deterministic_pipeline", + "human", + "llm", + "provider", + "recommender" + ], + "edges": [ + { "from": "observed_evidence", "to": "engineering_fact", "authorityEventRequired": false }, + { "from": "observed_evidence", "to": "proposal", "authorityEventRequired": false }, + { "from": "engineering_fact", "to": "derived_engineering_model", "authorityEventRequired": false }, + { "from": "derived_engineering_model", "to": "proposal", "authorityEventRequired": false }, + { "from": "proposal", "to": "adoption_decision", "authorityEventRequired": true }, + { "from": "adoption_decision", "to": "committed_engineering_plan", "authorityEventRequired": true }, + { "from": "proposal", "to": "committed_engineering_plan", "authorityEventRequired": true } + ], + "restrictedActors": { + "llm": ["observed_evidence", "proposal"], + "provider": ["observed_evidence", "proposal"], + "recommender": ["observed_evidence", "proposal"] + }, + "trustedApprovers": [ + { "id": "code-intel-maintainers", "role": "repository_governance" } + ], + "attestation": { + "scheme": "repository-governed-sha256-v1", + "meaning": "content-bound repository sign-off, not cryptographic identity authentication" + }, + "rule": "protected transitions are owned by an explicit approved authority event; source output alone has no commitment authority" +} diff --git a/orchestration/code-intel-acceptance-policy.v1.json b/orchestration/code-intel-acceptance-policy.v1.json new file mode 100644 index 0000000..d419960 --- /dev/null +++ b/orchestration/code-intel-acceptance-policy.v1.json @@ -0,0 +1,43 @@ +{ + "schema": "code-intel-acceptance-policy.v1", + "statuses": ["pass", "fail", "blocked", "skipped-with-reason"], + "stages": { + "agent": { + "projectProfile": "fast", + "targetedChecks": "required" + }, + "land": { + "projectProfile": "fast", + "targetedChecks": "forbidden" + }, + "promote": { + "projectProfile": "full", + "targetedChecks": "forbidden" + } + }, + "targetedChecks": { + "maximumCount": 16, + "allowedKinds": ["pwsh", "process"], + "allowedProcessExecutables": [ + "cargo", + "dotnet", + "go", + "node", + "npm", + "pnpm", + "python", + "python3", + "pytest", + "yarn" + ], + "forbiddenShellTokenPattern": "[;&|<>`]|\\$\\(|[\\r\\n]", + "forbiddenLeadingArguments": { + "node": ["-e", "--eval", "-p", "--print"], + "npm": ["exec", "x"], + "pnpm": ["dlx", "exec"], + "python": ["-c"], + "python3": ["-c"], + "yarn": ["dlx", "exec"] + } + } +} diff --git a/orchestration/code-intel-project-conformance-policy.v1.json b/orchestration/code-intel-project-conformance-policy.v1.json new file mode 100644 index 0000000..991e2b4 --- /dev/null +++ b/orchestration/code-intel-project-conformance-policy.v1.json @@ -0,0 +1,134 @@ +{ + "schema": "code-intel-project-conformance-policy.v1", + "sourceMethod": { + "project": "can1357/pon", + "uri": "https://github.com/can1357/pon", + "revision": "ab9067dbd2899c64c4d67a4bc27b8ad49472b126", + "adoption": "selective_method_reimplementation", + "licenseStatus": "unverified_no_declared_license_do_not_copy_code" + }, + "mechanisms": [ + { + "id": "reference-oracle", + "ponForm": "byte-exact comparison with CPython", + "codeIntelForm": "reviewed canonical machine-artifact fixtures", + "status": "implemented", + "evidence": ["test-parity-baseline.ps1", "tests/fixtures/parity"] + }, + { + "id": "conformance-corpus", + "ponForm": "JIT, AOT, and CPython conformance corpora", + "codeIntelForm": "repository-state and multi-language code-evidence corpora", + "status": "implemented", + "evidence": ["tests/fixtures/parity", "orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json"] + }, + { + "id": "normalized-output-parity", + "ponForm": "JIT and AOT paths must agree with the same external oracle", + "codeIntelForm": "supported evidence paths emit the same normalized artifact contracts", + "status": "implemented", + "evidence": ["test-parity-baseline.ps1", "crates/code-intel-cli/tests/pon_multilanguage_mapping.rs"] + }, + { + "id": "monotonic-floor", + "ponForm": "committed passing module floor cannot regress", + "codeIntelForm": "committed passing fixture set and count cannot regress", + "status": "implemented", + "evidence": ["test-parity-floor.ps1", "tests/fixtures/parity/parity-floor.json"] + }, + { + "id": "expected-divergence-ledger", + "ponForm": "intentional mismatches are explicit and reviewed", + "codeIntelForm": "known artifact divergences require scoped, owned, expiring entries", + "status": "designed", + "evidence": ["orchestration/acceptance/known-divergences.v1.json"] + }, + { + "id": "mutation-robustness", + "ponForm": "differential fuzzing rejects unexpected divergence", + "codeIntelForm": "negative mutations must fail the exact project gate that owns the invariant", + "status": "implemented", + "evidence": ["test-language-adapter-acceptance.ps1"] + }, + { + "id": "deterministic-stress", + "ponForm": "free-threading and repeated execution stress", + "codeIntelForm": "fresh-root and repeated-run artifact determinism", + "status": "partial", + "evidence": ["test-parity-baseline.ps1", "orchestration/acceptance/native-code-evidence-candidate.json"] + }, + { + "id": "performance-ratchet", + "ponForm": "benchmark speedup and regression floors", + "codeIntelForm": "representative scan latency and resource regression floors", + "status": "deferred", + "evidence": [] + } + ], + "profiles": { + "fast": { + "purpose": "local and pull-request conformance over implemented correctness mechanisms", + "acceptedMechanismStatuses": ["implemented", "partial"], + "requiredMechanisms": [ + "reference-oracle", + "conformance-corpus", + "normalized-output-parity", + "monotonic-floor", + "mutation-robustness", + "deterministic-stress" + ], + "requiredSuites": ["parity-floor", "adapter-contract", "multilanguage-corpus", "python314-development", "merge-queue-contract"] + }, + "full": { + "purpose": "release conformance; every mapped mechanism must be fully implemented", + "acceptedMechanismStatuses": ["implemented"], + "requiredMechanisms": [ + "reference-oracle", + "conformance-corpus", + "normalized-output-parity", + "monotonic-floor", + "expected-divergence-ledger", + "mutation-robustness", + "deterministic-stress", + "performance-ratchet" + ], + "requiredSuites": ["parity-floor", "adapter-contract", "multilanguage-corpus", "python314-development", "merge-queue-contract", "pipeline-smoke"] + } + }, + "suites": [ + { + "id": "parity-floor", + "mechanisms": ["reference-oracle", "monotonic-floor", "deterministic-stress"], + "command": { "kind": "pwsh", "file": "test-parity-floor.ps1", "args": [] } + }, + { + "id": "adapter-contract", + "mechanisms": ["mutation-robustness", "normalized-output-parity"], + "command": { "kind": "pwsh", "file": "test-language-adapter-acceptance.ps1", "args": [] } + }, + { + "id": "multilanguage-corpus", + "mechanisms": ["conformance-corpus", "normalized-output-parity"], + "command": { "kind": "process", "executable": "cargo", "args": ["test", "-p", "code-intel", "--test", "pon_multilanguage_mapping"] } + }, + { + "id": "python314-development", + "mechanisms": ["reference-oracle", "conformance-corpus", "normalized-output-parity", "deterministic-stress"], + "command": { "kind": "pwsh", "file": "Test-Python314PonCompatibility.ps1", "args": ["-Profile", "development", "-Json"] } + }, + { + "id": "merge-queue-contract", + "mechanisms": ["mutation-robustness", "deterministic-stress"], + "command": { "kind": "pwsh", "file": "test-multi-agent-merge-queue.ps1", "args": [] } + }, + { + "id": "pipeline-smoke", + "mechanisms": ["conformance-corpus", "deterministic-stress"], + "command": { + "kind": "pwsh", + "file": "test-code-intel-pipeline.ps1", + "args": ["-RepoPath", ".", "-SkipRepowise", "-AllowGraphMissing", "-SkipSentruxGate", "-SkipGitHubResearch", "-Mode", "normal"] + } + } + ] +} diff --git a/orchestration/decision-gap-rules.v1.json b/orchestration/decision-gap-rules.v1.json new file mode 100644 index 0000000..4088e36 --- /dev/null +++ b/orchestration/decision-gap-rules.v1.json @@ -0,0 +1,13 @@ +{ + "schema": "code-intel-decision-gap-rules.v1", + "choiceKinds": [ + "intent", + "priority", + "resource_allocation", + "risk_acceptance", + "tradeoff" + ], + "factKinds": [ + "missing_fact" + ] +} diff --git a/orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json b/orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json new file mode 100644 index 0000000..07d2751 --- /dev/null +++ b/orchestration/evidence/d02-clean-machine-verifier-attestation.v1.json @@ -0,0 +1,82 @@ +{ + "schema": "code-intel-clean-machine-verifier-attestation.v1", + "version": 1, + "verifiedAt": "2026-07-13T18:49:59Z", + "capability": "project.orientation-benchmark", + "source": { + "path": "crates/code-intel-cli/src/project_orientation_benchmark.rs", + "sha256": "9af2a8cf863dab62f7372ca454c6b5efddae60f71223e64064076fe00977833d" + }, + "environment": { + "cleanMachine": true, + "basis": "fresh disposable container, source-only copy, zero host mounts or caches, and no injected environment variables", + "image": "rust:1.96-bookworm", + "imageDigest": "sha256:a339861ae23e9abb272cea45dfafde21760d2ce6577a70f8a926153677902663", + "os": "Debian GNU/Linux 12 (bookworm)", + "architecture": "x86_64", + "toolchain": { + "rustc": "1.96.1 (31fca3adb 2026-06-26)", + "cargo": "1.96.1 (356927216 2026-06-26)", + "powershell": "7.6.3" + }, + "mountCount": 0, + "bindsPresent": false, + "taskVolumesPresent": false, + "privileged": false, + "deviceCount": 0, + "hostTargetCacheCopied": false, + "injectedEnvironmentVariableKeys": [], + "imageEnvironmentVariableKeys": ["PATH", "RUSTUP_HOME", "CARGO_HOME", "RUST_VERSION"], + "sourceCopyExclusions": [".git", "target", ".Codex", ".omx", "artifacts", "work"] + }, + "commands": [ + { + "command": "cargo test -p code-intel --test project_orientation_benchmark -- --nocapture", + "exitCode": 0, + "elapsedMs": 11239, + "result": "1 passed; 0 failed" + }, + { + "command": "target/debug/code-intel benchmark orientation --out /tmp/d02-benchmark --repetitions 3", + "exitCode": 0, + "elapsedMs": 898, + "result": "pass" + } + ], + "benchmark": { + "fixtureCount": 9, + "sampleCount": 54, + "repetitionsPerTemperature": 3, + "sizes": ["small", "medium", "large"], + "conditions": ["clean", "dirty", "provider_missing"], + "temperatures": ["cold", "warm"], + "typicalP50WallTimeMs": 6, + "typicalP95WallTimeMs": 10, + "coldP50WallTimeMs": 8, + "coldP95WallTimeMs": 30, + "warmP50WallTimeMs": 6, + "warmP95WallTimeMs": 17, + "fieldCorrectness": 1.0, + "unknownPrecision": 1.0, + "provenanceCompleteness": 1.0, + "verdict": "pass" + }, + "compatibilityAttempt": { + "image": "rust:1.88-bookworm", + "imageDigest": "sha256:af306cfa71d987911a781c37b59d7d67d934f49684058f96cf72079c3626bfe0", + "exitCode": 101, + "result": "compile blocked because std::fs::File::lock is unstable on Rust 1.88", + "includedInBenchmark": false + }, + "productionReport": { + "cleanMachine": false, + "externalVerificationComplete": false, + "statement": "This historical clean-machine attestation is bound to an earlier benchmark source digest. The current benchmark implementation passes local representative-corpus verification but requires a fresh independent clean-machine rerun before external verification can be complete." + }, + "cleanup": { + "containerDestroyed": true, + "containerRemaining": false, + "taskVolumesRemaining": false + }, + "authority": "verifier_attestation_only_no_runtime_or_release_authority" +} diff --git a/orchestration/evidence/final-commitment-reconciliation.json b/orchestration/evidence/final-commitment-reconciliation.json new file mode 100644 index 0000000..86ec94d --- /dev/null +++ b/orchestration/evidence/final-commitment-reconciliation.json @@ -0,0 +1,988 @@ +{ + "schema": "code-intel-final-commitment-reconciliation.v1", + "sourcePlan": "docs/plans/adr-0010-execution-plan.md", + "sourcePlanSha256": "8ba922970a66f55087ec9711f16e71e4ca1af492ec79da0554d0718b0e580d33", + "ticketCount": 69, + "allowedClaimStatuses": [ + "implemented_verified", + "implemented_pending_verification", + "implemented_blocked", + "retirement_blocked", + "not_implemented" + ], + "statusPolicy": "A planned, drafted, packet-only, or in-progress item cannot be mapped to an implemented status without implementation evidence and an independent verdict.", + "items": [ + { + "ticketId": "A00", + "title": "compatibility.parity-baseline", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test capability_exec" + ], + "artifacts": [ + "crates/code-intel-cli/tests/capability_exec.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A01", + "title": "capability.runtime-exec", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test capability_exec" + ], + "artifacts": [ + "crates/code-intel-cli/tests/capability_exec.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A02", + "title": "repository.snapshot-identity", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test snapshot_identity" + ], + "artifacts": [ + "crates/code-intel-cli/tests/snapshot_identity.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A03", + "title": "artifact.ref-verify", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test artifact_ref" + ], + "artifacts": [ + "crates/code-intel-cli/tests/artifact_ref.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A04", + "title": "evidence.admissibility-validate", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test evidence_admissibility" + ], + "artifacts": [ + "crates/code-intel-cli/tests/evidence_admissibility.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A05", + "title": "authority.transition-gate", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test authority_transition" + ], + "artifacts": [ + "crates/code-intel-cli/tests/authority_transition.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A09", + "title": "run.dag-coordinate", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test dag_run" + ], + "artifacts": [ + "crates/code-intel-cli/tests/dag_run.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A06", + "title": "artifact.stage-write", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test staged_artifact" + ], + "artifacts": [ + "crates/code-intel-cli/tests/staged_artifact.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A07", + "title": "run.commit", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test run_commit" + ], + "artifacts": [ + "crates/code-intel-cli/tests/run_commit.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "A08", + "title": "artifact.index-committed-only", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test artifact_index" + ], + "artifacts": [ + "crates/code-intel-cli/tests/artifact_index.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B01", + "title": "provider.repowise-adapt", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test repowise_adapter" + ], + "artifacts": [ + "crates/code-intel-cli/tests/repowise_adapter.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B02", + "title": "provider.graph-adapt", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test graph_adapter" + ], + "artifacts": [ + "crates/code-intel-cli/tests/graph_adapter.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B03", + "title": "provider.sentrux-adapt", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test sentrux_adapter" + ], + "artifacts": [ + "crates/code-intel-cli/tests/sentrux_adapter.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B04", + "title": "provider.codenexus-adapt", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test codenexus_adapter" + ], + "artifacts": [ + "crates/code-intel-cli/tests/codenexus_adapter.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B05", + "title": "repository.survival-scan", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test survival_scan" + ], + "artifacts": [ + "crates/code-intel-cli/tests/survival_scan.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B06", + "title": "advisory.workflow-recommend", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test method_select" + ], + "artifacts": [ + "crates/code-intel-cli/tests/method_select.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B07", + "title": "integration.registry-reconcile", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test capability_exec" + ], + "artifacts": [ + "crates/code-intel-cli/tests/capability_exec.rs", + "orchestration/integrations.json" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B08", + "title": "evidence.native-code", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test native_code_evidence" + ], + "artifacts": [ + "crates/code-intel-cli/tests/native_code_evidence.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B09", + "title": "diagnosis.hospital", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test hospital_diagnosis" + ], + "artifacts": [ + "crates/code-intel-cli/tests/hospital_diagnosis.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "B10", + "title": "doctor.envelope-adapt", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test doctor_envelope" + ], + "artifacts": [ + "crates/code-intel-cli/tests/doctor_envelope.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C00", + "title": "governance.ponytail-gate", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test ponytail_gate" + ], + "artifacts": [ + "crates/code-intel-cli/tests/ponytail_gate.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C01", + "title": "method.catalog", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test method_catalog" + ], + "artifacts": [ + "crates/code-intel-cli/tests/method_catalog.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C02", + "title": "method.select", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test method_select" + ], + "artifacts": [ + "crates/code-intel-cli/tests/method_select.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C03", + "title": "internalization.record-engine", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C04", + "title": "assistance.discover", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test assistance_discovery" + ], + "artifacts": [ + "crates/code-intel-cli/tests/assistance_discovery.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C05", + "title": "decision.gap-detect", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test decision_gap" + ], + "artifacts": [ + "crates/code-intel-cli/tests/decision_gap.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C06", + "title": "decision.request-response-port", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test decision_port" + ], + "artifacts": [ + "crates/code-intel-cli/tests/decision_port.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "C07", + "title": "decision.record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test decision_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/decision_record.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R01", + "title": "internalization.repowise-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R02", + "title": "internalization.graph-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R03", + "title": "internalization.sentrux-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R04", + "title": "internalization.codenexus-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R05", + "title": "internalization.repomix-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R06", + "title": "internalization.native-code-evidence-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R07", + "title": "internalization.cocoindex-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R08", + "title": "internalization.github-research-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-advisory-candidates.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R09", + "title": "internalization.rg-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R10", + "title": "internalization.git-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R11", + "title": "internalization.tree-sitter-v-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R12", + "title": "internalization.greenfield-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R13", + "title": "internalization.openspec-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R14", + "title": "internalization.spec-kit-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R15", + "title": "internalization.matt-flow-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R16", + "title": "internalization.gstack-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R17", + "title": "internalization.qiaomu-goal-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R18", + "title": "internalization.agent-loops-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R19", + "title": "internalization.metaharness-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R20", + "title": "internalization.yao-meta-skill-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R21", + "title": "internalization.ponytail-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R22", + "title": "internalization.mattpocock-skills-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R23", + "title": "internalization.linear-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R24", + "title": "internalization.obsidian-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R25", + "title": "internalization.llm-wiki-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "R26", + "title": "internalization.my-code-machine-record", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test internalization_record" + ], + "artifacts": [ + "crates/code-intel-cli/tests/internalization_record.rs", + "docs/internalization-r09-r26-records.md" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "D01", + "title": "project.orientation", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test project_orientation" + ], + "artifacts": [ + "crates/code-intel-cli/tests/project_orientation.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "D02", + "title": "project.orientation-benchmark", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test project_orientation_benchmark" + ], + "artifacts": [ + "crates/code-intel-cli/tests/project_orientation_benchmark.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "D03", + "title": "understanding.quadrant", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test understanding_quadrant" + ], + "artifacts": [ + "crates/code-intel-cli/tests/understanding_quadrant.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "D04", + "title": "delivery.light-speed-measure", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test delivery_light_speed" + ], + "artifacts": [ + "crates/code-intel-cli/tests/delivery_light_speed.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "E00", + "title": "compatibility.retirement-gate", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test compatibility_retirement_gate" + ], + "artifacts": [ + "crates/code-intel-cli/tests/compatibility_retirement_gate.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "E01", + "title": "compatibility.retirement-ticket-template", + "claimStatus": "implemented_verified", + "evidenceCommands": [ + "cargo test -p code-intel --test compatibility_retirement_ticket_template" + ], + "artifacts": [ + "crates/code-intel-cli/tests/compatibility_retirement_ticket_template.rs" + ], + "independentVerdict": "verified", + "blockers": [] + }, + { + "ticketId": "E02", + "title": "compatibility.retire-recommender-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-RecommenderRetirementPacket.ps1 -PacketRoot orchestration/retirements/e02-recommender" + ], + "artifacts": [ + "orchestration/retirements/e02-recommender/status.json", + "docs/compatibility-retire-recommender-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E03", + "title": "compatibility.retire-provider-preflight-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-ProviderPreflightRetirementPacket.ps1 -PacketRoot orchestration/retirements/e03-provider-preflight" + ], + "artifacts": [ + "orchestration/retirements/e03-provider-preflight/status.json", + "docs/compatibility-retire-provider-preflight-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E04", + "title": "compatibility.retire-codenexus-direct-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-CodeNexusDirectRetirementPacket.ps1 -PacketRoot orchestration/retirements/e04-codenexus-direct" + ], + "artifacts": [ + "orchestration/retirements/e04-codenexus-direct/status.json", + "docs/compatibility-retire-codenexus-direct-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E05", + "title": "compatibility.retire-publication-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-PublicationRetirementPacket.ps1 -PacketRoot orchestration/retirements/e05-publication" + ], + "artifacts": [ + "orchestration/retirements/e05-publication/status.json", + "docs/compatibility-retire-publication-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E07", + "title": "compatibility.retire-native-code-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-NativeCodeRetirementPacket.ps1 -PacketRoot orchestration/retirements/e07-native-code" + ], + "artifacts": [ + "orchestration/retirements/e07-native-code/status.json", + "docs/compatibility-retire-native-code-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E08", + "title": "compatibility.retire-hospital-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-HospitalRetirementPacket.ps1 -PacketRoot orchestration/retirements/e08-hospital" + ], + "artifacts": [ + "orchestration/retirements/e08-hospital/status.json", + "docs/compatibility-retire-hospital-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E09", + "title": "compatibility.retire-doctor-wrapper-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-DoctorWrapperRetirementPacket.ps1 -PacketRoot orchestration/retirements/e09-doctor-wrapper" + ], + "artifacts": [ + "orchestration/retirements/e09-doctor-wrapper/status.json", + "docs/compatibility-retire-doctor-wrapper-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E10", + "title": "compatibility.retire-index-branch", + "claimStatus": "retirement_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File tools/compatibility/Test-IndexRetirementPacket.ps1 -PacketRoot orchestration/retirements/e10-index" + ], + "artifacts": [ + "orchestration/retirements/e10-index/status.json", + "docs/compatibility-retire-index-branch.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "E00 decision remains blocked", + "deletionExecuted=false", + "retired=false" + ] + }, + { + "ticketId": "E06", + "title": "compatibility.facade-finalize", + "claimStatus": "implemented_blocked", + "evidenceCommands": [ + "pwsh -NoProfile -File test-compatibility-facade-finalize.ps1" + ], + "artifacts": [ + "Invoke-CompatibilityFacadeFinalize.ps1", + "test-compatibility-facade-finalize.ps1", + "orchestration/facade-finalize-policy.v1.json", + "orchestration/schemas/code-intel-compatibility-facade-finalize.v1.schema.json", + "docs/compatibility-facade-finalize.md" + ], + "independentVerdict": "blocked", + "blockers": [ + "independent audit implementation approved, but final facade approval remains blocked", + "E02-E05 and E07-E10 retirement dependencies are not completed", + "current audit exits 2 with approvalEligible=false and independentApproval=null" + ] + } + ] +} diff --git a/orchestration/examples/compatibility-retirement-ticket-template.v1.example.json b/orchestration/examples/compatibility-retirement-ticket-template.v1.example.json new file mode 100644 index 0000000..7e72c54 --- /dev/null +++ b/orchestration/examples/compatibility-retirement-ticket-template.v1.example.json @@ -0,0 +1,26 @@ +{ + "schema":"code-intel-compatibility-retirement-ticket-template.v1", + "snapshotIdentity":"snapshot-example", + "ticketId":"ticket-retire-recommender-branch", + "retirementId":"retire-recommender-branch", + "legacyBranch":{"capabilityId":"pipeline.legacy-recommender","branchId":"legacy.recommender","callPath":"run-code-intel.ps1::legacy.recommender"}, + "replacement":{"capabilityId":"advisory.workflow-recommend","dependencies":["D02"]}, + "affectedFiles":["run-code-intel.ps1"], + "evidence":{ + "golden":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-evidence.v1","type":"compatibility.retirement-evidence","path":"golden.json","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","consumedSnapshotIdentity":"snapshot-example"}, + "contract":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-evidence.v1","type":"compatibility.retirement-evidence","path":"contract.json","sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","consumedSnapshotIdentity":"snapshot-example"}, + "effects":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-evidence.v1","type":"compatibility.retirement-evidence","path":"effects.json","sha256":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","consumedSnapshotIdentity":"snapshot-example"}, + "usage":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-evidence.v1","type":"compatibility.retirement-evidence","path":"usage.json","sha256":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","consumedSnapshotIdentity":"snapshot-example"}, + "rollbackRehearsal":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-evidence.v1","type":"compatibility.retirement-evidence","path":"rollback.json","sha256":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","consumedSnapshotIdentity":"snapshot-example"}, + "deletionDiff":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-deletion-diff.v1","type":"compatibility.retirement-deletion-diff","path":"deletion-diff.json","sha256":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","consumedSnapshotIdentity":"snapshot-example"} + }, + "source":{ + "retirementDecision":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-decision.v1","type":"compatibility.retirement-decision","path":"decision.json","sha256":"1111111111111111111111111111111111111111111111111111111111111111","consumedSnapshotIdentity":"snapshot-example"}, + "retirementManifest":{"schema":"code-intel-artifact-ref.v1","artifactSchema":"code-intel-compatibility-retirement-manifest.v1","type":"compatibility.retirement-manifest","path":"manifest.json","sha256":"2222222222222222222222222222222222222222222222222222222222222222","consumedSnapshotIdentity":"snapshot-example"} + }, + "owner":"executor-recommender", + "verifier":"verifier-independent", + "observationExpiry":4102444800, + "status":"draft", + "authorityBoundary":"template_only_no_approval_or_deletion_authority" +} diff --git a/orchestration/facade-finalize-fixtures/doctor.json b/orchestration/facade-finalize-fixtures/doctor.json new file mode 100644 index 0000000..b13df9b --- /dev/null +++ b/orchestration/facade-finalize-fixtures/doctor.json @@ -0,0 +1 @@ +{"schema":"code-intel-facade-mode-audit-fixture.v1","mode":"doctor","available":false,"reason":"independent doctor public-mode capture is pending","nodes":[]} diff --git a/orchestration/facade-finalize-fixtures/full.json b/orchestration/facade-finalize-fixtures/full.json new file mode 100644 index 0000000..3614e0c --- /dev/null +++ b/orchestration/facade-finalize-fixtures/full.json @@ -0,0 +1 @@ +{"schema":"code-intel-facade-mode-audit-fixture.v1","mode":"full","available":false,"reason":"legacy E02-E05/E07-E09 branches remain on the public full path","nodes":[]} diff --git a/orchestration/facade-finalize-fixtures/lite.json b/orchestration/facade-finalize-fixtures/lite.json new file mode 100644 index 0000000..40f1d14 --- /dev/null +++ b/orchestration/facade-finalize-fixtures/lite.json @@ -0,0 +1 @@ +{"schema":"code-intel-facade-mode-audit-fixture.v1","mode":"lite","available":false,"reason":"A09 to A07/A08 public lite path is not proven","nodes":[]} diff --git a/orchestration/facade-finalize-fixtures/normal.json b/orchestration/facade-finalize-fixtures/normal.json new file mode 100644 index 0000000..8bb6410 --- /dev/null +++ b/orchestration/facade-finalize-fixtures/normal.json @@ -0,0 +1 @@ +{"schema":"code-intel-facade-mode-audit-fixture.v1","mode":"normal","available":false,"reason":"legacy E02-E05/E07-E09 branches remain on the public normal path","nodes":[]} diff --git a/orchestration/facade-finalize-policy.v1.json b/orchestration/facade-finalize-policy.v1.json new file mode 100644 index 0000000..d46a2bc --- /dev/null +++ b/orchestration/facade-finalize-policy.v1.json @@ -0,0 +1,37 @@ +{ + "schema": "code-intel-compatibility-facade-finalize-policy.v1", + "ticketId": "E06", + "dependencyTickets": [ + { "ticketId": "E02", "directory": "orchestration/retirements/e02-recommender", "branchId": "run-code-intel.workflow-recommender.inline", "path": "run-code-intel.ps1" }, + { "ticketId": "E03", "directory": "orchestration/retirements/e03-provider-preflight", "branchId": "run-code-intel.provider-preflight.test-wrapper", "path": "run-code-intel.ps1" }, + { "ticketId": "E04", "directory": "orchestration/retirements/e04-codenexus-direct", "branchId": "run-code-intel.codenexus-lite.direct", "path": "run-code-intel.ps1" }, + { "ticketId": "E05", "directory": "orchestration/retirements/e05-publication", "branchId": "run-code-intel.publication.legacy-staging-marker", "path": "run-code-intel.ps1" }, + { "ticketId": "E07", "directory": "orchestration/retirements/e07-native-code", "branchId": "run-code-intel.native-code.embedded", "path": "run-code-intel.ps1" }, + { "ticketId": "E08", "directory": "orchestration/retirements/e08-hospital", "branchId": "run-code-intel.hospital.embedded-diagnosis-render", "path": "run-code-intel.ps1" }, + { "ticketId": "E09", "directory": "orchestration/retirements/e09-doctor-wrapper", "branchId": "invoke-code-intel.doctor.direct-production", "path": "invoke-code-intel.ps1" }, + { "ticketId": "E10", "directory": "orchestration/retirements/e10-index", "branchId": "update-code-intel-index.legacy-compatibility-traversal", "path": "update-code-intel-index.ps1" } + ], + "retainedPowerShell": [ + { "surfaceId": "audit.facade-finalize", "path": "Invoke-CompatibilityFacadeFinalize.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "compatibility.facade-finalize", "expiresAt": null, "classification": "platform_glue" }, + { "surfaceId": "public.invoke", "path": "invoke-code-intel.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "runtime.code-intel", "expiresAt": null, "classification": "compatibility_facade" }, + { "surfaceId": "public.pipeline", "path": "run-code-intel.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "runtime.code-intel", "expiresAt": null, "classification": "compatibility_facade" }, + { "surfaceId": "bootstrap.doctor", "path": "check-code-intel-tools.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "doctor", "expiresAt": null, "classification": "platform_glue" }, + { "surfaceId": "bootstrap.fresh-machine", "path": "bootstrap-new-machine.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "bootstrap.fresh-machine", "expiresAt": null, "classification": "platform_glue" }, + { "surfaceId": "compat.index", "path": "update-code-intel-index.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "artifact.index-committed-only", "expiresAt": null, "classification": "compatibility_facade" }, + { "surfaceId": "compat.repowise", "path": "Invoke-ScopedRepowise.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "memory.repowise", "expiresAt": null, "classification": "platform_glue" }, + { "surfaceId": "compat.sentrux", "path": "Invoke-SentruxAgentTool.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "structure.sentrux", "expiresAt": null, "classification": "compatibility_facade" }, + { "surfaceId": "compat.codenexus", "path": "Invoke-CodeNexusLite.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "localization.codenexus-lite", "expiresAt": null, "classification": "compatibility_facade" }, + { "surfaceId": "compat.workflow-recommend", "path": "Invoke-WorkflowRecommendation.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "advisory.workflow-recommend", "expiresAt": null, "classification": "compatibility_facade" }, + { "surfaceId": "compat.openspec-detector", "path": "OpenSpec-Detector.ps1", "owner": "code-intel-pipeline", "registryParticipantId": "advisory.workflow-recommend", "expiresAt": null, "classification": "compatibility_facade" } + ], + "modeMatrix": [ + { "mode": "doctor", "fixture": "doctor.json", "requiredCapabilities": ["doctor"] }, + { "mode": "lite", "fixture": "lite.json", "requiredCapabilities": ["doctor", "repository.snapshot-identity", "inventory.rg", "evidence.native-code", "diagnosis.hospital", "run.commit", "artifact.index-committed-only"] }, + { "mode": "normal", "fixture": "normal.json", "requiredCapabilities": ["doctor", "repository.snapshot-identity", "inventory.rg", "evidence.native-code", "diagnosis.hospital", "run.commit", "artifact.index-committed-only"] }, + { "mode": "full", "fixture": "full.json", "requiredCapabilities": ["doctor", "repository.snapshot-identity", "inventory.rg", "evidence.native-code", "diagnosis.hospital", "run.commit", "artifact.index-committed-only"] } + ], + "parity": { + "a00Evidence": "orchestration/baseline-manifest.json", + "rollbackTickets": ["E02", "E03", "E04", "E05", "E07", "E08", "E09", "E10"] + } +} diff --git a/orchestration/integrations.json b/orchestration/integrations.json index 8db197a..1a4df8c 100644 --- a/orchestration/integrations.json +++ b/orchestration/integrations.json @@ -7,6 +7,171 @@ "rule": "Normal code-intel runs must use integrations registered here. Separately installed project-intelligence CLIs are allowed only as compatibility shims or optional accelerators.", "extensionRule": "Add new projects, scanners, memory systems, graph providers, or governance strategies here before wiring them into pipeline scripts." }, + "productionRegistry": { + "mode": "enforce", + "productionFiles": [ + "invoke-code-intel.ps1", + "run-code-intel.ps1" + ], + "participants": [ + { + "capabilityId": "doctor", + "status": "declared", + "callSite": { + "source": "invoke-code-intel.ps1", + "anchor": "$doctor = Join-Path $root \"check-code-intel-tools.ps1\"" + }, + "envelope": "legacy-powershell-script-result.v1", + "owner": "code-intel-pipeline", + "dependencies": [], + "effects": ["repo_read", "process_execute"], + "artifacts": ["doctor_json"] + }, + { + "capabilityId": "diagnosis.hospital", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$hospitalReport = New-CodeIntelHospitalReport" + }, + "envelope": "legacy-powershell-function-result.v1", + "owner": "code-intel-pipeline", + "dependencies": ["evidence.native-code", "graph.code-intel-understand", "structure.sentrux", "localization.codenexus-lite", "provider.runtime-ci-evidence"], + "effects": ["local_write"], + "artifacts": ["hospital.json", "hospital.md", "runtime-ci-summary.json"] + }, + { + "capabilityId": "pack.repomix", + "status": "deleted", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$repomixTool = Join-Path $PSScriptRoot \"Invoke-RepomixCodePack.ps1\"" + }, + "reviewedDeletion": { + "reviewer": "code-intel-maintainers", + "reviewedAt": "2026-07-14T00:00:00Z", + "evidence": "R05 audited host and npm cache: no Repomix executable or pinned package; production call site removed fail-closed" + } + }, + { + "capabilityId": "evidence.native-code", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$codeEvidence = New-CodeEvidenceLayer -RepoPath" + }, + "envelope": "legacy-powershell-function-result.v1", + "owner": "code-intel-pipeline", + "dependencies": ["repository.snapshot-identity", "inventory.rg"], + "effects": ["repo_read", "local_write"], + "artifacts": ["code-evidence-native-artifacts.v1"] + }, + { + "capabilityId": "evidence.cocoindex-code", + "status": "deleted", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$adapterConfig = Get-JsonProperty $adapters \"cocoindex-code\" $null" + }, + "reviewedDeletion": { + "reviewer": "code-intel-maintainers", + "reviewedAt": "2026-07-14T02:45:00+08:00", + "evidence": "R07 found zero semantic invocations and no measured unique value; configuration, command discovery, and production integration were removed while Native Code Evidence remained independent" + } + }, + { + "capabilityId": "research.github-solution", + "status": "deleted", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$githubResearchScript = Join-Path $PSScriptRoot \"Invoke-GitHubSolutionResearch.ps1\"" + }, + "reviewedDeletion": { + "reviewer": "code-intel-maintainers", + "reviewedAt": "2026-07-14T02:45:00+08:00", + "evidence": "R08 authenticated representative blocker query returned manual_required with zero candidates because the generated query was invalid; production call site removed rather than claiming unreproduced value" + } + }, + { + "capabilityId": "memory.repowise", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$scopedRepowiseScript = Join-Path $PSScriptRoot \"Invoke-ScopedRepowise.ps1\"" + }, + "envelope": "legacy-powershell-script-result.v1", + "owner": "code-intel-pipeline", + "dependencies": ["repository.snapshot-identity"], + "effects": ["repo_read", "process_execute", "local_write", "repo_mutation"], + "artifacts": ["repowise-status.json", "repowise-index.json", "repowise-docs.json"] + }, + { + "capabilityId": "graph.code-intel-understand", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$knowledgeGraph = Join-Path $understandDir \"knowledge-graph.json\"" + }, + "envelope": "legacy-artifact-read.v1", + "owner": "code-intel-pipeline", + "dependencies": ["repository.snapshot-identity", "inventory.rg"], + "effects": ["repo_read", "local_write"], + "artifacts": [".understand-anything/knowledge-graph.json"] + }, + { + "capabilityId": "structure.sentrux", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$sentruxAgentTool = Join-Path $PSScriptRoot \"Invoke-SentruxAgentTool.ps1\"" + }, + "envelope": "legacy-powershell-script-result.v1", + "owner": "code-intel-pipeline", + "dependencies": ["repository.snapshot-identity"], + "effects": ["repo_read", "process_execute", "local_write"], + "artifacts": ["sentrux-*.json"] + }, + { + "capabilityId": "localization.codenexus-lite", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "$codeNexusLiteTool = Join-Path $PSScriptRoot \"Invoke-CodeNexusLite.ps1\"" + }, + "envelope": "legacy-powershell-script-result.v1", + "owner": "code-intel-pipeline", + "dependencies": ["inventory.rg"], + "effects": ["repo_read", "process_execute", "local_write"], + "artifacts": ["codenexus-context.json"] + }, + { + "capabilityId": "run.commit", + "status": "declared", + "callSite": { + "source": "run-code-intel.ps1", + "anchor": "& $rustCli run commit" + }, + "envelope": "rust-cli-process.v1", + "owner": "code-intel-pipeline", + "dependencies": ["evidence.native-code"], + "effects": ["local_write", "atomic_publish"], + "artifacts": ["code-intel-run-commit.v1", "complete.json"] + }, + { + "capabilityId": "artifact.index-committed-only", + "status": "declared", + "callSite": { + "source": "invoke-code-intel.ps1", + "anchor": "$indexer = Join-Path $root \"update-code-intel-index.ps1\"" + }, + "envelope": "legacy-powershell-script-result.v1", + "owner": "code-intel-pipeline", + "dependencies": ["run.commit"], + "effects": ["local_write"], + "artifacts": ["code-intel-artifact-index.v1"] + } + ] + }, "stages": [ { "id": "preflight", @@ -29,8 +194,8 @@ { "id": "semantic_memory", "order": 30, - "required": true, - "description": "Long-term project memory, index, status, and optional docs." + "required": false, + "description": "Optional, non-blocking long-term project memory, index, status, and docs; included in the default plan when available." }, { "id": "architecture_graph", @@ -50,6 +215,12 @@ "required": true, "description": "Hotspot and reference localization for agents." }, + { + "id": "orientation", + "order": 65, + "required": true, + "description": "Reduced or full repository orientation assembled from snapshot-bound evidence." + }, { "id": "diagnosis", "order": 70, @@ -62,6 +233,24 @@ "required": false, "description": "Implementation-independent behavioral specs, test vectors, acceptance criteria, and provenance." }, + { + "id": "verification", + "order": 76, + "required": false, + "description": "Content-bound parity, measurement, approval, and retirement gates without mutation authority." + }, + { + "id": "landing_coordination", + "order": 77, + "required": false, + "description": "Optional, fail-closed serialization of accepted multi-agent worktree landings before human production promotion." + }, + { + "id": "publication", + "order": 78, + "required": true, + "description": "Atomic run publication after artifact validation and before index admission." + }, { "id": "artifact_index", "order": 80, @@ -80,15 +269,36 @@ "capabilities": [ "preflight", "tool_contract", - "repo_state" + "repo_state", + "enveloped_readiness_observation" ], "commands": { - "validate": "check-code-intel-tools.ps1 -RepoPath -RequireRepowise -Json" + "validate": "check-code-intel-tools.ps1 -RepoPath -RequireRepowise -Json", + "capabilityExec": "target/debug/code-intel.exe capability exec doctor --request --out --artifact-root " }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "doctor", + "contractVersion": 1, + "implementation": { + "id": "doctor.envelope.compat", + "version": "1.0.0", + "toolchainDigests": [ + "ab13db09de8fed217296e6f482dd204448a53780ec10b11dfefe2ef5e44763b4", + "7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9" + ] + }, + "determinism": "external_nondeterministic", + "allowedEffects": ["repo_read", "local_write"], + "dependencies": ["repo.snapshot"] + }, + "runtimeAdapter": "doctor.envelope.compat", "artifactContract": [ + "orchestration/schemas/code-intel-doctor-observation.v1.schema.json", + "doctor-observation.json", "doctor_json" ], - "extensionPoint": "Add new preflight checks here before changing installer or runner checks." + "extensionPoint": "PowerShell remains an observation-only bootstrap probe until E09; authoritative doctor execution uses the A01 envelope and never promotes readiness into conformance or admissibility." }, { "id": "runtime.code-intel", @@ -121,9 +331,14 @@ "providerList": "target/debug/code-intel.exe provider --action List --json", "providerPlan": "target/debug/code-intel.exe provider --action Plan --provider --operation --repo --json", "providerInvoke": "target/debug/code-intel.exe provider --action Invoke --provider --operation --repo --json", + "graphAdapt": "target/debug/code-intel.exe provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + "sentruxAdapt": "target/debug/code-intel.exe provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + "capabilityExec": "target/debug/code-intel.exe capability exec --request --out ", "doctor": "target/debug/code-intel.exe doctor --json", "resume": "target/debug/code-intel.exe resume --repo --json", - "classify": "target/debug/code-intel.exe classify --report --json" + "classify": "target/debug/code-intel.exe classify --report --json", + "evidenceValidate": "target/debug/code-intel.exe evidence validate --request --artifact-root ", + "dagCoordinate": "target/debug/code-intel.exe run dag-coordinate --repo --out " }, "artifactContract": [ "orchestration/integrations.json", @@ -146,7 +361,7 @@ "codenexus_doctor" ], "commands": { - "compat": "Invoke-CodeNexusLite.ps1 -RepoPath -ArtifactDir " + "compat": "pwsh -NoProfile -File \"$env:CODE_INTEL_HOME\\Invoke-CodeNexusLite.ps1\" -RepoPath ''" }, "artifactContract": [ "codenexus-context.json", @@ -154,6 +369,755 @@ ], "extensionPoint": "CodeNexus-lite is currently a demoted compatibility adapter. Restore a hardened worker here only after the Rust entrypoint exists again." }, + { + "id": "governance.ponytail-gate", + "stage": "preflight", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/Cargo.toml", + "capabilities": [ + "necessity_trace_admission", + "first_sufficient_solution_gate", + "authority_bypass_validation" + ], + "commands": { + "evaluate": "target/debug/code-intel.exe governance ponytail-gate --request " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-ponytail-gate.v1.schema.json", + "orchestration/ponytail-gate-policy.v1.json" + ], + "extensionPoint": "C00 owns necessity and first-sufficient admission only; product priority and code-style policy remain out of scope." + }, + { + "id": "repository.snapshot-identity", + "stage": "preflight", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/src/snapshot.rs", + "capabilities": ["portable_repository_identity", "explicit_dirty_overlay"], + "commands": { + "snapshotIdentity": "target/debug/code-intel.exe snapshot identity --repo --working-tree-policy --scope ", + "capabilityExec": "target/debug/code-intel.exe capability exec repo.snapshot --request --out " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "repo.snapshot", + "contractVersion": 1, + "implementation": { + "id": "repository.snapshot.compat", + "version": "1.0.0", + "toolchainDigests": [ + "b6d8eca4c8ce6da2787b63fdc1101ec76ea1c52e0ecdd6f83fe7dc263c7f0dc8" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["repo_read", "local_write"], + "dependencies": [] + }, + "runtimeAdapter": "repository.snapshot.compat", + "artifactContract": [ + "orchestration/schemas/code-intel-repository-snapshot.v1.schema.json", + "snapshot.json" + ], + "extensionPoint": "VCS providers may replace discovery only by preserving the versioned portable identity and overlay contract." + }, + { + "id": "development.multi-agent-workspace-preflight", + "stage": "preflight", + "owner": "code-intel-pipeline", + "kind": "internal-policy-adapter", + "required": false, + "entrypoint": "Invoke-MultiAgentWorkspacePreflight.ps1", + "capabilities": [ + "dirty_workspace_inventory", + "stable_inventory_digest", + "mutation_fail_closed", + "explicit_observation_only" + ], + "commands": { + "observe": "pwsh -NoProfile -File Invoke-MultiAgentWorkspacePreflight.ps1 -RepoPath '' -Intent observation -Json", + "admitMutation": "pwsh -NoProfile -File Invoke-MultiAgentWorkspacePreflight.ps1 -RepoPath '' -Intent mutation -Json" + }, + "artifactContract": [ + "orchestration/multi-agent-workspace-policy.v1.json" + ], + "effects": ["repo_read", "process_execute"], + "extensionPoint": "Multi-agent mutation must call admitMutation from the repository root. Ordinary code-intel analysis may use observation and never receives mutation authority from a dirty-root inventory." + }, + { + "id": "evidence.admissibility-validate", + "stage": "preflight", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/src/admissibility.rs", + "capabilities": ["provider_neutral_admissibility", "freshness_policy", "failure_semantics"], + "commands": { + "validate": "target/debug/code-intel.exe evidence validate --request --artifact-root " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-payload.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json" + ], + "extensionPoint": "Provider adapters emit this port contract; provider-specific logic must not enter the admissibility core." + }, + { + "id": "provider.repowise-adapt", + "stage": "semantic_memory", + "owner": "code-intel-pipeline", + "kind": "internal-adapter", + "required": false, + "entrypoint": "crates/code-intel-cli/src/providers.rs", + "capabilities": ["repowise_native_translation", "provider_neutral_admissibility", "independent_index_docs_status"], + "commands": { + "adapt": "target/debug/code-intel.exe provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + "facade": "run-code-intel.ps1 -RepowiseAdapterRequest -RepowiseAdapterArtifactRoot -RepowiseAdapterEvaluatedAt -RepowiseAdapterMaxAgeSeconds " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-repowise-route-result.v1.schema.json", + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json" + ], + "extensionPoint": "Repowise remains external; this adapter translates native observations and submits every emitted evidence request through A04 before any downstream consumer may use it." + }, + { + "id": "provider.graph-adapt", + "stage": "architecture_graph", + "owner": "code-intel-pipeline", + "kind": "internal-adapter", + "required": true, + "entrypoint": "crates/code-intel-cli/src/providers.rs", + "capabilities": [ + "internal_external_graph_translation", + "current_snapshot_binding", + "explicit_fallback_identity", + "provider_neutral_admissibility" + ], + "commands": { + "adapt": "target/debug/code-intel.exe provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + "facade": "run-code-intel.ps1 -GraphAdapterRequest -GraphAdapterArtifactRoot -GraphAdapterEvaluatedAt -GraphAdapterMaxAgeSeconds ", + "capabilityExec": "target/debug/code-intel.exe capability exec provider.graph-adapt --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "provider.graph-adapt", + "contractVersion": 1, + "implementation": { + "id": "provider.graph-builtin.compat", + "version": "1.0.0", + "toolchainDigests": [ + "a04226fab56751d014460e1596328e503b6a9afbf2efd5e84ab7b0790831149e", + "bc6c27f45e0d1e0ccd12546da02e221b6193cfb4a5df3d18096a6b335bf4114a", + "339536b6756cecd60b706faafb3ee9f53f8c1436ac6d2becb0448d27dc0b0322", + "52644a812174988ede91d98ddfec63c6a91f8478277d7bf74c73f106dd0f776b" + ] + }, + "determinism": "external_nondeterministic", + "allowedEffects": ["repo_read", "local_write"], + "dependencies": ["repo.snapshot"] + }, + "runtimeAdapter": "provider.graph-builtin.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": [ + "crates/code-intel-cli/src/builtin_provider_evidence.rs", + "crates/code-intel-cli/src/graph.rs", + "crates/code-intel-cli/src/graph_adapter.rs", + "crates/code-intel-cli/src/admissibility.rs" + ] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-architecture-graph-port.v1.schema.json", + "orchestration/schemas/code-intel-graph-route-result.v1.schema.json", + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json" + ], + "extensionPoint": "The adapter is the only admissible architecture-graph evidence route. Internal Rust and explicit Understand-compatible fallback outputs must preserve this port and pass A04; direct graph artifacts are not current anatomy." + }, + { + "id": "provider.sentrux-adapt", + "stage": "structure_governance", + "owner": "code-intel-pipeline", + "kind": "internal-adapter", + "required": true, + "entrypoint": "crates/code-intel-cli/src/providers.rs", + "capabilities": ["authoritative_rule_normalization", "effect_reconciliation", "provider_neutral_admissibility", "fail_closed_unknown_rules"], + "commands": { + "adapt": "target/debug/code-intel.exe provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + "facade": "run-code-intel.ps1 -SentruxAdapterRequest -SentruxAdapterArtifactRoot -SentruxAdapterEvaluatedAt -SentruxAdapterMaxAgeSeconds ", + "capabilityExec": "target/debug/code-intel.exe capability exec provider.sentrux-adapt --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "provider.sentrux-adapt", + "contractVersion": 1, + "implementation": { + "id": "provider.sentrux-builtin.compat", + "version": "1.0.0", + "toolchainDigests": [ + "a04226fab56751d014460e1596328e503b6a9afbf2efd5e84ab7b0790831149e", + "5aaf4f6e79dacf5b067efab1d07c9d2937000d97a22cf4c81ac9697edf0dc3d4", + "52644a812174988ede91d98ddfec63c6a91f8478277d7bf74c73f106dd0f776b" + ] + }, + "determinism": "external_nondeterministic", + "allowedEffects": ["repo_read", "local_write", "process_spawn"], + "dependencies": ["repo.snapshot"] + }, + "runtimeAdapter": "provider.sentrux-builtin.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": [ + "crates/code-intel-cli/src/builtin_provider_evidence.rs", + "crates/code-intel-cli/src/sentrux_adapter.rs", + "crates/code-intel-cli/src/admissibility.rs" + ] + }, + "artifactContract": ["orchestration/schemas/code-intel-structural-evidence-port.v1.schema.json", "orchestration/schemas/code-intel-sentrux-route-result.v1.schema.json", "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json"], + "extensionPoint": "The default provider executes real sentrux gate/check commands and normalizes their command-level observations through A04. Sentrux algorithms remain external, diagnosis policy stays in Hospital, and Invoke-SentruxAgentTool.ps1 is a legacy compatibility/rollback surface rather than the authoritative normal route." + }, + { + "id": "provider.session-adapt", + "stage": "verification", + "owner": "code-intel-pipeline", + "kind": "internal-adapter", + "required": false, + "entrypoint": "crates/code-intel-cli/src/session_evidence.rs", + "capabilities": [ + "mindwalk_trace_v1_translation", + "snapshot_binding", + "privacy_reduction", + "observability_grading", + "sentrux_path_join", + "advisory_review_signals" + ], + "commands": { + "adapt": "target/debug/code-intel.exe provider session-adapt --repo --trace [--hotspots ] [--out ]" + }, + "artifactContract": [ + "orchestration/schemas/code-intel-session-evidence.v1.schema.json" + ], + "extensionPoint": "Mindwalk extraction remains optional and replaceable. Code Intel owns path safety, privacy reduction, snapshot binding, structural joins, and advisory review semantics; no raw provider content gains gate or fact authority." + }, + { + "id": "provider.codenexus-adapt", + "stage": "localization", + "owner": "code-intel-pipeline", + "kind": "internal-adapter", + "required": false, + "entrypoint": "crates/code-intel-cli/src/providers.rs", + "capabilities": [ + "full_lite_provider_swap", + "provider_owned_storage_and_impact_semantics", + "current_snapshot_binding", + "provider_neutral_admissibility", + "provider_unavailable_diagnosis" + ], + "commands": { + "adapt": "target/debug/code-intel.exe provider codenexus-adapt --request --artifact-root --evaluated-at --max-age-seconds ", + "facade": "run-code-intel.ps1 -CodeNexusAdapterRequest -CodeNexusAdapterArtifactRoot -CodeNexusAdapterEvaluatedAt -CodeNexusAdapterMaxAgeSeconds " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-codenexus-port.v1.schema.json", + "orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json", + "orchestration/schemas/code-intel-evidence-provider-port.v1.schema.json", + "orchestration/schemas/code-intel-evidence-admissibility-result.v1.schema.json" + ], + "extensionPoint": "Full CodeNexus and explicit lite compatibility output share this adapter and A04 route. CodeNexus retains process, storage, retrieval, and impact semantics; absence remains provider-unavailable/unknown without fabricated facts." + }, + { + "id": "repository.survival-scan", + "stage": "orientation", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/src/survival_scan.rs", + "capabilities": [ + "provider_absence_survival", + "repository_identity_bootstrap", + "basic_rg_inventory", + "reduced_completeness_declaration", + "unknown_structural_verdict" + ], + "commands": { + "scan": "target/debug/code-intel.exe repository survival-scan --request --artifact-root ", + "facade": "run-code-intel.ps1 -SurvivalScanRequest -SurvivalScanArtifactRoot " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-repository-survival-scan-request.v1.schema.json", + "orchestration/schemas/code-intel-repository-survival-scan-result.v1.schema.json", + "orchestration/schemas/code-intel-repository-snapshot.v1.schema.json", + "orchestration/schemas/code-intel-codenexus-route-result.v1.schema.json" + ], + "extensionPoint": "B05 composes A01/A02/A03/A04 evidence only. It may add basic repository identity or inventory observations, but deeper structural intelligence remains provider-owned and must stay unknown while CodeNexus is unavailable." + }, + { + "id": "project.orientation", + "stage": "orientation", + "owner": "code-intel-pipeline", + "kind": "internal-atom", + "required": true, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "deterministic_project_orientation", + "purpose_unknown_preservation", + "claim_provenance", + "summary_compatible_projection" + ], + "commands": { + "capabilityExec": "target/debug/code-intel.exe capability exec project.orientation --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "project.orientation", + "contractVersion": 1, + "implementation": { + "id": "project.orientation.compat", + "version": "1.0.0", + "toolchainDigests": [ + "cdba6713cdb468882b8a04f226209c11b31e5f27d123197f5fc06c1ce1047458" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["local_write"], + "dependencies": ["repo.snapshot", "inventory.rg", "repository.survival-scan", "evidence.native-code"] + }, + "runtimeAdapter": "project.orientation.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": ["crates/code-intel-cli/src/project_orientation.rs"] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-project-orientation.v1.schema.json", + "project-orientation.json", + "project-orientation.md" + ], + "extensionPoint": "D01 composes verified evidence only. Purpose remains unknown without admitted purpose evidence; deeper architecture and semantic models remain outside this atom." + }, + { + "id": "understanding.quadrant", + "stage": "orientation", + "owner": "code-intel-pipeline", + "kind": "internal-atom", + "required": false, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "criticality_confidence_classification", + "visible_unknowns", + "stable_provenance_projection", + "method_card_read_only_consumer_boundary" + ], + "commands": { + "capabilityExec": "target/debug/code-intel.exe capability exec understanding.quadrant --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "understanding.quadrant", + "contractVersion": 1, + "implementation": { + "id": "understanding.quadrant.compat", + "version": "1.0.0", + "toolchainDigests": [ + "60fbb6e482158a08b3d4d21e1c0309d9b1b2b89be7cd4be39442a66339f2dfdf", + "7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["local_write"], + "dependencies": ["project.orientation"] + }, + "runtimeAdapter": "understanding.quadrant.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": [ + "crates/code-intel-cli/src/understanding_quadrant.rs", + "crates/code-intel-cli/src/capability_inventory.rs" + ] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-understanding-quadrant.v1.schema.json", + "understanding-quadrant.json" + ], + "extensionPoint": "D03 projects D01 claims into a deterministic criticality x confidence view. C01 Method Cards and C02 selection may consume the projection but cannot be supplied as classification inputs or rewrite its provenance, scores, or quadrant." + }, + { + "id": "project.orientation-benchmark", + "stage": "orientation", + "owner": "code-intel-pipeline", + "kind": "verification-atom", + "required": false, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "representative_orientation_corpus", + "cold_warm_wall_time_percentiles", + "field_unknown_provenance_quality_gates", + "cost_center_attribution" + ], + "commands": { + "benchmark": "target/debug/code-intel.exe benchmark orientation --out --repetitions <2..10>", + "capabilityExec": "target/debug/code-intel.exe capability exec project.orientation-benchmark --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "project.orientation-benchmark", + "contractVersion": 1, + "implementation": { + "id": "project.orientation-benchmark.compat", + "version": "1.0.0", + "toolchainDigests": [ + "f85db17bbf64ad5c61600bee6db92657640b31e5014a9f3d0d1fd39e1662eb92" + ] + }, + "determinism": "external_nondeterministic", + "allowedEffects": ["local_write"], + "dependencies": ["project.orientation"] + }, + "runtimeAdapter": "project.orientation-benchmark.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": ["crates/code-intel-cli/src/project_orientation_benchmark.rs"] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-project-orientation-benchmark-observations.v1.schema.json", + "orchestration/schemas/code-intel-project-orientation-benchmark.v1.schema.json", + "report.json", + "report.md" + ], + "extensionPoint": "D02 measures D01 without changing orientation semantics. A fast result without complete claim provenance is rejected; clean-machine repetition remains an independent verification condition." + }, + { + "id": "delivery.light-speed-measure", + "stage": "verification", + "owner": "code-intel-pipeline", + "kind": "measurement-atom", + "required": false, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "committed_run_value_stream_measurement", + "deterministic_queue_and_critical_path_attribution", + "mandatory_verification_protection", + "baseline_current_delta_without_schedule_promise" + ], + "commands": { + "capabilityExec": "target/debug/code-intel.exe capability exec delivery.light-speed-measure --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "delivery.light-speed-measure", + "contractVersion": 1, + "implementation": { + "id": "delivery.light-speed-measure.compat", + "version": "1.0.0", + "toolchainDigests": [ + "35befeb27f87bbdad5b5d081542208dc2aecf25af707f145fcbf58aa18f16444", + "7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9", + "e14f72c8191157263ee6964eb00250bc8235fe6cee715aa2b9cfa3362e47b1a8", + "b118ba78ff3ef4a189525c642184cbd320a268d2885681ab153544db554c5965", + "e3072b6b01a4f692c2ac75c7c4995747f9a0fd1ce4f32e1ab1599f348661f570", + "40c532ed3aa52144bdc8bb4422e04b23c54196d352ac9cb0f3e93a5a059af120" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["local_write"], + "dependencies": ["run.commit", "project.orientation-benchmark", "method.catalog"] + }, + "runtimeAdapter": "delivery.light-speed-measure.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": [ + "crates/code-intel-cli/src/delivery_light_speed.rs", + "crates/code-intel-cli/src/capability_inventory.rs", + "crates/code-intel-cli/src/artifact_ref.rs", + "orchestration/methods/catalog.v1.json", + "orchestration/methods/cards/critical-path-pert.v1.json", + "orchestration/methods/cards/value-stream-queue-delay.v1.json" + ] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-run-timing-events.v1.schema.json", + "orchestration/schemas/code-intel-run-commit.v1.schema.json", + "orchestration/schemas/code-intel-run-manifest.v1.schema.json", + "orchestration/schemas/code-intel-method-card.v1.schema.json", + "orchestration/methods/catalog.v1.json", + "orchestration/methods/cards/critical-path-pert.v1.json", + "orchestration/methods/cards/value-stream-queue-delay.v1.json", + "orchestration/schemas/code-intel-delivery-light-speed.v1.schema.json", + "light-speed-report.json", + "light-speed-report.md" + ], + "extensionPoint": "D04 derives observed baseline/current/delta value-stream and predecessor-closed critical-path measurements from A07-committed local opt-in events. It cannot create schedule or staffing commitments, and mandatory verification is protected from waste attribution." + }, + { + "id": "compatibility.retirement-gate", + "stage": "verification", + "owner": "code-intel-pipeline", + "kind": "approval-gate", + "required": false, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "single_branch_retirement_approval", + "content_bound_independent_evidence", + "cyclic_replacement_rejection", + "gain_ledger_projection_without_deletion_authority" + ], + "commands": { + "capabilityExec": "target/debug/code-intel.exe capability exec compatibility.retirement-gate --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "compatibility.retirement-gate", + "contractVersion": 1, + "implementation": { + "id": "compatibility.retirement-gate.compat", + "version": "1.0.0", + "toolchainDigests": [ + "864c14aba381c0ecb8262c91de0866f0415e4b88e815de2f564f1beddd3207d3", + "e14f72c8191157263ee6964eb00250bc8235fe6cee715aa2b9cfa3362e47b1a8", + "7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["local_write"], + "dependencies": ["run.commit", "artifact.index-committed-only", "provider.codenexus-adapt", "advisory.workflow-recommend", "governance.ponytail-gate", "project.orientation-benchmark"] + }, + "runtimeAdapter": "compatibility.retirement-gate.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": [ + "crates/code-intel-cli/src/compatibility_retirement_gate.rs", + "crates/code-intel-cli/src/artifact_ref.rs", + "crates/code-intel-cli/src/capability_inventory.rs" + ] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-compatibility-retirement-manifest.v1.schema.json", + "orchestration/schemas/code-intel-compatibility-retirement-evidence.v1.schema.json", + "orchestration/schemas/code-intel-compatibility-retirement-decision.v1.schema.json", + "compatibility-retirement-decision.json" + ], + "extensionPoint": "E00 approves one content-bound compatibility retirement subject and projects its Gain Ledger state. It cannot delete, reroute, or disable the legacy branch; those effects require a later branch-specific ticket." + }, + { + "id": "compatibility.retirement-ticket-template", + "stage": "behavior_specification", + "owner": "code-intel-pipeline", + "kind": "planning-template", + "required": false, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "single_branch_retirement_ticket", + "e00_content_bound_evidence_projection", + "closed_completeness_lint", + "no_approval_or_deletion_authority" + ], + "commands": { + "lint": "target/debug/code-intel.exe compatibility retirement-ticket lint --ticket --evaluated-at ", + "capabilityExec": "target/debug/code-intel.exe capability exec compatibility.retirement-ticket-template --request --out --artifact-root " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "compatibility.retirement-ticket-template", + "contractVersion": 1, + "implementation": { + "id": "compatibility.retirement-ticket-template.compat", + "version": "1.0.0", + "toolchainDigests": [ + "1b3875caa63b7b8ebaac5dfc38c2e8b858920127377f4b7da54c4ce5b585b9f3", + "e14f72c8191157263ee6964eb00250bc8235fe6cee715aa2b9cfa3362e47b1a8", + "7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["local_write"], + "dependencies": ["compatibility.retirement-gate"] + }, + "runtimeAdapter": "compatibility.retirement-ticket-template.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": [ + "crates/code-intel-cli/src/compatibility_retirement_ticket.rs", + "crates/code-intel-cli/src/artifact_ref.rs", + "crates/code-intel-cli/src/capability_inventory.rs" + ] + }, + "artifactContract": [ + "orchestration/schemas/code-intel-compatibility-retirement-ticket-template.v1.schema.json", + "orchestration/schemas/code-intel-compatibility-retirement-deletion-diff.v1.schema.json", + "compatibility-retirement-ticket.json" + ], + "extensionPoint": "E01 generates and validates one draft deletion ticket for one E00-approved legacy branch/call path. It cannot approve the subject, delete code, reroute calls, or execute rollback." + }, + { + "id": "compatibility.facade-finalize", + "stage": "verification", + "owner": "code-intel-pipeline", + "kind": "verification-audit", + "required": false, + "entrypoint": "Invoke-CompatibilityFacadeFinalize.ps1", + "capabilities": [ + "frozen_retirement_dependency_audit", + "retained_powershell_registry_owner_expiry_audit", + "public_mode_node_contract_audit", + "blocked_without_independent_approval" + ], + "commands": { + "audit": "pwsh -NoProfile -File Invoke-CompatibilityFacadeFinalize.ps1 -EvaluatedAt -OutFile -Json" + }, + "artifactContract": [ + "orchestration/facade-finalize-policy.v1.json", + "orchestration/schemas/code-intel-compatibility-facade-finalize.v1.schema.json", + "compatibility-facade-finalize.json" + ], + "effects": ["local_write"], + "extensionPoint": "E06 audits the frozen E02-E10 retirement packets, every retained PowerShell surface, A00 parity and doctor/lite/normal/full node contracts. It cannot approve, delete, reroute, or sign independent verification." + }, + { + "id": "advisory.workflow-recommend", + "stage": "behavior_specification", + "owner": "code-intel-pipeline", + "kind": "advisory-atom", + "required": false, + "entrypoint": "OpenSpec-Detector.ps1", + "capabilities": [ + "evidence_backed_workflow_proposal", + "candidate_comparison", + "zero_effect_recommendation", + "authority_separated_adoption" + ], + "commands": { + "atom": "OpenSpec-Detector.ps1 -RepoPath [-Auto]", + "facade": "Invoke-WorkflowRecommendation.ps1 -RepoPath [-Auto]", + "capabilityExec": "target/debug/code-intel.exe capability exec advisory.workflow-recommend --request --out ", + "pipeline": "run-code-intel.ps1 -Repo [-SkipOpenSpec] [-AutoOpenSpec]" + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "advisory.workflow-recommend", + "contractVersion": 1, + "implementation": { + "id": "advisory.workflow-recommend.compat", + "version": "1.0.0", + "toolchainDigests": [ + "03d9cbed70d83c59f7d9540fccc606ce0b2723135efd2c5e32943d367008a199", + "748c8b087c9d1a68f9aa5711cda200204ac0d05845058a1ee50058b161582de9" + ] + }, + "determinism": "deterministic", + "allowedEffects": [], + "dependencies": ["repo.snapshot"] + }, + "runtimeAdapter": "advisory.workflow-recommend.compat", + "artifactContract": [ + "orchestration/schemas/code-intel-capability-envelope.v1.schema.json", + "orchestration/schemas/code-intel-advisory-workflow-recommendation.v1.schema.json", + "orchestration/schemas/code-intel-authority-transition.v1.schema.json" + ], + "effects": [], + "extensionPoint": "OpenSpec OPSX, spec-kit, gstack, and matt-flow remain candidates. This atom emits proposals only; A05 authority is required before adoption, initialization, or execution." + }, + { + "id": "decision.request-response-port", + "stage": "behavior_specification", + "owner": "code-intel-pipeline", + "kind": "internal-port", + "required": true, + "entrypoint": "crates/code-intel-cli/src/decision_port.rs", + "capabilities": [ + "gap_bound_single_question", + "correlated_choice_or_free_form_response", + "actor_authority_and_evidence_validation", + "replaceable_in_memory_file_plain_text_native_adapters", + "branch_local_timeout_and_cancellation" + ], + "commands": { + "native": "target/debug/code-intel.exe decision request-response --request [--response |--cancel ] --now --branch ..." + }, + "artifactContract": [ + "orchestration/schemas/code-intel-decision-gap.v1.schema.json", + "orchestration/schemas/code-intel-decision-request.v1.schema.json", + "orchestration/schemas/code-intel-decision-response.v1.schema.json", + "orchestration/schemas/code-intel-decision-cancellation.v1.schema.json", + "orchestration/schemas/code-intel-decision-exchange-result.v1.schema.json" + ], + "extensionPoint": "C06 owns correlation and fail-closed request/response validation only. UI, chat, tmux, brokers, authority adoption, and durable C07 records remain outside the port." + }, + { + "id": "decision.record", + "stage": "behavior_specification", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/src/decision_record.rs", + "capabilities": [ + "durable_gap_resolution", + "gap_request_response_evidence_snapshot_binding", + "authority_event_replay_prevention", + "unchanged_evidence_replay_without_question", + "changed_or_stale_evidence_reopen", + "invalid_record_diagnosis" + ], + "commands": { + "record": "target/debug/code-intel.exe decision record --resolution --store ", + "replay": "target/debug/code-intel.exe decision replay --query --store " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-decision-gap.v1.schema.json", + "orchestration/schemas/code-intel-decision-request.v1.schema.json", + "orchestration/schemas/code-intel-decision-response.v1.schema.json", + "orchestration/schemas/code-intel-authority-transition.v1.schema.json", + "orchestration/schemas/code-intel-decision-record.v1.schema.json" + ], + "effects": ["write_decision_record"], + "extensionPoint": "C07 owns additive durable resolution and replay only. C06 remains the request transport; changed snapshots, changed evidence, and expired evidence reopen the gap." + }, + { + "id": "run.commit", + "stage": "publication", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/src/run_commit.rs", + "capabilities": [ + "a06_owned_restaging", + "a09_terminal_manifest_validation", + "same_volume_atomic_promotion", + "completion_marker_last" + ], + "commands": { + "run": "target/debug/code-intel.exe run commit --source-root --authority-root --manifest-ref --final-name ", + "facade": "run-code-intel.ps1 -RunCommitSourceRoot -RunCommitAuthorityRoot -RunCommitManifestRef -RunCommitFinalName " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-staged-artifact-set.v1.schema.json", + "orchestration/schemas/code-intel-run-manifest.v1.schema.json", + "orchestration/schemas/code-intel-run-commit.v1.schema.json" + ], + "extensionPoint": "A07 consumes a fully validated A09 manifest and A06-restaged refs. It owns publication only; A08 index admission remains separate." + }, + { + "id": "run.dag-coordinate", + "stage": "preflight", + "owner": "code-intel-pipeline", + "kind": "internal-primitive", + "required": true, + "entrypoint": "crates/code-intel-cli/src/dag_run.rs", + "capabilities": ["deterministic_dag", "bounded_concurrency", "resumable_run_state", "a03_verified_seeded_diagnosis"], + "commands": { + "run": "target/debug/code-intel.exe run dag-coordinate --repo --out [--doctor-tool-path-prefix ]", + "diagnosis": "target/debug/code-intel.exe run dag-coordinate --repo --out --diagnosis-inputs --seed-artifact-root ", + "facade": "run-code-intel.ps1 -RepoPath -ArtifactRoot -DagCoordinate" + }, + "artifactContract": [ + "orchestration/schemas/code-intel-run-dag.v1.schema.json", + "orchestration/schemas/code-intel-run-state.v1.schema.json", + "orchestration/schemas/code-intel-run-manifest.v1.schema.json" + ], + "extensionPoint": "Capability executors remain behind A01 envelopes; the coordinator has no provider or tool authority. Seeded diagnosis accepts only A03-verified, current-snapshot A04 admission Artifact Refs." + }, { "id": "inventory.rg", "stage": "inventory", @@ -166,30 +1130,92 @@ "text_inventory" ], "commands": { - "run": "run-code-intel.ps1 -RepoPath -Mode " + "run": "run-code-intel.ps1 -RepoPath -Mode ", + "capabilityExec": "target/debug/code-intel.exe capability exec inventory.rg --request --out " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "inventory.rg", + "contractVersion": 1, + "implementation": { + "id": "inventory.rg.compat", + "version": "1.0.0", + "toolchainDigests": [ + "43ced9ef578e6484423468e059c93ef0bc5eeeb35d23271451b2d8f1a16f9bb6" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["repo_read", "local_write"], + "dependencies": [] }, + "runtimeAdapter": "inventory.rg.compat", "artifactContract": [ "files.txt", "report.json.steps[rg file inventory]" ], "extensionPoint": "Add inventory engines behind this adapter; do not call new inventory CLIs directly from agents." }, + { + "id": "evidence.native-code", + "stage": "localization", + "owner": "code-intel-pipeline", + "kind": "internal-atom", + "required": true, + "entrypoint": "target/debug/code-intel.exe", + "capabilities": [ + "files", + "heuristic_symbols", + "file_chunks", + "symbol_containment", + "heuristic_imports", + "scorecard", + "agent_code_slice" + ], + "commands": { + "capabilityExec": "target/debug/code-intel.exe capability exec evidence.native-code --request --out " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "evidence.native-code", + "contractVersion": 1, + "implementation": { + "id": "evidence.native-code.compat", + "version": "1.0.0", + "toolchainDigests": [ + "c47c86ee56ba5bb9a16cfd5c8742b70f0e9a64d6da1f83289a0e65818defffa5" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["repo_read", "local_write"], + "dependencies": ["repo.snapshot", "inventory.rg"] + }, + "runtimeAdapter": "evidence.native-code.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": ["crates/code-intel-cli/src/native_code_evidence.rs"] + }, + "artifactContract": [ + "orchestration/schemas/code-evidence-native-artifacts.v1.schema.json" + ], + "extensionPoint": "Built-in deterministic baseline only. Parser coverage is heuristic and relationship/call-graph precision remains explicit unknown; external cocoindex and specialized semantic graphs stay separate adapters." + }, { "id": "memory.repowise", "stage": "semantic_memory", "owner": "code-intel-pipeline", "kind": "internalizing-adapter", - "required": true, + "required": false, "entrypoint": "Invoke-ScopedRepowise.ps1", "capabilities": [ "status", "index", "scoped_shadow_workspace", + "authority_gated_shadow_worktree_mutation", "docs" ], "commands": { - "status": "Invoke-ScopedRepowise.ps1 -RepoPath -ScopePaths -RootFiles ", - "docs": "run-code-intel.ps1 -RepoPath -Mode normal -RepowiseDocs" + "status": "Invoke-ScopedRepowise.ps1 -RepoPath -ScopePaths -RootFiles -AllowShadowWorktreeMutation", + "docs": "run-code-intel.ps1 -RepoPath -Mode normal -RepowiseDocs -AllowRepowiseShadowMutation" }, "artifactContract": [ ".repowise/state.json", @@ -197,7 +1223,7 @@ "report.json.steps[repowise*]", "hospital-report.json.report_quality.dimensions[chart]" ], - "extensionPoint": "Repowise CLI and Python imports must be hidden behind this adapter until the implementation is fully internalized." + "extensionPoint": "Repowise remains included in the default semantic-memory plan when available, but the integration is optional and failures are non-blocking for release. Repowise CLI and Python imports are hidden behind this adapter. Git worktree add/clean/sparse-checkout/reset mutations are limited to the adapter-owned shadow worktree and require explicit authority at both pipeline and adapter entrypoints." }, { "id": "graph.code-intel-understand", @@ -209,7 +1235,8 @@ "capabilities": [ "graph_state", "freshness", - "understand_compatible_artifact" + "understand_compatible_artifact", + "provider_graph_adapter_source" ], "commands": { "refresh": "target/debug/code-intel.exe graph --repo --language zh --write --json", @@ -219,7 +1246,7 @@ ".understand-anything/knowledge-graph.json", "report.json.steps[understand graph]" ], - "extensionPoint": "Graph providers must emit the Understand-compatible artifact contract through this internal Rust command before runner scripts depend on them." + "extensionPoint": "This command is the preferred native producer behind provider.graph-adapt. Its direct artifact is not admissible current anatomy until the adapter binds it to the requested snapshot and A04 accepts it." }, { "id": "graph.understand-external", @@ -230,7 +1257,9 @@ "entrypoint": "/understand", "capabilities": [ "manual_refresh_command", - "rich_agent_graph" + "rich_agent_graph", + "explicit_fallback_only", + "legacy_rollback_only" ], "commands": { "refresh": "/understand --language zh", @@ -239,7 +1268,7 @@ "artifactContract": [ ".understand-anything/knowledge-graph.json" ], - "extensionPoint": "Use only when the Rust graph provider fails or a richer external Understand Anything pass is explicitly requested." + "extensionPoint": "Use only as an explicitly selected fallback or legacy rollback. Output must enter provider.graph-adapt with a named fallback identity and pass A04; direct external artifacts never become current anatomy." }, { "id": "structure.sentrux", @@ -260,11 +1289,17 @@ "evolution" ], "commands": { - "rustTarget": "target/debug/code-intel.exe sentrux ", "rustDsm": "target/debug/code-intel.exe sentrux dsm ", - "scan": "Invoke-SentruxAgentTool.ps1 scan ", - "sessionStart": "Invoke-SentruxAgentTool.ps1 session_start ", - "sessionEnd": "Invoke-SentruxAgentTool.ps1 session_end " + "rustScan": "target/debug/code-intel.exe sentrux scan ", + "rustHealth": "target/debug/code-intel.exe sentrux health ", + "rustSessionStart": "target/debug/code-intel.exe sentrux session_start ", + "rustSessionEnd": "target/debug/code-intel.exe sentrux session_end ", + "rustCheckRules": "target/debug/code-intel.exe sentrux check_rules ", + "rustTestGaps": "target/debug/code-intel.exe sentrux test_gaps ", + "rustWhatIf": "target/debug/code-intel.exe sentrux what_if ", + "shimScan": "Invoke-SentruxAgentTool.ps1 scan ", + "shimSessionStart": "Invoke-SentruxAgentTool.ps1 session_start ", + "shimSessionEnd": "Invoke-SentruxAgentTool.ps1 session_end " }, "artifactContract": [ "sentrux-dsm.json", @@ -273,14 +1308,14 @@ "sentrux-evolution.json", "sentrux-what-if.json" ], - "extensionPoint": "External sentrux.exe is an accelerator; repo-owned lite core remains the normal-operation fallback." + "extensionPoint": "External sentrux.exe is an accelerator and Invoke-SentruxAgentTool.ps1 is an implementation/rollback behind provider.sentrux-adapt; neither direct output is diagnosis authority before A04." }, { "id": "localization.codenexus-lite", "stage": "localization", "owner": "code-intel-pipeline", "kind": "compatibility-adapter", - "required": true, + "required": false, "entrypoint": "Invoke-CodeNexusLite.ps1", "capabilities": [ "hotspot_context", @@ -288,20 +1323,20 @@ "reference_hits" ], "commands": { - "compat": "Invoke-CodeNexusLite.ps1 -RepoPath -ArtifactDir " + "compat": "pwsh -NoProfile -File \"$env:CODE_INTEL_HOME\\Invoke-CodeNexusLite.ps1\" -RepoPath ''" }, "artifactContract": [ "codenexus-context.json" ], - "extensionPoint": "Full CodeNexus backends must adapt to codenexus-context.json before replacing the compatibility adapter." + "extensionPoint": "This adapter is non-blocking and replaceable; Survival Scanner preserves localization when it is unavailable. Full CodeNexus backends must adapt to codenexus-context.json before replacing it." }, { "id": "diagnosis.hospital", "stage": "diagnosis", "owner": "code-intel-pipeline", - "kind": "internal-module", + "kind": "internal-atom", "required": true, - "entrypoint": "run-code-intel.ps1", + "entrypoint": "target/debug/code-intel.exe", "capabilities": [ "triage", "protocol", @@ -309,62 +1344,228 @@ "surgery_plan" ], "commands": { - "run": "run-code-intel.ps1 -RepoPath -Mode " + "capabilityExec": "target/debug/code-intel.exe capability exec diagnosis.hospital --request --out --artifact-root ", + "compatibility": "run-code-intel.ps1 -RepoPath -Mode " + }, + "capabilityDeclaration": { + "schema": "code-intel-capability-declaration.v1", + "id": "diagnosis.hospital", + "contractVersion": 1, + "implementation": { + "id": "diagnosis.hospital.compat", + "version": "1.0.0", + "toolchainDigests": [ + "391adefd7648ea7979585b22687a49fdaa7dcbf26f685ac4ee433861b4347c31" + ] + }, + "determinism": "deterministic", + "allowedEffects": ["local_write"], + "dependencies": [ + "provider.repowise-adapt", + "provider.graph-adapt", + "provider.sentrux-adapt", + "repository.survival-scan", + "evidence.native-code" + ] + }, + "runtimeAdapter": "diagnosis.hospital.compat", + "toolchainDigestEvidence": { + "algorithm": "sha256", + "inputs": ["crates/code-intel-cli/src/hospital_diagnosis.rs"] }, "artifactContract": [ + "orchestration/schemas/code-intel-hospital.v1.schema.json", "hospital.md", "hospital-report.json", "surgery-plan.md", "surgery-plan.json" ], - "extensionPoint": "New diagnosis modes attach here and preserve hospital-report.json stable fields." + "extensionPoint": "B09 consumes only A04 admission Artifact Refs. Legacy PowerShell remains a bounded compatibility adapter until E08; Markdown and surgery-plan Markdown are rebuildable views and never diagnosis authority." }, { - "id": "spec.greenfield", - "stage": "behavior_specification", + "id": "provider.runtime-inventory", + "stage": "preflight", + "owner": "code-intel-pipeline", + "kind": "internal-observer", + "required": false, + "entrypoint": "Invoke-ProviderRuntimeInventory.ps1", + "capabilities": [ + "secret_free_channel_discovery", + "bounded_executable_verification", + "configuration_broker_presence" + ], + "commands": { + "inventory": "pwsh -NoProfile -File Invoke-ProviderRuntimeInventory.ps1 -Json > " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-model-channel-inventory-request.v1.schema.json", + "orchestration/schemas/code-intel-model-channel-inventory-result.v1.schema.json" + ], + "extensionPoint": "Inventory records only non-secret observations. Authentication values, endpoint URLs, prompts, tokens, and credential-store contents are outside this integration contract." + }, + { + "id": "provider.route-model-work", + "stage": "semantic_memory", + "owner": "code-intel-pipeline", + "kind": "internal-policy-atom", + "required": false, + "entrypoint": "crates/code-intel-cli/src/model_channels.rs", + "capabilities": [ + "explicit_consumption_authorization", + "external_egress_gate", + "paid_spend_gate", + "pinned_adapter_routing", + "deterministic_degradation" + ], + "commands": { + "route": "target/debug/code-intel.exe model route --request --out ", + "validateInventory": "target/debug/code-intel.exe model inventory-validate --request ", + "synthesizeAndInvoke": "run-code-intel.ps1 -ModelInventoryResult -ModelRoutingResult -ModelPromptFile -ModelExecutableHandle -ModelAdapterArtifactRoot " + }, + "artifactContract": [ + "orchestration/schemas/code-intel-model-routing-request.v1.schema.json", + "orchestration/schemas/code-intel-model-routing-result.v1.schema.json", + "orchestration/schemas/code-intel-model-adapter-request.v1.schema.json", + "orchestration/schemas/code-intel-model-adapter-request.v2.schema.json", + "orchestration/schemas/code-intel-model-executable-handle.v1.schema.json", + "orchestration/schemas/code-intel-model-adapter-result.v1.schema.json", + "orchestration/schemas/code-intel-model-assistance-dossier.v1.schema.json" + ], + "extensionPoint": "Routing is deterministic and side-effect free. A discovered or authenticated channel never implies consumption, external-egress, or paid-spend authorization; pinned adapters do not fall back unless policy explicitly allows it." + }, + { + "id": "provider.file-boundary", + "stage": "semantic_memory", + "owner": "code-intel-pipeline", + "kind": "internal-observer", + "required": false, + "entrypoint": "crates/code-intel-cli/src/file_boundary.rs", + "capabilities": ["per_file_role", "stable_rule_ids", "snapshot_and_freshness_binding", "fail_closed_path_resolution"], + "commands": { "resolve": "target/debug/code-intel.exe provider file-boundary --request --out " }, + "artifactContract": [ + "orchestration/schemas/code-intel-file-boundary-document.v1.schema.json", + "orchestration/schemas/code-intel-file-boundary-request.v1.schema.json", + "orchestration/schemas/code-intel-file-boundary-result.v1.schema.json" + ], + "extensionPoint": "Pipeline-owned, optional, local read-only evidence. AIGX may be an upstream translator but is never a runtime dependency or trust bypass." + }, + { + "id": "provider.runtime-ci-evidence", + "stage": "diagnosis", + "owner": "code-intel-pipeline", + "kind": "internal-observer", + "required": false, + "entrypoint": "crates/code-intel-cli/src/runtime_ci_evidence.rs", + "capabilities": ["provider_neutral_runtime_evidence", "artifact_digest_binding", "snapshot_and_freshness_binding", "hospital_pet_health_summary"], + "commands": { "ingest": "target/debug/code-intel.exe provider runtime-ci-evidence --artifact-root --request --out " }, + "artifactContract": [ + "orchestration/schemas/code-intel-runtime-ci-ingest-request.v1.schema.json", + "orchestration/schemas/code-intel-runtime-ci-observation.v1.schema.json", + "orchestration/schemas/code-intel-runtime-ci-summary.v1.schema.json" + ], + "extensionPoint": "Explicit local JSON only. Missing, partial, stale, snapshot-mismatched, or unobserved evidence remains unknown and cannot become green." + }, + { + "id": "verification.code-intel-acceptance", + "stage": "verification", "owner": "code-intel-pipeline", - "kind": "provider-contract", + "kind": "internal-policy-adapter", "required": false, - "entrypoint": "Invoke-GreenfieldSpecExtraction.ps1", + "entrypoint": "Invoke-CodeIntelAcceptance.ps1", "capabilities": [ - "behavioral_specification", - "test_vectors", - "acceptance_criteria", - "provenance", - "clean_room_handoff" + "agent_targeted_plus_fast_acceptance", + "land_fast_acceptance", + "promote_full_acceptance", + "language_neutral_result_status" ], "commands": { - "plan": "Invoke-GreenfieldSpecExtraction.ps1 -RepoPath -Json", - "analyze": "Invoke-GreenfieldSpecExtraction.ps1 -RepoPath -Analyze -Json" + "agent": "pwsh -NoProfile -File Invoke-CodeIntelAcceptance.ps1 -Stage agent -TargetCheckJson '' -Json", + "land": "pwsh -NoProfile -File Invoke-CodeIntelAcceptance.ps1 -Stage land -Json", + "promote": "pwsh -NoProfile -File Invoke-CodeIntelAcceptance.ps1 -Stage promote -Json" }, "artifactContract": [ - "greenfield-manifest.json", - "greenfield-plan.md", - "greenfield-workspace/output/specs", - "greenfield-workspace/output/test-vectors", - "greenfield-workspace/output/validation", - "greenfield-workspace/provenance" + "orchestration/code-intel-acceptance-policy.v1.json", + "orchestration/schemas/code-intel-acceptance-result.v1.schema.json" ], - "extensionPoint": "Greenfield is a Claude Code plugin boundary. Code Intel owns the adapter and artifact manifest; the external plugin remains optional until its behavior is internalized or a non-interactive provider contract is available." + "effects": ["repo_read", "process_execute"], + "extensionPoint": "Language adapters supply argv-only targeted checks at agent stage. Queue landing consumes land; production promotion remains a human decision and consumes full acceptance without granting promotion authority." }, { - "id": "artifact.index", + "id": "intelligence.compete-project-score", + "stage": "orientation", + "owner": "code-intel-pipeline", + "kind": "optional-external-provider-adapter", + "required": false, + "entrypoint": "Invoke-CompeteProjectScore.ps1", + "capabilities": [ + "competitive_intelligence_task_preparation", + "upstream_compete_score_normalization", + "orca_agent_handoff" + ], + "commands": { + "prepare": "pwsh -NoProfile -File Invoke-CompeteProjectScore.ps1 -Action prepare -RepoPath '' -ArtifactRoot '' -CompeteRoot ''", + "score": "pwsh -NoProfile -File Invoke-CompeteProjectScore.ps1 -Action score -RepoPath '' -ArtifactRoot '' -CompeteRoot '' -CompeteDataPath ''" + }, + "artifactContract": [ + "competitive-intelligence-request.json", + "competitive-intelligence-prompt.md", + "competitive-score.json" + ], + "effects": ["repo_read", "process_execute", "local_write"], + "extensionPoint": "Optional market/product intelligence only. Reuse upstream compete scoring, keep network Agent execution explicit, and never feed this advisory score into structural gates or hospital discharge decisions." + }, + { + "id": "delivery.multi-agent-merge-queue", + "stage": "landing_coordination", + "owner": "code-intel-pipeline", + "kind": "optional-external-provider-adapter", + "required": false, + "entrypoint": "Invoke-MultiAgentMergeQueue.ps1", + "capabilities": [ + "repository_local_queue_discovery", + "acceptance_bound_fifo_landing", + "direct_push_protection_observation", + "human_production_promotion_boundary" + ], + "commands": { + "status": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action status -RepoPath '' -Json", + "validate": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action validate -RepoPath '' -Json", + "land": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action land -RepoPath '' -AllowRepositoryMutation -AllowNetworkPush", + "reconcile": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action reconcile -RepoPath ''", + "history": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action history -RepoPath ''" + }, + "artifactContract": [ + "orchestration/multi-agent-merge-queue-policy.v1.json", + "orchestration/schemas/code-intel-multi-agent-merge-queue-status.v1.schema.json" + ], + "effects": ["repo_read", "process_execute", "repo_mutation", "network"], + "extensionPoint": "Optional one-machine landing provider only. Project acceptance remains authoritative; the adapter never installs, initializes, promotes, prunes, previews, deletes worktrees, or enables emergency push." + }, + { + "id": "artifact.index-committed-only", "stage": "artifact_index", "owner": "code-intel-pipeline", - "kind": "internal-script", + "kind": "internal-adapter", "required": true, - "entrypoint": "update-code-intel-index.ps1", + "entrypoint": "crates/code-intel-cli/src/artifact_index.rs", "capabilities": [ - "artifact_discovery", - "latest_run_index" + "a07_marker_admission", + "manifest_artifact_ref_revalidation", + "latest_committed_run_index", + "rebuild_incremental_equivalence" ], "commands": { - "refresh": "update-code-intel-index.ps1" + "rebuild": "target/debug/code-intel.exe artifact index --artifact-root --output --operation rebuild", + "incremental": "target/debug/code-intel.exe artifact index --artifact-root --output --operation incremental --existing ", + "facade": "update-code-intel-index.ps1 -ArtifactRoot -OutputPath ", + "legacyCompatibility": "update-code-intel-index.ps1 -ArtifactRoot -OutputPath -LegacyCompatibilityMode" }, "artifactContract": [ - "artifact-index.json" + "orchestration/schemas/code-intel-run-commit.v1.schema.json", + "orchestration/schemas/code-intel-run-manifest.v1.schema.json", + "orchestration/schemas/code-intel-artifact-index.v1.schema.json" ], - "extensionPoint": "New artifact stores register here; readers keep using the index contract." + "extensionPoint": "A08 is a rebuildable materialized view and never run authority. Normal admission requires an A07 marker and revalidates its manifest and Artifact Refs; legacy traversal is explicit compatibility only." } ] } diff --git a/orchestration/internalization/agent-loops.json b/orchestration/internalization/agent-loops.json new file mode 100644 index 0000000..d5543e8 --- /dev/null +++ b/orchestration/internalization/agent-loops.json @@ -0,0 +1,22 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.agent-loops-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "awesome-agent-loops loop-pattern design reference", "kind": "design_reference", "source": { "uri": "unverified-upstream:awesome-agent-loops; local-reference=docs/agent-goal-intake.md", "revision": "unverified-upstream; local-doc-sha256:fe72627e85e99bf6bb4d4fbfd06d112a02b4229af1ceaf996631b2dbbb9bdc31" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until canonical upstream URI, exact revision, and license text are verified", "retain attribution and distinguish the local three-pattern vocabulary from upstream execution proof"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned intake vocabulary for goal-until-condition, interval loop, and recurring schedule patterns", "no awesome-agent-loops runtime dependency, loop execution, scheduling authority, scanner state mutation, or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-map:agent-loops", "local:agent-goal-intake:three-loop-patterns", "gap:agent-loops:upstream-revision", "gap:agent-loops:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:agent-goal-intake:outside-scanner", "local:adr-0002:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:trace:agent-loops:goal", "local:trace:agent-loops:loop", "local:trace:agent-loops:schedule", "gap:agent-loops:upstream-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "locally documented loop-pattern choices", "value": 3, "unit": "patterns" }, "cost": { "metric": "pipeline-owned loop semantic trace entries", "value": 3, "unit": "entries" }, "benefitEvidence": { "evidenceIds": ["local:agent-goal-intake:three-loop-patterns"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:agent-loops"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:agent-loops:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:agent-loops:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify canonical catalog source, exact revision, license, and upstream semantics for goal, loop, and schedule; do not infer execution compatibility from local vocabulary", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:agent-loops:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "docs/agent-goal-intake.md", "description": "Pipeline-owned three-pattern intake vocabulary with no loop runtime", "evidenceIds": ["local:agent-goal-intake:three-loop-patterns", "local:agent-goal-intake:outside-scanner"] } ], + "rollback": { "strategy": "remove the awesome-agent-loops attribution and record while retaining the internal loop vocabulary and scanner contracts", "evidence": { "evidenceIds": ["local:adr-0002:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retire the catalog reference independently or replace it with a pinned licensed pattern source", "replacementCriteria": ["replacement has verified provenance and license", "three local pattern meanings remain independently tested", "removal changes no scanner contract, state, or execution authority"], "evidence": { "evidenceIds": ["local:adr-0002:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["catalog revision or license remains unverifiable", "pattern selection shows no measured utility", "reference introduces loop execution or scanner coupling"], "evidence": { "evidenceIds": ["local:record:agent-loops"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:agent-loops", "gap:agent-loops:upstream-revision", "gap:agent-loops:license", "gap:agent-loops:upstream-conformance"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/c03-r05-r12-measurements.json b/orchestration/internalization/c03-r05-r12-measurements.json new file mode 100644 index 0000000..1eb2281 --- /dev/null +++ b/orchestration/internalization/c03-r05-r12-measurements.json @@ -0,0 +1,111 @@ +{ + "schema": "code-intel-c03-internalization-measurements.v1", + "measuredAt": "2026-07-14T03:41:00+08:00", + "host": "windows-x86_64-audited-workspace", + "observations": { + "r05Repomix": { + "commandAvailable": false, + "npmRegistryMetadataEntries": 3, + "npmPackageTarballEntries": 3, + "npmInstalledOrExtractedExecutableEntries": 0, + "cacheAuditDefinition": "npm cache contains three registry metadata entries and three retrievable Repomix package tarballs, but no installed/extracted executable tree and no verified runnable Repomix command.", + "productionCallSitesAfterReview": 0, + "productionRegistryStatus": "deleted", + "reason": "Cached tarball payload bytes do not establish an installed runnable command or pinned production conformance; B07 reviewed deletion remains valid." + }, + "r06NativeCodeEvidence": { + "command": "cargo test -p code-intel --test native_code_evidence --quiet", + "testsPassed": 4, + "fixtureFiles": 2, + "producedArtifactRefs": 8, + "normalizedLegacyParityArtifacts": 6, + "parserKind": "line-heuristic", + "unsupportedLanguageRelationshipPrecision": "unknown", + "suiteElapsedMs": 27110, + "sourceSha256": "c47c86ee56ba5bb9a16cfd5c8742b70f0e9a64d6da1f83289a0e65818defffa5", + "testSha256": "08c5d7f3bc9b5cc65fa8dcb9ebbc9eb9d7359b56c070ad9e4516254d09bd7dcb", + "labeledCorpus": { + "fixture": "orchestration/internalization/fixtures/r06-native-labeled-corpus.json", + "samples": 12, + "supportedHeuristicSamples": 10, + "unsupportedSamples": 2, + "truePositives": 6, + "falsePositives": 2, + "falseNegatives": 2, + "trueNegatives": 2, + "precision": 0.75, + "recall": 0.75, + "supportedCoverage": 0.833333, + "elapsedMs": 3620 + } + }, + "r07Cocoindex": { + "installedVersion": "0.2.37", + "productionSemanticInvocations": 0, + "productionRegistryStatus": "deleted", + "productionIntegrationPresent": false, + "commandDiscoveryCallSites": 0, + "comparisonStatus": "reviewed-retirement-no-unique-value" + }, + "r08GitHubResearch": { + "command": "pwsh -NoProfile -File test-github-solution-research.ps1", + "fixtureElapsedMs": 855.4559, + "readOnlyCommandFamilies": 1, + "rateLimitFallbacksPassed": 1, + "credentialRedactionFixturesPassed": 1, + "externalWriteCommands": 0, + "representativeLiveRun": { + "measuredAt": "2026-07-14T03:39:12.3361140+08:00", + "elapsedMs": 13591.4758, + "blocker": "error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "status": "manual_required", + "reasonCode": "invalid-query", + "candidates": 0, + "resolutionAtK": 0, + "reproducible": false, + "rateCostRequests": 1, + "externalWrites": 0, + "generatedQuery": "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "invocationExitCodes": [1, 1, 0, 0], + "resultArtifact": "orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json", + "resultArtifactSha256": "f838958011ac2ec9e8a525f5d0d0d249dd78c8fd481628f84061b97eef463b6c", + "returnedUrls": [], + "returnedSourceRevisions": [] + }, + "productionRegistryStatus": "deleted" + }, + "r10Git": { + "installedVersion": "2.54.0.windows.1", + "command": "cargo test -p code-intel --test snapshot_identity --quiet", + "testsPassed": 12, + "suiteElapsedMs": 27509.7763, + "sourceSha256": "ed40e9b578b7af2e5b28f19536190b620e98ea5beaa5eced01dd5b3306bf3b4f", + "testSha256": "e04f936cc457c400820c46dbd09c1f6251e74e9791eca08be4c6051314b8d87b", + "revParseSamples": 20, + "revParseP50Ms": 167.65, + "revParseP95Ms": 422.294, + "revParseMaxMs": 445.58, + "mutationCommandsInSnapshotAdapter": 0, + "localLicensePath": "C:/Program Files/Git/LICENSE.txt", + "localLicenseSha256": "5b2198d1645f767585e8a88ac0499b04472164c0d2da22e75ecf97ef443ab32e", + "alternateVcsContractFixture": "orchestration/internalization/fixtures/r10-alternate-vcs-port.json", + "alternateMismatchExitCode": 65, + "alternateMismatchPublishesArtifacts": false, + "alternateAdapterActuallyExecuted": true, + "repowiseShadowMutationAuthorityDefault": "deny", + "repowiseDeclaredEffects": ["repo_read", "process_execute", "local_write", "repo_mutation"], + "rollbackRoutesTested": ["git", "unversioned-explicit-overlay"] + }, + "r12Greenfield": { + "command": "pwsh -NoProfile -File test-greenfield-integration.ps1", + "fixtureElapsedMs": 2127.4285, + "planArtifacts": 2, + "explicitAnalyzeFixtures": 1, + "autoAnalyzeWithoutFlag": 0, + "externalProviderWrites": 0, + "reviewAuthorityGranted": false, + "externalPluginProductionIntegrationPresent": false, + "retirementReason": "No executable plugin source revision, local license, effect review, or reviewed generated-spec value could be bound; only the pipeline-owned plan-only handoff remains." + } + } +} diff --git a/orchestration/internalization/claude-code-merge-queue.json b/orchestration/internalization/claude-code-merge-queue.json new file mode 100644 index 0000000..b79abde --- /dev/null +++ b/orchestration/internalization/claude-code-merge-queue.json @@ -0,0 +1,155 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.claude-code-merge-queue-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "claude-code-merge-queue landing provider adapter", + "kind": "adapted_capability", + "source": { + "uri": "https://github.com/2233admin/claude-code-merge-queue", + "revision": "e7a76958dbd3953b84f12abbc2e6bd755aafce53; version:0.5.1; local-adapter-sha256:7a5bf1613548a48c9ef0b8a1727b5db108e9c3ba2745aefaac9d8163394f0a85; local-conformance-sha256:b84d6d341bbc1224c082784e2c28af5ddbb802b36d8a88ed83143a135f968c65; local-policy-sha256:158ee8bafe3bd19c76d495860b8737c614854ebd0319014e7c7b03d9d4579d18; local-doc-sha256:804aed2aeb82d5c28d48b91f2a3242aefce8e22d734db0b70cfaabaf44f8ab17" + }, + "license": { + "id": "MIT", + "obligations": [ + "retain the upstream copyright and MIT permission notice when distributing copied or substantial upstream portions", + "keep the exact source URI, revision, version, and license attribution for the external provider adapter" + ] + } + }, + "adoption": { + "rung": "adapt", + "ownedBoundary": [ + "Pipeline owns readiness observation, explicit mutation and network authority checks, orchestration registration, and project-acceptance binding", + "the external provider owns worktree lane allocation, FIFO locking, rebase, push, synchronization, and provider-specific hooks", + "ordinary code-intel analysis has no queue dependency and production promotion remains outside agent and adapter authority" + ], + "necessityEvidence": { + "evidenceIds": ["local:merge-queue:multi-agent-race-gap", "local:merge-queue:eight-readiness-gates", "gap:merge-queue:representative-cluster-throughput"], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "compatibilityEvidence": { + "evidenceIds": ["local:merge-queue:optional-stage", "local:merge-queue:repository-local-provider", "local:merge-queue:no-normal-run-callsite"], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "conformanceEvidence": { + "evidenceIds": ["local:merge-queue:ready-fixture-land", "local:merge-queue:authority-rejection", "local:merge-queue:acceptance-and-promotion-rejection", "gap:merge-queue:independent-provider-security-review"], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "authorityRequirements": { + "repositoryGovernedAttestation": true + }, + "operationTrace": [ + { + "integrationId": "delivery.multi-agent-merge-queue", + "operation": "status", + "command": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action status -RepoPath '' -Json", + "implementationIdentity": { "providerId": "claude-code-merge-queue", "implementationId": "pipeline.adapter.v1", "activation": "optional" }, + "source": { "path": "Invoke-MultiAgentMergeQueue.ps1", "sha256": "7a5bf1613548a48c9ef0b8a1727b5db108e9c3ba2745aefaac9d8163394f0a85" }, + "conformance": { "path": "test-multi-agent-merge-queue.ps1", "sha256": "b84d6d341bbc1224c082784e2c28af5ddbb802b36d8a88ed83143a135f968c65", "testName": "PASS multi-agent merge queue" } + }, + { + "integrationId": "delivery.multi-agent-merge-queue", + "operation": "validate", + "command": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action validate -RepoPath '' -Json", + "implementationIdentity": { "providerId": "claude-code-merge-queue", "implementationId": "pipeline.adapter.v1", "activation": "optional" }, + "source": { "path": "Invoke-MultiAgentMergeQueue.ps1", "sha256": "7a5bf1613548a48c9ef0b8a1727b5db108e9c3ba2745aefaac9d8163394f0a85" }, + "conformance": { "path": "test-multi-agent-merge-queue.ps1", "sha256": "b84d6d341bbc1224c082784e2c28af5ddbb802b36d8a88ed83143a135f968c65", "testName": "PASS multi-agent merge queue" } + }, + { + "integrationId": "delivery.multi-agent-merge-queue", + "operation": "land", + "command": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action land -RepoPath '' -AllowRepositoryMutation -AllowNetworkPush", + "implementationIdentity": { "providerId": "claude-code-merge-queue", "implementationId": "pipeline.adapter.v1", "activation": "explicit-authority" }, + "source": { "path": "Invoke-MultiAgentMergeQueue.ps1", "sha256": "7a5bf1613548a48c9ef0b8a1727b5db108e9c3ba2745aefaac9d8163394f0a85" }, + "conformance": { "path": "test-multi-agent-merge-queue.ps1", "sha256": "b84d6d341bbc1224c082784e2c28af5ddbb802b36d8a88ed83143a135f968c65", "testName": "PASS multi-agent merge queue" } + }, + { + "integrationId": "delivery.multi-agent-merge-queue", + "operation": "reconcile", + "command": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action reconcile -RepoPath ''", + "implementationIdentity": { "providerId": "claude-code-merge-queue", "implementationId": "pipeline.adapter.v1", "activation": "optional" }, + "source": { "path": "Invoke-MultiAgentMergeQueue.ps1", "sha256": "7a5bf1613548a48c9ef0b8a1727b5db108e9c3ba2745aefaac9d8163394f0a85" }, + "conformance": { "path": "test-multi-agent-merge-queue.ps1", "sha256": "b84d6d341bbc1224c082784e2c28af5ddbb802b36d8a88ed83143a135f968c65", "testName": "PASS multi-agent merge queue" } + }, + { + "integrationId": "delivery.multi-agent-merge-queue", + "operation": "history", + "command": "pwsh -NoProfile -File Invoke-MultiAgentMergeQueue.ps1 -Action history -RepoPath ''", + "implementationIdentity": { "providerId": "claude-code-merge-queue", "implementationId": "pipeline.adapter.v1", "activation": "optional" }, + "source": { "path": "Invoke-MultiAgentMergeQueue.ps1", "sha256": "7a5bf1613548a48c9ef0b8a1727b5db108e9c3ba2745aefaac9d8163394f0a85" }, + "conformance": { "path": "test-multi-agent-merge-queue.ps1", "sha256": "b84d6d341bbc1224c082784e2c28af5ddbb802b36d8a88ed83143a135f968c65", "testName": "PASS multi-agent merge queue" } + } + ], + "economics": { + "benefit": { "metric": "fail-closed landing readiness gates", "value": 8, "unit": "gates" }, + "cost": { "metric": "optional project runtime dependencies", "value": 1, "unit": "dependencies" }, + "benefitEvidence": { "evidenceIds": ["local:merge-queue:eight-readiness-gates", "gap:merge-queue:representative-cluster-throughput"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "costEvidence": { "evidenceIds": ["local:merge-queue:repository-local-provider"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "assurance": { + "maintenanceEvidence": { "evidenceIds": ["local:merge-queue:source-revision-version-pinned", "gap:merge-queue:update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "securityEvidence": { "evidenceIds": ["local:merge-queue:explicit-mutation-network-authority", "local:merge-queue:no-install-init-promote-prune", "gap:merge-queue:independent-provider-security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "update": { + "policy": "Before 2026-10-13, compare upstream revision and provider behavior, re-run adapter conformance, measure representative multi-agent landing throughput, and review provider security before production enablement", + "nextCheckAt": 1791676800, + "evidence": { "evidenceIds": ["gap:merge-queue:update-review", "gap:merge-queue:representative-cluster-throughput", "gap:merge-queue:independent-provider-security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "ownedModifications": [ + { "path": "Invoke-MultiAgentMergeQueue.ps1", "description": "Pipeline-owned readiness and explicit-authority compatibility adapter", "evidenceIds": ["local:merge-queue:eight-readiness-gates", "local:merge-queue:explicit-mutation-network-authority"] }, + { "path": "orchestration/multi-agent-merge-queue-policy.v1.json", "description": "Pinned provider, required gates, allowed actions, and authority boundary", "evidenceIds": ["local:merge-queue:source-revision-version-pinned", "local:merge-queue:no-install-init-promote-prune"] }, + { "path": "orchestration/integrations.json", "description": "Optional landing-coordination stage and provider operation registry", "evidenceIds": ["local:merge-queue:optional-stage", "local:merge-queue:no-normal-run-callsite"] }, + { "path": "orchestration/schemas/code-intel-multi-agent-merge-queue-status.v1.schema.json", "description": "Checked machine-readable readiness result contract", "evidenceIds": ["local:merge-queue:eight-readiness-gates"] }, + { "path": "test-multi-agent-merge-queue.ps1", "description": "Positive delegation and fail-closed authority, acceptance, and promotion fixtures", "evidenceIds": ["local:merge-queue:ready-fixture-land", "local:merge-queue:authority-rejection", "local:merge-queue:acceptance-and-promotion-rejection"] }, + { "path": "test-integration-orchestration.ps1", "description": "Registry and path-quoting acceptance for the optional delivery adapter", "evidenceIds": ["local:merge-queue:optional-stage", "local:merge-queue:explicit-mutation-network-authority"] }, + { "path": "orchestration/code-intel-project-conformance-policy.v1.json", "description": "Adds the merge-queue contract to unified fast and full project acceptance", "evidenceIds": ["local:merge-queue:acceptance-and-promotion-rejection"] }, + { "path": ".github/workflows/ci.yml", "description": "Runs parser, orchestration, unified conformance, and cross-platform merge-queue contract checks", "evidenceIds": ["local:merge-queue:ready-fixture-land", "local:merge-queue:authority-rejection"] }, + { "path": "docs/multi-agent-merge-queue.md", "description": "Human integration, acceptance binding, authority, and limits contract", "evidenceIds": ["local:merge-queue:optional-stage", "local:merge-queue:no-normal-run-callsite"] }, + { "path": "docs/plans/multi-agent-merge-queue-idea.md", "description": "Accepted scope, success criteria, constraints, and replacement questions", "evidenceIds": ["local:merge-queue:multi-agent-race-gap", "local:merge-queue:no-normal-run-callsite"] } + ], + "rollback": { + "strategy": "remove the optional stage, integration, adapter, policy, schema, tests, documentation, and this record; code-intel analysis and project conformance remain independent", + "evidence": { "evidenceIds": ["local:merge-queue:optional-stage", "local:merge-queue:no-normal-run-callsite"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "exit": { + "strategy": "replace the provider adapter when a language-neutral or distributed queue satisfies the same fail-closed landing and authority contract, or retire it if measured cluster value is absent", + "replacementCriteria": [ + "accepted lanes are serialized without bypassing project conformance", + "direct pushes remain protected and conflicts fail closed", + "repository mutation and network push require explicit authority", + "production promotion remains human-only" + ], + "evidence": { "evidenceIds": ["local:merge-queue:eight-readiness-gates", "gap:merge-queue:representative-cluster-throughput"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "representative multi-agent runs show no meaningful reduction in landing races or queue delay", + "the provider no longer preserves fail-closed checks, crash-safe FIFO behavior, or manual promotion", + "a distributed queue replaces the one-machine provider while satisfying the owned contract" + ], + "evidence": { "evidenceIds": ["local:record:claude-code-merge-queue", "gap:merge-queue:representative-cluster-throughput", "gap:merge-queue:update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1783900800, + "replacementRecordId": null, + "evidenceIds": [ + "local:record:claude-code-merge-queue", + "local:merge-queue:source-revision-version-pinned", + "local:merge-queue:ready-fixture-land", + "local:merge-queue:authority-rejection", + "gap:merge-queue:representative-cluster-throughput", + "gap:merge-queue:update-review", + "gap:merge-queue:independent-provider-security-review" + ], + "authorityEvent": null + }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "codex-merge-queue-internalization" } +} diff --git a/orchestration/internalization/cocoindex.json b/orchestration/internalization/cocoindex.json new file mode 100644 index 0000000..cf685db --- /dev/null +++ b/orchestration/internalization/cocoindex.json @@ -0,0 +1,14 @@ +{ + "schema": "code-intel-internalization-record.v1", "id": "internalization.cocoindex-record", "projectId": "code-intel-pipeline", + "subject": { "name": "cocoindex-code reviewed retirement record", "kind": "evidence_provider", "source": { "uri": "https://github.com/cocoindex-io/cocoindex-code; installed-via=uv-tool", "revision": "installed-version:0.2.37; production-semantic-invocations:0; production-registry-status:deleted; production-integration-present:false; measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420" }, "license": { "id": "Apache-2.0-INSTALLED-PACKAGE-METADATA", "obligations": ["retain this provenance only as retirement evidence", "a future proposal must start with a new pinned, licensed, measured, authority-reviewed record"] } }, + "adoption": { "rung": "invoke", "ownedBoundary": ["Pipeline removed configuration lookup, executable discovery, production declaration, and integration registration after zero measured unique value", "Native Code Evidence remains independent; the legacy outcome file is a static reviewed-retirement tombstone and cannot invoke cocoindex-code"], "necessityEvidence": { "evidenceIds": ["local:r07:reviewed-deletion", "local:r07:production-semantic-invocations-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:r07:native-baseline-independent", "local:r07:legacy-tombstone-only"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r07:b07-registry-reconciled", "local:r07:command-discovery-call-sites-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "economics": { "benefit": { "metric": "production semantic invocations retained", "value": 0, "unit": "invocations" }, "cost": { "metric": "production integration paths retained", "value": 0, "unit": "paths" }, "benefitEvidence": { "evidenceIds": ["local:r07:production-semantic-invocations-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:r07:production-integration-present-false"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r07:reviewed-deletion"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r07:no-command-discovery", "local:r07:no-provider-effects"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Do not enable or probe the installed command; future reconsideration requires a new authority-reviewed record with unique A/B value and full data, model, storage, dependency, and license effects", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["local:r07:delete-until-rejustified"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "orchestration/integrations.json", "description": "R07 reviewed-deletion removes the production participant and integration while preserving Native Code Evidence", "evidenceIds": ["local:r07:reviewed-deletion", "local:r07:native-baseline-independent"] }], + "rollback": { "strategy": "continue through Native Code Evidence; no external provider, model, storage, credential, or network path is required", "evidence": { "evidenceIds": ["local:r07:native-baseline-independent"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retain only this audit record and the compatibility tombstone until the legacy artifact version is retired", "replacementCriteria": ["new proposal proves unique value over the pinned Native labeled corpus", "all effects, provenance, license, rollback, and authority are explicit"], "evidence": { "evidenceIds": ["local:r07:reviewed-deletion"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "completed", "triggers": ["no measured unique value over Native Code Evidence", "no production semantic invocation exists", "B07 production declaration and integration are deleted"], "evidence": { "evidenceIds": ["local:r07:reviewed-deletion", "local:r07:production-semantic-invocations-0", "local:r07:production-integration-present-false"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r07:reviewed-deletion", "local:r07:native-baseline-independent", "local:r07:production-semantic-invocations-0", "gap:cocoindex:future-production-authority"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/codenexus.json b/orchestration/internalization/codenexus.json new file mode 100644 index 0000000..ba55a86 --- /dev/null +++ b/orchestration/internalization/codenexus.json @@ -0,0 +1,28 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.codenexus-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "CodeNexus full-provider and lite compatibility boundary", "kind": "evidence_provider", "source": { "uri": "unverified-upstream:codenexus; local-adapter=crates/code-intel-cli/src/codenexus_adapter.rs", "revision": "unverified-upstream; local-adapter-sha256:645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8; local-conformance-sha256:26d8edf0332f605b452a59a2b99858bd497d198b5c1b5b87cbceedd7a3a87681" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not vendor, revive, install, or production-enable a full CodeNexus worker from this record until source, exact revision, and license are verified", "retain external full-provider ownership and Pipeline-owned lite compatibility provenance separately"] } }, + "adoption": { + "rung": "adapt", + "ownedBoundary": ["Pipeline-owned B04 CodeNexus port, full/lite identity validation, current-snapshot binding, provider-unavailable diagnosis, and A04 route", "CodeNexus owns process, indexing, storage, retrieval, perception, and impact semantics; Invoke-CodeNexusLite.ps1 is an explicit fallback/legacy rollback only"], + "necessityEvidence": { "evidenceIds": ["local:registry:provider.codenexus-adapt", "local:registry:runtime.code-nexus-lite", "local:registry:localization.codenexus-lite", "gap:codenexus:upstream-revision", "gap:codenexus:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b04:full-lite-provider-swap", "local:b04:storage-process-boundary", "local:b04:provider-unavailable-is-unknown", "gap:codenexus:full-provider-runtime-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:b04:codenexus-adapter-conformance:26d8edf0332f605b452a59a2b99858bd497d198b5c1b5b87cbceedd7a3a87681", "local:b04:production-operation-trace", "local:b04:full-lite-swap-trace", "gap:codenexus:full-provider-exact-revision-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "operationTrace": [ + { "integrationId": "provider.codenexus-adapt", "operation": "adapt", "command": "target/debug/code-intel.exe provider codenexus-adapt --request --artifact-root --evaluated-at --max-age-seconds ", "implementationIdentity": { "providerId": "codenexus.full", "implementationId": "codenexus.service.v1", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/codenexus_adapter.rs", "sha256": "645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8" }, "conformance": { "path": "crates/code-intel-cli/tests/codenexus_adapter.rs", "sha256": "26d8edf0332f605b452a59a2b99858bd497d198b5c1b5b87cbceedd7a3a87681", "testName": "production_route_runs_full_lite_and_unavailable_through_a04" } }, + { "integrationId": "provider.codenexus-adapt", "operation": "facade", "command": "run-code-intel.ps1 -CodeNexusAdapterRequest -CodeNexusAdapterArtifactRoot -CodeNexusAdapterEvaluatedAt -CodeNexusAdapterMaxAgeSeconds ", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "explicit_fallback" }, "source": { "path": "crates/code-intel-cli/src/codenexus_adapter.rs", "sha256": "645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8" }, "conformance": { "path": "crates/code-intel-cli/tests/codenexus_adapter.rs", "sha256": "26d8edf0332f605b452a59a2b99858bd497d198b5c1b5b87cbceedd7a3a87681", "testName": "production_registry_facade_and_route_schema_are_declared" } }, + { "integrationId": "runtime.code-nexus-lite", "operation": "compat", "command": "pwsh -NoProfile -File \"$env:CODE_INTEL_HOME\\Invoke-CodeNexusLite.ps1\" -RepoPath ''", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "explicit_fallback" }, "source": { "path": "Invoke-CodeNexusLite.ps1", "sha256": "fe23311437bd5558dadeb9cb913971e5664c8b2a687897e7b397c444b6308697" }, "conformance": { "path": "test-codenexus-adapter-contract.ps1", "sha256": "857ee2ae2d7e658fa4cdff04fb3327bc8e2501332ffe5695d881ea5c67aa0027", "testName": "Invoke-LiteScriptEndToEnd" } }, + { "integrationId": "localization.codenexus-lite", "operation": "compat", "command": "pwsh -NoProfile -File \"$env:CODE_INTEL_HOME\\Invoke-CodeNexusLite.ps1\" -RepoPath ''", "implementationIdentity": { "providerId": "codenexus.lite-compat", "implementationId": "invoke-codenexus-lite.ps1", "activation": "legacy_rollback" }, "source": { "path": "Invoke-CodeNexusLite.ps1", "sha256": "fe23311437bd5558dadeb9cb913971e5664c8b2a687897e7b397c444b6308697" }, "conformance": { "path": "test-codenexus-adapter-contract.ps1", "sha256": "857ee2ae2d7e658fa4cdff04fb3327bc8e2501332ffe5695d881ea5c67aa0027", "testName": "Invoke-LiteScriptEndToEnd" } } + ], + "economics": { "benefit": { "metric": "B04 provider identities exercised by swap conformance", "value": 2, "unit": "implementations" }, "cost": { "metric": "registered full/lite lifecycle identities", "value": 2, "unit": "implementations" }, "benefitEvidence": { "evidenceIds": ["local:b04:full-lite-provider-swap", "gap:codenexus:measured-localization-value"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:b04:production-operation-trace", "gap:codenexus:index-storage-latency-cost-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:b04:lite-rollback-preserved", "gap:codenexus:full-provider-maintenance-status"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:b04:no-shared-database", "gap:codenexus:full-provider-security-review", "gap:codenexus:storage-data-handling-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify full-provider source/revision/license, maintenance/security, storage/data handling, current swap conformance, and measured localization value/cost; preserve lite and research-only status until closure", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:codenexus:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "crates/code-intel-cli/src/codenexus_adapter.rs", "description": "Pipeline-owned B04 port and adapter; provider data remains opaque", "evidenceIds": ["local:b04:adapter-source-sha256:645675312135932dfce365a8dfc14e214cec78ee733f248606547b3eaa56edc8", "local:b04:codenexus-adapter-conformance:26d8edf0332f605b452a59a2b99858bd497d198b5c1b5b87cbceedd7a3a87681"] }, { "path": "Invoke-CodeNexusLite.ps1", "description": "Pipeline-owned lite compatibility implementation and legacy rollback, not full CodeNexus semantics", "evidenceIds": ["local:b04:lite-rollback-preserved", "gap:codenexus:lite-retirement-proof"] } ], + "rollback": { "strategy": "retain explicit CodeNexus-lite fallback or provider-unavailable survival path behind B04 without reviving a full worker", "evidence": { "evidenceIds": ["local:b04:lite-rollback-preserved", "local:b04:provider-unavailable-is-unknown"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace full or lite implementations independently while preserving the B04 port, opaque data boundary, and unavailable semantics", "replacementCriteria": ["replacement has verified ownership, revision, license, and implementation identity", "full/lite swap and provider-unavailable fixtures pass", "no database, process, retrieval, ranking, or impact semantics move into Pipeline core"], "evidence": { "evidenceIds": ["local:b04:full-lite-provider-swap", "local:b04:storage-process-boundary", "gap:codenexus:full-provider-exit-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["full provider passes B04 and Survival Scanner with measured value", "lite has no production callers and rollback is proven", "source, license, maintenance, or security remains unverifiable"], "evidence": { "evidenceIds": ["local:b04:lite-rollback-preserved", "gap:codenexus:lite-retirement-proof", "gap:codenexus:measured-localization-value"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:b04:codenexus-adapter-conformance:26d8edf0332f605b452a59a2b99858bd497d198b5c1b5b87cbceedd7a3a87681", "local:b04:production-operation-trace", "local:b04:full-lite-swap-trace", "gap:codenexus:upstream-revision", "gap:codenexus:license", "gap:codenexus:measured-localization-value"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json b/orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json new file mode 100644 index 0000000..0886b1d --- /dev/null +++ b/orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json @@ -0,0 +1,101 @@ +{ + "schema": "github-solution-research.v1", + "generatedAt": "2026-07-14T03:39:12.3361140+08:00", + "repo": "D:\\projects\\_tools\\code-intel-pipeline", + "mode": "normal", + "status": "manual_required", + "reason": "Invalid search query \"( rust compile blocker error[E0432]:\\\" unresolved import crate::authority in compatibility_retirement_gate.rs\\\" ) type:pr\".\r\nThe search query contains invalid syntax.", + "queries": [ + { + "step": "rust compile blocker", + "query": "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "firstErrorLine": "error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "tokens": [] + } + ], + "candidates": [], + "evidenceLinks": [], + "invocations": [ + { + "query": "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "surface": "issues", + "argv": [ + "search", + "issues", + "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "--limit", + "5", + "--json", + "title,url,state,updatedAt,repository" + ], + "exitCode": 1 + }, + { + "query": "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "surface": "prs", + "argv": [ + "search", + "prs", + "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "--limit", + "5", + "--json", + "title,url,state,updatedAt,repository" + ], + "exitCode": 1 + }, + { + "query": "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "surface": "repos", + "argv": [ + "search", + "repos", + "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "--archived=false", + "--sort", + "stars", + "--order", + "desc", + "--limit", + "5", + "--json", + "fullName,url,description,stargazersCount,forksCount,language,license,pushedAt,isArchived" + ], + "exitCode": 0 + }, + { + "query": "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "surface": "code", + "argv": [ + "search", + "code", + "rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "--limit", + "5", + "--json", + "path,url,repository,sha" + ], + "exitCode": 0 + } + ], + "failedSteps": [ + { + "name": "rust compile blocker", + "status": "failed", + "error": "error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs", + "output": "" + } + ], + "failureClassifications": [ + { + "category": "compiler_error", + "name": "rust compile blocker" + } + ], + "sentruxFailures": null, + "nextStep": "Run github-solution-research with the failed step names, first error lines, tool/package names, and any version constraints.", + "exitCriteria": [ + "GitHub issue, PR, code, release, or repository evidence explains an applicable solution", + "or GitHub evidence is explicitly recorded as insufficient and local-only diagnosis continues" + ] +} diff --git a/orchestration/internalization/evidence/r08-live-20260714/github-solution-research.md b/orchestration/internalization/evidence/r08-live-20260714/github-solution-research.md new file mode 100644 index 0000000..3ef95f6 --- /dev/null +++ b/orchestration/internalization/evidence/r08-live-20260714/github-solution-research.md @@ -0,0 +1,24 @@ +# GitHub Solution Research + +- Status: manual_required +- Reason: Invalid search query "( rust compile blocker error[E0432]:\" unresolved import crate::authority in compatibility_retirement_gate.rs\" ) type:pr". +The search query contains invalid syntax. +- Repo: D:\projects\_tools\code-intel-pipeline +- Mode: normal + +## Generated Queries + +- rust compile blocker: `rust compile blocker error[E0432]: unresolved import crate::authority in compatibility_retirement_gate.rs` + +## Evidence Candidates + +- No GitHub evidence candidates were generated automatically. + +## Required Follow-Up + +Run github-solution-research with the failed step names, first error lines, tool/package names, and any version constraints. + +## Exit Criteria + +- GitHub issue, PR, code, release, or repository evidence explains an applicable solution +- or GitHub evidence is explicitly recorded as insufficient and local-only diagnosis continues diff --git a/orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json b/orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json new file mode 100644 index 0000000..37dc515 --- /dev/null +++ b/orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json @@ -0,0 +1,74 @@ +{ + "schema": "code-intel-pon-multilanguage-code-evidence-fixture.v1", + "contract": { + "files": ["path", "language", "bytes", "lines", "textHash", "source"], + "symbols": ["id", "kind", "name", "file", "startLine", "endLine", "language", "confidence", "source"], + "imports": ["file", "line", "target", "language", "confidence", "source"] + }, + "samples": [ + { + "path": "main.py", + "language": "python", + "supported": true, + "content": "from helpers import greet\n\ndef main():\n return greet()\n", + "expectedSymbols": [{ "kind": "function", "name": "main" }], + "expectedImports": ["helpers"] + }, + { + "path": "main.js", + "language": "javascript", + "supported": true, + "content": "import helper from \"./helper.js\";\nfunction main() { return helper(); }\n", + "expectedSymbols": [{ "kind": "function", "name": "main" }], + "expectedImports": ["./helper.js"] + }, + { + "path": "main.ts", + "language": "typescript", + "supported": true, + "content": "import { Thing } from \"./types\";\nexport function build(): Thing { return {} as Thing; }\n", + "expectedSymbols": [{ "kind": "function", "name": "build" }], + "expectedImports": ["./types"] + }, + { + "path": "main.rs", + "language": "rust", + "supported": true, + "content": "use crate::helper;\n\npub fn main() { helper::run(); }\n", + "expectedSymbols": [{ "kind": "function", "name": "main" }], + "expectedImports": ["crate::helper"] + }, + { + "path": "main.go", + "language": "go", + "supported": true, + "content": "package main\n\nimport \"fmt\"\n\nfunc Main() { fmt.Println(\"ok\") }\n", + "expectedSymbols": [{ "kind": "function", "name": "Main" }], + "expectedImports": ["fmt"] + }, + { + "path": "Main.java", + "language": "java", + "supported": true, + "content": "public class Main {}\n", + "expectedSymbols": [{ "kind": "class", "name": "Main" }], + "expectedImports": [] + }, + { + "path": "main.ps1", + "language": "powershell", + "supported": true, + "content": "function Invoke-Main { 'ok' }\n", + "expectedSymbols": [{ "kind": "function", "name": "Invoke-Main" }], + "expectedImports": [] + }, + { + "path": "Main.cs", + "language": "csharp", + "supported": false, + "content": "public class Main {}\n", + "expectedSymbols": [], + "expectedImports": [] + } + ] +} diff --git a/orchestration/internalization/fixtures/r06-native-labeled-corpus.json b/orchestration/internalization/fixtures/r06-native-labeled-corpus.json new file mode 100644 index 0000000..03fdbf2 --- /dev/null +++ b/orchestration/internalization/fixtures/r06-native-labeled-corpus.json @@ -0,0 +1,18 @@ +{ + "schema": "code-intel-r06-native-labeled-corpus.v1", + "labelPolicy": "symbolPresent is a manual semantic label; prediction is at least one native-minimal symbol emitted for the file", + "samples": [ + { "path": "python_positive.py", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "# 中文:有效函数\ndef greet(name):\n return name\n" }, + { "path": "javascript_positive.js", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "// Español: función real\nfunction saludar(nombre) { return nombre; }\n" }, + { "path": "rust_positive.rs", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "// 日本語: 実関数\npub fn greet() {}\n" }, + { "path": "go_positive.go", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "// Português: função real\nfunc Saudacao() {}\n" }, + { "path": "powershell_positive.ps1", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "# Français : fonction réelle\nfunction Get-Greeting { 'bonjour' }\n" }, + { "path": "java_positive.java", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "// Deutsch: echte Klasse\npublic class Greeting {}\n" }, + { "path": "python_false_positive.py", "languageClass": "supported", "polarity": "negative", "symbolPresent": false, "content": "TEXT = '''\ndef ghost():\n pass\n'''\n" }, + { "path": "javascript_false_positive.js", "languageClass": "supported", "polarity": "negative", "symbolPresent": false, "content": "const text = `\nfunction ghost() {}\n`;\n" }, + { "path": "python_false_negative.py", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "async def fetch_data():\n return 1\n" }, + { "path": "java_false_negative.java", "languageClass": "supported", "polarity": "positive", "symbolPresent": true, "content": "public String greet() { return \"hi\"; }\n" }, + { "path": "csharp_true_negative.cs", "languageClass": "unsupported", "polarity": "negative", "symbolPresent": false, "content": "// Unsupported language control\npublic class NotClaimed {}\n" }, + { "path": "text_true_negative.txt", "languageClass": "unsupported", "polarity": "negative", "symbolPresent": false, "content": "function words are prose, not code evidence.\n" } + ] +} diff --git a/orchestration/internalization/fixtures/r10-alternate-vcs-port.json b/orchestration/internalization/fixtures/r10-alternate-vcs-port.json new file mode 100644 index 0000000..cf41aa7 --- /dev/null +++ b/orchestration/internalization/fixtures/r10-alternate-vcs-port.json @@ -0,0 +1,26 @@ +{ + "schema": "code-intel-alternate-vcs-snapshot-port.v1", + "providerNeutralInput": { + "repositoryRoot": "", + "workingTreePolicy": "head_only|explicit_overlay", + "scope": [""] + }, + "requiredOutput": { + "schema": "code-intel-repository-snapshot.v1", + "identity": "sha256:", + "inputDigest": "", + "head": "", + "provider": "", + "scope": [""] + }, + "mismatch": { + "condition": "provider output does not reproduce portable identity, scope, policy, or input digest", + "exitCode": 65, + "publishesArtifacts": false + }, + "rollback": { + "routes": ["git", "unversioned-explicit-overlay"], + "authorityChange": false, + "networkOrMutationAdded": false + } +} diff --git a/orchestration/internalization/frontier.json b/orchestration/internalization/frontier.json new file mode 100644 index 0000000..f95c3f2 --- /dev/null +++ b/orchestration/internalization/frontier.json @@ -0,0 +1,95 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.frontier-quality-loop-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "frontier quality-loop design reference", + "kind": "design_reference", + "source": { + "uri": "https://github.com/apoorvjain25/frontier", + "revision": "0b39eee3ec9ed0905ad7303772653c7e9bd17831; local-doc-sha256:0c45f253c6436414ba73ad6b11acd53b760073b67387f02c01ceccb49bbadb93" + }, + "license": { + "id": "MIT", + "obligations": [ + "retain the Frontier copyright and MIT permission notice when distributing copied text or substantial portions", + "keep source and pinned revision attribution in the Pipeline-owned semantic note" + ] + } + }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": [ + "Pipeline-owned semantics for evidence-bound findings, author-reviewer separation, earned stop conditions, and advisory rule-candidate distillation", + "no Frontier runtime dependency, skill installation, agent invocation, craft-standard vendoring, automatic policy mutation, or model-routing authority" + ], + "necessityEvidence": { + "evidenceIds": ["local:frontier-quality-loop:semantic-map", "local:frontier-analysis:code-intel-slice", "gap:frontier:representative-defect-capture-measurement"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "compatibilityEvidence": { + "evidenceIds": ["local:frontier-quality-loop:existing-owner-map", "local:frontier-quality-loop:no-runtime-coupling"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "conformanceEvidence": { + "evidenceIds": ["local:frontier-quality-loop:four-semantics", "local:frontier-quality-loop:distillation-shape", "gap:frontier:independent-conformance-review"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "economics": { + "benefit": { "metric": "selectively absorbed quality-loop semantics", "value": 4, "unit": "semantics" }, + "cost": { "metric": "new runtime dependencies or production invocations", "value": 0, "unit": "dependencies" }, + "benefitEvidence": { "evidenceIds": ["local:frontier-quality-loop:four-semantics", "gap:frontier:representative-defect-capture-measurement"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "costEvidence": { "evidenceIds": ["local:frontier-quality-loop:no-runtime-coupling"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "assurance": { + "maintenanceEvidence": { "evidenceIds": ["local:frontier:revision-pinned", "gap:frontier:update-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "securityEvidence": { "evidenceIds": ["local:frontier-quality-loop:no-runtime-coupling", "gap:frontier:independent-security-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "update": { + "policy": "Before 2026-10-13, compare the pinned revision with upstream and measure whether distilled rule candidates catch repeated defects without creating noisy or automatic policy changes", + "nextCheckAt": 1791849600, + "evidence": { "evidenceIds": ["gap:frontier:update-review", "gap:frontier:representative-defect-capture-measurement"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "ownedModifications": [ + { + "path": "docs/frontier-quality-loop.md", + "description": "Pipeline-owned selective semantic mapping and advisory rule-candidate contract", + "evidenceIds": ["local:frontier-quality-loop:semantic-map", "local:frontier-quality-loop:distillation-shape", "local:frontier-quality-loop:no-runtime-coupling"] + } + ], + "rollback": { + "strategy": "delete the semantic note and this research record; existing artifact, verifier, Sentrux, and completion contracts remain unchanged", + "evidence": { "evidenceIds": ["local:frontier-quality-loop:existing-owner-map", "local:frontier-quality-loop:no-runtime-coupling"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "exit": { + "strategy": "remove Frontier attribution after the four semantics are independently specified and regression-tested, or retire the semantics if measurement shows no value", + "replacementCriteria": [ + "local contracts preserve evidence-bound findings and explicit unverified states", + "independent review and earned completion remain covered by executable checks", + "rule candidates cannot mutate governed policy without maintainer authority" + ], + "evidence": { "evidenceIds": ["local:frontier-quality-loop:existing-owner-map", "gap:frontier:representative-defect-capture-measurement"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "the source revision or license can no longer be verified", + "the semantics duplicate executable Pipeline contracts without adding defect-capture value", + "distillation creates noisy rules or bypasses maintainer authority" + ], + "evidence": { "evidenceIds": ["local:record:frontier-quality-loop", "gap:frontier:representative-defect-capture-measurement"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1784073600, + "replacementRecordId": null, + "evidenceIds": ["local:record:frontier-quality-loop", "local:frontier:revision-pinned", "gap:frontier:representative-defect-capture-measurement", "gap:frontier:independent-conformance-review", "gap:frontier:update-review", "gap:frontier:independent-security-review"], + "authorityEvent": null + }, + "provenance": { "recordedAt": 1784073600, "recordedBy": "codex-frontier-review" } +} diff --git a/orchestration/internalization/git.json b/orchestration/internalization/git.json new file mode 100644 index 0000000..d597cd8 --- /dev/null +++ b/orchestration/internalization/git.json @@ -0,0 +1,18 @@ +{ + "schema": "code-intel-internalization-record.v1", "id": "internalization.git-record", "projectId": "code-intel-pipeline", + "subject": { "name": "Git read-only repository protocol dependency", "kind": "adapted_capability", "source": { "uri": "https://git-scm.com; installed-path=C:/Program Files/Git/cmd/git.exe", "revision": "installed-version:2.54.0.windows.1; local-license-sha256:5b2198d1645f767585e8a88ac0499b04472164c0d2da22e75ecf97ef443ab32e; local-snapshot-source-sha256:24b557147fc8465d492490ef28c839f2eb3613fb28d8d4b0354c1d9a4caa3878; local-conformance-sha256:75e3b09c6d3e461cba59a652f5316187dcebd8195e10bab5c918e00af4076e51; measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420" }, "license": { "id": "GPL-2.0-only-LOCAL-LICENSE-COPY-BOUND", "obligations": ["retain C:/Program Files/Git/LICENSE.txt and its bound digest with package provenance before redistribution", "restrict current adapter authority to read-only repository inspection; mutation commands require a separately registered and A05-gated capability"] } }, + "adoption": { "rung": "invoke", "ownedBoundary": ["Pipeline owns portable Snapshot Identity, explicit head_only and explicit_overlay semantics, scope normalization, Git command argument construction, output parsing, and failure taxonomy", "Git owns repository storage and protocol behavior; commit, checkout, reset, clean, add, index mutation, fetch, push, config writes, hooks, credentials, and network operations are out of scope"], "necessityEvidence": { "evidenceIds": ["local:a02:repository.snapshot-identity", "local:b07:snapshot-registered", "local:r10:installed-git-2.54.0.windows.1"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:r10:head-overlay-fixtures", "local:r10:unborn-shallow-gitlink-lfs-fixtures", "local:r10:alternate-vcs-adapter-actually-executed", "local:r10:alternate-vcs-mismatch-fail-closed", "local:r10:rollback-to-git-or-unversioned"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r10:operation-trace", "local:r10:read-only-command-audit", "local:r10:alternate-mismatch-exit-65", "gap:git:representative-platform-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "operationTrace": [ + { "integrationId": "repository.snapshot-identity", "operation": "snapshotIdentity", "command": "target/debug/code-intel.exe snapshot identity --repo --working-tree-policy --scope ", "implementationIdentity": { "providerId": "git-system-dependency", "implementationId": "git-2.54.0.windows.1+snapshot-v1", "activation": "required read-only repository inspection" }, "source": { "path": "crates/code-intel-cli/src/snapshot.rs", "sha256": "24b557147fc8465d492490ef28c839f2eb3613fb28d8d4b0354c1d9a4caa3878" }, "conformance": { "path": "crates/code-intel-cli/tests/snapshot_identity.rs", "sha256": "75e3b09c6d3e461cba59a652f5316187dcebd8195e10bab5c918e00af4076e51", "testName": "alternate_vcs_contract_fixture_is_fail_closed_and_rolls_back_to_unversioned" } }, + { "integrationId": "repository.snapshot-identity", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec repo.snapshot --request --out ", "implementationIdentity": { "providerId": "git-system-dependency", "implementationId": "git-2.54.0.windows.1+repository.snapshot.compat", "activation": "required A01 read-only envelope" }, "source": { "path": "crates/code-intel-cli/src/snapshot.rs", "sha256": "24b557147fc8465d492490ef28c839f2eb3613fb28d8d4b0354c1d9a4caa3878" }, "conformance": { "path": "crates/code-intel-cli/tests/snapshot_identity.rs", "sha256": "75e3b09c6d3e461cba59a652f5316187dcebd8195e10bab5c918e00af4076e51", "testName": "head_snapshot_binds_gitlink_lfs_pointer_and_case_sensitive_scope" } } + ], + "economics": { "benefit": { "metric": "snapshot identity conformance tests", "value": 12, "unit": "tests" }, "cost": { "metric": "twenty-sample git rev-parse p95 latency", "value": 422.294, "unit": "milliseconds" }, "benefitEvidence": { "evidenceIds": ["local:r10:head-overlay-fixtures-12", "local:r10:read-only-command-audit", "local:r10:mutation-commands-0", "local:r10:alternate-vcs-contract-fixture"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:r10:rev-parse-samples-20", "local:r10:rev-parse-p50-ms-167.65", "local:r10:rev-parse-p95-ms-422.294", "local:r10:snapshot-suite-ms-27509.7763", "gap:git:representative-platform-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r10:pinned-installed-version", "gap:git:package-update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r10:read-only-command-audit", "local:r10:no-network-no-credential-no-mutation", "gap:git:package-supply-chain-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Do not upgrade Git implicitly; retain installer/package and license provenance, rerun all snapshot fixtures and command audit, and measure representative latency before approval", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:git:update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "crates/code-intel-cli/src/snapshot.rs", "description": "Pipeline-owned portable snapshot semantics and read-only Git invocation adapter", "evidenceIds": ["local:r10:snapshot-source-sha256:24b557147fc8465d492490ef28c839f2eb3613fb28d8d4b0354c1d9a4caa3878", "local:r10:conformance-sha256:75e3b09c6d3e461cba59a652f5316187dcebd8195e10bab5c918e00af4076e51", "local:r10:local-license-sha256:5b2198d1645f767585e8a88ac0499b04472164c0d2da22e75ecf97ef443ab32e"] }], + "rollback": { "strategy": "rollback-to-git-or-unversioned explicit_overlay after any alternate provider mismatch; exit 65 and publish no artifacts; never add mutation authority", "evidence": { "evidenceIds": ["local:r10:unversioned-fail-closed-fixtures", "local:r10:alternate-vcs-mismatch-fail-closed", "local:r10:rollback-to-git-or-unversioned"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace Git only behind the provider-neutral alternate-vcs-contract-fixture while preserving portable identity and explicit overlays", "replacementCriteria": ["alternate VCS passes head, dirty, unborn, shallow, gitlink, LFS, symlink, scope, case, and missing-provider fixtures", "mismatch exits 65 without artifacts and rollback-to-git-or-unversioned remains tested", "read-only and mutation authority remain structurally separate", "latency, maintenance, security, package provenance, rollback, and license evidence are complete"], "evidence": { "evidenceIds": ["local:r10:alternate-vcs-adapter-actually-executed", "local:r10:alternate-vcs-mismatch-fail-closed", "local:r10:rollback-to-git-or-unversioned"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["an alternate VCS implementation passes the pinned provider-neutral port and full fixture matrix", "installed Git identity, package provenance, or security posture becomes unacceptable", "repository snapshot no longer uses Git"] , "evidence": { "evidenceIds": ["local:r10:operation-trace", "local:r10:alternate-vcs-contract-fixture"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r10:operation-trace", "local:r10:installed-git-2.54.0.windows.1", "local:r10:local-license-sha256", "local:r10:alternate-vcs-adapter-actually-executed", "local:r10:alternate-vcs-mismatch-fail-closed", "local:r10:rollback-to-git-or-unversioned", "local:r10:rev-parse-p95-ms-422.294", "gap:git:representative-platform-matrix"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/github-research.json b/orchestration/internalization/github-research.json new file mode 100644 index 0000000..d054cc0 --- /dev/null +++ b/orchestration/internalization/github-research.json @@ -0,0 +1,14 @@ +{ + "schema": "code-intel-internalization-record.v1", "id": "internalization.github-research-record", "projectId": "code-intel-pipeline", + "subject": { "name": "GitHub Solution Research reviewed retirement record", "kind": "adapted_capability", "source": { "uri": "https://github.com/cli/cli; historical-local-adapter=Invoke-GitHubSolutionResearch.ps1", "revision": "installed-gh-version:2.95.0; representative-live-run:manual_required-invalid-query; run-artifact:orchestration/internalization/evidence/r08-live-20260714/github-solution-research.json; run-artifact-sha256:f838958011ac2ec9e8a525f5d0d0d249dd78c8fd481628f84061b97eef463b6c; candidates:0; reproducible:false; production-registry-status:deleted; measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420" }, "license": { "id": "MIT-GH-CLI-UPSTREAM-CLAIM-PLUS-SOURCE-ATTRIBUTION", "obligations": ["retain historical URL attribution and quotation obligations", "never persist credentials or restore production network reads without new reproducible value evidence and authority"] } }, + "adoption": { "rung": "adapt", "ownedBoundary": ["The authenticated representative blocker query took 13591.4758 ms and returned manual_required with zero candidates because the generated query was invalid", "Pipeline removed the production network/credential call site instead of treating offline controls as blocker-resolution value"], "necessityEvidence": { "evidenceIds": ["local:r08:representative-live-run", "local:r08:run-artifact-sha256-f838958011ac2ec9e8a525f5d0d0d249dd78c8fd481628f84061b97eef463b6c", "local:r08:resolution-at-k-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:r08:reproducible-false", "local:r08:invalid-query"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r08:b07-reviewed-deletion", "local:r08:production-call-sites-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "economics": { "benefit": { "metric": "representative blocker resolution@k", "value": 0, "unit": "resolved-blockers" }, "cost": { "metric": "representative failed live-query latency", "value": 13591.4758, "unit": "milliseconds" }, "benefitEvidence": { "evidenceIds": ["local:r08:candidates-0", "local:r08:resolution-at-k-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:r08:live-elapsed-ms-13591.4758", "local:r08:rate-cost-requests-1", "local:r08:external-writes-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r08:invalid-query", "local:r08:reviewed-deletion"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r08:production-network-read-removed", "local:r08:production-credential-read-removed", "local:r08:external-writes-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Do not restore the production adapter from offline fixtures; a new proposal must pin query syntax, source revisions and URLs, quotation obligations, resolution@k, latency, rate cost, reproducibility limits, and read-only authority", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["local:r08:delete-until-reproducible"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "run-code-intel.ps1", "description": "R08 removes the failure-conditioned GitHub network and credential call site after the representative query was unreproducible", "evidenceIds": ["local:r08:b07-reviewed-deletion", "local:r08:production-call-sites-0"] }], + "rollback": { "strategy": "retain local diagnosis and explicit manual research outside production; no credential, network, or remote write effect is required", "evidence": { "evidenceIds": ["local:r08:local-diagnosis-independent", "local:r08:external-writes-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "keep the adapter script only as historical research material or delete it separately; production remains local-only", "replacementCriteria": ["representative queries are syntactically valid and reproducible", "returned revisions, URLs, quotation obligations, resolution@k, latency, rate cost, and review limits are pinned", "all network and credential effects remain explicit and read-only"], "evidence": { "evidenceIds": ["local:r08:reviewed-deletion"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "completed", "triggers": ["representative blocker query is not reproducible", "resolution@k is zero", "B07 reviewed deletion removed the production call site and integration"], "evidence": { "evidenceIds": ["local:r08:invalid-query", "local:r08:resolution-at-k-0", "local:r08:b07-reviewed-deletion"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r08:representative-live-run", "local:r08:invalid-query", "local:r08:reviewed-deletion", "gap:github-research:future-production-authority"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/graph.json b/orchestration/internalization/graph.json new file mode 100644 index 0000000..d1874df --- /dev/null +++ b/orchestration/internalization/graph.json @@ -0,0 +1,31 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.graph-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "architecture graph provider boundary", "kind": "adapted_capability", "source": { "uri": "unverified-upstream:understand-anything; local-adapter=crates/code-intel-cli/src/graph_adapter.rs", "revision": "unverified-upstream; local-adapter-sha256:339536b6756cecd60b706faafb3ee9f53f8c1436ac6d2becb0448d27dc0b0322; local-conformance-sha256:9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not infer external Understand provider ownership, revision, or license from the Pipeline-owned Rust graph implementation", "retain distinct internal and explicit-fallback implementation identities and provenance"] } }, + "adoption": { + "rung": "adapt", + "ownedBoundary": ["Pipeline-owned B02 architecture-graph port, internal Rust implementation identity, snapshot binding, and A04 route", "external Understand-compatible fallback remains separately owned, explicitly selected, and non-authoritative until adapted; no external implementation is vendored by this record"], + "necessityEvidence": { "evidenceIds": ["local:registry:provider.graph-adapt", "local:registry:graph.code-intel-understand", "local:registry:graph.understand-external", "gap:graph:upstream-revision", "gap:graph:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b02:internal-external-identity-separation", "local:b02:explicit-fallback-only", "gap:graph:external-provider-runtime-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:b02:graph-adapter-conformance:9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "local:b02:internal-operation-trace", "local:b02:external-fallback-operation-trace", "gap:graph:external-upstream-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "operationTrace": [ + { "integrationId": "provider.graph-adapt", "operation": "adapt", "command": "target/debug/code-intel.exe provider graph-adapt --request --artifact-root --evaluated-at --max-age-seconds ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "graph-adapter.v1", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/graph_adapter.rs", "sha256": "339536b6756cecd60b706faafb3ee9f53f8c1436ac6d2becb0448d27dc0b0322" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "testName": "public_route_usage_registry_facade_and_schemas_are_real" } }, + { "integrationId": "provider.graph-adapt", "operation": "facade", "command": "run-code-intel.ps1 -GraphAdapterRequest -GraphAdapterArtifactRoot -GraphAdapterEvaluatedAt -GraphAdapterMaxAgeSeconds ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "graph-adapter.v1", "activation": "compatibility_facade" }, "source": { "path": "crates/code-intel-cli/src/graph_adapter.rs", "sha256": "339536b6756cecd60b706faafb3ee9f53f8c1436ac6d2becb0448d27dc0b0322" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "testName": "public_route_usage_registry_facade_and_schemas_are_real" } }, + { "integrationId": "provider.graph-adapt", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec provider.graph-adapt --request --out --artifact-root ", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "provider.graph-builtin.compat", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", "sha256": "a04226fab56751d014460e1596328e503b6a9afbf2efd5e84ab7b0790831149e" }, "conformance": { "path": "crates/code-intel-cli/tests/dag_run.rs", "sha256": "14c8519a9c762c88c464c6be6a2f49a42dc78339c8eea4312e67b52e427c53b8", "testName": "production_dag_output_commits_and_enters_the_authoritative_index" } }, + { "integrationId": "graph.code-intel-understand", "operation": "refresh", "command": "target/debug/code-intel.exe graph --repo --language zh --write --json", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "internal-rust-graph", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/graph.rs", "sha256": "bc6c27f45e0d1e0ccd12546da02e221b6193cfb4a5df3d18096a6b335bf4114a" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "testName": "internal_and_external_current_outputs_share_one_port_and_provenance_schema" } }, + { "integrationId": "graph.code-intel-understand", "operation": "refreshFull", "command": "target/debug/code-intel.exe graph --repo --language zh --full --write --json", "implementationIdentity": { "providerId": "code-intel.graph", "implementationId": "internal-rust-graph", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/graph.rs", "sha256": "bc6c27f45e0d1e0ccd12546da02e221b6193cfb4a5df3d18096a6b335bf4114a" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "testName": "internal_and_external_current_outputs_share_one_port_and_provenance_schema" } }, + { "integrationId": "graph.understand-external", "operation": "refresh", "command": "/understand --language zh", "implementationIdentity": { "providerId": "understand-anything", "implementationId": "unverified-upstream", "activation": "explicit_fallback" }, "source": { "path": "docs/graph-provider-adapter.md", "sha256": "9cc96bb7bbd50b6d526dc908be730c90c96f46ed0728cbc51c6abebd7ac94d49" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "testName": "fallback_identity_and_payload_identity_cannot_be_relabelled" } }, + { "integrationId": "graph.understand-external", "operation": "refreshFull", "command": "/understand --language zh --full", "implementationIdentity": { "providerId": "understand-anything", "implementationId": "unverified-upstream", "activation": "legacy_rollback" }, "source": { "path": "docs/graph-provider-adapter.md", "sha256": "9cc96bb7bbd50b6d526dc908be730c90c96f46ed0728cbc51c6abebd7ac94d49" }, "conformance": { "path": "crates/code-intel-cli/tests/graph_adapter.rs", "sha256": "9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "testName": "fallback_identity_and_payload_identity_cannot_be_relabelled" } } + ], + "economics": { "benefit": { "metric": "B02 implementation identities exercised by swap conformance", "value": 2, "unit": "implementations" }, "cost": { "metric": "registered internal and external implementation lifecycles", "value": 2, "unit": "implementations" }, "benefitEvidence": { "evidenceIds": ["local:b02:internal-external-identity-separation", "gap:graph:representative-utility-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:b02:production-operation-traces", "gap:graph:latency-storage-cost-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:b02:replaceable-provider-port", "gap:graph:external-maintenance-status"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:b02:stale-snapshot-rejection", "gap:graph:external-security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, independently verify the external provider source, revision, license, maintenance/security status, runtime compatibility, and measured graph utility/cost; never project internal evidence onto the fallback", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:graph:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "crates/code-intel-cli/src/graph_adapter.rs", "description": "Pipeline-owned B02 port and adapter with distinct internal/external identities", "evidenceIds": ["local:b02:adapter-source-sha256:339536b6756cecd60b706faafb3ee9f53f8c1436ac6d2becb0448d27dc0b0322", "local:b02:graph-adapter-conformance:9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1"] } ], + "rollback": { "strategy": "select the declared legacy fallback behind provider.graph-adapt or retain the internal Rust provider; direct graph artifacts remain inadmissible", "evidence": { "evidenceIds": ["local:b02:explicit-fallback-only", "local:b02:replaceable-provider-port"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace either implementation independently while preserving the B02 port and stale-snapshot rejection", "replacementCriteria": ["replacement has a distinct implementation identity and source provenance", "both internal and fallback conformance fixtures pass without shared claims", "direct artifacts cannot bypass A04"], "evidence": { "evidenceIds": ["local:b02:replaceable-provider-port", "gap:graph:external-exit-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["external revision or license remains unverifiable", "fallback cannot pass current snapshot and Windows/runtime conformance", "one implementation has no callers and a replacement passes B02"], "evidence": { "evidenceIds": ["local:b02:internal-external-identity-separation", "gap:graph:retirement-proof"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:b02:graph-adapter-conformance:9e22af3f1e018dd1e55d58cf45b3b600bdf46f9a6cd9fe3ede1878416f074bc1", "local:b02:internal-operation-trace", "local:b02:external-fallback-operation-trace", "gap:graph:upstream-revision", "gap:graph:license", "gap:graph:representative-utility-measurement"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/greenfield.json b/orchestration/internalization/greenfield.json new file mode 100644 index 0000000..789ad2b --- /dev/null +++ b/orchestration/internalization/greenfield.json @@ -0,0 +1,14 @@ +{ + "schema": "code-intel-internalization-record.v1", "id": "internalization.greenfield-record", "projectId": "code-intel-pipeline", + "subject": { "name": "Pipeline-owned plan-only specification handoff; external Greenfield plugin retired", "kind": "selectively_owned_implementation", "source": { "uri": "local:Invoke-GreenfieldSpecExtraction.ps1; external-plugin-reference=https://greenfield.tools", "revision": "pipeline-owned-plan-only; external-plugin-retired; production-integration-present:false; measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420" }, "license": { "id": "PROJECT-LICENSE-PIPELINE-OWNED-PLAN-ONLY", "obligations": ["do not represent the local plan contract as execution or provenance for an external plugin", "generated specifications, tests, validation, and provenance never grant specification or implementation authority"] } }, + "adoption": { "rung": "reimplement", "ownedBoundary": ["Pipeline owns only the plan prompt, workspace layout, manifest, expected artifact locations, exclude validation, and review-required handoff", "The external plugin source, runtime, license, generated content, data effects, and value are not adopted; spec.greenfield was removed from production integration"], "necessityEvidence": { "evidenceIds": ["local:r12:pipeline-owned-plan-only", "local:r12:external-plugin-retired"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:r12:plan-only-fixture", "local:r12:auto-analyze-without-flag-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r12:plan-artifacts-2", "local:r12:explicit-analyze-fixture-1", "local:r12:production-integration-present-false"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "economics": { "benefit": { "metric": "pipeline-owned plan and explicit fixture paths", "value": 2, "unit": "paths" }, "cost": { "metric": "offline plan-contract fixture elapsed time", "value": 2127.4285, "unit": "milliseconds" }, "benefitEvidence": { "evidenceIds": ["local:r12:plan-artifacts-2", "local:r12:explicit-analyze-fixture-1", "local:r12:auto-analyze-without-flag-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:r12:fixture-elapsed-ms-2127.4285", "local:r12:external-provider-writes-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r12:plan-only-stable-contract", "local:r12:external-plugin-retired"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r12:no-auto-execution", "local:r12:no-external-plugin-production-path"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Maintain the internal plan-only contract independently; any external provider proposal starts as a new record with exact source, license, effects, reviewed generated output, value, cost, rollback, and authority", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["local:r12:pipeline-owned-plan-only"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "Invoke-GreenfieldSpecExtraction.ps1", "description": "Pipeline-owned plan-only handoff independent of the retired external plugin", "evidenceIds": ["local:r12:pipeline-owned-plan-only", "local:r12:no-auto-execution"] }], + "rollback": { "strategy": "use the local manifest and manual handoff or delete the plan helper without changing evidence, planning, specification, or implementation authority", "evidence": { "evidenceIds": ["local:r12:plan-only-handoff", "local:r12:no-auto-execution"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retain or remove the pipeline-owned plan helper independently; the external plugin integration is already deleted", "replacementCriteria": ["replacement remains implementation-independent and review-required", "external providers require separate pinned source, license, effect, value, cost, and authority evidence"], "evidence": { "evidenceIds": ["local:r12:external-plugin-retired", "local:r12:pipeline-owned-plan-only"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "completed", "triggers": ["external plugin source and license were not pinned", "real reviewed generated-spec value was not measured", "spec.greenfield external production integration was removed"], "evidence": { "evidenceIds": ["local:r12:external-plugin-retired", "local:r12:production-integration-present-false"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r12:pipeline-owned-plan-only", "local:r12:external-plugin-retired", "local:r12:production-integration-present-false", "gap:greenfield:future-external-provider-authority"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/gstack.json b/orchestration/internalization/gstack.json new file mode 100644 index 0000000..2216d70 --- /dev/null +++ b/orchestration/internalization/gstack.json @@ -0,0 +1,22 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.gstack-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "gstack advisory candidate", "kind": "design_reference", "source": { "uri": "unverified-upstream:gstack; local-reference=docs/openspec-detector.md", "revision": "unverified-upstream; local-doc-sha256:02d4b04482a1bdb3650a4bd27e500a79ec675dc86ecbacb4780a9de54a710d2a" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until canonical upstream URI, exact revision, and license are verified", "retain the local reference provenance and gap notice"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned delivery and quality advisory candidate data only", "no gstack runtime dependency", "no skill execution, deployment, commit, publish, or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-capability-map:gstack-row", "gap:gstack:canonical-source", "gap:gstack:upstream-revision", "gap:gstack:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b06:proposal-effects-empty", "local:b06:no-execution-authority"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:atom:one-gstack-branch", "local:b06:a01-envelope-parity"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "gstack candidate branches traced in the advisory atom", "value": 1, "unit": "branches" }, "cost": { "metric": "pipeline-owned gstack lifecycle records", "value": 1, "unit": "records" }, "benefitEvidence": { "evidenceIds": ["local:atom:one-gstack-branch"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:gstack"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:gstack:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:gstack:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, establish canonical source URI, exact revision, license, release status, and relevant workflow semantics", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:gstack:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "OpenSpec-Detector.ps1", "description": "Pipeline-owned gstack delivery/quality recommendation branch; does not execute gstack", "evidenceIds": ["local:atom:one-gstack-branch", "local:b06:no-execution-authority"] } ], + "rollback": { "strategy": "delete the gstack candidate record and alternative while preserving facts, plan authority, and proposal schema", "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace with a pinned licensed delivery/quality candidate or retire the reference", "replacementCriteria": ["canonical provenance and license are verified", "replacement passes conformance, measurement, and zero-execution-authority tests"], "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["canonical source cannot be established", "candidate adds no unique measured advisory value", "execution-authority isolation regresses"], "evidence": { "evidenceIds": ["local:record:gstack"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:gstack", "gap:gstack:canonical-source", "gap:gstack:upstream-revision", "gap:gstack:license"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/linear.json b/orchestration/internalization/linear.json new file mode 100644 index 0000000..e91d5f3 --- /dev/null +++ b/orchestration/internalization/linear.json @@ -0,0 +1,8 @@ +{ + "schema":"code-intel-internalization-record.v1","id":"internalization.linear-record","projectId":"code-intel-pipeline","subject":{"name":"Linear optional project-state projection","kind":"design_reference","source":{"uri":"https://linear.app; local-policy=docs/project-management-support.md","revision":"service-api-revision-unverified; local-policy-sha256:d03a5535198f3e45f9572b7f9bb4a23e77ba636c751636d8c697166ea5a5c9c3; local-boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba"},"license":{"id":"PROPRIETARY-SERVICE-TERMS-UNVERIFIED","obligations":["no connector install, credential storage, API call, issue creation, or status mutation is authorized by this record","review applicable service/API terms before any separately approved projection"]}}, + "authorityRequirements":{"repositoryGovernedAttestation":true}, + "adoption":{"rung":"invoke","ownedBoundary":["reference-only optional projection outside scanner runtime","exactly one mutable task-state authority; scanner artifacts remain engineering evidence authority; credentials remain user-scoped"],"necessityEvidence":{"evidenceIds":["local:r23:no-current-use","gap:linear:scope-authority"],"checkedAt":1783900800,"expiresAt":1791676800},"compatibilityEvidence":{"evidenceIds":["local:r23:no-scanner-write","local:r23:single-authority-policy"],"checkedAt":1783900800,"expiresAt":1791676800},"conformanceEvidence":{"evidenceIds":["local:r23:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba","gap:linear:approved-projection-conformance"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "economics":{"benefit":{"metric":"current scanner operations requiring Linear","value":0,"unit":"operations"},"cost":{"metric":"unauthorized external mutable surfaces avoided","value":1,"unit":"services"},"benefitEvidence":{"evidenceIds":["local:r23:no-current-use"],"checkedAt":1783900800,"expiresAt":1791676800},"costEvidence":{"evidenceIds":["local:r23:no-credential-storage","gap:linear:service-cost-review"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "assurance":{"maintenanceEvidence":{"evidenceIds":["local:r23:no-runtime-dependency","gap:linear:api-update-review"],"checkedAt":1783900800,"expiresAt":1791676800},"securityEvidence":{"evidenceIds":["local:r23:no-credential-storage","local:r23:no-external-write","gap:linear:data-security-review"],"checkedAt":1783900800,"expiresAt":1791676800}},"update":{"policy":"Keep out of scanner scope; review only after an explicit project-authority request with service terms, credentials, effects, quotas, and migration plan","nextCheckAt":1791676800,"evidence":{"evidenceIds":["gap:linear:scope-authority"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "ownedModifications":[{"path":"docs/project-management-support.md","description":"Pipeline-owned no-runtime, single-authority, and optional-projection policy","evidenceIds":["local:r23:policy-sha256:d03a5535198f3e45f9572b7f9bb4a23e77ba636c751636d8c697166ea5a5c9c3","local:r23:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba"]}],"rollback":{"strategy":"remove Linear references or stop projection without changing scanner artifacts or task state authority","evidence":{"evidenceIds":["local:r23:no-current-use"],"checkedAt":1783900800,"expiresAt":1791676800}},"exit":{"strategy":"remain out of scope or migrate an explicitly approved projection while preserving one task authority","replacementCriteria":["authority event names the sole task-state owner","credentials/effects and migration are separately approved","scanner artifacts remain authoritative technical evidence"],"evidence":{"evidenceIds":["gap:linear:scope-authority"],"checkedAt":1783900800,"expiresAt":1791676800}},"retirement":{"status":"candidate","triggers":["no approved project projection exists","service terms, quota, or security are unacceptable","local Git-backed work control remains sufficient"],"evidence":{"evidenceIds":["local:r23:no-current-use"],"checkedAt":1783900800,"expiresAt":1791676800}},"lifecycle":{"previousStatus":null,"status":"out_of_scope","effectiveAt":1783900800,"replacementRecordId":null,"evidenceIds":["local:r23:no-current-use","gap:linear:scope-authority","gap:linear:service-api-revision","gap:linear:service-terms"],"authorityEvent":{"schema":"code-intel-authority-event.v1","id":"authority.internalization.linear.out-of-scope","decision":"approved","approver":{"id":"code-intel-maintainers","role":"repository_governance"},"evidenceIds":["local:r23:no-current-use","gap:linear:scope-authority","gap:linear:service-api-revision","gap:linear:service-terms"],"issuedAt":1783900800,"expiresAt":1791676800,"attestation":{"scheme":"repository-governed-sha256-v1","digest":"1ea73e32dc2419c3d0f164e9bb1a438998e3b2cf69188417783270b1566b57c0"}}},"provenance":{"recordedAt":1783900800,"recordedBy":"dependency-expert"} +} diff --git a/orchestration/internalization/llm-wiki.json b/orchestration/internalization/llm-wiki.json new file mode 100644 index 0000000..c948e84 --- /dev/null +++ b/orchestration/internalization/llm-wiki.json @@ -0,0 +1,187 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.llm-wiki-record", + "projectId": "code-intel-pipeline", + "authorityRequirements": { + "repositoryGovernedAttestation": true + }, + "subject": { + "name": "LLM Wiki optional generated knowledge projection", + "kind": "design_reference", + "source": { + "uri": "unbound-provider:llm-wiki; local-policy=docs/project-management-support.md", + "revision": "model-and-provider-unbound; local-policy-sha256:d03a5535198f3e45f9572b7f9bb4a23e77ba636c751636d8c697166ea5a5c9c3; local-boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba" + }, + "license": { + "id": "UNKNOWN-PROVIDER-AND-MODEL-TERMS", + "obligations": [ + "no model/provider call, data upload, generated fact promotion, wiki write, or UI dependency is authorized", + "bind provider, model, terms, data policy, and output provenance before any separately approved use" + ] + } + }, + "adoption": { + "rung": "invoke", + "ownedBoundary": [ + "non-authoritative view-only concept outside scanner runtime", + "generated wiki text is never Observed Evidence or Engineering Fact without independent admitted source evidence" + ], + "necessityEvidence": { + "evidenceIds": [ + "local:r25:no-current-use", + "gap:llm-wiki:scope-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "compatibilityEvidence": { + "evidenceIds": [ + "local:r25:no-runtime-provider", + "local:r25:artifact-authority-isolation" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "conformanceEvidence": { + "evidenceIds": [ + "local:r25:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba", + "gap:llm-wiki:approved-view-conformance" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "economics": { + "benefit": { + "metric": "current required generated wiki views", + "value": 0, + "unit": "views" + }, + "cost": { + "metric": "model/provider runtime dependencies introduced", + "value": 0, + "unit": "dependencies" + }, + "benefitEvidence": { + "evidenceIds": [ + "local:r25:no-current-use" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "costEvidence": { + "evidenceIds": [ + "local:r25:no-model-runtime", + "gap:llm-wiki:model-cost-measurement" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "assurance": { + "maintenanceEvidence": { + "evidenceIds": [ + "local:r25:no-runtime-provider", + "gap:llm-wiki:model-update-policy" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "securityEvidence": { + "evidenceIds": [ + "local:r25:no-data-upload", + "gap:llm-wiki:data-classification-review", + "gap:llm-wiki:provider-security-review" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "update": { + "policy": "Remain provider/model agnostic and outside runtime; any proposal must bind model/version, data effects, provenance, cost, security, authority, and deletion", + "nextCheckAt": 1791676800, + "evidence": { + "evidenceIds": [ + "gap:llm-wiki:scope-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "ownedModifications": [ + { + "path": "docs/project-management-support.md", + "description": "Pipeline-owned non-authoritative knowledge-view boundary", + "evidenceIds": [ + "local:r25:policy-sha256:d03a5535198f3e45f9572b7f9bb4a23e77ba636c751636d8c697166ea5a5c9c3", + "local:r25:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba" + ] + } + ], + "rollback": { + "strategy": "delete generated projections and retain source docs/artifacts; no model state is required by Pipeline", + "evidence": { + "evidenceIds": [ + "local:r25:no-current-use" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "exit": { + "strategy": "remain out of scope or replace with a provider-independent read-only projection", + "replacementCriteria": [ + "generated text cannot promote itself to fact", + "provider/model/data/security/effects are explicit", + "source artifacts survive deletion of the entire view" + ], + "evidence": { + "evidenceIds": [ + "local:r25:artifact-authority-isolation" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "no approved measured use exists", + "provider/model provenance is unavailable", + "data or hallucination risk exceeds benefit" + ], + "evidence": { + "evidenceIds": [ + "local:r25:no-current-use" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "lifecycle": { + "previousStatus": null, + "status": "out_of_scope", + "effectiveAt": 1783900800, + "replacementRecordId": null, + "evidenceIds": [ + "local:r25:no-current-use", + "gap:llm-wiki:scope-authority", + "gap:llm-wiki:model-provider", + "gap:llm-wiki:terms" + ], + "authorityEvent": { + "schema": "code-intel-authority-event.v1", + "id": "authority.internalization.llm-wiki.out-of-scope", + "decision": "approved", + "approver": { "id": "code-intel-maintainers", "role": "repository_governance" }, + "evidenceIds": ["local:r25:no-current-use", "gap:llm-wiki:scope-authority", "gap:llm-wiki:model-provider", "gap:llm-wiki:terms"], + "issuedAt": 1783900800, + "expiresAt": 1791676800, + "attestation": { "scheme": "repository-governed-sha256-v1", "digest": "5477b6ee31d771e93376487f5cfe2e678c5bc8b241eb5006679d8548a9c0c261" } + } + }, + "provenance": { + "recordedAt": 1783900800, + "recordedBy": "dependency-expert" + } +} diff --git a/orchestration/internalization/matt-flow.json b/orchestration/internalization/matt-flow.json new file mode 100644 index 0000000..5eff7fb --- /dev/null +++ b/orchestration/internalization/matt-flow.json @@ -0,0 +1,22 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.matt-flow-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "matt-flow absorbed advisory concept", "kind": "design_reference", "source": { "uri": "https://github.com/mattpocock/skills", "revision": "unverified-upstream; local-doc-sha256:02d4b04482a1bdb3650a4bd27e500a79ec675dc86ecbacb4780a9de54a710d2a" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until exact upstream revision and license text are verified", "retain attribution for the locally documented idea-to-ship concept trace"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned idea-to-ship advisory concept and repository-signal rules", "no mattpocock/skills runtime dependency", "no issue creation, implementation, commit, or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:docs:matt-flow-source-trace", "gap:matt-flow:upstream-revision", "gap:matt-flow:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b06:proposal-effects-empty", "local:b06:no-external-write-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:atom:one-matt-flow-branch", "local:b06:a01-envelope-parity"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "matt-flow candidate branches traced in the advisory atom", "value": 1, "unit": "branches" }, "cost": { "metric": "pipeline-owned matt-flow lifecycle records", "value": 1, "unit": "records" }, "benefitEvidence": { "evidenceIds": ["local:atom:one-matt-flow-branch"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:matt-flow"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:matt-flow:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:matt-flow:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify source revision, license, current skill semantics, and retained concept trace", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:matt-flow:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "OpenSpec-Detector.ps1", "description": "Pipeline-owned matt-flow candidate behavior derived from local repository signals", "evidenceIds": ["local:atom:one-matt-flow-branch", "local:b06:no-external-write-boundary"] } ], + "rollback": { "strategy": "remove matt-flow candidate data while preserving the stable advisory schema and other alternatives", "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace the concept source with a pinned licensed reference or retire the source coupling", "replacementCriteria": ["source-to-internal-concept trace remains behaviorally tested", "replacement passes measurement and zero-external-effect checks"], "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["concept trace cannot be tied to a verified source", "candidate behavior duplicates another measured alternative", "maintenance or security review remains missing"], "evidence": { "evidenceIds": ["local:record:matt-flow"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:matt-flow", "gap:matt-flow:upstream-revision", "gap:matt-flow:license"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/mattpocock-skills.json b/orchestration/internalization/mattpocock-skills.json new file mode 100644 index 0000000..a8bb25d --- /dev/null +++ b/orchestration/internalization/mattpocock-skills.json @@ -0,0 +1,27 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.mattpocock-skills-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "mattpocock/skills absorbed project-management concepts", "kind": "selectively_owned_implementation", "source": { "uri": "https://github.com/mattpocock/skills", "revision": "unverified-upstream; local-adr-sha256:1431a4dfbe2462f67ea541842000e1c9f94c2cb71b1b168ae558b332245d97ec" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until exact upstream revision and license text are verified", "retain attribution for issue, triage, domain-documentation, and workflow concept traces"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["issue-tracker choice contract", "triage label vocabulary", "domain-documentation layout", "idea-to-ship workflow advisory concept", "no mattpocock/skills runtime dependency or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:adr-0006:absorbed-concepts", "gap:mattpocock-skills:upstream-revision", "gap:mattpocock-skills:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:project-management-support:no-runtime", "local:b06:no-external-write-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:trace:issue-choice", "local:trace:triage-vocabulary", "local:trace:domain-doc-layout", "local:trace:workflow-concept"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "retained concepts independently traced to pipeline-owned boundaries", "value": 4, "unit": "concepts" }, "cost": { "metric": "pipeline-owned concept trace entries", "value": 4, "unit": "entries" }, "benefitEvidence": { "evidenceIds": ["local:trace:issue-choice", "local:trace:triage-vocabulary", "local:trace:domain-doc-layout", "local:trace:workflow-concept"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:mattpocock-skills"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:mattpocock-skills:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:mattpocock-skills:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify upstream revision/license and recheck each retained concept; delete any concept without behavioral evidence", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:mattpocock-skills:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ + { "path": "docs/adr/0006-project-management-support-as-agent-intake.md", "description": "Pipeline-owned issue-tracker choice boundary", "evidenceIds": ["local:trace:issue-choice"] }, + { "path": "docs/project-management-support.md", "description": "Pipeline-owned triage vocabulary boundary", "evidenceIds": ["local:trace:triage-vocabulary"] }, + { "path": "docs/project-management-support.md", "description": "Pipeline-owned domain-documentation layout boundary", "evidenceIds": ["local:trace:domain-doc-layout"] }, + { "path": "docs/openspec-detector.md", "description": "Pipeline-owned idea-to-ship workflow advisory concept", "evidenceIds": ["local:trace:workflow-concept", "local:b06:no-external-write-boundary"] } + ], + "rollback": { "strategy": "remove individual unused source references while retaining pipeline-owned project-management contracts and audit history", "evidence": { "evidenceIds": ["local:reference-map:replace-independently"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retire the source reference independently; retain only concepts with local behavioral contracts or replace their provenance", "replacementCriteria": ["every retained concept has an independent local contract and conformance test", "unused concepts are deleted", "no external-write authority is introduced"], "evidence": { "evidenceIds": ["local:reference-map:replace-independently"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["upstream revision/license remains unverifiable", "a retained concept lacks independent evidence", "the source reference creates runtime or external-write coupling"], "evidence": { "evidenceIds": ["local:record:mattpocock-skills"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:mattpocock-skills", "gap:mattpocock-skills:upstream-revision", "gap:mattpocock-skills:license"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/metaharness.json b/orchestration/internalization/metaharness.json new file mode 100644 index 0000000..6951b3a --- /dev/null +++ b/orchestration/internalization/metaharness.json @@ -0,0 +1,22 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.metaharness-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "MetaHarness packaging and distribution design reference", "kind": "design_reference", "source": { "uri": "unverified-upstream:MetaHarness; local-reference=docs/harness-factory-reference.md", "revision": "unverified-upstream; local-doc-sha256:de64db6ed990b262748e3e4c00d0de588fa778831dd5f021762c203c63bed87b" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until canonical upstream URI, exact revision, and license text are verified", "do not retain reference-only status past the review expiry without an approved authority event"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned future harness concerns: branded CLI, host bundles, release gates, SBOM/provenance, static analysis, and team package layout", "reference-only distribution guidance; no MetaHarness runtime dependency, scanner facts, package generation, release signing, publishing, or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-map:metaharness", "local:harness-reference:six-design-concerns", "gap:metaharness:upstream-revision", "gap:metaharness:license", "gap:metaharness:retention-authority"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:harness-reference:outside-scanner", "local:adr-0003:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:trace:metaharness:branded-cli", "local:trace:metaharness:host-bundles", "local:trace:metaharness:release-gates", "local:trace:metaharness:sbom-provenance", "local:trace:metaharness:static-analysis", "local:trace:metaharness:package-layout", "gap:metaharness:upstream-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "locally documented harness design concerns", "value": 6, "unit": "concerns" }, "cost": { "metric": "pipeline-owned harness semantic trace entries", "value": 6, "unit": "entries" }, "benefitEvidence": { "evidenceIds": ["local:harness-reference:six-design-concerns"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:metaharness"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:metaharness:update-check", "gap:metaharness:retention-authority"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:metaharness:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "By 2026-10-11, obtain an authority-approved retain, retire, or out-of-scope decision and verify source revision/license; absent both, retire the reference", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:metaharness:update-check", "gap:metaharness:retention-authority"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "docs/harness-factory-reference.md", "description": "Pipeline-owned future packaging concerns and explicit scanner isolation", "evidenceIds": ["local:harness-reference:six-design-concerns", "local:harness-reference:outside-scanner"] } ], + "rollback": { "strategy": "delete the MetaHarness name and lifecycle record; retain stable scanner, artifact-consumer, intake, and release contracts", "evidence": { "evidenceIds": ["local:adr-0003:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retire, mark out of scope, or replace the reference without changing scanner behavior", "replacementCriteria": ["authority approves retain, retire, or out-of-scope state before evidence expiry", "replacement source and license are verified", "removal proof confirms no runtime invocation or scanner contract change"], "evidence": { "evidenceIds": ["local:adr-0003:reference-removable", "gap:metaharness:retention-authority"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["no authority-approved retention decision by evidence expiry", "upstream source or license remains unverifiable", "distribution reference creates runtime coupling or no longer has measured design value"], "evidence": { "evidenceIds": ["local:record:metaharness", "gap:metaharness:retention-authority"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:metaharness", "gap:metaharness:upstream-revision", "gap:metaharness:license", "gap:metaharness:retention-authority", "gap:metaharness:upstream-conformance"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/mindwalk.json b/orchestration/internalization/mindwalk.json new file mode 100644 index 0000000..99c7832 --- /dev/null +++ b/orchestration/internalization/mindwalk.json @@ -0,0 +1,217 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.mindwalk-session-evidence-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "Mindwalk session evidence adapter and selectively owned normalization", + "kind": "selectively_owned_implementation", + "source": { + "uri": "https://github.com/cosmtrek/mindwalk", + "revision": "e208b6b8504138843f671e031f28129b66003a67" + }, + "license": { + "id": "MIT", + "obligations": [ + "retain the upstream copyright and MIT permission notice if Mindwalk source is copied or distributed", + "keep Pipeline-owned reimplementation distinct from upstream parser, storage, UI, and LLM behavior" + ] + } + }, + "adoption": { + "rung": "adapt", + "ownedBoundary": [ + "Pipeline owns snapshot binding, repository-relative path normalization, privacy reduction, Sentrux joins, advisory signals, schema, and conformance tests", + "Mindwalk remains an optional trace producer; its parser, Go implementation, persistence, UI, summaries, and LLM workflow are not vendored or required", + "session evidence is advisory only and is not a fact source, gate, or automatic scan dependency" + ], + "necessityEvidence": { + "evidenceIds": [ + "local:mindwalk:prototype-real-session-117-joins", + "local:mindwalk:production-smoke-19-matched-targets" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + }, + "compatibilityEvidence": { + "evidenceIds": [ + "local:mindwalk:trace-v1-compatible-e208b6b8504138843f671e031f28129b66003a67", + "gap:mindwalk:future-trace-schema-compatibility" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + }, + "conformanceEvidence": { + "evidenceIds": [ + "local:mindwalk:adapter-source-sha256:fff59db3f54abee633b7174704c912c9bccf06c83afae1272e6bb1159eff1cb6", + "local:mindwalk:adapter-tests-sha256:f6d3e4eb4e3b23208017b2591e73eee9adf2f7abcc5721b23e50d57a72875977", + "local:mindwalk:production-smoke-sha256:33c3c0c28e3c6b7d06ff53043606641c38c3a4890edcd3f2c7bddfc0f737aa45" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "operationTrace": [ + { + "integrationId": "provider.session-adapt", + "operation": "adapt", + "command": "target/debug/code-intel.exe provider session-adapt --repo --trace [--hotspots ] [--out ]", + "implementationIdentity": { + "providerId": "mindwalk", + "implementationId": "code-intel.session-evidence.rust.v1", + "activation": "optional_advisory" + }, + "source": { + "path": "crates/code-intel-cli/src/session_evidence.rs", + "sha256": "fff59db3f54abee633b7174704c912c9bccf06c83afae1272e6bb1159eff1cb6" + }, + "conformance": { + "path": "crates/code-intel-cli/tests/session_evidence.rs", + "sha256": "f6d3e4eb4e3b23208017b2591e73eee9adf2f7abcc5721b23e50d57a72875977", + "testName": "normalizes_private_trace_and_joins_structural_evidence" + } + } + ], + "economics": { + "benefit": { + "metric": "repository targets joined to structural evidence in representative production smoke", + "value": 19, + "unit": "targets" + }, + "cost": { + "metric": "normalized representative session evidence artifact size", + "value": 38415, + "unit": "bytes" + }, + "benefitEvidence": { + "evidenceIds": [ + "local:mindwalk:production-smoke-19-matched-targets", + "local:mindwalk:production-smoke-11-structural-attention-touches" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + }, + "costEvidence": { + "evidenceIds": [ + "local:mindwalk:production-smoke-artifact-bytes-38415", + "gap:mindwalk:representative-latency-and-maintenance-cost" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "assurance": { + "maintenanceEvidence": { + "evidenceIds": [ + "local:mindwalk:single-rust-adapter-boundary", + "gap:mindwalk:future-trace-schema-compatibility" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + }, + "securityEvidence": { + "evidenceIds": [ + "local:mindwalk:no-raw-marks-summaries-or-absolute-paths", + "local:mindwalk:regular-file-symlink-and-size-guards", + "gap:mindwalk:hostile-trace-fuzzing" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "update": { + "policy": "Before 2026-10-21, recheck Mindwalk trace schema and license, run hostile-trace coverage, and measure representative latency and value before considering automatic invocation or production authority", + "nextCheckAt": 1792524981, + "evidence": { + "evidenceIds": [ + "gap:mindwalk:future-trace-schema-compatibility", + "gap:mindwalk:hostile-trace-fuzzing", + "gap:mindwalk:representative-latency-and-maintenance-cost" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "ownedModifications": [ + { + "path": "crates/code-intel-cli/src/session_evidence.rs", + "description": "Pipeline-owned native Rust privacy, snapshot, normalization, structural join, and advisory signal boundary", + "evidenceIds": [ + "local:mindwalk:adapter-source-sha256:fff59db3f54abee633b7174704c912c9bccf06c83afae1272e6bb1159eff1cb6" + ] + }, + { + "path": "orchestration/schemas/code-intel-session-evidence.v1.schema.json", + "description": "Pipeline-owned normalized session evidence contract", + "evidenceIds": [ + "local:mindwalk:session-evidence-schema-v1" + ] + }, + { + "path": "orchestration/integrations.json", + "description": "Optional provider.session-adapt registry declaration without automatic scan invocation", + "evidenceIds": [ + "local:registry:provider.session-adapt" + ] + } + ], + "rollback": { + "strategy": "disable or remove provider.session-adapt while retaining the existing Code Intel scan and Sentrux structure workflow; no default scan path depends on this adapter", + "evidence": { + "evidenceIds": [ + "local:mindwalk:optional-provider-boundary", + "local:mindwalk:no-default-scan-dependency" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "exit": { + "strategy": "replace the external trace producer or delete the adapter without changing the normalized evidence contract or core scan workflow", + "replacementCriteria": [ + "replacement emits bounded event, target, error, edit, and verification observability grades", + "replacement passes privacy, path containment, snapshot identity, non-overwrite, and structural join conformance", + "replacement remains optional until separately approved for automatic invocation" + ], + "evidence": { + "evidenceIds": [ + "local:mindwalk:provider-neutral-session-evidence-contract", + "local:mindwalk:adapter-conformance-tests" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "representative sessions show no actionable value beyond existing Code Intel artifacts", + "Mindwalk trace compatibility or privacy cannot be maintained within the bounded adapter", + "an internal trace producer makes the external Mindwalk adapter redundant" + ], + "evidence": { + "evidenceIds": [ + "local:mindwalk:optional-provider-boundary", + "gap:mindwalk:representative-latency-and-maintenance-cost" + ], + "checkedAt": 1784748981, + "expiresAt": 1792524981 + } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1784748981, + "replacementRecordId": null, + "evidenceIds": [ + "local:mindwalk:adapter-conformance-tests", + "local:mindwalk:production-smoke-19-matched-targets", + "gap:mindwalk:hostile-trace-fuzzing", + "gap:mindwalk:representative-latency-and-maintenance-cost" + ], + "authorityEvent": null + }, + "provenance": { + "recordedAt": 1784748981, + "recordedBy": "code-intel-maintainer" + } +} diff --git a/orchestration/internalization/my-code-machine.json b/orchestration/internalization/my-code-machine.json new file mode 100644 index 0000000..200503b --- /dev/null +++ b/orchestration/internalization/my-code-machine.json @@ -0,0 +1,204 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.my-code-machine-record", + "projectId": "code-intel-pipeline", + "authorityRequirements": { + "repositoryGovernedAttestation": true + }, + "subject": { + "name": "my-code-machine proposed host-control reference", + "kind": "design_reference", + "source": { + "uri": "unverified-local-external-project:my-code-machine; local-proposal=docs/adr/0001-merge-mcm-rust-unified.md", + "revision": "external-project-revision-and-license-unverified; local-merge-proposal-sha256:5c547087668014faf95dd0f4501ef7d384994d8250456ba25d6720be0f7dd1d8; local-no-big-bang-adr-sha256:1c8800fe6bbc5032d325a5e6ce2a93774e9c7fdc17143251c4a5c44ff5bb774d" + }, + "license": { + "id": "UNKNOWN-RESEARCH-ONLY", + "obligations": [ + "no source import, merge, rewrite, host mutation, sync, credential access, or migration is authorized", + "verify external source revision/license and split every effectful capability into separately approved parity tickets" + ] + } + }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": [ + "decision record for a possible separate machine/sync boundary only", + "current external Python project retains implementation; Pipeline scanner remains repository-read oriented and owns no host cleanup or sync effects" + ], + "necessityEvidence": { + "evidenceIds": [ + "local:r26:proposal-reconciliation", + "gap:my-code-machine:adoption-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "compatibilityEvidence": { + "evidenceIds": [ + "local:r26:no-big-bang-boundary", + "gap:my-code-machine:atom-parity", + "gap:my-code-machine:rollback-drill" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "conformanceEvidence": { + "evidenceIds": [ + "local:r26:no-machine-crate", + "gap:my-code-machine:effect-contracts", + "gap:my-code-machine:capability-measurement" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "economics": { + "benefit": { + "metric": "currently proven host capabilities internalized", + "value": 0, + "unit": "capabilities" + }, + "cost": { + "metric": "unclosed migration gates", + "value": 6, + "unit": "gaps" + }, + "benefitEvidence": { + "evidenceIds": [ + "local:r26:no-machine-crate", + "gap:my-code-machine:capability-measurement" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "costEvidence": { + "evidenceIds": [ + "gap:my-code-machine:source-revision", + "gap:my-code-machine:license", + "gap:my-code-machine:atom-parity", + "gap:my-code-machine:effect-contracts", + "gap:my-code-machine:security-review", + "gap:my-code-machine:rollback-drill" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "assurance": { + "maintenanceEvidence": { + "evidenceIds": [ + "local:r26:external-tool-remains-separate", + "gap:my-code-machine:update-review" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "securityEvidence": { + "evidenceIds": [ + "local:r26:no-host-mutation-authority", + "gap:my-code-machine:credential-and-sync-security", + "gap:my-code-machine:effect-contracts" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "update": { + "policy": "Defer adoption; before any atom ticket verify source/license, measure one capability, define effects/authority, prove parity and rollback, then request an explicit A05 decision", + "nextCheckAt": 1791676800, + "evidence": { + "evidenceIds": [ + "gap:my-code-machine:adoption-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "ownedModifications": [ + { + "path": "docs/adr/0001-merge-mcm-rust-unified.md", + "description": "Historical merge proposal retained as input, not current implementation authority", + "evidenceIds": [ + "local:r26:merge-proposal-sha256:5c547087668014faf95dd0f4501ef7d384994d8250456ba25d6720be0f7dd1d8" + ] + }, + { + "path": "docs/adr/0010-tool-neutral-engineering-intelligence-core.md", + "description": "Current no-big-bang, atom-by-atom boundary governing any future migration", + "evidenceIds": [ + "local:r26:no-big-bang-adr-sha256:1c8800fe6bbc5032d325a5e6ce2a93774e9c7fdc17143251c4a5c44ff5bb774d" + ] + } + ], + "rollback": { + "strategy": "keep the current external tool separate; abandon any proposed atom without altering scanner contracts or external state", + "evidence": { + "evidenceIds": [ + "local:r26:external-tool-remains-separate", + "gap:my-code-machine:rollback-drill" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "exit": { + "strategy": "close the merge proposal as deferred/out of scope, or adopt only separately verified atoms behind machine/sync ports", + "replacementCriteria": [ + "each atom has parity, effect authority, security review, measurement, rollback, and retirement", + "no big-bang rewrite or shared scanner/host-mutation boundary", + "external tool remains recoverable until the adopted atom passes exit drill" + ], + "evidence": { + "evidenceIds": [ + "local:r26:no-big-bang-boundary", + "gap:my-code-machine:adoption-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "authority formally defers or rejects adoption", + "measured value does not exceed migration/security cost", + "external project provenance or effect safety cannot be verified" + ], + "evidence": { + "evidenceIds": [ + "local:r26:no-machine-crate", + "gap:my-code-machine:adoption-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "lifecycle": { + "previousStatus": null, + "status": "out_of_scope", + "effectiveAt": 1783900800, + "replacementRecordId": null, + "evidenceIds": [ + "local:r26:no-machine-crate", + "local:r26:no-big-bang-boundary", + "gap:my-code-machine:source-revision", + "gap:my-code-machine:license", + "gap:my-code-machine:adoption-authority" + ], + "authorityEvent": { + "schema": "code-intel-authority-event.v1", + "id": "authority.internalization.my-code-machine.out-of-scope", + "decision": "approved", + "approver": { "id": "code-intel-maintainers", "role": "repository_governance" }, + "evidenceIds": ["local:r26:no-machine-crate", "local:r26:no-big-bang-boundary", "gap:my-code-machine:source-revision", "gap:my-code-machine:license", "gap:my-code-machine:adoption-authority"], + "issuedAt": 1783900800, + "expiresAt": 1791676800, + "attestation": { "scheme": "repository-governed-sha256-v1", "digest": "bb5054d2b81e03dc7c13878a49fbd81919bb0e58d913258249b02b8688980d40" } + } + }, + "provenance": { + "recordedAt": 1783900800, + "recordedBy": "dependency-expert/architect" + } +} diff --git a/orchestration/internalization/native-code-evidence.json b/orchestration/internalization/native-code-evidence.json new file mode 100644 index 0000000..2588a11 --- /dev/null +++ b/orchestration/internalization/native-code-evidence.json @@ -0,0 +1,15 @@ +{ + "schema": "code-intel-internalization-record.v1", "id": "internalization.native-code-evidence-record", "projectId": "code-intel-pipeline", + "subject": { "name": "Native Code Evidence deterministic baseline", "kind": "selectively_owned_implementation", "source": { "uri": "local:crates/code-intel-cli/src/native_code_evidence.rs", "revision": "pipeline-owned-v1; local-runtime-sha256:c47c86ee56ba5bb9a16cfd5c8742b70f0e9a64d6da1f83289a0e65818defffa5; local-conformance-sha256:08c5d7f3bc9b5cc65fa8dcb9ebbc9eb9d7359b56c070ad9e4516254d09bd7dcb; measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420" }, "license": { "id": "PROJECT-LICENSE-PIPELINE-OWNED", "obligations": ["retain repository license and provenance for the owned implementation", "never relabel line heuristics as parser, call-graph, semantic, or framework precision"] } }, + "adoption": { "rung": "reimplement", "ownedBoundary": ["Pipeline owns deterministic file, line-heuristic symbol, file-chunk, containment, heuristic import, scorecard, and agent-slice artifacts", "Pipeline does not claim AST parsing, dynamic calls, cross-file call graph, framework semantics, embeddings, or cocoindex behavior"], "necessityEvidence": { "evidenceIds": ["local:b07:evidence.native-code-required", "local:b08:legacy-parity"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:b08:a01-a03-a09-artifact-parity", "local:r06:unsupported-language-unknown"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r06:operation-trace", "local:b08:native-contract-tests", "local:r06:labeled-corpus-samples-12", "local:r06:precision-0.75", "local:r06:recall-0.75", "local:r06:supported-coverage-0.833333"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "operationTrace": [{ "integrationId": "evidence.native-code", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec evidence.native-code --request --out ", "implementationIdentity": { "providerId": "code-intel-pipeline", "implementationId": "evidence.native-code.compat-1.0.0", "activation": "required deterministic production baseline" }, "source": { "path": "crates/code-intel-cli/src/native_code_evidence.rs", "sha256": "c47c86ee56ba5bb9a16cfd5c8742b70f0e9a64d6da1f83289a0e65818defffa5" }, "conformance": { "path": "crates/code-intel-cli/tests/native_code_evidence.rs", "sha256": "08c5d7f3bc9b5cc65fa8dcb9ebbc9eb9d7359b56c070ad9e4516254d09bd7dcb", "testName": "labeled_multilingual_corpus_quantifies_native_symbol_precision_recall_and_coverage" } }], + "economics": { "benefit": { "metric": "B08 normalized legacy parity artifacts", "value": 6, "unit": "artifacts" }, "cost": { "metric": "four-test B08 fixture suite elapsed time", "value": 27110, "unit": "milliseconds" }, "benefitEvidence": { "evidenceIds": ["local:b08:normalized-parity-artifacts-6", "local:r06:artifact-refs-8", "local:r06:fixture-files-2", "local:r06:labeled-corpus-samples-12", "local:r06:true-positive-6", "local:r06:false-positive-2", "local:r06:false-negative-2"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:r06:suite-elapsed-ms-27110", "local:r06:labeled-corpus-elapsed-ms-3620", "gap:native-code:framework-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r06:pipeline-owned-toolchain-digest", "local:b08:contract-tests"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r06:repo-read-local-write-only", "local:r06:no-network-provider"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Any heuristic or artifact change must refresh the toolchain digest, rerun B08 parity and deterministic replay, and publish precision/cost measurements before widening claims", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["local:r06:toolchain-digest-policy", "gap:native-code:measurement-refresh"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "crates/code-intel-cli/src/native_code_evidence.rs", "description": "Pipeline-owned deterministic baseline with explicit line-heuristic labels and a manually labeled representative corpus", "evidenceIds": ["local:r06:runtime-sha256:c47c86ee56ba5bb9a16cfd5c8742b70f0e9a64d6da1f83289a0e65818defffa5", "local:r06:conformance-sha256:08c5d7f3bc9b5cc65fa8dcb9ebbc9eb9d7359b56c070ad9e4516254d09bd7dcb", "local:r06:labeled-corpus-samples-12"] }], + "rollback": { "strategy": "route through the preserved legacy producer while retaining the registered artifact contract and explicit unknown semantics", "evidence": { "evidenceIds": ["local:b08:legacy-parity", "local:r06:compatibility-route"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace the baseline only behind evidence.native-code after exact contract, failure, determinism, and measurement parity", "replacementCriteria": ["replacement preserves A01, A03, A09 and artifact identities", "precision and recall improve over the pinned 0.75/0.75 labeled-corpus baseline without hidden network or model effects", "legacy rollback and deterministic replay remain tested"], "evidence": { "evidenceIds": ["local:b08:legacy-parity", "local:r06:labeled-corpus-baseline", "gap:native-code:replacement-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["a replacement proves full artifact and failure parity with better measured value", "heuristic maintenance cost exceeds measured value", "the production declaration is removed"], "evidence": { "evidenceIds": ["local:b07:evidence.native-code-required", "gap:native-code:retirement-comparison"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r06:operation-trace", "local:b08:legacy-parity", "local:r06:suite-elapsed-ms-27110", "local:r06:labeled-corpus-samples-12", "local:r06:precision-0.75", "local:r06:recall-0.75", "gap:native-code:framework-matrix"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/obsidian.json b/orchestration/internalization/obsidian.json new file mode 100644 index 0000000..4d91913 --- /dev/null +++ b/orchestration/internalization/obsidian.json @@ -0,0 +1,186 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.obsidian-record", + "projectId": "code-intel-pipeline", + "authorityRequirements": { + "repositoryGovernedAttestation": true + }, + "subject": { + "name": "Obsidian optional knowledge projection", + "kind": "design_reference", + "source": { + "uri": "https://obsidian.md; local-policy=docs/project-management-support.md", + "revision": "product-version-unbound; local-policy-sha256:d03a5535198f3e45f9572b7f9bb4a23e77ba636c751636d8c697166ea5a5c9c3; local-boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba" + }, + "license": { + "id": "PROPRIETARY-APPLICATION-TERMS-UNVERIFIED", + "obligations": [ + "no Obsidian install, plugin, vault write, UI automation, or task-authority migration is authorized", + "repo artifacts remain technical evidence authority" + ] + } + }, + "adoption": { + "rung": "invoke", + "ownedBoundary": [ + "view-only optional projection outside scanner runtime by default", + "Obsidian cannot replace scanner artifacts; task authority requires a separate explicit singular-authority decision" + ], + "necessityEvidence": { + "evidenceIds": [ + "local:r24:no-current-use", + "gap:obsidian:scope-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "compatibilityEvidence": { + "evidenceIds": [ + "local:r24:view-only-boundary", + "local:r24:artifact-authority-isolation" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "conformanceEvidence": { + "evidenceIds": [ + "local:r24:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba", + "gap:obsidian:approved-projection-conformance" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "economics": { + "benefit": { + "metric": "current required Obsidian projections", + "value": 0, + "unit": "projections" + }, + "cost": { + "metric": "external UI/runtime dependencies introduced", + "value": 0, + "unit": "dependencies" + }, + "benefitEvidence": { + "evidenceIds": [ + "local:r24:no-current-use" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "costEvidence": { + "evidenceIds": [ + "local:r24:no-ui-runtime", + "gap:obsidian:projection-cost-measurement" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "assurance": { + "maintenanceEvidence": { + "evidenceIds": [ + "local:r24:no-runtime-dependency", + "gap:obsidian:product-update-review" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + }, + "securityEvidence": { + "evidenceIds": [ + "local:r24:no-vault-write", + "gap:obsidian:plugin-and-vault-security-review" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "update": { + "policy": "Remain view-only and out of runtime; review product/plugin/version, data effects, measurement, and singular authority only after explicit approval", + "nextCheckAt": 1791676800, + "evidence": { + "evidenceIds": [ + "gap:obsidian:scope-authority" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "ownedModifications": [ + { + "path": "docs/project-management-support.md", + "description": "Pipeline-owned knowledge-view and artifact-authority boundary", + "evidenceIds": [ + "local:r24:policy-sha256:d03a5535198f3e45f9572b7f9bb4a23e77ba636c751636d8c697166ea5a5c9c3", + "local:r24:boundary-test-sha256:559f58c2ae5079593c2389b0cbeb00336a6ca9f94f257679ea1735aae85a58ba" + ] + } + ], + "rollback": { + "strategy": "delete or stop the optional view without changing repo docs, artifacts, or task authority", + "evidence": { + "evidenceIds": [ + "local:r24:view-only-boundary" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "exit": { + "strategy": "remove the projection or replace it with another read-only knowledge view", + "replacementCriteria": [ + "scanner artifacts remain authority", + "no UI/plugin/runtime dependency enters the scanner", + "task authority remains explicit and singular" + ], + "evidence": { + "evidenceIds": [ + "local:r24:artifact-authority-isolation" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "no approved measured use exists", + "view risks replacing source artifacts", + "plugin or vault security is unacceptable" + ], + "evidence": { + "evidenceIds": [ + "local:r24:no-current-use" + ], + "checkedAt": 1783900800, + "expiresAt": 1791676800 + } + }, + "lifecycle": { + "previousStatus": null, + "status": "out_of_scope", + "effectiveAt": 1783900800, + "replacementRecordId": null, + "evidenceIds": [ + "local:r24:no-current-use", + "gap:obsidian:scope-authority", + "gap:obsidian:product-version", + "gap:obsidian:terms" + ], + "authorityEvent": { + "schema": "code-intel-authority-event.v1", + "id": "authority.internalization.obsidian.out-of-scope", + "decision": "approved", + "approver": { "id": "code-intel-maintainers", "role": "repository_governance" }, + "evidenceIds": ["local:r24:no-current-use", "gap:obsidian:scope-authority", "gap:obsidian:product-version", "gap:obsidian:terms"], + "issuedAt": 1783900800, + "expiresAt": 1791676800, + "attestation": { "scheme": "repository-governed-sha256-v1", "digest": "1fd51ebc8c48bd1dd2645b701a13d1d9a1e37b03b4200435516ed37e412d460f" } + } + }, + "provenance": { + "recordedAt": 1783900800, + "recordedBy": "dependency-expert" + } +} diff --git a/orchestration/internalization/openspec.json b/orchestration/internalization/openspec.json new file mode 100644 index 0000000..cae1828 --- /dev/null +++ b/orchestration/internalization/openspec.json @@ -0,0 +1,32 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.openspec-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "OpenSpec OPSX advisory candidate", "kind": "design_reference", "source": { "uri": "https://github.com/Fission-AI/OpenSpec", "revision": "unverified-upstream; local-atom-sha256:03d9cbed70d83c59f7d9540fccc606ce0b2723135efd2c5e32943d367008a199" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until the exact upstream revision and license text are verified", "retain this provenance and gap notice while the candidate remains in research"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned advisory candidate data and recommendation rules only", "no OpenSpec runtime dependency", "no openspec init, filesystem initialization, commitment, or adoption authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-capability-map:openspec-row", "gap:openspec:upstream-revision", "gap:openspec:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b06:proposal-effects-empty", "local:b06:no-auto-init-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:atom:openspec-opsx-five-occurrences", "local:b06:a01-envelope-parity"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { + "benefit": { "metric": "OpenSpec OPSX token occurrences traced in the current advisory atom", "value": 5, "unit": "occurrences" }, + "cost": { "metric": "pipeline-owned OpenSpec lifecycle records", "value": 1, "unit": "records" }, + "benefitEvidence": { "evidenceIds": ["local:atom:openspec-opsx-five-occurrences"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "costEvidence": { "evidenceIds": ["local:record:openspec"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "assurance": { + "maintenanceEvidence": { "evidenceIds": ["gap:openspec:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "securityEvidence": { "evidenceIds": ["gap:openspec:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "update": { "policy": "Before 2026-10-11, verify the upstream commit, LICENSE, release status, and candidate semantics; keep research-only until all checks are admitted", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:openspec:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ + { "path": "OpenSpec-Detector.ps1", "description": "Pipeline-owned OpenSpec candidate detection and advisory rendering; does not contain or invoke the upstream runtime", "evidenceIds": ["local:atom:openspec-opsx-five-occurrences", "local:b06:no-auto-init-boundary"] } + ], + "rollback": { "strategy": "delete this candidate record and remove the OpenSpec alternative from candidate data without changing the advisory schema or authority boundary", "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace the reference with a pinned, licensed candidate record or retire it while retaining audit provenance", "replacementCriteria": ["replacement has exact upstream revision and verified license obligations", "replacement passes proposal conformance and no-auto-init tests"], "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["upstream provenance or license remains unverifiable at next check", "candidate no longer contributes unique measured advisory value", "security or maintenance evidence cannot be admitted"], "evidence": { "evidenceIds": ["local:record:openspec"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:openspec", "gap:openspec:upstream-revision", "gap:openspec:license"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/pon-multilanguage.json b/orchestration/internalization/pon-multilanguage.json new file mode 100644 index 0000000..2af8aba --- /dev/null +++ b/orchestration/internalization/pon-multilanguage.json @@ -0,0 +1,215 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.pon-multilanguage-code-evidence-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "pon-inspired multilingual frontend normalization", + "kind": "design_reference", + "source": { + "uri": "https://github.com/can1357/pon", + "revision": "ab9067dbd2899c64c4d67a4bc27b8ad49472b126; local-plan-sha256:13aba02afe50ba96b51f8af8f25a5f4adbf485737d4ccc4897d3f769aee7cea9; local-doc-sha256:ab190f0b1d5df7b552567bd89b48eef177b2d1318d19a6eb3bc828bbfcdc23ab; local-test-sha256:0479a8d9e2addd550048ca97e1379cbaa17de3cad17a48f11cf0b9e1468062ec; local-fixture-sha256:26429a991641d142404eca462fdfb23b5256fdf3501724b63b6d441faa58cd6d" + }, + "license": { + "id": "UNKNOWN-RESEARCH-ONLY", + "obligations": [ + "do not copy, vendor, distribute, or execute pon implementation code or fixtures until a license is declared and verified", + "retain source and revision attribution for the independently specified frontend-normalization method" + ] + } + }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": [ + "Pipeline reuses its existing Code Evidence v1 files, symbols, symbol-chunks, imports, and coverage artifacts as one language-neutral structural fact contract", + "the multilingual conformance floor covers Python, JavaScript, TypeScript, Rust, Go, Java, and PowerShell while C# remains an explicit unsupported control", + "no pon runtime dependency, Python runtime semantics, new intermediate representation, parser claim, type claim, call graph, or automatic fixture mutation" + ], + "necessityEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:existing-code-evidence-contract", + "local:pon-multilanguage:seven-supported-languages", + "gap:pon-multilanguage:parser-backed-adapter" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "compatibilityEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:runtime-unchanged", + "local:pon-multilanguage:artifact-schemas-v1", + "local:pon-multilanguage:unsupported-csharp-explicit" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "conformanceEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:targeted-rust-test-pass", + "local:pon-multilanguage:seven-supported-languages", + "local:pon-multilanguage:unsupported-csharp-no-fabrication", + "gap:pon-multilanguage:independent-verifier" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "economics": { + "benefit": { + "metric": "supported source languages pinned to one Code Evidence field contract", + "value": 7, + "unit": "languages" + }, + "cost": { + "metric": "new runtime dependencies", + "value": 0, + "unit": "dependencies" + }, + "benefitEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:seven-supported-languages", + "local:pon-multilanguage:artifact-schemas-v1" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "costEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:no-runtime-dependency", + "local:pon-multilanguage:runtime-unchanged" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "assurance": { + "maintenanceEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:source-revision-pinned", + "local:pon-multilanguage:fixture-digest-pinned", + "gap:pon-multilanguage:update-review" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "securityEvidence": { + "evidenceIds": [ + "local:pon-multilanguage:no-runtime-dependency", + "gap:pon-multilanguage:upstream-license", + "gap:pon-multilanguage:independent-security-review" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "update": { + "policy": "Before 2026-10-13, recheck upstream license and revision, measure a parser-backed language adapter against the shared contract, and obtain independent review before widening semantic claims", + "nextCheckAt": 1791849600, + "evidence": { + "evidenceIds": [ + "gap:pon-multilanguage:update-review", + "gap:pon-multilanguage:upstream-license", + "gap:pon-multilanguage:parser-backed-adapter" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "ownedModifications": [ + { + "path": "docs/plans/pon-multilanguage-code-evidence-idea.md", + "description": "Idea file and acceptance boundary for the multilingual mapping proof", + "evidenceIds": [ + "local:pon-multilanguage:existing-code-evidence-contract", + "local:pon-multilanguage:no-runtime-dependency" + ] + }, + { + "path": "docs/pon-multilanguage-code-evidence.md", + "description": "Pipeline-owned mapping, precision boundary, provenance, exclusions, and widening path", + "evidenceIds": [ + "local:pon-multilanguage:source-revision-pinned", + "local:pon-multilanguage:artifact-schemas-v1" + ] + }, + { + "path": "crates/code-intel-cli/tests/pon_multilanguage_mapping.rs", + "description": "Rust conformance test proving one field contract across supported languages and explicit unknown behavior for unsupported code", + "evidenceIds": [ + "local:pon-multilanguage:targeted-rust-test-pass", + "local:pon-multilanguage:unsupported-csharp-no-fabrication" + ] + }, + { + "path": "orchestration/internalization/fixtures/pon-multilanguage-code-evidence.json", + "description": "Eight-sample cross-language mapping floor with seven supported languages and one unsupported control", + "evidenceIds": [ + "local:pon-multilanguage:seven-supported-languages", + "local:pon-multilanguage:fixture-digest-pinned" + ] + } + ], + "rollback": { + "strategy": "delete the new idea file, mapping note, integration test, fixture, and this record; the existing Code Evidence runtime and artifacts remain unchanged", + "evidence": { + "evidenceIds": [ + "local:pon-multilanguage:runtime-unchanged", + "local:pon-multilanguage:rollback-independent" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "exit": { + "strategy": "retire the pon design reference after the multilingual contract becomes a fully Pipeline-owned specification or remove the floor if it stops detecting adapter drift", + "replacementCriteria": [ + "supported languages continue to emit the same files, symbols, and imports field shapes", + "unsupported languages remain explicit and cannot fabricate structural facts", + "parser-backed adapters preserve the v1 compatibility boundary while reporting stronger precision separately" + ], + "evidence": { + "evidenceIds": [ + "local:pon-multilanguage:rollback-independent", + "gap:pon-multilanguage:parser-backed-adapter" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "the fixture duplicates a future native multilingual contract", + "the mapping floor produces no adapter-drift signal during the review window", + "upstream provenance or license cannot be maintained" + ], + "evidence": { + "evidenceIds": [ + "local:record:pon-multilanguage-code-evidence", + "gap:pon-multilanguage:parser-backed-adapter" + ], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1784073600, + "replacementRecordId": null, + "evidenceIds": [ + "local:record:pon-multilanguage-code-evidence", + "local:pon-multilanguage:source-revision-pinned", + "local:pon-multilanguage:targeted-rust-test-pass", + "gap:pon-multilanguage:upstream-license", + "gap:pon-multilanguage:parser-backed-adapter", + "gap:pon-multilanguage:independent-verifier", + "gap:pon-multilanguage:update-review", + "gap:pon-multilanguage:independent-security-review" + ], + "authorityEvent": null + }, + "provenance": { + "recordedAt": 1784073600, + "recordedBy": "codex-pon-multilanguage-review" + } +} diff --git a/orchestration/internalization/pon-project-conformance.json b/orchestration/internalization/pon-project-conformance.json new file mode 100644 index 0000000..c108fa8 --- /dev/null +++ b/orchestration/internalization/pon-project-conformance.json @@ -0,0 +1,106 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.pon-project-conformance-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "pon conformance method mapped to Code Intel project acceptance", + "kind": "design_reference", + "source": { + "uri": "https://github.com/can1357/pon", + "revision": "ab9067dbd2899c64c4d67a4bc27b8ad49472b126; local-plan-sha256:dcacf4b5143309986bc5d9e5981a057e5563253d3bd92f8b77fccdc3812e543d; local-doc-sha256:88560c989a8831e318d03df434f2b8c63cccb30895a268432115a18ba26c5c6c; local-policy-sha256:3bdfbb7ed41c0bdcc584c2f54f47145851f37863f048de9ceb81deea3cfcddfc; local-gate-sha256:1a8dcac8e09a14ef4df8ae482da02b41e67a248278d425b1a513723d97aacf8a; local-test-sha256:304f39aae16488b30da68fb4145818d25e8a967249867f5900720d4dfc56212f; local-ledger-sha256:8a9e6849b50806659462a5446e355e160f3238b2fc2ffe897c3411ed6efcf80a" + }, + "license": { + "id": "UNKNOWN-RESEARCH-ONLY", + "obligations": [ + "do not copy, vendor, distribute, or execute pon implementation code or fixtures until a license is declared and verified", + "retain pinned attribution for independently reimplemented conformance-method semantics" + ] + } + }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": [ + "Pipeline owns an executable fast/full project-conformance policy composed from existing local suites", + "the component language-adapter gate is evidence under project conformance rather than the project policy itself", + "full acceptance fails closed until divergence handling, deterministic stress, and performance ratchets are implemented" + ], + "necessityEvidence": { + "evidenceIds": ["local:pon-project-conformance:project-policy-gap", "local:pon-project-conformance:eight-mechanism-map"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "compatibilityEvidence": { + "evidenceIds": ["local:pon-project-conformance:composes-existing-suites", "local:pon-project-conformance:no-runtime-dependency", "local:pon-project-conformance:clean-normal-scan"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "conformanceEvidence": { + "evidenceIds": ["local:pon-project-conformance:fast-five-suites-pass", "local:pon-project-conformance:full-fails-closed", "local:pon-project-conformance:policy-weakening-rejected", "gap:pon-project-conformance:independent-verifier"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "economics": { + "benefit": { "metric": "mapped mechanisms exercised by fast project acceptance", "value": 6, "unit": "mechanisms" }, + "cost": { "metric": "new runtime dependencies", "value": 0, "unit": "dependencies" }, + "benefitEvidence": { "evidenceIds": ["local:pon-project-conformance:fast-five-suites-pass", "local:pon-project-conformance:eight-mechanism-map"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "costEvidence": { "evidenceIds": ["local:pon-project-conformance:no-runtime-dependency"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "assurance": { + "maintenanceEvidence": { "evidenceIds": ["local:pon-project-conformance:source-revision-pinned", "local:pon-project-conformance:policy-weakening-rejected", "gap:pon-project-conformance:update-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "securityEvidence": { "evidenceIds": ["local:pon-project-conformance:no-runtime-dependency", "gap:pon-project-conformance:upstream-license", "gap:pon-project-conformance:independent-security-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "update": { + "policy": "Before 2026-10-13, recheck the upstream revision/license and review whether the three full-profile gaps have executable evidence", + "nextCheckAt": 1791849600, + "evidence": { "evidenceIds": ["gap:pon-project-conformance:update-review", "gap:pon-project-conformance:active-divergence-ledger", "gap:pon-project-conformance:performance-ratchet"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "ownedModifications": [ + { "path": "docs/plans/pon-project-conformance-mapping-idea.md", "description": "Problem statement, mapping decision, and constraints", "evidenceIds": ["local:pon-project-conformance:project-policy-gap"] }, + { "path": "docs/code-intel-project-conformance.md", "description": "Human-readable project acceptance contract and authority boundary", "evidenceIds": ["local:pon-project-conformance:eight-mechanism-map"] }, + { "path": "orchestration/code-intel-project-conformance-policy.v1.json", "description": "Machine-owned mechanism mapping, immutable profile requirements, and executable suite declarations", "evidenceIds": ["local:pon-project-conformance:policy-weakening-rejected"] }, + { "path": "Test-CodeIntelProjectConformance.ps1", "description": "Executable fast/full project gate", "evidenceIds": ["local:pon-project-conformance:fast-five-suites-pass", "local:pon-project-conformance:full-fails-closed"] }, + { "path": "test-code-intel-project-conformance.ps1", "description": "Positive and fail-closed mutation coverage for the project gate", "evidenceIds": ["local:pon-project-conformance:policy-weakening-rejected"] }, + { "path": "orchestration/acceptance/known-divergences.v1.json", "description": "Empty design placeholder; not yet an active comparison exception surface", "evidenceIds": ["gap:pon-project-conformance:active-divergence-ledger"] } + ], + "rollback": { + "strategy": "remove the project policy, runner, runner test, mapping documents, inactive ledger, and this record; existing component tests and parity floor remain unchanged", + "evidence": { "evidenceIds": ["local:pon-project-conformance:composes-existing-suites", "local:pon-project-conformance:rollback-independent"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "exit": { + "strategy": "retire the pon attribution after the conformance method becomes an independently maintained Pipeline specification, or remove the wrapper if it adds no signal beyond its component suites", + "replacementCriteria": [ + "project acceptance keeps a pinned oracle, corpus, monotonic floor, mutation checks, and fast/full separation", + "release acceptance cannot omit unfinished mapped mechanisms", + "every required suite remains executable and machine-readable" + ], + "evidence": { "evidenceIds": ["local:pon-project-conformance:rollback-independent", "gap:pon-project-conformance:independent-local-specification"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "the wrapper duplicates a future native project-conformance coordinator", + "the fast/full profiles produce no additional regression signal", + "upstream provenance cannot be maintained" + ], + "evidence": { "evidenceIds": ["local:record:pon-project-conformance", "gap:pon-project-conformance:independent-local-specification"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1784073600, + "replacementRecordId": null, + "evidenceIds": [ + "local:record:pon-project-conformance", + "local:pon-project-conformance:source-revision-pinned", + "local:pon-project-conformance:fast-five-suites-pass", + "local:pon-project-conformance:full-fails-closed", + "gap:pon-project-conformance:upstream-license", + "gap:pon-project-conformance:active-divergence-ledger", + "gap:pon-project-conformance:performance-ratchet", + "gap:pon-project-conformance:independent-verifier" + ], + "authorityEvent": null + }, + "provenance": { "recordedAt": 1784073600, "recordedBy": "codex-pon-project-conformance-review" } +} diff --git a/orchestration/internalization/pon-python314-development.json b/orchestration/internalization/pon-python314-development.json new file mode 100644 index 0000000..8f1194d --- /dev/null +++ b/orchestration/internalization/pon-python314-development.json @@ -0,0 +1,107 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.pon-python314-development-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "CPython 3.14-first development with optional Pon parity", + "kind": "design_reference", + "source": { + "uri": "https://github.com/can1357/pon", + "revision": "ab9067dbd2899c64c4d67a4bc27b8ad49472b126; local-plan-sha256:c8beddcd8168c804679bc4bca89dc8341ccadc3127348d4b2195a050660f6e9c; local-doc-sha256:acb4e37bb7eb4a6bb09ad8f539d884280f28f284f549e61697d218578475ad03; local-policy-sha256:73d9afd8bb6f5d2892a557929a38dfea45694b3a71b8cd41c70dd93b4fff93b3; local-gate-sha256:e0ff9490f383d782d1cd687adaceec19cd06adf7d64377871a1802164eadb154; local-test-sha256:854c01bc788b4ef63f8c93788591f8e1000a94c53992a2ec29fce32c4c715819; local-manifest-sha256:67e7d8ba21cadf39f92cc92577859fedae1ceb14b091344260d65cee9d96b58e" + }, + "license": { + "id": "UNKNOWN-RESEARCH-ONLY", + "obligations": [ + "do not copy, vendor, distribute, install, or automatically execute pon source until a license is declared and verified", + "retain source attribution for the independently implemented differential-development method" + ] + } + }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": [ + "CPython 3.14 is the authoritative runtime and repository Python entry points compile against it", + "Pon is optional for development but mandatory for a pon-candidate compatibility claim", + "only ponRequired corpus cases participate in the current alternative-backend claim", + "the gate never installs Pon and no real Pon runtime was available during this implementation" + ], + "necessityEvidence": { + "evidenceIds": ["local:python314:installed-runtime", "local:python314:default-python-is-3.10", "local:python314:explicit-runtime-agreement-needed"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "compatibilityEvidence": { + "evidenceIds": ["local:python314:project-files-compile", "local:python314:three-corpus-cases-pass", "local:python314:no-runtime-install", "local:python314:project-fast-suite-pass"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "conformanceEvidence": { + "evidenceIds": ["local:python314:development-pass-pon-unavailable", "local:python314:missing-pon-candidate-rejected", "local:python314:matching-test-backend-pass", "local:python314:divergent-test-backend-rejected", "gap:python314:real-pon-execution", "gap:python314:independent-verifier"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "economics": { + "benefit": { "metric": "reviewed CPython 3.14 compatibility corpus", "value": 3, "unit": "cases" }, + "cost": { "metric": "new installed runtime dependencies", "value": 0, "unit": "dependencies" }, + "benefitEvidence": { "evidenceIds": ["local:python314:three-corpus-cases-pass", "local:python314:project-files-compile"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "costEvidence": { "evidenceIds": ["local:python314:no-runtime-install"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "assurance": { + "maintenanceEvidence": { "evidenceIds": ["local:python314:source-revision-pinned", "local:python314:corpus-manifest-pinned", "gap:python314:update-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "securityEvidence": { "evidenceIds": ["local:python314:no-runtime-install", "gap:python314:upstream-license", "gap:python314:real-pon-security-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "update": { + "policy": "Before 2026-10-13, recheck Pon license/revision, run the required corpus against an operator-supplied real Pon executable, and review promotion of additional Python 3.14 cases", + "nextCheckAt": 1791849600, + "evidence": { "evidenceIds": ["gap:python314:update-review", "gap:python314:real-pon-execution", "gap:python314:upstream-license"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "ownedModifications": [ + { "path": "docs/plans/python314-pon-development-lane-idea.md", "description": "Development agreement, boundaries, and verification plan", "evidenceIds": ["local:python314:explicit-runtime-agreement-needed"] }, + { "path": "docs/python314-pon-development.md", "description": "Team-facing CPython-first and Pon-candidate usage contract", "evidenceIds": ["local:python314:development-pass-pon-unavailable"] }, + { "path": "orchestration/python314-pon-development-policy.v1.json", "description": "Pinned runtime authority, project files, corpus, and profile requirements", "evidenceIds": ["local:python314:source-revision-pinned", "local:python314:corpus-manifest-pinned"] }, + { "path": "Test-Python314PonCompatibility.ps1", "description": "Executable discovery, compile, corpus, and optional differential gate", "evidenceIds": ["local:python314:project-files-compile", "local:python314:divergent-test-backend-rejected"] }, + { "path": "test-python314-pon-compatibility.ps1", "description": "Positive, unavailable, and divergent backend coverage", "evidenceIds": ["local:python314:matching-test-backend-pass", "local:python314:missing-pon-candidate-rejected"] }, + { "path": "tests/fixtures/python314-compat/manifest.v1.json", "description": "Three-case corpus with two Pon-required cases and one CPython-only Python 3.14 feature", "evidenceIds": ["local:python314:three-corpus-cases-pass", "local:python314:corpus-manifest-pinned"] }, + { "path": ".github/workflows/ci.yml", "description": "Additive Python 3.14 baseline plus Windows and cross-platform compatibility execution; pre-existing user workflow edits preserved", "evidenceIds": ["local:python314:ci-python314-baseline", "local:python314:cross-platform-gate"] } + ], + "rollback": { + "strategy": "remove the Python 3.14/Pon policy, gate, test, corpus, documentation, project-conformance suite entry, and this record; existing Python utilities remain unchanged", + "evidence": { "evidenceIds": ["local:python314:no-runtime-install", "local:python314:rollback-independent"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "exit": { + "strategy": "retire the Pon reference when the runtime lane becomes an independently owned Python backend compatibility standard, or remove the optional backend if real Pon never passes the required corpus", + "replacementCriteria": [ + "CPython 3.14 remains the explicit semantic authority", + "alternative backends cannot claim compatibility without differential execution", + "CPython-only language features remain visible rather than being removed to improve parity" + ], + "evidence": { "evidenceIds": ["local:python314:rollback-independent", "gap:python314:real-pon-execution", "gap:python314:independent-local-specification"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "a native runtime compatibility coordinator replaces the PowerShell gate", + "real Pon cannot satisfy the required corpus during the review window", + "upstream provenance or license cannot be maintained" + ], + "evidence": { "evidenceIds": ["local:record:pon-python314-development", "gap:python314:real-pon-execution"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1784073600, + "replacementRecordId": null, + "evidenceIds": [ + "local:record:pon-python314-development", + "local:python314:source-revision-pinned", + "local:python314:development-pass-pon-unavailable", + "local:python314:divergent-test-backend-rejected", + "gap:python314:real-pon-execution", + "gap:python314:upstream-license", + "gap:python314:independent-verifier" + ], + "authorityEvent": null + }, + "provenance": { "recordedAt": 1784073600, "recordedBy": "codex-python314-pon-development-review" } +} diff --git a/orchestration/internalization/pon.json b/orchestration/internalization/pon.json new file mode 100644 index 0000000..954638b --- /dev/null +++ b/orchestration/internalization/pon.json @@ -0,0 +1,94 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.pon-conformance-ratchet-record", + "projectId": "code-intel-pipeline", + "subject": { + "name": "pon conformance-ratchet design reference", + "kind": "design_reference", + "source": { + "uri": "https://github.com/can1357/pon", + "revision": "ab9067dbd2899c64c4d67a4bc27b8ad49472b126; local-doc-sha256:4183b6cbee2eaf7d6ad7b2c7eaeef326c6ba6d8e8572080ec961df3dec256846; local-checker-sha256:da5725e5383f80f7d5280b2d9700fae5e813028bb8e993bf8297b636fe68f118; local-floor-sha256:0c228b9641d643d13a648879f0a3d84e3b56d6668ca913d067e47c138818d8f6" + }, + "license": { + "id": "UNKNOWN-RESEARCH-ONLY", + "obligations": [ + "do not copy, vendor, distribute, or execute pon implementation code or fixtures until a license is declared and verified", + "retain source and revision attribution for the independently reimplemented method semantics" + ] + } + }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": [ + "Pipeline-owned monotonic set-and-count floor over existing parity fixtures with explicit review for promotion", + "no pon runtime dependency, compiler code, CPython corpus, divergence waiver, performance claim, or automatic baseline mutation" + ], + "necessityEvidence": { + "evidenceIds": ["local:pon-ratchet:five-existing-parity-cases", "local:pon-ratchet:corpus-level-gap", "gap:pon-ratchet:representative-regression-capture"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "compatibilityEvidence": { + "evidenceIds": ["local:pon-ratchet:existing-oracle-unchanged", "local:pon-ratchet:no-runtime-dependency", "local:pon-ratchet:rollback-independent"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + }, + "conformanceEvidence": { + "evidenceIds": ["local:pon-ratchet:five-floor-cases-pass", "local:pon-ratchet:missing-case-rejected", "local:pon-ratchet:empty-review-rejected", "gap:pon-ratchet:independent-verifier"], + "checkedAt": 1784073600, + "expiresAt": 1791849600 + } + }, + "economics": { + "benefit": { "metric": "existing parity cases protected by corpus floor", "value": 5, "unit": "cases" }, + "cost": { "metric": "new runtime dependencies", "value": 0, "unit": "dependencies" }, + "benefitEvidence": { "evidenceIds": ["local:pon-ratchet:five-floor-cases-pass", "gap:pon-ratchet:representative-regression-capture"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "costEvidence": { "evidenceIds": ["local:pon-ratchet:no-runtime-dependency"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "assurance": { + "maintenanceEvidence": { "evidenceIds": ["local:pon-ratchet:source-revision-pinned", "gap:pon-ratchet:update-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 }, + "securityEvidence": { "evidenceIds": ["local:pon-ratchet:no-runtime-dependency", "gap:pon-ratchet:upstream-license", "gap:pon-ratchet:independent-security-review"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "update": { + "policy": "Before 2026-10-13, recheck upstream license and revision, collect a real corpus-level regression or progress event, and obtain independent review before production enablement", + "nextCheckAt": 1791849600, + "evidence": { "evidenceIds": ["gap:pon-ratchet:update-review", "gap:pon-ratchet:upstream-license", "gap:pon-ratchet:representative-regression-capture"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "ownedModifications": [ + { "path": "docs/plans/pon-parity-floor-idea.md", "description": "Idea file and acceptance boundary for the selective internalization", "evidenceIds": ["local:pon-ratchet:corpus-level-gap", "local:pon-ratchet:no-runtime-dependency"] }, + { "path": "docs/pon-conformance-ratchet.md", "description": "Pipeline-owned semantic mapping, exclusions, provenance, and exit criteria", "evidenceIds": ["local:pon-ratchet:source-revision-pinned", "local:pon-ratchet:existing-oracle-unchanged"] }, + { "path": "test-parity-floor.ps1", "description": "Monotonic floor checker and explicitly reviewed updater over the existing oracle", "evidenceIds": ["local:pon-ratchet:five-floor-cases-pass", "local:pon-ratchet:missing-case-rejected", "local:pon-ratchet:empty-review-rejected"] }, + { "path": "tests/fixtures/parity/parity-floor.json", "description": "Initial five-case committed parity floor", "evidenceIds": ["local:pon-ratchet:five-existing-parity-cases", "local:pon-ratchet:five-floor-cases-pass"] } + ], + "rollback": { + "strategy": "delete the floor checker, floor file, semantic note, idea file, and this record; the existing per-case parity oracle and fixtures remain unchanged", + "evidence": { "evidenceIds": ["local:pon-ratchet:existing-oracle-unchanged", "local:pon-ratchet:rollback-independent"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "exit": { + "strategy": "retire the source reference after independent local specification or remove the wrapper if it produces no corpus-level signal beyond per-case tests", + "replacementCriteria": [ + "the passing fixture set and minimum count remain machine-readable", + "updates cannot remove old passing cases or lower the count", + "promotion continues to require explicit review evidence" + ], + "evidence": { "evidenceIds": ["local:pon-ratchet:rollback-independent", "gap:pon-ratchet:representative-regression-capture"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "retirement": { + "status": "candidate", + "triggers": [ + "no corpus-level regression or progress signal is observed during the review window", + "the wrapper duplicates a future native floor contract", + "upstream provenance or license cannot be maintained" + ], + "evidence": { "evidenceIds": ["local:record:pon-conformance-ratchet", "gap:pon-ratchet:representative-regression-capture"], "checkedAt": 1784073600, "expiresAt": 1791849600 } + }, + "lifecycle": { + "previousStatus": null, + "status": "research", + "effectiveAt": 1784073600, + "replacementRecordId": null, + "evidenceIds": ["local:record:pon-conformance-ratchet", "local:pon-ratchet:source-revision-pinned", "gap:pon-ratchet:upstream-license", "gap:pon-ratchet:representative-regression-capture", "gap:pon-ratchet:independent-verifier", "gap:pon-ratchet:update-review", "gap:pon-ratchet:independent-security-review"], + "authorityEvent": null + }, + "provenance": { "recordedAt": 1784073600, "recordedBy": "codex-pon-review" } +} diff --git a/orchestration/internalization/ponytail.json b/orchestration/internalization/ponytail.json new file mode 100644 index 0000000..1c28d29 --- /dev/null +++ b/orchestration/internalization/ponytail.json @@ -0,0 +1,13 @@ +{ + "schema":"code-intel-internalization-record.v1","id":"internalization.ponytail-record","projectId":"code-intel-pipeline", + "subject":{"name":"Ponytail governance source reference","kind":"selectively_owned_implementation","source":{"uri":"unverified-upstream:ponytail; local-runtime=crates/code-intel-cli/src/ponytail_gate.rs","revision":"upstream-revision-unverified; local-runtime-sha256:22e0627106c9a38ff432b91ac0a8f578ecd61c01846bc699ef9a5b32e2d9469f; local-conformance-sha256:5cdd47fb2c97d09c70ddee472c331e8c98d72b1e35f1e6de4d8072c1b30e37a4"},"license":{"id":"UNKNOWN-RESEARCH-ONLY","obligations":["do not vendor or distribute an upstream Ponytail runtime until canonical source, exact revision, and license are verified","Pipeline-owned C00 behavior remains independently specified and testable if the source reference is retired"]}}, + "adoption":{"rung":"reimplement","ownedBoundary":["Pipeline owns C00 report_only/enforce policy, Value Filter, Necessity Trace, safety exceptions, authority-event checks, and deterministic result contract","Ponytail is a governance concept provenance source only; it is not an external runtime, provider, scanner, or authority"],"necessityEvidence":{"evidenceIds":["local:c00:executable-necessity-gate","gap:ponytail:canonical-source","gap:ponytail:upstream-revision","gap:ponytail:license"],"checkedAt":1783900800,"expiresAt":1791676800},"compatibilityEvidence":{"evidenceIds":["local:c00:report-only-rollback","local:c00:safety-exceptions"],"checkedAt":1783900800,"expiresAt":1791676800},"conformanceEvidence":{"evidenceIds":["local:r21:runtime-sha256:22e0627106c9a38ff432b91ac0a8f578ecd61c01846bc699ef9a5b32e2d9469f","local:r21:conformance-sha256:5cdd47fb2c97d09c70ddee472c331e8c98d72b1e35f1e6de4d8072c1b30e37a4","gap:ponytail:source-semantic-comparison"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "economics":{"benefit":{"metric":"C00 behavior cases bound to executable conformance","value":12,"unit":"tests"},"cost":{"metric":"source-reference gaps","value":4,"unit":"gaps"},"benefitEvidence":{"evidenceIds":["local:r21:conformance-sha256:5cdd47fb2c97d09c70ddee472c331e8c98d72b1e35f1e6de4d8072c1b30e37a4","gap:ponytail:representative-rejection-measurement"],"checkedAt":1783900800,"expiresAt":1791676800},"costEvidence":{"evidenceIds":["gap:ponytail:canonical-source","gap:ponytail:upstream-revision","gap:ponytail:license","gap:ponytail:runtime-overhead-measurement"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "assurance":{"maintenanceEvidence":{"evidenceIds":["local:c00:policy-schema-tests","gap:ponytail:upstream-update-review"],"checkedAt":1783900800,"expiresAt":1791676800},"securityEvidence":{"evidenceIds":["local:c00:no-network-no-external-write","local:c00:safety-exceptions","gap:ponytail:source-security-review"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "update":{"policy":"Recheck upstream provenance and semantic deltas without coupling C00 to an external runtime; benchmark rejection yield and overhead before changing enforcement","nextCheckAt":1791676800,"evidence":{"evidenceIds":["gap:ponytail:update-review"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "ownedModifications":[{"path":"crates/code-intel-cli/src/ponytail_gate.rs","description":"Pipeline-owned executable C00 governance contract; not an upstream Ponytail runtime","evidenceIds":["local:r21:runtime-sha256:22e0627106c9a38ff432b91ac0a8f578ecd61c01846bc699ef9a5b32e2d9469f","local:r21:conformance-sha256:5cdd47fb2c97d09c70ddee472c331e8c98d72b1e35f1e6de4d8072c1b30e37a4"]}], + "rollback":{"strategy":"return C00 to report_only while preserving requests/results and the Pipeline-owned semantic contract","evidence":{"evidenceIds":["local:c00:report-only-rollback"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "exit":{"strategy":"retire the Ponytail source reference without removing C00; replace provenance only after behavioral comparison","replacementCriteria":["Value Filter, Necessity Trace, safety exceptions, and authority checks remain executable","replacement has verified provenance/license and measured utility","no external runtime or governance authority is introduced"],"evidence":{"evidenceIds":["local:r21:source-removal-isolation","gap:ponytail:source-semantic-comparison"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "retirement":{"status":"candidate","triggers":["canonical source or license remains unverifiable","source reference adds no measured behavior beyond local C00","replacement provenance passes semantic comparison"],"evidence":{"evidenceIds":["local:r21:source-removal-isolation"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "lifecycle":{"previousStatus":null,"status":"research","effectiveAt":1783900800,"replacementRecordId":null,"evidenceIds":["local:r21:runtime-sha256:22e0627106c9a38ff432b91ac0a8f578ecd61c01846bc699ef9a5b32e2d9469f","gap:ponytail:canonical-source","gap:ponytail:upstream-revision","gap:ponytail:license","gap:ponytail:representative-rejection-measurement"],"authorityEvent":null},"provenance":{"recordedAt":1783900800,"recordedBy":"dependency-expert"} +} diff --git a/orchestration/internalization/qiaomu-goal.json b/orchestration/internalization/qiaomu-goal.json new file mode 100644 index 0000000..4e0fa49 --- /dev/null +++ b/orchestration/internalization/qiaomu-goal.json @@ -0,0 +1,22 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.qiaomu-goal-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "qiaomu-goal-meta-skill goal-authoring design reference", "kind": "design_reference", "source": { "uri": "unverified-upstream:qiaomu-goal-meta-skill; local-reference=docs/agent-goal-intake.md", "revision": "unverified-upstream; local-doc-sha256:fe72627e85e99bf6bb4d4fbfd06d112a02b4229af1ceaf996631b2dbbb9bdc31" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until canonical upstream URI, exact revision, and license text are verified", "retain attribution and explicit gaps for every absorbed goal-contract semantic"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned Agent Goal Intake task-contract vocabulary: outcome, verification, constraints, boundaries, iteration policy, completion evidence, and pause conditions", "no qiaomu runtime dependency, scanner invocation, repository evidence authority, goal execution, scheduling, or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-map:qiaomu-goal", "local:agent-goal-intake:seven-contract-fields", "gap:qiaomu-goal:upstream-revision", "gap:qiaomu-goal:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:agent-goal-intake:outside-scanner", "local:adr-0002:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:trace:qiaomu-outcome", "local:trace:qiaomu-verification", "local:trace:qiaomu-constraints", "local:trace:qiaomu-boundaries", "local:trace:qiaomu-iteration-policy", "local:trace:qiaomu-completion-evidence", "local:trace:qiaomu-pause-conditions", "gap:qiaomu-goal:upstream-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "locally documented qiaomu-derived goal-contract semantics", "value": 7, "unit": "semantics" }, "cost": { "metric": "pipeline-owned source-to-contract trace entries", "value": 7, "unit": "entries" }, "benefitEvidence": { "evidenceIds": ["local:agent-goal-intake:seven-contract-fields"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:qiaomu-goal"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:qiaomu-goal:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:qiaomu-goal:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify canonical source, exact revision, license, and upstream meaning of each retained goal-contract field; keep research-only if any proof remains unavailable", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:qiaomu-goal:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "docs/agent-goal-intake.md", "description": "Pipeline-owned seven-field Agent Goal Intake contract and explicit scanner isolation", "evidenceIds": ["local:agent-goal-intake:seven-contract-fields", "local:agent-goal-intake:outside-scanner"] } ], + "rollback": { "strategy": "remove qiaomu attribution and this lifecycle record while retaining the versioned pipeline-owned Agent Goal Intake contract", "evidence": { "evidenceIds": ["local:adr-0002:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retire or replace the reference independently without changing scanner or task-contract behavior", "replacementCriteria": ["replacement provenance and license are verified", "all retained goal semantics have independent local contract and conformance evidence", "removal leaves scanner artifacts and authority boundaries unchanged"], "evidence": { "evidenceIds": ["local:adr-0002:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["upstream revision or license remains unverifiable", "absorbed semantics lack independent behavioral evidence", "the reference creates scanner runtime or execution-authority coupling"], "evidence": { "evidenceIds": ["local:record:qiaomu-goal"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:qiaomu-goal", "gap:qiaomu-goal:upstream-revision", "gap:qiaomu-goal:license", "gap:qiaomu-goal:upstream-conformance"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/repomix.json b/orchestration/internalization/repomix.json new file mode 100644 index 0000000..fb1e467 --- /dev/null +++ b/orchestration/internalization/repomix.json @@ -0,0 +1,14 @@ +{ + "schema": "code-intel-internalization-record.v1", "id": "internalization.repomix-record", "projectId": "code-intel-pipeline", + "subject": { "name": "Repomix repository packaging capability", "kind": "adapted_capability", "source": { "uri": "https://www.npmjs.com/package/repomix; repository=https://github.com/yamadashy/repomix", "revision": "npm-observed-latest:1.16.0; audited-host-command:unavailable; npm-registry-metadata-entries:3; npm-package-tarball-entries:3; npm-installed-or-extracted-executable-entries:0; measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420" }, "license": { "id": "MIT-UPSTREAM-PACKAGE-METADATA-LOCAL-COPY-MISSING", "obligations": ["retain exact installed package provenance and MIT notice before any future production use", "a new authority-approved record and B07 declaration are required before restoring invocation"] } }, + "adoption": { "rung": "invoke", "ownedBoundary": ["Pipeline retains only a dormant adapter and audited research record; B07 production call site and integration declaration were removed", "Repomix traversal, compression, token counting, secret scanning, remote retrieval, and package behavior are not production capabilities"], "necessityEvidence": { "evidenceIds": ["local:r05:b07-reviewed-deletion", "local:r05:zero-production-call-sites"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:r05:adapter-fixture-only", "gap:repomix:installed-package-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r05:deleted-registry-reconciliation", "gap:repomix:production-output-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "economics": { "benefit": { "metric": "production packaging invocations retained", "value": 0, "unit": "call-sites" }, "cost": { "metric": "audited installed or extracted Repomix executable trees", "value": 0, "unit": "executables" }, "benefitEvidence": { "evidenceIds": ["local:r05:production-call-sites-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:r05:command-unavailable", "local:r05:npm-registry-metadata-entries-3", "local:r05:npm-package-tarball-entries-3", "local:r05:npm-installed-or-extracted-executable-entries-0"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r05:reviewed-deletion", "gap:repomix:future-update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r05:no-production-process-or-remote-effects", "gap:repomix:future-supply-chain-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Do not install or restore implicitly; any future proposal must pin package integrity and license, then rerun representative output, token, latency, security, and alternative measurements", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["local:r05:delete-until-justified"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "orchestration/integrations.json", "description": "Reviewed deletion records zero Repomix production call sites and removes the integration declaration", "evidenceIds": ["local:r05:b07-reviewed-deletion", "local:r05:measurement-sha256:f395874dcc667e6c224270150cf7dad90f0cce530229eb001d7eb43557722420"] }], + "rollback": { "strategy": "retain the current summary-first workflow; restoration requires a new pinned record, conformance evidence, measurement, and B07 reconciliation", "evidence": { "evidenceIds": ["local:r05:summary-first-fallback", "local:r05:zero-production-call-sites"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "delete the dormant adapter and research record when historical audit value is no longer required", "replacementCriteria": ["inventory and Native Code Evidence remain existing repository-orientation alternatives", "a replacement proves bounded artifacts, secret handling, local-only defaults, latency, size, and token value", "production restoration is separately authority approved"], "evidence": { "evidenceIds": ["local:r05:inventory-native-alternatives", "local:r05:reviewed-deletion"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "completed", "triggers": ["no installed/extracted executable tree or verified runnable command", "no production conformance or measured advantage", "B07 reviewed deletion removed the call site"], "evidence": { "evidenceIds": ["local:r05:command-unavailable", "local:r05:npm-registry-metadata-classified", "local:r05:npm-installed-or-extracted-executable-entries-0", "local:r05:deleted-registry-reconciliation"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r05:b07-reviewed-deletion", "local:r05:production-call-sites-0", "gap:repomix:future-production-authority"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/repowise.json b/orchestration/internalization/repowise.json new file mode 100644 index 0000000..8424ead --- /dev/null +++ b/orchestration/internalization/repowise.json @@ -0,0 +1,28 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.repowise-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "Repowise provider boundary", "kind": "evidence_provider", "source": { "uri": "unverified-upstream:repowise; local-adapter=crates/code-intel-cli/src/repowise_adapter.rs", "revision": "unverified-upstream; local-adapter-sha256:a1ee9b3958746b2506b39be4365b9321e1809cbad6a12681573c616d289bf178; local-conformance-sha256:72f458e9ebcc3828a9c0ae1b43a1b72f53303e07294195f89584e221e027eba4" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not vendor, distribute, upgrade, or production-enable Repowise from this record until canonical upstream URI, exact revision, and license text are verified", "retain provider identity, quota, snapshot, and index/docs provenance at the B01 port"] } }, + "adoption": { + "rung": "adapt", + "ownedBoundary": ["Pipeline-owned B01 translation of Repowise status, index, and docs outcomes into the Evidence Provider Port and A04", "Repowise owns provider internals, quota, remote service behavior, index semantics, and generated documentation; this record grants no install, network, or upgrade authority"], + "necessityEvidence": { "evidenceIds": ["local:registry:provider.repowise-adapt", "local:registry:memory.repowise", "gap:repowise:upstream-revision", "gap:repowise:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b01:status-index-docs-separated", "local:b01:facade-route", "gap:repowise:upstream-cli-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:b01:repowise-adapter-conformance:bc59f5f22671a14669b7a053f0d075ae64baabc7fabb730d608e3535609a9922", "local:b01:production-operation-trace", "gap:repowise:upstream-exact-revision-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "operationTrace": [ + { "integrationId": "provider.repowise-adapt", "operation": "adapt", "command": "target/debug/code-intel.exe provider repowise-adapt --request --artifact-root --evaluated-at --max-age-seconds ", "implementationIdentity": { "providerId": "repowise.external", "implementationId": "repowise.native.v0.9.0", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/repowise_adapter.rs", "sha256": "a1ee9b3958746b2506b39be4365b9321e1809cbad6a12681573c616d289bf178" }, "conformance": { "path": "crates/code-intel-cli/tests/repowise_route.rs", "sha256": "72f458e9ebcc3828a9c0ae1b43a1b72f53303e07294195f89584e221e027eba4", "testName": "public_route_translates_and_a04_validates_success_quota_and_index_only" } }, + { "integrationId": "provider.repowise-adapt", "operation": "facade", "command": "run-code-intel.ps1 -RepowiseAdapterRequest -RepowiseAdapterArtifactRoot -RepowiseAdapterEvaluatedAt -RepowiseAdapterMaxAgeSeconds ", "implementationIdentity": { "providerId": "repowise.external", "implementationId": "repowise.native.v0.9.0", "activation": "compatibility_facade" }, "source": { "path": "crates/code-intel-cli/src/repowise_adapter.rs", "sha256": "a1ee9b3958746b2506b39be4365b9321e1809cbad6a12681573c616d289bf178" }, "conformance": { "path": "crates/code-intel-cli/tests/repowise_route.rs", "sha256": "72f458e9ebcc3828a9c0ae1b43a1b72f53303e07294195f89584e221e027eba4", "testName": "registry_declares_the_exact_public_route" } }, + { "integrationId": "memory.repowise", "operation": "status", "command": "Invoke-ScopedRepowise.ps1 -RepoPath -ScopePaths -RootFiles -AllowShadowWorktreeMutation", "implementationIdentity": { "providerId": "repowise.external", "implementationId": "Invoke-ScopedRepowise.ps1", "activation": "compatibility" }, "source": { "path": "Invoke-ScopedRepowise.ps1", "sha256": "c3f8d9db036b1b2083a856d75f46d618076649de9bc4b303f02fce1229f36bb3" }, "conformance": { "path": "crates/code-intel-cli/tests/repowise_route.rs", "sha256": "72f458e9ebcc3828a9c0ae1b43a1b72f53303e07294195f89584e221e027eba4", "testName": "public_route_preserves_missing_cli_and_incomplete_docs_without_fake_facts" } }, + { "integrationId": "memory.repowise", "operation": "docs", "command": "run-code-intel.ps1 -RepoPath -Mode normal -RepowiseDocs -AllowRepowiseShadowMutation", "implementationIdentity": { "providerId": "repowise.external", "implementationId": "Invoke-ScopedRepowise.ps1", "activation": "compatibility" }, "source": { "path": "Invoke-ScopedRepowise.ps1", "sha256": "c3f8d9db036b1b2083a856d75f46d618076649de9bc4b303f02fce1229f36bb3" }, "conformance": { "path": "crates/code-intel-cli/tests/repowise_route.rs", "sha256": "72f458e9ebcc3828a9c0ae1b43a1b72f53303e07294195f89584e221e027eba4", "testName": "public_route_translates_and_a04_validates_success_quota_and_index_only" } } + ], + "economics": { "benefit": { "metric": "B01 production operations traced through the registered adapter", "value": 2, "unit": "operations" }, "cost": { "metric": "registered B01 command surfaces requiring lifecycle evidence", "value": 2, "unit": "operations" }, "benefitEvidence": { "evidenceIds": ["local:b01:production-operation-trace", "gap:repowise:representative-value-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:registry:provider.repowise-adapt", "gap:repowise:quota-latency-cost-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:b01:rollback-compatible", "gap:repowise:maintenance-status"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:b01:snapshot-and-provenance-validation", "gap:repowise:security-review", "gap:repowise:quota-data-handling-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify canonical source, exact version, license, maintenance/security posture, quota behavior, and representative index/docs measurements; keep this record research-only while any gap remains", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:repowise:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "crates/code-intel-cli/src/repowise_adapter.rs", "description": "Pipeline-owned B01 provider-neutral adapter; no Repowise implementation vendoring", "evidenceIds": ["local:b01:adapter-source-sha256:a1ee9b3958746b2506b39be4365b9321e1809cbad6a12681573c616d289bf178", "local:b01:repowise-adapter-conformance:bc59f5f22671a14669b7a053f0d075ae64baabc7fabb730d608e3535609a9922"] } ], + "rollback": { "strategy": "disable provider.repowise-adapt and return to the declared compatibility route without deleting provider state or changing A04", "evidence": { "evidenceIds": ["local:b01:rollback-compatible"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace Repowise behind the Evidence Provider Port or remove the optional provider without changing core admissibility", "replacementCriteria": ["replacement passes B01 status/index/docs separation and A04 conformance", "snapshot, quota, provenance, and failure semantics remain explicit", "Repowise state export or deliberate abandonment is documented"], "evidence": { "evidenceIds": ["local:b01:provider-port-replaceability", "gap:repowise:state-exit-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["canonical revision or license remains unverifiable", "quota, security, or maintenance evidence remains unavailable", "a replacement passes B01 and the Repowise route has no production callers"], "evidence": { "evidenceIds": ["local:registry:provider.repowise-adapt", "gap:repowise:retirement-proof"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:b01:repowise-adapter-conformance:bc59f5f22671a14669b7a053f0d075ae64baabc7fabb730d608e3535609a9922", "local:b01:production-operation-trace", "gap:repowise:upstream-revision", "gap:repowise:license", "gap:repowise:representative-value-measurement"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/rg.json b/orchestration/internalization/rg.json new file mode 100644 index 0000000..f17ef22 --- /dev/null +++ b/orchestration/internalization/rg.json @@ -0,0 +1,20 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.rg-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "ripgrep inventory executable", "kind": "adapted_capability", "source": { "uri": "https://github.com/BurntSushi/ripgrep; installed-evidence=rg 15.1.0 (rev af60c2de9d)", "revision": "installed-version:15.1.0-af60c2de9d; local-native-source-sha256:7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9; local-conformance-sha256:c69081d7fb1043d45dcf7a357dffe5fc28f98ffd9ca7dfaca25eb258692e63df" }, "license": { "id": "MIT-OR-Unlicense-UPSTREAM-CLAIM-LOCAL-COPY-MISSING", "obligations": ["do not redistribute or upgrade from this record until the selected license text and package provenance are retained locally", "preserve exact inventory scope, exclusions, order normalization, snapshot lease, and failure semantics"] } }, + "adoption": { "rung": "invoke", "ownedBoundary": ["Pipeline owns inventory.rg request normalization, snapshot-controlled mirror, exclusions, artifact contract, and failure mapping", "ripgrep owns executable search and traversal behavior; this record does not authorize upgrade, vendoring, or reimplementation"], "necessityEvidence": { "evidenceIds": ["local:registry:inventory.rg:required", "local:r09:installed-rg-15.1.0-af60c2de9d", "gap:rg:package-provenance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "compatibilityEvidence": { "evidenceIds": ["local:a00:inventory-parity", "local:r09:cross-platform-contract", "gap:rg:replacement-command-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "conformanceEvidence": { "evidenceIds": ["local:r09:scope-exclusion-conformance", "local:r09:operation-trace", "gap:rg:representative-platform-matrix"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "operationTrace": [ + { "integrationId": "inventory.rg", "operation": "run", "command": "run-code-intel.ps1 -RepoPath -Mode ", "implementationIdentity": { "providerId": "ripgrep", "implementationId": "rg-15.1.0-af60c2de9d-via-compat-facade", "activation": "required production facade" }, "source": { "path": "run-code-intel.ps1", "sha256": "c795801332e2f0b752fa2dd771a38d49e0dd1902ff466630e94240a1204fc22b" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "c69081d7fb1043d45dcf7a357dffe5fc28f98ffd9ca7dfaca25eb258692e63df", "testName": "normalized_inventory_matches_real_legacy_runner_with_custom_exclude" } }, + { "integrationId": "inventory.rg", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec inventory.rg --request --out ", "implementationIdentity": { "providerId": "ripgrep", "implementationId": "inventory.rg.compat+rg-15.1.0-af60c2de9d", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/capability_inventory.rs", "sha256": "7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9" }, "conformance": { "path": "crates/code-intel-cli/tests/capability_exec.rs", "sha256": "c69081d7fb1043d45dcf7a357dffe5fc28f98ffd9ca7dfaca25eb258692e63df", "testName": "inventory_rg_exec_emits_one_result_and_stable_real_rg_artifact" } } + ], + "economics": { "benefit": { "metric": "registered production inventory operations with recomputable invocation trace", "value": 2, "unit": "operations" }, "cost": { "metric": "unclosed executable lifecycle gaps", "value": 4, "unit": "gaps" }, "benefitEvidence": { "evidenceIds": ["local:r09:operation-trace", "local:r09:scope-exclusion-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["gap:rg:package-provenance", "gap:rg:local-license-copy", "gap:rg:replacement-command-drill", "gap:rg:latency-p50-p95-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:r09:pinned-installed-version", "gap:rg:upstream-maintenance-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:r09:no-network-read-only-invocation", "gap:rg:package-supply-chain-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Do not upgrade rg implicitly; before 2026-10-11 retain package provenance/license, rerun cross-platform conformance and benchmark, and exercise the replacement adapter", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:rg:update-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [{ "path": "crates/code-intel-cli/src/capability_inventory.rs", "description": "Pipeline-owned inventory adapter and snapshot-controlled invocation boundary", "evidenceIds": ["local:r09:native-source-sha256:7152c417c594225692bd1910dffc222d25f96158b1b908a58c4e30b688b6e2e9", "local:r09:conformance-sha256:c69081d7fb1043d45dcf7a357dffe5fc28f98ffd9ca7dfaca25eb258692e63df"] }], + "rollback": { "strategy": "route inventory.rg to the preserved compatibility facade without changing the artifact contract or upgrading rg", "evidence": { "evidenceIds": ["local:a00:inventory-parity", "gap:rg:replacement-command-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace the executable only behind inventory.rg after exact artifact and failure parity", "replacementCriteria": ["alternate command passes scope, exclusion, symlink, ignore, empty-repository, and snapshot fixtures", "representative latency p50/p95 and cost do not regress beyond the approved budget", "new executable has pinned provenance, license, security, update, rollback, and retirement evidence"], "evidence": { "evidenceIds": ["gap:rg:replacement-command-drill", "gap:rg:latency-p50-p95-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["replacement passes the complete inventory contract", "installed executable identity or package provenance becomes unverifiable", "security or maintenance policy rejects the pinned package"], "evidence": { "evidenceIds": ["local:r09:operation-trace", "gap:rg:replacement-command-drill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:r09:operation-trace", "local:r09:installed-rg-15.1.0-af60c2de9d", "gap:rg:package-provenance", "gap:rg:local-license-copy", "gap:rg:replacement-command-drill", "gap:rg:latency-p50-p95-measurement"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/sentrux.json b/orchestration/internalization/sentrux.json new file mode 100644 index 0000000..b3ddb4e --- /dev/null +++ b/orchestration/internalization/sentrux.json @@ -0,0 +1,38 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.sentrux-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "Sentrux provider and retained shim boundary", "kind": "adapted_capability", "source": { "uri": "unverified-upstream:sentrux; local-adapter=crates/code-intel-cli/src/sentrux_adapter.rs", "revision": "unverified-upstream; local-adapter-sha256:5aaf4f6e79dacf5b067efab1d07c9d2937000d97a22cf4c81ac9697edf0dc3d4; local-conformance-sha256:7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not vendor, upgrade, distribute, or retire the Sentrux accelerator/shim until canonical upstream revision and license are verified", "keep Pipeline-owned normalization, rules, shim, and external accelerator ownership distinct"] } }, + "adoption": { + "rung": "adapt", + "ownedBoundary": ["Pipeline-owned B03 collection normalization, authoritative rule-kind validation, effect reconciliation, A04 route, native Rust DSM analysis kernel, and retained Invoke-SentruxAgentTool.ps1 rollback", "Sentrux algorithms and diagnosis semantics remain provider-owned; the external executable is an accelerator and direct output is never diagnosis authority"], + "necessityEvidence": { "evidenceIds": ["local:registry:provider.sentrux-adapt", "local:registry:structure.sentrux", "gap:sentrux:upstream-revision", "gap:sentrux:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b03:shim-rollback-identity", "local:b03:fail-closed-unknown-rules", "gap:sentrux:upstream-windows-conformance", "gap:sentrux:upstream-plugin-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:b03:sentrux-adapter-conformance:7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "local:b03:production-operation-trace", "gap:sentrux:upstream-exact-revision-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "operationTrace": [ + { "integrationId": "structure.sentrux", "operation": "rustDsm", "command": "target/debug/code-intel.exe sentrux dsm ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "sentrux-rust-analysis.v1", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux_analysis.rs", "sha256": "74a5eee3e46938663fb846aa91dd87899ccba2902eda97faff6294cb1576670c" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_analysis.rs", "sha256": "9376da0e4b889b7e2c88843b66170004d60bbe798501561d7e5d3f1a9c8a5204", "testName": "dsm_snapshot_preserves_contract_and_excludes_non_governed_source" } }, + { "integrationId": "provider.sentrux-adapt", "operation": "adapt", "command": "target/debug/code-intel.exe provider sentrux-adapt --request --artifact-root --evaluated-at --max-age-seconds ", "implementationIdentity": { "providerId": "sentrux.provider", "implementationId": "sentrux-adapter.v1", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux_adapter.rs", "sha256": "5aaf4f6e79dacf5b067efab1d07c9d2937000d97a22cf4c81ac9697edf0dc3d4" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "public_route_complete_is_eligible_but_never_emits_facts" } }, + { "integrationId": "provider.sentrux-adapt", "operation": "facade", "command": "run-code-intel.ps1 -SentruxAdapterRequest -SentruxAdapterArtifactRoot -SentruxAdapterEvaluatedAt -SentruxAdapterMaxAgeSeconds ", "implementationIdentity": { "providerId": "sentrux.provider", "implementationId": "sentrux-adapter.v1", "activation": "compatibility_facade" }, "source": { "path": "crates/code-intel-cli/src/sentrux_adapter.rs", "sha256": "5aaf4f6e79dacf5b067efab1d07c9d2937000d97a22cf4c81ac9697edf0dc3d4" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "public_route_complete_is_eligible_but_never_emits_facts" } }, + { "integrationId": "provider.sentrux-adapt", "operation": "capabilityExec", "command": "target/debug/code-intel.exe capability exec provider.sentrux-adapt --request --out --artifact-root ", "implementationIdentity": { "providerId": "sentrux.provider", "implementationId": "provider.sentrux-builtin.compat", "activation": "required production capability envelope" }, "source": { "path": "crates/code-intel-cli/src/builtin_provider_evidence.rs", "sha256": "a04226fab56751d014460e1596328e503b6a9afbf2efd5e84ab7b0790831149e" }, "conformance": { "path": "crates/code-intel-cli/tests/dag_run.rs", "sha256": "14c8519a9c762c88c464c6be6a2f49a42dc78339c8eea4312e67b52e427c53b8", "testName": "production_dag_output_commits_and_enters_the_authoritative_index" } }, + { "integrationId": "structure.sentrux", "operation": "rustScan", "command": "target/debug/code-intel.exe sentrux scan ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "complete_normalizes_every_authoritative_kind_and_passes_a04" } }, + { "integrationId": "structure.sentrux", "operation": "rustHealth", "command": "target/debug/code-intel.exe sentrux health ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "complete_normalizes_every_authoritative_kind_and_passes_a04" } }, + { "integrationId": "structure.sentrux", "operation": "rustSessionStart", "command": "target/debug/code-intel.exe sentrux session_start ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "complete_normalizes_every_authoritative_kind_and_passes_a04" } }, + { "integrationId": "structure.sentrux", "operation": "rustSessionEnd", "command": "target/debug/code-intel.exe sentrux session_end ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "complete_normalizes_every_authoritative_kind_and_passes_a04" } }, + { "integrationId": "structure.sentrux", "operation": "rustCheckRules", "command": "target/debug/code-intel.exe sentrux check_rules ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "unknown_kind_is_fail_closed_even_when_provider_labels_it_pass" } }, + { "integrationId": "structure.sentrux", "operation": "rustTestGaps", "command": "target/debug/code-intel.exe sentrux test_gaps ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "complete_missing_known_kind_is_downgraded_and_payload_relabel_is_rejected" } }, + { "integrationId": "structure.sentrux", "operation": "rustWhatIf", "command": "target/debug/code-intel.exe sentrux what_if ", "implementationIdentity": { "providerId": "code-intel.sentrux", "implementationId": "internal-rust", "activation": "primary" }, "source": { "path": "crates/code-intel-cli/src/sentrux.rs", "sha256": "1652512d8dbf1e8dbc399af96dd1af2c546ced95599130289e271a83b10eb764" }, "conformance": { "path": "crates/code-intel-cli/tests/sentrux_adapter.rs", "sha256": "7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "testName": "effect_mismatch_and_inconsistent_rule_are_rejected" } }, + { "integrationId": "structure.sentrux", "operation": "shimScan", "command": "Invoke-SentruxAgentTool.ps1 scan ", "implementationIdentity": { "providerId": "sentrux.compat", "implementationId": "Invoke-SentruxAgentTool.ps1", "activation": "legacy_rollback" }, "source": { "path": "Invoke-SentruxAgentTool.ps1", "sha256": "1584c810347d4dcf6461ed573e79ba7611861f2e79f7bd9c3d200b1e57013594" }, "conformance": { "path": "test-sentrux-failure-normalization.ps1", "sha256": "207691ff731a8eaad33ec13fc3d2ac690588de1aa58c09b8eae7d6681683908d", "testName": "Sentrux failure normalization tests passed" } }, + { "integrationId": "structure.sentrux", "operation": "shimSessionStart", "command": "Invoke-SentruxAgentTool.ps1 session_start ", "implementationIdentity": { "providerId": "sentrux.compat", "implementationId": "Invoke-SentruxAgentTool.ps1", "activation": "legacy_rollback" }, "source": { "path": "Invoke-SentruxAgentTool.ps1", "sha256": "1584c810347d4dcf6461ed573e79ba7611861f2e79f7bd9c3d200b1e57013594" }, "conformance": { "path": "test-sentrux-failure-normalization.ps1", "sha256": "207691ff731a8eaad33ec13fc3d2ac690588de1aa58c09b8eae7d6681683908d", "testName": "Sentrux failure normalization tests passed" } }, + { "integrationId": "structure.sentrux", "operation": "shimSessionEnd", "command": "Invoke-SentruxAgentTool.ps1 session_end ", "implementationIdentity": { "providerId": "sentrux.compat", "implementationId": "Invoke-SentruxAgentTool.ps1", "activation": "legacy_rollback" }, "source": { "path": "Invoke-SentruxAgentTool.ps1", "sha256": "1584c810347d4dcf6461ed573e79ba7611861f2e79f7bd9c3d200b1e57013594" }, "conformance": { "path": "test-sentrux-failure-normalization.ps1", "sha256": "207691ff731a8eaad33ec13fc3d2ac690588de1aa58c09b8eae7d6681683908d", "testName": "Sentrux failure normalization tests passed" } } + ], + "economics": { "benefit": { "metric": "B03 production adapter operations traced to conformance", "value": 2, "unit": "operations" }, "cost": { "metric": "registered B03 command surfaces plus retained shim lifecycle", "value": 3, "unit": "surfaces" }, "benefitEvidence": { "evidenceIds": ["local:b03:production-operation-trace", "gap:sentrux:representative-rule-value-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:b03:shim-retained", "gap:sentrux:latency-maintenance-cost-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["local:b03:shim-rollback-identity", "gap:sentrux:upstream-maintenance-status"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["local:b03:effect-reconciliation", "gap:sentrux:external-binary-security-review", "gap:sentrux:plugin-supply-chain-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify upstream revision/license, Windows and plugin conformance, binary/plugin security, maintenance status, and representative rule utility/cost; retain the shim and research-only status until closure", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:sentrux:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "crates/code-intel-cli/src/sentrux_adapter.rs", "description": "Pipeline-owned B03 structural-evidence adapter and normalization boundary", "evidenceIds": ["local:b03:adapter-source-sha256:5aaf4f6e79dacf5b067efab1d07c9d2937000d97a22cf4c81ac9697edf0dc3d4", "local:b03:sentrux-adapter-conformance:7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d"] }, { "path": "crates/code-intel-cli/src/sentrux_analysis.rs", "description": "native Rust governed-source, file/function metric, module graph, and DSM risk kernel", "evidenceIds": ["local:sentrux:rust-dsm-source-sha256:74a5eee3e46938663fb846aa91dd87899ccba2902eda97faff6294cb1576670c", "local:sentrux:rust-dsm-conformance-sha256:9376da0e4b889b7e2c88843b66170004d60bbe798501561d7e5d3f1a9c8a5204"] }, { "path": "Invoke-SentruxAgentTool.ps1", "description": "retained compatibility implementation and rollback path, not provider authority", "evidenceIds": ["local:b03:shim-retained", "gap:sentrux:shim-retirement-proof"] } ], + "rollback": { "strategy": "keep Invoke-SentruxAgentTool.ps1 as the declared implementation/rollback behind provider.sentrux-adapt; never bypass A04", "evidence": { "evidenceIds": ["local:b03:shim-rollback-identity", "local:b03:fail-closed-unknown-rules"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace the accelerator or shim independently while preserving B03 normalized evidence and failure semantics", "replacementCriteria": ["upstream Windows and plugin conformance is current", "replacement passes authoritative rule and effect reconciliation fixtures", "rollback remains exercised until the replacement has an exit drill"], "evidence": { "evidenceIds": ["local:b03:adapter-replaceability", "gap:sentrux:upstream-windows-conformance", "gap:sentrux:upstream-plugin-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["shim has no production callers after upstream Windows/plugin conformance passes", "external binary provenance or security remains unverifiable", "replacement passes B03 and rollback/exit drills"], "evidence": { "evidenceIds": ["local:b03:shim-retained", "gap:sentrux:shim-retirement-proof", "gap:sentrux:upstream-windows-conformance", "gap:sentrux:upstream-plugin-conformance"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:b03:sentrux-adapter-conformance:7b05684ad8b8c6072480a82ac63233782e15b1b70b9023ae35d1c195fe50172d", "local:b03:production-operation-trace", "gap:sentrux:upstream-revision", "gap:sentrux:license", "gap:sentrux:upstream-windows-conformance", "gap:sentrux:upstream-plugin-conformance"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/spec-kit.json b/orchestration/internalization/spec-kit.json new file mode 100644 index 0000000..d38b53f --- /dev/null +++ b/orchestration/internalization/spec-kit.json @@ -0,0 +1,27 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.spec-kit-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "spec-kit advisory candidate", "kind": "design_reference", "source": { "uri": "https://github.com/github/spec-kit", "revision": "unverified-upstream; local-atom-sha256:03d9cbed70d83c59f7d9540fccc606ce0b2723135efd2c5e32943d367008a199" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until the exact upstream revision and license text are verified", "retain this provenance and gap notice while the candidate remains in research"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned greenfield-fit advisory candidate data only", "no spec-kit runtime dependency", "no specify init, filesystem initialization, commitment, or adoption authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-capability-map:spec-kit-row", "gap:spec-kit:upstream-revision", "gap:spec-kit:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:b06:proposal-effects-empty", "local:b06:no-auto-init-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:atom:spec-kit-eight-occurrences", "local:b06:a01-envelope-parity"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { + "benefit": { "metric": "spec-kit token occurrences traced in the current advisory atom", "value": 8, "unit": "occurrences" }, + "cost": { "metric": "pipeline-owned spec-kit lifecycle records", "value": 1, "unit": "records" }, + "benefitEvidence": { "evidenceIds": ["local:atom:spec-kit-eight-occurrences"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "costEvidence": { "evidenceIds": ["local:record:spec-kit"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:spec-kit:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:spec-kit:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify the upstream commit, LICENSE, releases, and greenfield-fit semantics; keep research-only until admitted", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:spec-kit:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "OpenSpec-Detector.ps1", "description": "Pipeline-owned spec-kit fit heuristic and advisory rendering; no upstream execution", "evidenceIds": ["local:atom:spec-kit-eight-occurrences", "local:b06:no-auto-init-boundary"] } ], + "rollback": { "strategy": "delete this candidate record and remove the spec-kit alternative without changing facts, commitments, or advisory authority", "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "replace with a pinned, licensed greenfield candidate or retire while retaining audit provenance", "replacementCriteria": ["fit evidence uses repository facts rather than tool name alone", "replacement has exact revision, license, measurements, and no-auto-init conformance"], "evidence": { "evidenceIds": ["local:b06:candidate-removal-boundary"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["greenfield-fit claim is not evidence-bound", "upstream provenance or license remains unverifiable", "candidate loses unique measured advisory value"], "evidence": { "evidenceIds": ["local:record:spec-kit"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:spec-kit", "gap:spec-kit:upstream-revision", "gap:spec-kit:license"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/internalization/tree-sitter-v.json b/orchestration/internalization/tree-sitter-v.json new file mode 100644 index 0000000..e50ab41 --- /dev/null +++ b/orchestration/internalization/tree-sitter-v.json @@ -0,0 +1,13 @@ +{ + "schema":"code-intel-internalization-record.v1","id":"internalization.tree-sitter-v-record","projectId":"code-intel-pipeline", + "subject":{"name":"tree-sitter-v Sentrux overlay grammar","kind":"adapted_capability","source":{"uri":"https://github.com/nedpals/tree-sitter-v","revision":"upstream-revision-unverified; local-installer-sha256:e7d1ecb989c69274fbfa6bee88e30102224a6890e9b361fb543c0481e0902732; local-plugin-metadata-sha256:1ae47b3f033d48ea23427e4ce694c2ddb5ebf258db804d7ec75d6e7c61d1b140; local-compiled-windows-x86_64-sha256:921dec08ca60455fca2794148bee852f91e2d2aa8853a85ca62a42bccdcf216f; local-abi-alias-source-sha256:b455dc671370533361bbbf3112259d9d5ae99ef28c9f760a8faa6cf4f0fac558; local-license-sha256:a8544b84012a8d88c6fa562fb1fd9d87661d9c96b339c9d3d3215e2122ae7f0b"},"license":{"id":"MIT","obligations":["retain the tree-sitter-v MIT license and attribution with any distributed compiled grammar","do not claim reproducible or approved binary provenance until source revision, toolchain, artifact digest, and ABI verification are bound"]}}, + "adoption":{"rung":"adapt","ownedBoundary":["Pipeline owns only overlay packaging, ABI alias, plugin metadata, installer, digest verification, and rollback","Sentrux owns parser/plugin loading and structural-evidence semantics; tree-sitter-v upstream owns grammar implementation; neither ownership is transferred to Pipeline"],"necessityEvidence":{"evidenceIds":["local:reference-map:tree-sitter-v-active-optional","gap:tree-sitter-v:pinned-upstream-revision"],"checkedAt":1783900800,"expiresAt":1791676800},"compatibilityEvidence":{"evidenceIds":["local:r11:plugin-abi-13-declaration","local:r11:compiled-windows-x86_64-sha256:921dec08ca60455fca2794148bee852f91e2d2aa8853a85ca62a42bccdcf216f","gap:tree-sitter-v:upstream-sentrux-comparison"],"checkedAt":1783900800,"expiresAt":1791676800},"conformanceEvidence":{"evidenceIds":["local:r11:abi-alias-source-sha256:b455dc671370533361bbbf3112259d9d5ae99ef28c9f760a8faa6cf4f0fac558","gap:tree-sitter-v:reproducible-build","gap:tree-sitter-v:v-fixture-conformance","gap:tree-sitter-v:abi-symbol-verification"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "economics":{"benefit":{"metric":"locally declared V grammar plugin capability","value":1,"unit":"optional-language-capabilities"},"cost":{"metric":"unclosed reproducibility and conformance gaps","value":4,"unit":"gaps"},"benefitEvidence":{"evidenceIds":["local:r11:plugin-abi-13-declaration","gap:tree-sitter-v:measured-v-value"],"checkedAt":1783900800,"expiresAt":1791676800},"costEvidence":{"evidenceIds":["gap:tree-sitter-v:pinned-upstream-revision","gap:tree-sitter-v:abi-symbol-verification","gap:tree-sitter-v:reproducible-build","gap:tree-sitter-v:v-fixture-conformance"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "assurance":{"maintenanceEvidence":{"evidenceIds":["local:r11:installer-source-sha256:e7d1ecb989c69274fbfa6bee88e30102224a6890e9b361fb543c0481e0902732","gap:tree-sitter-v:upstream-maintenance-review"],"checkedAt":1783900800,"expiresAt":1791676800},"securityEvidence":{"evidenceIds":["local:r11:license-copy-sha256:a8544b84012a8d88c6fa562fb1fd9d87661d9c96b339c9d3d3215e2122ae7f0b","gap:tree-sitter-v:compiler-toolchain-provenance","gap:tree-sitter-v:binary-security-review"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "update":{"policy":"Pin an exact reviewed upstream revision, rebuild with a recorded toolchain, verify digest/ABI/V fixtures, compare the upstream Sentrux plugin, and retain MIT notice before enablement","nextCheckAt":1791676800,"evidence":{"evidenceIds":["gap:tree-sitter-v:update-rebuild"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "ownedModifications":[{"path":"Install-SentruxVlangOverlay.ps1","description":"Pipeline-owned overlay installer and verification boundary only; not Sentrux parsing or tree-sitter-v grammar ownership","evidenceIds":["local:r11:installer-source-sha256:e7d1ecb989c69274fbfa6bee88e30102224a6890e9b361fb543c0481e0902732"]},{"path":"overlays/sentrux/vlang/plugin.toml","description":"Pipeline-owned Sentrux plugin metadata and ABI alias declaration","evidenceIds":["local:r11:plugin-metadata-sha256:1ae47b3f033d48ea23427e4ce694c2ddb5ebf258db804d7ec75d6e7c61d1b140"]},{"path":"overlays/sentrux/vlang/src/sentrux_vlang_alias.c","description":"Pipeline-owned ABI alias source; exported-symbol verification remains an explicit gap","evidenceIds":["local:r11:abi-alias-source-sha256:b455dc671370533361bbbf3112259d9d5ae99ef28c9f760a8faa6cf4f0fac558","gap:tree-sitter-v:abi-symbol-verification"]},{"path":"overlays/sentrux/vlang/grammars/windows-x86_64.dll","description":"Locally retained compiled overlay artifact with bound digest; upstream revision/toolchain reproducibility remains unverified","evidenceIds":["local:r11:compiled-windows-x86_64-sha256:921dec08ca60455fca2794148bee852f91e2d2aa8853a85ca62a42bccdcf216f","gap:tree-sitter-v:reproducible-build"]}], + "rollback":{"strategy":"remove or disable only the optional vlang overlay and retain Sentrux without V parsing","evidence":{"evidenceIds":["local:r11:optional-overlay-boundary","gap:tree-sitter-v:rollback-drill"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "exit":{"strategy":"retire the overlay when upstream Sentrux supplies a pinned plugin that passes the same ABI and V fixtures","replacementCriteria":["upstream plugin revision and license are pinned","compiled artifact digest and ABI symbol are verified","V conformance and rollback tests pass"],"evidence":{"evidenceIds":["gap:tree-sitter-v:upstream-sentrux-comparison"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "retirement":{"status":"candidate","triggers":["upstream Sentrux plugin reaches conformance","reproducible binary provenance cannot be established","measured V-language value does not justify overlay maintenance"],"evidence":{"evidenceIds":["gap:tree-sitter-v:retirement-proof"],"checkedAt":1783900800,"expiresAt":1791676800}}, + "lifecycle":{"previousStatus":null,"status":"research","effectiveAt":1783900800,"replacementRecordId":null,"evidenceIds":["local:r11:optional-overlay-boundary","local:r11:compiled-windows-x86_64-sha256:921dec08ca60455fca2794148bee852f91e2d2aa8853a85ca62a42bccdcf216f","gap:tree-sitter-v:pinned-upstream-revision","gap:tree-sitter-v:reproducible-build","gap:tree-sitter-v:v-fixture-conformance"],"authorityEvent":null},"provenance":{"recordedAt":1783900800,"recordedBy":"dependency-expert"} +} diff --git a/orchestration/internalization/yao-meta-skill.json b/orchestration/internalization/yao-meta-skill.json new file mode 100644 index 0000000..6c8cd5d --- /dev/null +++ b/orchestration/internalization/yao-meta-skill.json @@ -0,0 +1,22 @@ +{ + "schema": "code-intel-internalization-record.v1", + "id": "internalization.yao-meta-skill-record", + "projectId": "code-intel-pipeline", + "subject": { "name": "yao-meta-skill skill-engineering benchmark reference", "kind": "design_reference", "source": { "uri": "unverified-upstream:yao-meta-skill; local-reference=docs/skill-development-benchmark.md", "revision": "unverified-upstream; local-doc-sha256:a6f959b4e57f138d8ffb3be9bf830f3b91ac357136c0a75f268e1ecefd9556b7" }, "license": { "id": "UNKNOWN-RESEARCH-ONLY", "obligations": ["do not distribute, vendor, invoke, or production-enable until canonical upstream URI, exact revision, and license text are verified", "a local benchmark documentation test is not evidence that yao-meta-skill executed or that upstream behavior conforms"] } }, + "adoption": { + "rung": "reimplement", + "ownedBoundary": ["pipeline-owned benchmark criteria: lean entrypoint, trigger exclusions, portable metadata, eval fixtures, failure library, evidence-separated review, release gates, and adoption-drift feedback", "no yao-meta-skill runtime dependency, upstream execution claim, scanner evidence authority, skill publication, installation, or external-write authority"], + "necessityEvidence": { "evidenceIds": ["local:reference-map:yao-meta-skill", "local:skill-benchmark:eight-criteria", "gap:yao-meta-skill:upstream-revision", "gap:yao-meta-skill:license"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "compatibilityEvidence": { "evidenceIds": ["local:skill-benchmark:no-runtime", "local:adr-0004:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, + "conformanceEvidence": { "evidenceIds": ["local:trace:yao:lean-entrypoint", "local:trace:yao:trigger-exclusions", "local:trace:yao:portable-metadata", "local:trace:yao:eval-fixtures", "local:trace:yao:failure-library", "local:trace:yao:evidence-review", "local:trace:yao:release-gates", "local:trace:yao:adoption-drift", "gap:yao-meta-skill:upstream-conformance", "gap:yao-meta-skill:behavioral-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } + }, + "economics": { "benefit": { "metric": "locally documented absorbed skill benchmark criteria", "value": 8, "unit": "criteria" }, "cost": { "metric": "pipeline-owned benchmark semantic trace entries", "value": 8, "unit": "entries" }, "benefitEvidence": { "evidenceIds": ["local:skill-benchmark:eight-criteria", "gap:yao-meta-skill:behavioral-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "costEvidence": { "evidenceIds": ["local:record:yao-meta-skill"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "assurance": { "maintenanceEvidence": { "evidenceIds": ["gap:yao-meta-skill:update-check"], "checkedAt": 1783900800, "expiresAt": 1791676800 }, "securityEvidence": { "evidenceIds": ["gap:yao-meta-skill:security-review"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "update": { "policy": "Before 2026-10-11, verify canonical source, exact revision, license, and upstream semantic mapping; collect behavioral utility evidence beyond the local documentation contract check", "nextCheckAt": 1791676800, "evidence": { "evidenceIds": ["gap:yao-meta-skill:update-check", "gap:yao-meta-skill:behavioral-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "ownedModifications": [ { "path": "docs/skill-development-benchmark.md", "description": "Pipeline-owned eight-criterion skill benchmark and explicit statement that the local check does not run upstream", "evidenceIds": ["local:skill-benchmark:eight-criteria", "local:skill-benchmark:no-runtime"] }, { "path": "test-skill-development-benchmark.ps1", "description": "Local documentation and boundary contract check; not upstream execution or behavioral conformance evidence", "evidenceIds": ["local:skill-benchmark:doc-contract-only", "gap:yao-meta-skill:behavioral-measurement"] } ], + "rollback": { "strategy": "remove yao-meta-skill attribution and this record while preserving the versioned internal benchmark criteria and local gate", "evidence": { "evidenceIds": ["local:adr-0004:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "exit": { "strategy": "retire or replace the source reference independently; retain only benchmark criteria with local semantic and behavioral evidence", "replacementCriteria": ["replacement provenance and license are verified", "semantic mapping covers all retained criteria", "behavioral measurements go beyond a local documentation test pass", "removal leaves the internal benchmark gate and scanner contracts stable"], "evidence": { "evidenceIds": ["local:adr-0004:reference-removable"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "retirement": { "status": "candidate", "triggers": ["source revision or license remains unverifiable", "criteria have no independent behavioral utility evidence", "reference creates runtime coupling or upstream-execution claims"], "evidence": { "evidenceIds": ["local:record:yao-meta-skill", "gap:yao-meta-skill:behavioral-measurement"], "checkedAt": 1783900800, "expiresAt": 1791676800 } }, + "lifecycle": { "previousStatus": null, "status": "research", "effectiveAt": 1783900800, "replacementRecordId": null, "evidenceIds": ["local:record:yao-meta-skill", "gap:yao-meta-skill:upstream-revision", "gap:yao-meta-skill:license", "gap:yao-meta-skill:upstream-conformance", "gap:yao-meta-skill:behavioral-measurement"], "authorityEvent": null }, + "provenance": { "recordedAt": 1783900800, "recordedBy": "dependency-expert" } +} diff --git a/orchestration/language-adapter-acceptance-policy.v1.json b/orchestration/language-adapter-acceptance-policy.v1.json new file mode 100644 index 0000000..432b098 --- /dev/null +++ b/orchestration/language-adapter-acceptance-policy.v1.json @@ -0,0 +1,69 @@ +{ + "schema": "code-intel-language-adapter-acceptance-policy.v1", + "claimLevels": ["inventory", "structural", "semantic", "behavioral"], + "allowedEffects": ["repo_read", "local_write"], + "requiredArtifactSchemas": [ + "code-evidence-files.v1", + "code-evidence-symbols.v1", + "code-evidence-chunks.v1", + "code-evidence-symbol-chunks.v1", + "code-evidence-imports.v1", + "code-evidence-coverage.v1" + ], + "stages": { + "research": { + "minimums": { + "languages": 1, + "labeledSamples": 1, + "precision": 0.0, + "recall": 0.0, + "declaredCoverage": 0.0, + "deterministicReplays": 1, + "parityArtifacts": 0, + "semanticOracleCases": 1, + "behavioralOracleCases": 1 + }, + "requirements": { + "knownLicense": false, + "rollbackTested": false, + "independentVerification": false + } + }, + "candidate": { + "minimums": { + "languages": 1, + "labeledSamples": 12, + "precision": 0.75, + "recall": 0.75, + "declaredCoverage": 0.8, + "deterministicReplays": 3, + "parityArtifacts": 6, + "semanticOracleCases": 25, + "behavioralOracleCases": 25 + }, + "requirements": { + "knownLicense": true, + "rollbackTested": true, + "independentVerification": false + } + }, + "production": { + "minimums": { + "languages": 1, + "labeledSamples": 50, + "precision": 0.9, + "recall": 0.9, + "declaredCoverage": 0.95, + "deterministicReplays": 10, + "parityArtifacts": 6, + "semanticOracleCases": 100, + "behavioralOracleCases": 100 + }, + "requirements": { + "knownLicense": true, + "rollbackTested": true, + "independentVerification": true + } + } + } +} diff --git a/orchestration/method-selection-rules.v1.json b/orchestration/method-selection-rules.v1.json new file mode 100644 index 0000000..e2d6af0 --- /dev/null +++ b/orchestration/method-selection-rules.v1.json @@ -0,0 +1,77 @@ +{ + "schema": "code-intel-method-selection-rules.v1", + "rules": [ + { + "methodId": "contract-testing", + "signalIds": ["integration-drift", "late-interface-failure"], + "contraindications": [ + { "signalId": "no-observable-contract", "cardText": "The boundary has no observable contract" }, + { "signalId": "private-implementation-assertions", "cardText": "Tests would assert private implementation details instead of compatibility" } + ] + }, + { + "methodId": "critical-path-pert", + "signalIds": ["dependency-congestion", "schedule-slippage"], + "contraindications": [ + { "signalId": "work-not-decomposable", "cardText": "Work cannot be decomposed into dependency-linked activities" }, + { "signalId": "unmodeled-resource-dominance", "cardText": "Resource contention dominates but is absent from the model" } + ] + }, + { + "methodId": "fault-tree-analysis", + "signalIds": ["hazardous-top-event", "multi-cause-failure"], + "contraindications": [ + { "signalId": "top-event-undefined", "cardText": "The analysis question is exploratory rather than a defined top event" }, + { "signalId": "boolean-model-invalid", "cardText": "Temporal or continuous dynamics invalidate Boolean-event modeling" } + ] + }, + { + "methodId": "fmea", + "signalIds": ["design-change-risk", "recurrent-component-failure"], + "contraindications": [ + { "signalId": "single-known-top-event", "cardText": "A single known top event is the only analysis target" }, + { "signalId": "ratings-treated-as-probabilities", "cardText": "Scores would be used as precise probabilities without calibration" } + ] + }, + { + "methodId": "pdca", + "signalIds": ["repeated-corrective-action", "uncontrolled-improvement"], + "contraindications": [ + { "signalId": "irreversible-uncontrolled-change", "cardText": "The proposed action is irreversible without prior risk controls" }, + { "signalId": "no-measurable-trial", "cardText": "No measurable target or bounded trial can be defined" } + ] + }, + { + "methodId": "root-cause-analysis", + "signalIds": ["causal-uncertainty", "recurrent-incident"], + "contraindications": [ + { "signalId": "prospective-enumeration-request", "cardText": "The request is to enumerate prospective failure modes rather than explain an observed effect" }, + { "signalId": "opinion-only-causality", "cardText": "Evidence is limited to an unaudited opinion or post hoc story" } + ] + }, + { + "methodId": "spc", + "signalIds": ["process-variation", "unstable-quality"], + "contraindications": [ + { "signalId": "one-off-data", "cardText": "Data are one-off rather than a repeated process" }, + { "signalId": "control-limit-as-specification", "cardText": "Specification conformance is being inferred solely from control limits" } + ] + }, + { + "methodId": "strangler-migration", + "signalIds": ["high-risk-rewrite", "replaceable-seams"], + "contraindications": [ + { "signalId": "no-isolatable-seam", "cardText": "There is no isolatable routing or responsibility seam" }, + { "signalId": "unsafe-dual-operation", "cardText": "Dual operation creates unacceptable safety or consistency risk" } + ] + }, + { + "methodId": "value-stream-queue-delay", + "signalIds": ["handoff-wait", "long-lead-time"], + "contraindications": [ + { "signalId": "aggregate-counts-only", "cardText": "Only aggregate completion counts exist with no item flow" }, + { "signalId": "dependency-schedule-only", "cardText": "A dependency schedule is being mistaken for an operational queue model" } + ] + } + ] +} diff --git a/orchestration/methods/cards/contract-testing.v1.json b/orchestration/methods/cards/contract-testing.v1.json new file mode 100644 index 0000000..05ddf1b --- /dev/null +++ b/orchestration/methods/cards/contract-testing.v1.json @@ -0,0 +1,37 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "contract-testing", + "version": "1.0.0", + "name": "Contract testing", + "problemSignals": [ + { "id": "integration-drift", "description": "Consumer expectations and provider behavior change independently." }, + { "id": "late-interface-failure", "description": "Interface incompatibility is detected only in integrated environments." } + ], + "requiredEvidence": [ + { "id": "consumer-provider-contracts", "description": "Versioned requests, responses, invariants, and compatibility expectations." }, + { "id": "provider-verification-context", "description": "Deterministic provider state and verifier inputs for each interaction." } + ], + "assumptions": ["The interaction boundary can be isolated", "Examples encode externally observable behavior rather than implementation details"], + "deterministicSteps": [ + { "id": "normalize-contracts", "action": "Normalize each consumer expectation into a versioned interaction contract.", "requires": ["evidence:consumer-provider-contracts"], "produces": ["contract-suite"] }, + { "id": "verify-provider", "action": "Replay every normalized interaction against the declared provider state and record exact mismatches.", "requires": ["evidence:provider-verification-context", "step:normalize-contracts"], "produces": ["compatibility-report"] } + ], + "outputs": [ + { "id": "contract-suite", "description": "Versioned executable interaction expectations." }, + { "id": "compatibility-report", "description": "Per-interaction pass, fail, or unverifiable result with evidence." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["All production-relevant interactions are represented", "Provider states are reproducible", "Every contract passes"] }, + { "level": "medium", "whenAll": ["Core interactions are represented", "Known optional interactions remain unverified"] }, + { "level": "unknown", "whenAll": ["Provider state or interaction coverage cannot be established"] } + ], + "cost": { "relative": "medium", "drivers": ["Number of consumer-provider pairs", "Provider-state fixture maintenance"] }, + "contraindications": ["The boundary has no observable contract", "Tests would assert private implementation details instead of compatibility"], + "implementationPorts": [ + { "id": "contract-runner", "kind": "deterministic_tool", "contract": ["Accept versioned interactions and provider states", "Emit per-interaction evidence without deployment authority"] } + ], + "source": { "title": "Consumer-driven contract testing practice", "version": "catalog interpretation 1.0", "reference": "C01 engineering-method baseline" }, + "applicabilityBoundary": { "inScope": ["Service APIs", "Message schemas", "Library-facing interfaces"], "outOfScope": ["Whole-system performance qualification", "Internal refactoring with no contract change"] }, + "relatedMethodIds": ["strangler-migration"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/critical-path-pert.v1.json b/orchestration/methods/cards/critical-path-pert.v1.json new file mode 100644 index 0000000..caee683 --- /dev/null +++ b/orchestration/methods/cards/critical-path-pert.v1.json @@ -0,0 +1,37 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "critical-path-pert", + "version": "1.0.0", + "name": "Critical path and PERT analysis", + "problemSignals": [ + { "id": "schedule-slippage", "description": "Delivery dates move without a traceable dependency explanation." }, + { "id": "dependency-congestion", "description": "Multiple activities wait on shared predecessors or scarce resources." } + ], + "requiredEvidence": [ + { "id": "activity-network", "description": "Activities with explicit predecessor relationships and completion criteria." }, + { "id": "activity-duration-estimates", "description": "Optimistic, most-likely, and pessimistic durations or justified fixed durations." } + ], + "assumptions": ["The activity graph is acyclic", "Duration estimates use a consistent unit and scope"], + "deterministicSteps": [ + { "id": "validate-network", "action": "Validate activity identities, predecessor closure, acyclicity, and duration units.", "requires": ["evidence:activity-network", "evidence:activity-duration-estimates"], "produces": ["validated-network"] }, + { "id": "calculate-paths", "action": "Compute expected durations, earliest/latest times, slack, and critical paths using declared formulas.", "requires": ["step:validate-network"], "produces": ["schedule-model"] } + ], + "outputs": [ + { "id": "validated-network", "description": "Closed dependency graph with normalized duration evidence." }, + { "id": "schedule-model", "description": "Expected schedule, slack, critical activities, and uncertainty assumptions." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["All dependencies are evidenced", "Duration ranges are calibrated from comparable work"] }, + { "level": "medium", "whenAll": ["Dependency closure is complete", "Some durations rely on expert estimates"] }, + { "level": "unknown", "whenAll": ["The graph is incomplete or cyclic", "Duration units or scopes conflict"] } + ], + "cost": { "relative": "medium", "drivers": ["Activity count", "Estimate calibration effort", "Dependency volatility"] }, + "contraindications": ["Work cannot be decomposed into dependency-linked activities", "Resource contention dominates but is absent from the model"], + "implementationPorts": [ + { "id": "network-scheduler", "kind": "deterministic_tool", "contract": ["Reject cycles and missing predecessors", "Emit formulas, slack, and all critical paths"] } + ], + "source": { "title": "Critical Path Method and Program Evaluation and Review Technique", "version": "catalog interpretation 1.0", "reference": "Established network scheduling practice" }, + "applicabilityBoundary": { "inScope": ["Dependency-driven delivery planning", "Schedule sensitivity analysis"], "outOfScope": ["Product priority decisions", "Unmodeled resource-allocation optimization"] }, + "relatedMethodIds": ["value-stream-queue-delay"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/fault-tree-analysis.v1.json b/orchestration/methods/cards/fault-tree-analysis.v1.json new file mode 100644 index 0000000..d01ffea --- /dev/null +++ b/orchestration/methods/cards/fault-tree-analysis.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "fault-tree-analysis", + "version": "1.0.0", + "name": "Fault tree analysis", + "problemSignals": [ + { "id": "hazardous-top-event", "description": "A specific undesirable system event requires deductive decomposition." }, + { "id": "multi-cause-failure", "description": "Failure may require combinations of lower-level events." } + ], + "requiredEvidence": [ + { "id": "top-event-definition", "description": "A precise observable top event with system boundary and operating condition." }, + { "id": "system-causal-structure", "description": "Component functions, interfaces, failure evidence, and independence assumptions." } + ], + "assumptions": ["Boolean gate semantics are adequate for the modeled event", "Common-cause dependencies are declared explicitly"], + "deterministicSteps": [ + { "id": "bound-top-event", "action": "Fix the top event, system boundary, operating state, and success/failure semantics.", "requires": ["evidence:top-event-definition"], "produces": ["bounded-top-event"] }, + { "id": "decompose-tree", "action": "Deductively expand causes with explicit AND/OR gates until justified basic events or stopping rules.", "requires": ["evidence:system-causal-structure", "step:bound-top-event"], "produces": ["fault-tree"] }, + { "id": "derive-cut-sets", "action": "Derive minimal cut sets and identify unsupported independence or probability assumptions.", "requires": ["step:decompose-tree"], "produces": ["cut-set-report"] } + ], + "outputs": [ + { "id": "bounded-top-event", "description": "Unambiguous failure event and analysis boundary." }, + { "id": "fault-tree", "description": "Traceable Boolean causal tree with stopping rules." }, + { "id": "cut-set-report", "description": "Minimal cut sets and assumption gaps." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["Top event and boundaries are observable", "Basic events and dependencies are evidenced", "Cut sets are independently checked"] }, + { "level": "medium", "whenAll": ["Tree logic is complete", "Some basic-event rates or independence assumptions are unverified"] }, + { "level": "unknown", "whenAll": ["Top event is ambiguous", "Causal structure cannot support gate selection"] } + ], + "cost": { "relative": "high", "drivers": ["System decomposition depth", "Common-cause analysis", "Probability-data quality"] }, + "contraindications": ["The analysis question is exploratory rather than a defined top event", "Temporal or continuous dynamics invalidate Boolean-event modeling"], + "implementationPorts": [ + { "id": "fault-tree-modeler", "kind": "modeling_tool", "contract": ["Preserve gate semantics and evidence links", "Derive reproducible minimal cut sets"] } + ], + "source": { "title": "IEC 61025 Fault tree analysis", "version": "IEC 61025:2006 boundary summary", "reference": "IEC 61025" }, + "applicabilityBoundary": { "inScope": ["Deductive analysis of a defined undesirable event", "Minimal cut-set reasoning"], "outOfScope": ["Open-ended root-cause discovery", "Continuous-time simulation without event abstraction"] }, + "relatedMethodIds": ["fmea", "root-cause-analysis"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/fmea.v1.json b/orchestration/methods/cards/fmea.v1.json new file mode 100644 index 0000000..ec8d76c --- /dev/null +++ b/orchestration/methods/cards/fmea.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "fmea", + "version": "1.0.0", + "name": "Failure modes and effects analysis", + "problemSignals": [ + { "id": "design-change-risk", "description": "A design or process change introduces uncertain failure modes." }, + { "id": "recurrent-component-failure", "description": "Component or process-step failures need systematic prioritization." } + ], + "requiredEvidence": [ + { "id": "functions-and-requirements", "description": "Bounded functions, requirements, interfaces, and operating conditions." }, + { "id": "failure-mode-evidence", "description": "Observed failures, analogous history, tests, or justified expert elicitation." } + ], + "assumptions": ["Analysis granularity is consistent", "Severity, occurrence, and detection scales are defined before scoring"], + "deterministicSteps": [ + { "id": "enumerate-functions", "action": "Enumerate bounded functions and requirements at one declared analysis level.", "requires": ["evidence:functions-and-requirements"], "produces": ["function-boundary"] }, + { "id": "enumerate-failure-modes", "action": "For each function enumerate failure modes, effects, causes, controls, and evidence source.", "requires": ["evidence:failure-mode-evidence", "step:enumerate-functions"], "produces": ["failure-mode-table"] }, + { "id": "rank-actions", "action": "Apply the declared ordinal scales and rank recommended risk-reduction actions without treating rank as observed probability.", "requires": ["step:enumerate-failure-modes"], "produces": ["risk-action-register"] } + ], + "outputs": [ + { "id": "function-boundary", "description": "Declared FMEA scope and analysis level." }, + { "id": "failure-mode-table", "description": "Traceable modes, effects, causes, controls, and evidence." }, + { "id": "risk-action-register", "description": "Prioritized proposed controls with scale limitations." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["Functions and interfaces are complete", "Failure modes have operational or test evidence", "Scales are calibrated"] }, + { "level": "medium", "whenAll": ["Scope is complete", "Some occurrence or detection ratings rely on expert judgment"] }, + { "level": "unknown", "whenAll": ["Analysis level is mixed", "Failure-mode coverage cannot be assessed"] } + ], + "cost": { "relative": "high", "drivers": ["Function count", "Cross-functional review", "Failure-history quality"] }, + "contraindications": ["A single known top event is the only analysis target", "Scores would be used as precise probabilities without calibration"], + "implementationPorts": [ + { "id": "fmea-register", "kind": "deterministic_tool", "contract": ["Preserve function-to-mode-to-effect traceability", "Keep rating scales and evidence explicit"] } + ], + "source": { "title": "IEC 60812 Failure modes and effects analysis", "version": "IEC 60812:2018 boundary summary", "reference": "IEC 60812" }, + "applicabilityBoundary": { "inScope": ["Inductive failure-mode enumeration", "Risk-control prioritization"], "outOfScope": ["Precise reliability probability estimation", "Deductive minimal cut-set derivation"] }, + "relatedMethodIds": ["fault-tree-analysis", "root-cause-analysis"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/pdca.v1.json b/orchestration/methods/cards/pdca.v1.json new file mode 100644 index 0000000..c0e5018 --- /dev/null +++ b/orchestration/methods/cards/pdca.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "pdca", + "version": "1.0.0", + "name": "Plan-Do-Check-Act", + "problemSignals": [ + { "id": "uncontrolled-improvement", "description": "Changes are made without a baseline, prediction, or learning loop." }, + { "id": "repeated-corrective-action", "description": "Corrective actions recur without standardized follow-through." } + ], + "requiredEvidence": [ + { "id": "baseline-and-target", "description": "Operational definition, baseline measure, target, and observation window." }, + { "id": "change-hypothesis", "description": "A bounded change, predicted effect, risks, and rollback condition." } + ], + "assumptions": ["The change can be bounded and observed", "The check interval is long enough to detect the predicted effect"], + "deterministicSteps": [ + { "id": "plan", "action": "Record baseline, target, hypothesis, change scope, safeguards, and check criteria.", "requires": ["evidence:baseline-and-target", "evidence:change-hypothesis"], "produces": ["cycle-plan"] }, + { "id": "do-check", "action": "Apply only the bounded trial and compare observed measures with the plan using the declared criteria.", "requires": ["step:plan"], "produces": ["learning-record"] }, + { "id": "act", "action": "Classify the result as standardize, adapt, abandon, or repeat and preserve the evidence for the next cycle.", "requires": ["step:do-check"], "produces": ["next-cycle-decision"] } + ], + "outputs": [ + { "id": "cycle-plan", "description": "Bounded improvement hypothesis and evaluation contract." }, + { "id": "learning-record", "description": "Observed comparison against the prediction." }, + { "id": "next-cycle-decision", "description": "Evidence-linked proposal for standardization or another cycle." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["Baseline and target are operationally defined", "Trial scope is controlled", "Observed window meets the plan"] }, + { "level": "medium", "whenAll": ["Trial is bounded", "Confounders or sample size limit attribution"] }, + { "level": "unknown", "whenAll": ["No baseline exists", "The change and check criteria were altered during the cycle"] } + ], + "cost": { "relative": "medium", "drivers": ["Measurement delay", "Trial isolation", "Rollback and standardization effort"] }, + "contraindications": ["The proposed action is irreversible without prior risk controls", "No measurable target or bounded trial can be defined"], + "implementationPorts": [ + { "id": "pdca-ledger", "kind": "manual", "contract": ["Bind each phase to one cycle identity", "Do not convert the Act phase into automatic adoption authority"] } + ], + "source": { "title": "Shewhart-Deming improvement cycle", "version": "catalog interpretation 1.0", "reference": "Established PDCA quality practice" }, + "applicabilityBoundary": { "inScope": ["Controlled iterative improvement", "Corrective-action learning loops"], "outOfScope": ["Unbounded transformation programs", "Automatic approval of successful trials"] }, + "relatedMethodIds": ["root-cause-analysis", "spc"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/root-cause-analysis.v1.json b/orchestration/methods/cards/root-cause-analysis.v1.json new file mode 100644 index 0000000..74069b2 --- /dev/null +++ b/orchestration/methods/cards/root-cause-analysis.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "root-cause-analysis", + "version": "1.0.0", + "name": "Root-cause analysis", + "problemSignals": [ + { "id": "recurrent-incident", "description": "The same failure or symptom recurs after local remediation." }, + { "id": "causal-uncertainty", "description": "Multiple plausible contributing factors lack discriminating evidence." } + ], + "requiredEvidence": [ + { "id": "incident-timeline", "description": "Time-ordered observations, changes, controls, and recovery actions." }, + { "id": "causal-tests", "description": "Reproduction, counterfactual, comparison, or intervention evidence for candidate causes." } + ], + "assumptions": ["The analyzed effect is operationally defined", "Contributing conditions are not mislabeled as a single universal root"], + "deterministicSteps": [ + { "id": "bound-effect", "action": "Define the effect, scope, occurrence window, and non-occurrence comparison.", "requires": ["evidence:incident-timeline"], "produces": ["bounded-problem"] }, + { "id": "build-causal-chain", "action": "Enumerate candidate causal chains and distinguish observations, assumptions, and missing evidence.", "requires": ["step:bound-effect"], "produces": ["causal-hypotheses"] }, + { "id": "test-causes", "action": "Apply the declared causal tests and retain supported, rejected, and unresolved hypotheses separately.", "requires": ["evidence:causal-tests", "step:build-causal-chain"], "produces": ["cause-assessment"] } + ], + "outputs": [ + { "id": "bounded-problem", "description": "Operational effect and analysis boundary." }, + { "id": "causal-hypotheses", "description": "Traceable candidate causal chains and evidence gaps." }, + { "id": "cause-assessment", "description": "Supported, rejected, and unresolved causes with test evidence." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["Timeline is complete", "Cause predicts occurrence and non-occurrence", "Intervention or reproduction supports the chain"] }, + { "level": "medium", "whenAll": ["Multiple independent observations support the chain", "Direct intervention evidence is unavailable"] }, + { "level": "unknown", "whenAll": ["Only temporal correlation exists", "Competing hypotheses lack discriminating evidence"] } + ], + "cost": { "relative": "medium", "drivers": ["Reproduction difficulty", "Evidence retention quality", "Number of competing hypotheses"] }, + "contraindications": ["The request is to enumerate prospective failure modes rather than explain an observed effect", "Evidence is limited to an unaudited opinion or post hoc story"], + "implementationPorts": [ + { "id": "causal-evidence-ledger", "kind": "manual", "contract": ["Separate observation from hypothesis", "Preserve rejected and unresolved alternatives"] } + ], + "source": { "title": "Evidence-based root-cause analysis practice", "version": "catalog interpretation 1.0", "reference": "Causal investigation baseline" }, + "applicabilityBoundary": { "inScope": ["Observed incidents and recurring defects", "Causal hypothesis testing"], "outOfScope": ["Prospective hazard enumeration", "Blame assignment"] }, + "relatedMethodIds": ["fault-tree-analysis", "fmea", "pdca"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/spc.v1.json b/orchestration/methods/cards/spc.v1.json new file mode 100644 index 0000000..5aa918e --- /dev/null +++ b/orchestration/methods/cards/spc.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "spc", + "version": "1.0.0", + "name": "Statistical process control", + "problemSignals": [ + { "id": "process-variation", "description": "A repeated process exhibits variation whose common or special causes are unclear." }, + { "id": "unstable-quality", "description": "Quality measures change over time despite nominally similar inputs." } + ], + "requiredEvidence": [ + { "id": "time-ordered-measurements", "description": "Sequential measurements with timestamps, subgroup rules, and missing-data markers." }, + { "id": "measurement-system-definition", "description": "Operational measure, sampling plan, resolution, and known measurement error." } + ], + "assumptions": ["Sampling order is preserved", "The selected chart matches data type, subgrouping, and distributional conditions"], + "deterministicSteps": [ + { "id": "qualify-measurements", "action": "Validate measure definition, ordering, subgrouping, missingness, and chart preconditions.", "requires": ["evidence:time-ordered-measurements", "evidence:measurement-system-definition"], "produces": ["qualified-series"] }, + { "id": "calculate-control-chart", "action": "Calculate center line and control limits from the declared baseline and chart formula.", "requires": ["step:qualify-measurements"], "produces": ["control-chart"] }, + { "id": "classify-signals", "action": "Apply declared special-cause rules without treating control limits as specification limits.", "requires": ["step:calculate-control-chart"], "produces": ["variation-assessment"] } + ], + "outputs": [ + { "id": "qualified-series", "description": "Validated ordered sample and chart-selection rationale." }, + { "id": "control-chart", "description": "Reproducible center line, limits, and plotted sequence." }, + { "id": "variation-assessment", "description": "Common/special-cause signals and violated assumptions." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["Measurement system and subgrouping are adequate", "Baseline is stable", "Chart assumptions hold"] }, + { "level": "medium", "whenAll": ["Ordering is reliable", "Limited baseline size or measurement uncertainty remains"] }, + { "level": "unknown", "whenAll": ["Samples are not time ordered", "Chart type or measurement system cannot be justified"] } + ], + "cost": { "relative": "medium", "drivers": ["Sampling cadence", "Measurement-system analysis", "Baseline length"] }, + "contraindications": ["Data are one-off rather than a repeated process", "Specification conformance is being inferred solely from control limits"], + "implementationPorts": [ + { "id": "control-chart-engine", "kind": "statistical_engine", "contract": ["Require explicit chart and subgroup rules", "Emit formulas, limits, and triggered rules"] } + ], + "source": { "title": "Shewhart statistical process control", "version": "catalog interpretation 1.0", "reference": "Established SPC practice" }, + "applicabilityBoundary": { "inScope": ["Repeated-process stability", "Time-ordered variation signals"], "outOfScope": ["One-time project estimates", "Specification acceptance without capability analysis"] }, + "relatedMethodIds": ["pdca"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/strangler-migration.v1.json b/orchestration/methods/cards/strangler-migration.v1.json new file mode 100644 index 0000000..6061cc2 --- /dev/null +++ b/orchestration/methods/cards/strangler-migration.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "strangler-migration", + "version": "1.0.0", + "name": "Strangler migration", + "problemSignals": [ + { "id": "high-risk-rewrite", "description": "A wholesale replacement would combine behavior discovery, migration, and cutover risk." }, + { "id": "replaceable-seams", "description": "Traffic or responsibilities can be routed through explicit bounded seams." } + ], + "requiredEvidence": [ + { "id": "legacy-behavior-contracts", "description": "Observed legacy inputs, outputs, side effects, and failure semantics at candidate seams." }, + { "id": "routing-and-rollback-controls", "description": "Traffic allocation, observability, fallback, and state-consistency mechanisms." } + ], + "assumptions": ["A migration seam can be isolated", "Old and new paths can coexist under explicit routing and data-consistency rules"], + "deterministicSteps": [ + { "id": "select-seam", "action": "Bound one responsibility and document its legacy behavior, dependencies, and rollback route.", "requires": ["evidence:legacy-behavior-contracts"], "produces": ["migration-seam"] }, + { "id": "define-increment", "action": "Define new-path contracts, routing fraction, state handling, observability, and rollback thresholds.", "requires": ["evidence:routing-and-rollback-controls", "step:select-seam"], "produces": ["increment-plan"] }, + { "id": "evaluate-retirement", "action": "Compare both paths under the declared contract and produce retirement evidence or an unresolved gap list.", "requires": ["step:define-increment"], "produces": ["retirement-evidence"] } + ], + "outputs": [ + { "id": "migration-seam", "description": "Bounded legacy responsibility and interface contract." }, + { "id": "increment-plan", "description": "Reversible routing and coexistence plan." }, + { "id": "retirement-evidence", "description": "Parity, rollback, and dependency-removal evidence; not an adoption decision." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["Legacy behavior is covered by contracts", "Routing and rollback are proven", "State consistency is verified"] }, + { "level": "medium", "whenAll": ["Seam and rollback are explicit", "Long-tail behavior or state migration remains partially observed"] }, + { "level": "unknown", "whenAll": ["No isolatable seam exists", "Rollback or dual-run consistency cannot be established"] } + ], + "cost": { "relative": "high", "drivers": ["Coexistence duration", "State migration", "Contract and routing instrumentation"] }, + "contraindications": ["There is no isolatable routing or responsibility seam", "Dual operation creates unacceptable safety or consistency risk"], + "implementationPorts": [ + { "id": "migration-router", "kind": "deterministic_tool", "contract": ["Apply explicit routing policy", "Preserve rollback and per-path observations without automatic cutover"] } + ], + "source": { "title": "Strangler Fig application migration pattern", "version": "catalog interpretation 1.0", "reference": "Incremental legacy-system replacement practice" }, + "applicabilityBoundary": { "inScope": ["Incremental system replacement", "Contract-bounded traffic migration"], "outOfScope": ["Automatic production cutover", "Systems with inseparable state and behavior"] }, + "relatedMethodIds": ["contract-testing"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/cards/value-stream-queue-delay.v1.json b/orchestration/methods/cards/value-stream-queue-delay.v1.json new file mode 100644 index 0000000..d631b7c --- /dev/null +++ b/orchestration/methods/cards/value-stream-queue-delay.v1.json @@ -0,0 +1,39 @@ +{ + "schema": "code-intel-method-card.v1", + "id": "value-stream-queue-delay", + "version": "1.0.0", + "name": "Value-stream and queue-delay analysis", + "problemSignals": [ + { "id": "long-lead-time", "description": "Elapsed delivery time greatly exceeds active processing time." }, + { "id": "handoff-wait", "description": "Work repeatedly waits between states, teams, or approval queues." } + ], + "requiredEvidence": [ + { "id": "item-state-timestamps", "description": "Per-item entry and exit timestamps for defined workflow states." }, + { "id": "work-demand-and-capacity", "description": "Arrival rate, work-in-process, service capacity, batching, and rework observations." } + ], + "assumptions": ["Workflow states and timestamp semantics are consistent", "Items are comparable or explicitly segmented"], + "deterministicSteps": [ + { "id": "normalize-flow", "action": "Normalize item histories into active, waiting, blocked, rework, and completed intervals.", "requires": ["evidence:item-state-timestamps"], "produces": ["value-stream-map"] }, + { "id": "quantify-queues", "action": "Calculate lead time, touch time, queue delay, WIP, arrival/service rates, and segmentation with declared formulas.", "requires": ["evidence:work-demand-and-capacity", "step:normalize-flow"], "produces": ["queue-delay-profile"] }, + { "id": "locate-constraint", "action": "Identify observed delay concentration and distinguish capacity, batching, blocking, and rework hypotheses.", "requires": ["step:quantify-queues"], "produces": ["constraint-assessment"] } + ], + "outputs": [ + { "id": "value-stream-map", "description": "State-by-state active and waiting intervals." }, + { "id": "queue-delay-profile", "description": "Segmented flow and queue metrics with formulas." }, + { "id": "constraint-assessment", "description": "Evidence-backed delay concentration and unresolved causes." } + ], + "confidenceRules": [ + { "level": "high", "whenAll": ["State timestamps cover the full flow", "Demand and capacity windows align", "Segmentation explains item mix"] }, + { "level": "medium", "whenAll": ["Lead and queue times are reliable", "Capacity or rework evidence is incomplete"] }, + { "level": "unknown", "whenAll": ["Timestamp semantics conflict", "Missing states make wait and touch time indistinguishable"] } + ], + "cost": { "relative": "medium", "drivers": ["Workflow-state count", "Timestamp cleanup", "Item segmentation"] }, + "contraindications": ["Only aggregate completion counts exist with no item flow", "A dependency schedule is being mistaken for an operational queue model"], + "implementationPorts": [ + { "id": "flow-metrics-engine", "kind": "deterministic_tool", "contract": ["Preserve per-item timestamps and segment definitions", "Emit formulas and missing-data diagnostics"] } + ], + "source": { "title": "Value-stream mapping and queueing-flow practice", "version": "catalog interpretation 1.0", "reference": "Lean flow and queue-delay baseline" }, + "applicabilityBoundary": { "inScope": ["Operational workflow delay", "Lead-time and WIP analysis"], "outOfScope": ["Dependency-only project scheduling", "Priority or staffing authorization"] }, + "relatedMethodIds": ["critical-path-pert"], + "executionPolicy": "catalog_only_no_selection_or_execution" +} diff --git a/orchestration/methods/catalog.v1.json b/orchestration/methods/catalog.v1.json new file mode 100644 index 0000000..5a464bc --- /dev/null +++ b/orchestration/methods/catalog.v1.json @@ -0,0 +1,16 @@ +{ + "schema": "code-intel-method-catalog.v1", + "catalogVersion": "1.0.0", + "selectionPolicy": "none_catalog_only", + "cards": [ + { "id": "contract-testing", "path": "cards/contract-testing.v1.json" }, + { "id": "critical-path-pert", "path": "cards/critical-path-pert.v1.json" }, + { "id": "fault-tree-analysis", "path": "cards/fault-tree-analysis.v1.json" }, + { "id": "fmea", "path": "cards/fmea.v1.json" }, + { "id": "pdca", "path": "cards/pdca.v1.json" }, + { "id": "root-cause-analysis", "path": "cards/root-cause-analysis.v1.json" }, + { "id": "spc", "path": "cards/spc.v1.json" }, + { "id": "strangler-migration", "path": "cards/strangler-migration.v1.json" }, + { "id": "value-stream-queue-delay", "path": "cards/value-stream-queue-delay.v1.json" } + ] +} diff --git a/orchestration/multi-agent-merge-queue-policy.v1.json b/orchestration/multi-agent-merge-queue-policy.v1.json new file mode 100644 index 0000000..ee42bb8 --- /dev/null +++ b/orchestration/multi-agent-merge-queue-policy.v1.json @@ -0,0 +1,35 @@ +{ + "schema": "code-intel-multi-agent-merge-queue-policy.v1", + "source": { + "project": "2233admin/claude-code-merge-queue", + "uri": "https://github.com/2233admin/claude-code-merge-queue", + "revision": "e7a76958dbd3953b84f12abbc2e6bd755aafce53", + "version": "0.5.1", + "license": "MIT" + }, + "provider": { + "id": "claude-code-merge-queue", + "minimumVersion": "0.5.1", + "resolution": "repository_local_only" + }, + "requiredLandingGates": [ + "git-repository", + "provider-local-install", + "provider-version", + "provider-config", + "acceptance-check-configured", + "checks-required", + "direct-push-protection", + "human-promotion-boundary" + ], + "allowedActions": ["status", "validate", "land", "reconcile", "history"], + "forbiddenActions": ["init", "uninstall", "promote", "prune", "preview", "emergency-push"], + "authority": { + "agentsMayLandAcceptedLanes": true, + "agentsMayPromoteProduction": false, + "landRequiresExplicitRepositoryMutation": true, + "landRequiresExplicitNetworkPush": true, + "adapterMayInstallOrInitialize": false, + "adapterMayDeleteWorktrees": false + } +} diff --git a/orchestration/multi-agent-workspace-policy.v1.json b/orchestration/multi-agent-workspace-policy.v1.json new file mode 100644 index 0000000..eaeb635 --- /dev/null +++ b/orchestration/multi-agent-workspace-policy.v1.json @@ -0,0 +1,29 @@ +{ + "schema": "code-intel-multi-agent-workspace-policy.v1", + "defaultIntent": "mutation", + "inspectionAuthority": "observation_only", + "requirements": { + "repositoryRootRequired": true, + "dirtyRootBlocksMutation": true, + "observationMustBeExplicit": true, + "observationMayModifyRepository": false, + "inventoryFormat": "git-status-porcelain-v1-z", + "inventoryIncludesUntrackedFiles": "all", + "inventoryHash": "sha256-canonical-json-v1" + }, + "exitCodes": { + "allowed": 0, + "invalidPolicy": 2, + "dirtyRootMutationDenied": 20, + "repositoryRootRequired": 21, + "inspectionFailed": 22 + }, + "invariants": [ + "never-clean", + "never-stash", + "never-reset", + "never-commit", + "never-write-inspected-repository", + "observation-is-not-mutation-authority" + ] +} diff --git a/orchestration/ponytail-gate-policy.v1.json b/orchestration/ponytail-gate-policy.v1.json new file mode 100644 index 0000000..151c268 --- /dev/null +++ b/orchestration/ponytail-gate-policy.v1.json @@ -0,0 +1,40 @@ +{ + "schema": "code-intel-ponytail-gate-policy.v1", + "changeKinds": ["artifact", "dependency", "abstraction", "file", "test", "documentation", "process"], + "operations": ["add", "delete", "reuse"], + "allowedCurrentValueSources": [ + "operator_requested_outcome", + "committed_engineering_plan_deliverable", + "verified_defect_or_risk", + "required_contract_or_gate", + "evidence_closing_spike", + "approved_debt_reduction" + ], + "forbiddenValueSources": ["future_maybe"], + "firstSufficientSolutionRungs": [ + "do_nothing", + "repository_reuse", + "standard_library", + "platform_native", + "installed_dependency", + "one_liner", + "smallest_local_implementation" + ], + "nonFilterableRequirements": [ + "verification", + "evidence", + "safety", + "error_handling", + "accessibility", + "data_loss_prevention", + "artifact_contract" + ], + "bypassAuthoritySchema": "code-intel-authority-event.v1", + "rules": { + "currentValue": "every change must name one allowed current value source backed by known evidence", + "firstSufficient": "every rung below the selected rung must be rejected once with known evidence", + "protectedRequirements": "verification, evidence, safety, and the documented engineering boundaries cannot be filtered out", + "bypass": "only a scoped, approved, unexpired, unreplayed A05 authority event covering value-source, lower-rung, and all required trace evidence may bypass a value or rung rejection", + "modes": "report_only retains rejection traces without blocking; enforce blocks when any rejection remains" + } +} diff --git a/orchestration/python314-pon-development-policy.v1.json b/orchestration/python314-pon-development-policy.v1.json new file mode 100644 index 0000000..acc464f --- /dev/null +++ b/orchestration/python314-pon-development-policy.v1.json @@ -0,0 +1,36 @@ +{ + "schema": "code-intel-python314-pon-development-policy.v1", + "sourceMethod": { + "project": "can1357/pon", + "uri": "https://github.com/can1357/pon", + "revision": "ab9067dbd2899c64c4d67a4bc27b8ad49472b126", + "adoption": "independent_conformance_method_reimplementation", + "licenseStatus": "unverified_no_declared_license_do_not_copy_or_auto_execute_upstream" + }, + "authority": { + "runtime": "cpython", + "requiredMajor": 3, + "requiredMinor": 14 + }, + "projectPythonFiles": [ + "Run-ScopedRepowiseDocs.py", + "test-scoped-repowise-validator.py" + ], + "corpus": "tests/fixtures/python314-compat/manifest.v1.json", + "pon": { + "defaultCommand": "pon", + "scriptArgsBeforePath": ["run"] + }, + "profiles": { + "development": { + "requirePon": false, + "runPonWhenAvailable": true, + "requireProjectCompile": true + }, + "pon-candidate": { + "requirePon": true, + "runPonWhenAvailable": true, + "requireProjectCompile": true + } + } +} diff --git a/orchestration/retirements/e02-recommender/compatibility-retirement-deletion-diff.json b/orchestration/retirements/e02-recommender/compatibility-retirement-deletion-diff.json new file mode 100644 index 0000000..3506b1f --- /dev/null +++ b/orchestration/retirements/e02-recommender/compatibility-retirement-deletion-diff.json @@ -0,0 +1 @@ +{"schema":"code-intel-compatibility-retirement-deletion-diff.v1","snapshotIdentity":"e119dc1d30127b412bda7b9ebc103519e8c6ed11eabdad82e7f057e60607e3ae","retirementId":"retire-recommender-branch","legacyBranchId":"run-code-intel.workflow-recommender.inline","affectedFiles":["run-code-intel.ps1"],"deletionsOnly":true,"summary":"Proposed deletion is limited to the retired inline recommender adapter markers; provider-preflight and all other branches are excluded. Summary is non-authoritative.","patch":{"algorithm":"replayable-delete-only-v1","sha256":"56a3c27605550d872218fba33782264b9cffe90596fdb6b91e3ea744f2410823","files":[{"baseBlobSha256":"90f6d1abcf1d83eecd916dc059b894271066d8b941d8b49f5f824dda4902a6af","baseText":"#requires -Version 7.2\n\nparam(\n [string]$Repo = \"\",\n [string]$RepoPath = \"\",\n\n [string]$Config = \"\",\n\n [ValidateSet(\"auto\", \"windows\", \"macos\", \"linux\")]\n [string]$Platform = \"auto\",\n\n [ValidateSet(\"lite\", \"normal\", \"full\")]\n [string]$Mode = \"normal\",\n\n [string]$Language = \"\",\n\n [string]$ArtifactRoot = \"\",\n [string]$SentruxPath = \"\",\n [string]$RepowiseWorkspaceRoot = \"\",\n [string]$RepowiseShadowRoot = \"\",\n [string[]]$RepowiseScopePaths = @(),\n [string[]]$RepowiseRootFiles = @(),\n [int]$RepowiseTimeoutSeconds = 600,\n [string]$RepowiseProvider = \"\",\n [string]$RepowiseModel = \"\",\n [string]$RepowiseReasoning = \"\",\n [string]$ModelRoutingResult = \"\",\n [string]$ModelAdapterRequest = \"\",\n [string]$ModelAdapterArtifactRoot = \"\",\n [string]$RepowiseAdapterRequest = \"\",\n [string]$RepowiseAdapterArtifactRoot = \"\",\n [long]$RepowiseAdapterEvaluatedAt = 0,\n [long]$RepowiseAdapterMaxAgeSeconds = 0,\n [string]$GraphAdapterRequest = \"\",\n [string]$GraphAdapterArtifactRoot = \"\",\n [long]$GraphAdapterEvaluatedAt = 0,\n [long]$GraphAdapterMaxAgeSeconds = 0,\n [string]$SentruxAdapterRequest = \"\",\n [string]$SentruxAdapterArtifactRoot = \"\",\n [long]$SentruxAdapterEvaluatedAt = 0,\n [long]$SentruxAdapterMaxAgeSeconds = 0,\n [string]$CodeNexusAdapterRequest = \"\",\n [string]$CodeNexusAdapterArtifactRoot = \"\",\n [long]$CodeNexusAdapterEvaluatedAt = 0,\n [long]$CodeNexusAdapterMaxAgeSeconds = 0,\n [string]$SurvivalScanRequest = \"\",\n [string]$SurvivalScanArtifactRoot = \"\",\n [string]$RunCommitSourceRoot = \"\",\n [string]$RunCommitAuthorityRoot = \"\",\n [string]$RunCommitManifestRef = \"\",\n [string]$RunCommitFinalName = \"\",\n [string[]]$InventoryExclude = @(),\n\n [switch]$DagCoordinate,\n\n [switch]$SaveSentruxBaseline,\n [switch]$AutoSaveMissingSentruxBaseline,\n [switch]$SkipRepowise,\n [switch]$RepowiseDocs,\n [switch]$AllowRepowiseShadowMutation,\n [switch]$SkipRepomix,\n [ValidateSet(\"xml\", \"markdown\", \"json\", \"plain\")]\n [string]$RepomixStyle = \"markdown\",\n [switch]$RepomixCompress,\n [switch]$SkipSentrux,\n[switch]$SkipSentruxCheck,\n[switch]$SkipSentruxGate,\n[switch]$RequireUnderstandGraph,\n[switch]$SkipGitHubResearch,\n[switch]$WorkspaceAdd,\n[switch]$SkipOpenSpec,\n[switch]$AutoOpenSpec\n)\n\nSet-StrictMode -Version Latest\n$ErrorActionPreference = \"Stop\"\n\n$platformModule = Join-Path (Join-Path $PSScriptRoot \"tools\") \"code-intel-platform.psm1\"\nImport-Module $platformModule -Force\n$effectivePlatform = Get-CodeIntelPlatform -Platform $Platform\n$codeIntelPaths = Get-CodeIntelPaths -Platform $effectivePlatform -Root $PSScriptRoot\n\n[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()\n$OutputEncoding = [System.Text.UTF8Encoding]::new()\n$env:PYTHONIOENCODING = \"utf-8\"\n$env:PYTHONUTF8 = \"1\"\n$env:TERM = \"xterm\"\n$env:NO_COLOR = \"1\"\n$env:RICH_FORCE_TERMINAL = \"0\"\n\nif (-not [string]::IsNullOrWhiteSpace($ModelAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($ModelAdapterArtifactRoot)) { throw \"Model adapter facade requires an artifact root\" }\n $delegateScript = Join-Path $PSScriptRoot \"Invoke-ModelChannelDelegate.ps1\"\n if (-not (Test-Path -LiteralPath $delegateScript -PathType Leaf)) { throw \"Model channel delegate is missing: $delegateScript\" }\n & $delegateScript -Request $ModelAdapterRequest -ArtifactRoot $ModelAdapterArtifactRoot\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($RepowiseAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($RepowiseAdapterArtifactRoot) -or\n $RepowiseAdapterEvaluatedAt -lt 0 -or\n $RepowiseAdapterMaxAgeSeconds -le 0) {\n throw \"Repowise adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = Join-Path $PSScriptRoot \"target\\debug\\code-intel.exe\"\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Repowise adapter binary is missing: $rustCli\"\n }\n & $rustCli provider repowise-adapt `\n --request $RepowiseAdapterRequest `\n --artifact-root $RepowiseAdapterArtifactRoot `\n --evaluated-at $RepowiseAdapterEvaluatedAt `\n --max-age-seconds $RepowiseAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($GraphAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($GraphAdapterArtifactRoot) -or\n $GraphAdapterEvaluatedAt -lt 0 -or\n $GraphAdapterMaxAgeSeconds -le 0) {\n throw \"Graph adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = Join-Path $PSScriptRoot \"target\\debug\\code-intel.exe\"\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Graph adapter binary is missing: $rustCli\"\n }\n & $rustCli provider graph-adapt `\n --request $GraphAdapterRequest `\n --artifact-root $GraphAdapterArtifactRoot `\n --evaluated-at $GraphAdapterEvaluatedAt `\n --max-age-seconds $GraphAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($SentruxAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($SentruxAdapterArtifactRoot) -or\n $SentruxAdapterEvaluatedAt -lt 0 -or\n $SentruxAdapterMaxAgeSeconds -le 0) {\n throw \"Sentrux adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = Join-Path $PSScriptRoot \"target\\debug\\code-intel.exe\"\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) { throw \"Sentrux adapter binary is missing: $rustCli\" }\n & $rustCli provider sentrux-adapt --request $SentruxAdapterRequest --artifact-root $SentruxAdapterArtifactRoot --evaluated-at $SentruxAdapterEvaluatedAt --max-age-seconds $SentruxAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($CodeNexusAdapterRequest)) {\n if ([string]::IsNullOrWhiteSpace($CodeNexusAdapterArtifactRoot) -or\n $CodeNexusAdapterEvaluatedAt -lt 0 -or\n $CodeNexusAdapterMaxAgeSeconds -le 0) {\n throw \"CodeNexus adapter facade requires artifact root, non-negative evaluated-at, and positive max-age\"\n }\n $rustCli = Join-Path $PSScriptRoot \"target\\debug\\code-intel.exe\"\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"CodeNexus adapter binary is missing: $rustCli\"\n }\n & $rustCli provider codenexus-adapt `\n --request $CodeNexusAdapterRequest `\n --artifact-root $CodeNexusAdapterArtifactRoot `\n --evaluated-at $CodeNexusAdapterEvaluatedAt `\n --max-age-seconds $CodeNexusAdapterMaxAgeSeconds\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($SurvivalScanRequest)) {\n if ([string]::IsNullOrWhiteSpace($SurvivalScanArtifactRoot)) {\n throw \"Repository survival scan facade requires an artifact root\"\n }\n $rustCli = Join-Path $PSScriptRoot \"target\\debug\\code-intel.exe\"\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Repository survival scan binary is missing: $rustCli\"\n }\n & $rustCli repository survival-scan `\n --request $SurvivalScanRequest `\n --artifact-root $SurvivalScanArtifactRoot\n exit $LASTEXITCODE\n}\n\nif (-not [string]::IsNullOrWhiteSpace($RunCommitManifestRef)) {\n if ([string]::IsNullOrWhiteSpace($RunCommitSourceRoot) -or\n [string]::IsNullOrWhiteSpace($RunCommitAuthorityRoot) -or\n [string]::IsNullOrWhiteSpace($RunCommitFinalName)) {\n throw \"Run commit facade requires source root, authority root, manifest Artifact Ref, and final name\"\n }\n $rustCli = Join-Path $PSScriptRoot \"target\\debug\\code-intel.exe\"\n if (-not (Test-Path -LiteralPath $rustCli -PathType Leaf)) {\n throw \"Run commit binary is missing: $rustCli\"\n }\n & $rustCli run commit `\n --source-root $RunCommitSourceRoot `\n --authority-root $RunCommitAuthorityRoot `\n --manifest-ref $RunCommitManifestRef `\n --final-name $RunCommitFinalName\n exit $LASTEXITCODE\n}\n\nfunction Resolve-Repo {\n param([string]$Path)\n\n $item = Get-Item -LiteralPath $Path -ErrorAction Stop\n if (-not $item.PSIsContainer) {\n throw \"Repo path is not a directory: $Path\"\n }\n return $item.FullName\n}\n\nfunction Find-RepoConfigByPath {\n param([object]$ReposConfig, [string]$ResolvedRepoPath)\n\n if ($null -eq $ReposConfig -or [string]::IsNullOrWhiteSpace($ResolvedRepoPath)) { return $null }\n $normalizedRepoPath = [System.IO.Path]::TrimEndingDirectorySeparator($ResolvedRepoPath)\n foreach ($entry in $ReposConfig.PSObject.Properties) {\n $configuredPath = Get-JsonProperty $entry.Value \"path\"\n if ([string]::IsNullOrWhiteSpace([string]$configuredPath)) { continue }\n try {\n $resolvedConfiguredPath = Resolve-Repo ([string]$configuredPath)\n }\n catch {\n continue\n }\n $normalizedConfiguredPath = [System.IO.Path]::TrimEndingDirectorySeparator($resolvedConfiguredPath)\n if ([string]::Equals($normalizedConfiguredPath, $normalizedRepoPath, [System.StringComparison]::OrdinalIgnoreCase)) {\n return $entry.Value\n }\n }\n return $null\n}\n\nfunction Test-CommandAvailable {\n param([string]$Name)\n return [bool](Get-Command $Name -ErrorAction SilentlyContinue)\n}\n\nfunction Test-GitRepository {\nparam([string]$Path)\n\nif (-not (Test-CommandAvailable \"git\")) { return $false }\n$output = & git -C $Path rev-parse --is-inside-work-tree 2>$null\nreturn ($LASTEXITCODE -eq 0 -and [string]$output -eq \"true\")\n}\n\n# Workflow recommendations are owned by the standalone advisory atom in OpenSpec-Detector.ps1.\n\nfunction Get-JsonProperty {\n param(\n [object]$Object,\n [string]$Name\n )\n\n if ($null -eq $Object) { return $null }\n $prop = $Object.PSObject.Properties[$Name]\n if ($null -eq $prop) { return $null }\n return $prop.Value\n}\n\nfunction Resolve-ConfigString {\n param(\n [string]$Value,\n [object]$RepoConfig,\n [object]$ConfigData,\n [string]$Name,\n [string[]]$EnvNames = @(),\n [string]$Default = \"\"\n )\n\n if (-not [string]::IsNullOrWhiteSpace($Value)) { return $Value }\n\n $repoValue = Get-JsonProperty $RepoConfig $Name\n if (-not [string]::IsNullOrWhiteSpace([string]$repoValue)) { return [string]$repoValue }\n\n $globalValue = Get-JsonProperty $ConfigData $Name\n if (-not [string]::IsNullOrWhiteSpace([string]$globalValue)) { return [string]$globalValue }\n\n foreach ($envName in $EnvNames) {\n $envValue = [Environment]::GetEnvironmentVariable($envName, \"Process\")\n if ([string]::IsNullOrWhiteSpace($envValue)) {\n $envValue = [Environment]::GetEnvironmentVariable($envName, \"User\")\n }\n if (-not [string]::IsNullOrWhiteSpace($envValue)) { return $envValue }\n }\n\n return $Default\n}\n\nfunction Normalize-RepowiseProvider {\n param([string]$Provider)\n if ([string]::IsNullOrWhiteSpace($Provider)) { return \"mock\" }\n $normalized = $Provider.Trim()\n if ($normalized -ieq \"ccw\") { return \"codex_cli\" }\n return $normalized\n}\n\nfunction Get-RepowiseProviderArgs {\n param(\n [string]$Provider,\n [string]$Model,\n [string]$Reasoning\n )\n\n $args = @(\"--provider\", $Provider)\n if (-not [string]::IsNullOrWhiteSpace($Model)) { $args += @(\"--model\", $Model) }\n if (-not [string]::IsNullOrWhiteSpace($Reasoning)) { $args += @(\"--reasoning\", $Reasoning) }\n return $args\n}\n\nfunction Get-DefaultArtifactRoot {\n return (Get-CodeIntelArtifactRoot -Platform $effectivePlatform)\n}\n\nfunction Get-DefaultShadowRoot {\n return (Get-CodeIntelShadowRoot -Platform $effectivePlatform)\n}\n\nfunction Resolve-ChildPath {\n param(\n [string]$Base,\n [string]$Path\n )\n\n if ([string]::IsNullOrWhiteSpace($Path)) { return $Base }\n if ([System.IO.Path]::IsPathRooted($Path)) { return (Resolve-Repo $Path) }\n return Resolve-Repo (Join-Path $Base $Path)\n}\n\nfunction Invoke-LoggedStep {\n param(\n [string]$Name,\n [scriptblock]$Body\n )\n\n $started = Get-Date\n $entry = [ordered]@{\n name = $Name\n startedAt = $started.ToString(\"o\")\n status = \"running\"\n exitCode = $null\n output = \"\"\n error = \"\"\n finishedAt = $null\n durationMs = $null\n }\n\n try {\n $global:LASTEXITCODE = 0\n $previousErrorActionPreference = $ErrorActionPreference\n try {\n $ErrorActionPreference = \"Continue\"\n $output = & $Body 2>&1\n }\n finally {\n $ErrorActionPreference = $previousErrorActionPreference\n }\n $entry.output = ($output | ForEach-Object { $_.ToString() } | Out-String).Trim()\n if ($global:LASTEXITCODE -ne 0) {\n throw \"Command exited with code $global:LASTEXITCODE\"\n }\n $entry.status = \"passed\"\n $entry.exitCode = 0\n }\n catch {\n $entry.status = \"failed\"\n if ($global:LASTEXITCODE -ne 0) {\n $entry.exitCode = $global:LASTEXITCODE\n }\n else {\n $entry.exitCode = 1\n }\n $entry.error = $_.Exception.Message\n if ([string]::IsNullOrWhiteSpace([string]$entry.output)) {\n $entry.output = ($_ | Out-String).Trim()\n }\n }\n finally {\n $finished = Get-Date\n $entry.finishedAt = $finished.ToString(\"o\")\n $entry.durationMs = [int]($finished - $started).TotalMilliseconds\n }\n\n return [pscustomobject]$entry\n}\n\nfunction Convert-OptionalRepowiseTimeout {\n param([object]$Step)\n\n if ($null -eq $Step) { return $Step }\n $blob = (([string]$Step.error) + \"`n\" + ([string]$Step.output)).ToLowerInvariant()\n if ([string]$Step.status -eq \"failed\" -and [string]$Step.name -like \"repowise*\" -and $blob -match \"timed out after\") {\n $Step.status = \"skipped\"\n $Step.exitCode = $null\n $Step.output = \"Optional Repowise step skipped after timeout. $($Step.error)\"\n $Step.error = \"\"\n }\n return $Step\n}\n\nfunction Get-RelativePathSafe {\n param(\n [string]$Base,\n [string]$Path\n )\n\n try {\n return [System.IO.Path]::GetRelativePath($Base, $Path)\n }\n catch {\n try {\n $baseFull = [System.IO.Path]::GetFullPath($Base)\n $pathFull = [System.IO.Path]::GetFullPath($Path)\n if (-not $baseFull.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {\n $baseFull = $baseFull + [System.IO.Path]::DirectorySeparatorChar\n }\n if ((Test-Path -LiteralPath $pathFull -PathType Container) -and -not $pathFull.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {\n $pathFull = $pathFull + [System.IO.Path]::DirectorySeparatorChar\n }\n $relative = ([uri]$baseFull).MakeRelativeUri([uri]$pathFull).ToString()\n $relative = [uri]::UnescapeDataString($relative).Replace(\"/\", [System.IO.Path]::DirectorySeparatorChar)\n if ([string]::IsNullOrWhiteSpace($relative)) { return \".\" }\n return $relative\n }\n catch {\n return $Path\n }\n }\n}\n\nfunction Get-StepFailureCategory {\n param([object]$Step)\n\n $name = [string]$Step.name\n $status = [string]$Step.status\n $blob = (([string]$Step.error) + \"`n\" + ([string]$Step.output)).ToLowerInvariant()\n\n if ($name -eq \"understand graph\" -and ($status -eq \"failed\" -or $status -eq \"manual_required\")) {\n return \"graph_missing\"\n }\n if ($name -like \"sentrux*\" -and ($status -eq \"failed\" -or $status -eq \"manual_required\")) {\n return \"sentrux_fail\"\n }\n if (($name -like \"repowise*\" -or $name -eq \"provider preflight\") -and $blob -match \"rate_limit|quota|usage limit exceeded|error code: 429|too many requests|provider_quota\") {\n return \"provider_quota\"\n }\n if (($name -like \"repowise*\" -or $name -eq \"provider preflight\") -and $blob -match \"provider_unavailable|model_not_found|not_found_error|error code: 404|status code: 404\") {\n return \"provider_unavailable\"\n }\n if (($name -like \"repowise*\" -or $name -eq \"provider preflight\") -and $blob -match \"config_error|authentication_error|invalid api key|not authorized|token not match\") {\n return \"config_error\"\n }\n if ($status -eq \"failed\") {\n return \"local_tool_error\"\n }\n return $null\n}\n\nfunction Test-GitHubSolutionResearchRequired {\nparam([object]$FailureCounts)\n\n if ($null -eq $FailureCounts) { return $false }\n if ($FailureCounts.localToolError -gt 0) { return $true }\n if ($FailureCounts.sentruxFail -gt 0) { return $true }\n if ($FailureCounts.providerQuota -gt 0) { return $true }\n\n return $false\n}\n\nfunction Complete-NodeLintHygieneStep {\n param(\n [System.Collections.Specialized.OrderedDictionary]$Step,\n [datetime]$Started\n )\n\n $finished = Get-Date\n $Step[\"finishedAt\"] = $finished.ToString(\"o\")\n $Step[\"durationMs\"] = [int]($finished - $Started).TotalMilliseconds\n return [pscustomobject]$Step\n}\n\nfunction Get-NodeLintHygieneStep {\n param(\n [string]$RepoPath,\n [bool]$RgAvailable\n )\n\n $started = Get-Date\n $step = [ordered]@{\n name = \"node lint hygiene\"\n startedAt = $started.ToString(\"o\")\n status = \"skipped\"\n exitCode = $null\n output = \"\"\n error = \"\"\n finishedAt = \"\"\n durationMs = 0\n }\n\n try {\n $packageJson = Join-Path $RepoPath \"package.json\"\n if (-not (Test-Path -LiteralPath $packageJson -PathType Leaf)) {\n $step[\"output\"] = \"No package.json found.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $package = Get-Content -LiteralPath $packageJson -Raw | ConvertFrom-Json\n $scripts = Get-JsonProperty $package \"scripts\"\n $lintScript = [string](Get-JsonProperty $scripts \"lint\")\n if ([string]::IsNullOrWhiteSpace($lintScript) -or $lintScript -notmatch \"\\beslint\\b\") {\n $step[\"output\"] = \"No root ESLint lint script detected.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n if (-not $RgAvailable) {\n $step[\"output\"] = \"rg unavailable; skip static ESLint asset-boundary check.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $rgArgs = @(\n \"--files\",\n \"--hidden\",\n \"--no-ignore\",\n \"-g\", \"!**/.git/**\",\n \"-g\", \"!**/node_modules/**\",\n \"-g\", \"!**/dist/**\",\n \"-g\", \"!**/build/**\",\n $RepoPath\n )\n $repoFiles = @(& rg @rgArgs 2>$null)\n $global:LASTEXITCODE = 0\n $normalizedFiles = @($repoFiles | ForEach-Object { ([string]$_).Replace(\"\\\", \"/\") })\n\n $assetPatterns = New-Object System.Collections.Generic.List[string]\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)apps/[^/]+/public/charting_library/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"apps/*/public/charting_library/**\")\n }\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)apps/[^/]+/public/datafeeds/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"apps/*/public/datafeeds/**\")\n }\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)packages/[^/]+/vendor/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"packages/*/vendor/**\")\n }\n if (@($normalizedFiles | Where-Object { $_ -match \"(^|/)vendor/\" } | Select-Object -First 1).Count -gt 0) {\n $assetPatterns.Add(\"vendor/**\")\n }\n\n if ($assetPatterns.Count -eq 0) {\n $step[\"status\"] = \"passed\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root ESLint lint script detected; no known generated/vendor static asset directories found.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $configNames = @(\"eslint.config.js\", \"eslint.config.mjs\", \"eslint.config.cjs\", \".eslintignore\", \".eslintrc\", \".eslintrc.json\", \".eslintrc.js\", \".eslintrc.cjs\")\n $configFiles = @($configNames | ForEach-Object {\n $candidate = Join-Path $RepoPath $_\n if (Test-Path -LiteralPath $candidate -PathType Leaf) { $candidate }\n })\n if ($configFiles.Count -eq 0) {\n $step[\"status\"] = \"manual_required\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root lint script uses ESLint and known generated/vendor static asset dirs exist, but no root ESLint config or ignore file was found. Add ignores for: $($assetPatterns -join ', '), then run root lint before push.\"\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n }\n\n $configText = (($configFiles | ForEach-Object { Get-Content -LiteralPath $_ -Raw }) -join [Environment]::NewLine).Replace(\"\\\", \"/\")\n $missing = New-Object System.Collections.Generic.List[string]\n foreach ($pattern in $assetPatterns) {\n $covered = $false\n if ($pattern -eq \"apps/*/public/charting_library/**\") {\n $covered = ($configText -match \"charting_library|apps/\\*/public|\\*\\*/public|public/\\*\\*\")\n }\n elseif ($pattern -eq \"apps/*/public/datafeeds/**\") {\n $covered = ($configText -match \"datafeeds|apps/\\*/public|\\*\\*/public|public/\\*\\*\")\n }\n elseif ($pattern -eq \"packages/*/vendor/**\" -or $pattern -eq \"vendor/**\") {\n $covered = ($configText -match \"vendor\")\n }\n\n if (-not $covered) {\n $missing.Add($pattern)\n }\n }\n\n if ($missing.Count -gt 0) {\n $step[\"status\"] = \"manual_required\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root lint script uses ESLint and known generated/vendor static asset dirs exist, but ignore coverage appears incomplete for: $($missing -join ', '). Add explicit ESLint ignores or run root lint before push.\"\n }\n else {\n $step[\"status\"] = \"passed\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Root ESLint lint script has ignore coverage for known generated/vendor static asset dirs: $($assetPatterns -join ', ').\"\n }\n }\n catch {\n $step[\"status\"] = \"manual_required\"\n $step[\"exitCode\"] = 0\n $step[\"output\"] = \"Node lint hygiene check could not complete. Run root lint before push and inspect generated/vendor asset ignores.\"\n $step[\"error\"] = $_.Exception.Message\n }\n finally {\n $finished = Get-Date\n $step[\"finishedAt\"] = $finished.ToString(\"o\")\n $step[\"durationMs\"] = [int]($finished - $started).TotalMilliseconds\n }\n\n return (Complete-NodeLintHygieneStep -Step $step -Started $started)\n}\n\nfunction New-GitHubSolutionResearchNotApplicable {\n return [ordered]@{\n status = \"not_applicable\"\n required = $false\n path = \"\"\n markdown = \"\"\n reason = \"No blocker category requires GitHub solution research.\"\n candidates = 0\n queries = 0\n evidenceLinks = @()\n exitCriteria = @(\"GitHub research is not required for clean, graph-missing, governance-only, or surgery-plan-only scans.\")\n }\n}\n\nfunction Join-StatusNames {\n param(\n [object[]]$Items,\n [string]$Empty = \"none\"\n )\n\n if ($Items.Count -eq 0) { return $Empty }\n return (($Items | ForEach-Object { \"$($_.name)=$($_.status)\" }) -join \"; \")\n}\n\nfunction Read-JsonFileSafe {\n param([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {\n return $null\n }\n try {\n return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json\n }\n catch {\n return $null\n }\n}\n\nfunction Get-CodeEvidenceLanguage {\n param([string]$Extension)\n\n switch ($Extension.ToLowerInvariant()) {\n \".ps1\" { return \"powershell\" }\n \".psm1\" { return \"powershell\" }\n \".py\" { return \"python\" }\n \".js\" { return \"javascript\" }\n \".jsx\" { return \"javascript\" }\n \".mjs\" { return \"javascript\" }\n \".cjs\" { return \"javascript\" }\n \".ts\" { return \"typescript\" }\n \".tsx\" { return \"typescript\" }\n \".rs\" { return \"rust\" }\n \".go\" { return \"go\" }\n \".java\" { return \"java\" }\n \".cs\" { return \"csharp\" }\n default { return \"text\" }\n }\n}\n\nfunction New-CodeEvidenceNativeSymbol {\n param(\n [string]$RelativePath,\n [string]$Language,\n [int]$LineNumber,\n [string]$Kind,\n [string]$Name\n )\n\n return [ordered]@{\n id = \"$RelativePath#$Kind`:$Name\"\n kind = $Kind\n name = $Name\n file = $RelativePath\n startLine = $LineNumber\n endLine = $LineNumber\n language = $Language\n confidence = 0.55\n source = \"native-minimal\"\n }\n}\n\nfunction Get-CodeEvidencePowerShellSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*function\\s+([A-Za-z0-9_\\-:]+)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[1] }\n }\n return $null\n}\n\nfunction Get-CodeEvidencePythonSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(def|class)\\s+([A-Za-z_][A-Za-z0-9_]*)') {\n $kind = if ($Matches[1] -eq \"class\") { \"class\" } else { \"function\" }\n return [ordered]@{ kind = $kind; name = $Matches[2] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceJavaScriptSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(export\\s+)?(async\\s+)?function\\s+([A-Za-z_$][A-Za-z0-9_$]*)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[3] }\n }\n if ($Line -match '^\\s*(export\\s+)?(const|let|var)\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*=\\s*(async\\s*)?(\\([^)]*\\)|[A-Za-z_$][A-Za-z0-9_$]*)\\s*=>') {\n return [ordered]@{ kind = \"function\"; name = $Matches[3] }\n }\n if ($Line -match '^\\s*(export\\s+)?(class|interface)\\s+([A-Za-z_$][A-Za-z0-9_$]*)') {\n return [ordered]@{ kind = $Matches[2]; name = $Matches[3] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceRustSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(pub\\s+)?(async\\s+)?fn\\s+([A-Za-z_][A-Za-z0-9_]*)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[3] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceGoSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*func\\s+(\\([^)]+\\)\\s*)?([A-Za-z_][A-Za-z0-9_]*)') {\n return [ordered]@{ kind = \"function\"; name = $Matches[2] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceJavaSymbol {\n param([string]$Line)\n\n if ($Line -match '^\\s*(public|private|protected)?\\s*(class|interface|enum)\\s+([A-Za-z_][A-Za-z0-9_]*)') {\n return [ordered]@{ kind = $Matches[2]; name = $Matches[3] }\n }\n return $null\n}\n\nfunction Get-CodeEvidenceSymbolCandidate {\n param(\n [string]$Language,\n [string]$Line\n )\n\n switch ($Language) {\n \"powershell\" { return Get-CodeEvidencePowerShellSymbol $Line }\n \"python\" { return Get-CodeEvidencePythonSymbol $Line }\n \"javascript\" { return Get-CodeEvidenceJavaScriptSymbol $Line }\n \"typescript\" { return Get-CodeEvidenceJavaScriptSymbol $Line }\n \"rust\" { return Get-CodeEvidenceRustSymbol $Line }\n \"go\" { return Get-CodeEvidenceGoSymbol $Line }\n \"java\" { return Get-CodeEvidenceJavaSymbol $Line }\n default { return $null }\n }\n}\n\nfunction Get-CodeEvidenceSymbols {\n param(\n [string]$RelativePath,\n [string]$Language,\n [string[]]$Lines\n )\n\n $symbols = New-Object System.Collections.Generic.List[object]\n for ($i = 0; $i -lt $Lines.Count; $i++) {\n $candidate = Get-CodeEvidenceSymbolCandidate -Language $Language -Line ([string]$Lines[$i])\n if ($null -eq $candidate -or [string]::IsNullOrWhiteSpace([string]$candidate[\"name\"])) {\n continue\n }\n\n $symbols.Add((New-CodeEvidenceNativeSymbol `\n -RelativePath $RelativePath `\n -Language $Language `\n -LineNumber ($i + 1) `\n -Kind ([string]$candidate[\"kind\"]) `\n -Name ([string]$candidate[\"name\"])))\n }\n return $symbols.ToArray()\n}\n\nfunction Get-CodeEvidenceImports {\nparam(\n[string]$RelativePath,\n[string]$Language,\n[string[]]$Lines\n )\n\n $imports = New-Object System.Collections.Generic.List[object]\n for ($i = 0; $i -lt $Lines.Count; $i++) {\n $line = [string]$Lines[$i]\n $target = \"\"\n if ($Language -in @(\"javascript\", \"typescript\") -and $line -match 'from\\s+[\"'']([^\"'']+)[\"'']') {\n $target = $Matches[1]\n } elseif ($Language -in @(\"javascript\", \"typescript\") -and $line -match 'require\\([\"'']([^\"'']+)[\"'']\\)') {\n $target = $Matches[1]\n } elseif ($Language -eq \"python\" -and $line -match '^\\s*(from|import)\\s+([A-Za-z0-9_\\.]+)') {\n $target = $Matches[2]\n } elseif ($Language -eq \"rust\" -and $line -match '^\\s*use\\s+([^;]+);') {\n $target = $Matches[1].Trim()\n } elseif ($Language -eq \"go\" -and $line -match '^\\s*import\\s+[\"'']([^\"'']+)[\"'']') {\n $target = $Matches[1]\n } elseif ($line -match '^\\s*#include\\s+[<\"]([^>\"]+)[>\"]') {\n $target = $Matches[1]\n }\n\n if (-not [string]::IsNullOrWhiteSpace($target)) {\n $imports.Add([ordered]@{\n file = $RelativePath\n line = $i + 1\n target = $target\n language = $Language\n confidence = 0.6\n source = \"native-minimal\"\n })\n }\n }\nreturn $imports.ToArray()\n}\n\nfunction New-AgentCodeSliceRanking {\nparam(\n[object[]]$Files,\n[object[]]$Symbols,\n[object[]]$Imports\n)\n\n$symbolsByFile = @{}\nforeach ($symbol in @($Symbols)) {\n$file = [string]$symbol.file\nif ([string]::IsNullOrWhiteSpace($file)) { continue }\nif (-not $symbolsByFile.ContainsKey($file)) {\n$symbolsByFile[$file] = New-Object System.Collections.Generic.List[object]\n}\n$symbolsByFile[$file].Add($symbol)\n}\n\n$importsByFile = @{}\nforeach ($import in @($Imports)) {\n$file = [string]$import.file\nif ([string]::IsNullOrWhiteSpace($file)) { continue }\nif (-not $importsByFile.ContainsKey($file)) {\n$importsByFile[$file] = New-Object System.Collections.Generic.List[object]\n}\n$importsByFile[$file].Add($import)\n}\n\n$rankedFiles = New-Object System.Collections.Generic.List[object]\nforeach ($file in @($Files)) {\n$path = [string]$file.path\nif ([string]::IsNullOrWhiteSpace($path)) { continue }\n\n$reasons = New-Object System.Collections.Generic.List[string]\n$score = 0\nif ($path -match '(^|/)(index|main|app|server|cli)\\.') {\n$reasons.Add(\"entrypoint\")\n$score += 40\n}\nif ($path -match '(test|spec)\\.' -or $path -match '(^|/)(tests?|spec)/') {\n$reasons.Add(\"test\")\n$score += 35\n}\nif ($symbolsByFile.ContainsKey($path) -and $symbolsByFile[$path].Count -gt 0) {\n$reasons.Add(\"symbols\")\n$score += [Math]::Min(20, 5 * $symbolsByFile[$path].Count)\n}\nif ($importsByFile.ContainsKey($path) -and $importsByFile[$path].Count -gt 0) {\n$reasons.Add(\"imports\")\n$score += [Math]::Min(15, 5 * $importsByFile[$path].Count)\n}\nif ($score -eq 0) {\n$reasons.Add(\"inventory\")\n$score = 1\n}\n\n$rankedFiles.Add([ordered]@{\npath = $path\nlanguage = [string]$file.language\nscore = $score\nreasons = @($reasons.ToArray())\nsymbols = if ($symbolsByFile.ContainsKey($path)) { @($symbolsByFile[$path] | ForEach-Object { $_.name }) } else { @() }\nimports = if ($importsByFile.ContainsKey($path)) { @($importsByFile[$path] | ForEach-Object { $_.target }) } else { @() }\n})\n}\n\n$ordered = @($rankedFiles.ToArray() | Sort-Object -Property @{ Expression = \"score\"; Descending = $true }, @{ Expression = \"path\"; Descending = $false })\nreturn [ordered]@{\nschema = \"agent-code-slice-ranking.v1\"\nstrategy = \"native-evidence-default\"\nfiles = $ordered\n}\n}\n\nfunction Write-CodeEvidenceAgentSlices {\nparam(\n[string]$AgentDir,\n[string]$SliceDir,\n[object[]]$Files,\n[object[]]$Symbols,\n[object[]]$Imports,\n[object]$CocoOutcome\n)\n\n$ranking = New-AgentCodeSliceRanking -Files $Files -Symbols $Symbols -Imports $Imports\n$rankingPath = Join-Path $AgentDir \"ranking.json\"\n$ranking | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $rankingPath -Encoding UTF8\n\n$agentIndexPath = Join-Path $AgentDir \"index.md\"\n@(\n\"# Agent Code Map\",\n\"\",\n\"## Status\",\n\"- Code Evidence Layer: ok\",\n\"- Native minimal layer: enabled\",\n\"- Ranking: [ranking.json](ranking.json)\",\n\"- Native retrieval slice: [native-retrieval](slices/native-retrieval.md)\",\n\"- cocoindex-code adapter: $($CocoOutcome.status) ($($CocoOutcome.reasonCode))\",\n\"\",\n\"## Full Dumps\",\n\"- [files](../full/files.json)\",\n\"- [symbols](../full/symbols.json)\",\n\"- [chunks](../full/chunks.json)\",\n\"- [symbol chunks](../full/symbol-chunks.json)\",\n\"- [imports](../full/imports.json)\",\n\"\",\n\"## Slices\",\n\"- [native retrieval](slices/native-retrieval.md)\",\n\"- [entrypoints](slices/entrypoints.md)\",\n\"- [tests](slices/tests.md)\",\n\"- [risk hotspots](slices/risk-hotspots.md)\"\n) | Set-Content -LiteralPath $agentIndexPath -Encoding UTF8\n\n$topRanked = @($ranking.files | Select-Object -First 20)\n@(\n\"# Native Retrieval Slice\",\n\"\",\n\"- Strategy: native-evidence-default\",\n\"- Source: Code Evidence files/symbols/imports only\",\n\"\",\n\"## Ranked Files\"\n) + @($topRanked | ForEach-Object {\n\"- $($_.path) score=$($_.score) reasons=$(@($_.reasons) -join ',')\"\n}) | Set-Content -LiteralPath (Join-Path $SliceDir \"native-retrieval.md\") -Encoding UTF8\n\n$entrypoints = @($Files | Where-Object { $_.path -match '(^|/)(index|main|app|server|cli)\\.' } | Select-Object -First 20)\n@(\"# Entrypoints\", \"\") + @($entrypoints | ForEach-Object { \"- $($_.path) ($($_.language))\" }) | Set-Content -LiteralPath (Join-Path $SliceDir \"entrypoints.md\") -Encoding UTF8\n\n$tests = @($Files | Where-Object { $_.path -match '(test|spec)\\.' -or $_.path -match '(^|/)(tests?|spec)/' } | Select-Object -First 30)\n@(\"# Tests\", \"\") + @($tests | ForEach-Object { \"- $($_.path) ($($_.language))\" }) | Set-Content -LiteralPath (Join-Path $SliceDir \"tests.md\") -Encoding UTF8\n\n@(\n\"# Risk Hotspots\",\n\"\",\n\"- Native minimal layer does not calculate complexity.\",\n\"- Treat file-sized chunks as fallback evidence until structural chunking is enabled.\",\n\"- cocoindex-code adapter outcome: $($CocoOutcome.status) ($($CocoOutcome.reasonCode)).\"\n) | Set-Content -LiteralPath (Join-Path $SliceDir \"risk-hotspots.md\") -Encoding UTF8\n\nreturn [ordered]@{\nagentIndex = $agentIndexPath\nranking = $rankingPath\nnativeRetrieval = Join-Path $SliceDir \"native-retrieval.md\"\n}\n}\n\nfunction New-CodeEvidenceLayer {\nparam(\n[string]$RepoPath,\n[string]$RunDir,\n[object[]]$Files,\n[object]$CodeEvidenceConfig = $null\n)\n\n$root = Join-Path $RunDir \"code-evidence\"\n$fullDir = Join-Path $root \"merged\\full\"\n$agentDir = Join-Path $root \"merged\\agent\"\n$sliceDir = Join-Path $agentDir \"slices\"\n$adapterDir = Join-Path $root \"adapters\\cocoindex-code\"\nforeach ($dir in @($fullDir, $agentDir, $sliceDir, $adapterDir)) {\nNew-Item -ItemType Directory -Force -Path $dir | Out-Null\n}\n\n$fileRows = New-Object System.Collections.Generic.List[object]\n$symbols = New-Object System.Collections.Generic.List[object]\n$chunks = New-Object System.Collections.Generic.List[object]\n$symbolChunks = New-Object System.Collections.Generic.List[object]\n$imports = New-Object System.Collections.Generic.List[object]\n\nforeach ($file in @($Files)) {\n$fileText = [string]$file\nif ([string]::IsNullOrWhiteSpace($fileText)) { continue }\n$fullPath = if ([System.IO.Path]::IsPathRooted($fileText)) { $fileText } else { Join-Path $RepoPath $fileText }\nif (-not (Test-Path -LiteralPath $fullPath -PathType Leaf)) { continue }\n\n$relativePath = (Get-RelativePathSafe $RepoPath $fullPath).Replace(\"\\\", \"/\")\n$extension = [System.IO.Path]::GetExtension($fullPath)\n$language = Get-CodeEvidenceLanguage -Extension $extension\n$content = Get-Content -LiteralPath $fullPath -Raw -ErrorAction SilentlyContinue\n if ($null -eq $content) { $content = \"\" }\n $lines = if ([string]::IsNullOrEmpty($content)) { @() } else { @($content -split \"`r?`n\") }\n $lines = @($lines)\n $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($content)\n $hashBytes = [System.Security.Cryptography.SHA256]::HashData($contentBytes)\n$hash = [System.BitConverter]::ToString($hashBytes).Replace(\"-\", \"\").ToLowerInvariant()\n\n$fileRows.Add([ordered]@{\npath = $relativePath\nlanguage = $language\nbytes = $contentBytes.Length\nlines = $lines.Count\ntextHash = $hash\nsource = \"native-minimal\"\n})\n\n$fileSymbols = @(Get-CodeEvidenceSymbols -RelativePath $relativePath -Language $language -Lines $lines)\nforeach ($symbol in $fileSymbols) { $symbols.Add($symbol) }\n\n$chunkId = \"$relativePath#file\"\n$chunks.Add([ordered]@{\nid = $chunkId\nfile = $relativePath\nstartLine = 1\nendLine = [Math]::Max(1, $lines.Count)\nkind = \"file\"\ncontainsSymbols = @($fileSymbols | ForEach-Object { $_.id })\ntextHash = $hash\nsource = \"native-minimal\"\n})\n\nforeach ($symbol in $fileSymbols) {\n$symbolChunks.Add([ordered]@{\nsymbolId = $symbol.id\nchunkId = $chunkId\nrelation = \"contained_by\"\nconfidence = 0.55\n})\n}\n\nforeach ($import in @(Get-CodeEvidenceImports -RelativePath $relativePath -Language $language -Lines $lines)) {\n$imports.Add($import)\n}\n}\n\n$fileRowsArray = @($fileRows.ToArray())\n$symbolsArray = @($symbols.ToArray())\n$chunksArray = @($chunks.ToArray())\n$symbolChunksArray = @($symbolChunks.ToArray())\n$importsArray = @($imports.ToArray())\n\n([ordered]@{ schema = \"code-evidence-files.v1\"; files = $fileRowsArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"files.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-symbols.v1\"; symbols = $symbolsArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"symbols.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-chunks.v1\"; chunks = $chunksArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"chunks.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-symbol-chunks.v1\"; mappings = $symbolChunksArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"symbol-chunks.json\") -Encoding UTF8\n([ordered]@{ schema = \"code-evidence-imports.v1\"; imports = $importsArray }) | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $fullDir \"imports.json\") -Encoding UTF8\n\n# R07 reviewed retirement: this compatibility artifact is a static tombstone.\n# There is intentionally no configuration lookup, executable discovery, or provider invocation.\n$cocoOutcome = [ordered]@{\nschema = \"code-evidence-adapter-outcome.v1\"\nadapter = \"cocoindex-code\"\nenabled = $false\nrequired = $false\nstatus = \"skipped\"\nfatal = $false\nreasonCode = \"reviewed_deletion\"\nreason = \"cocoindex-code is a reviewed retirement tombstone; legacy configuration cannot restore discovery or invocation.\"\ncommand = \"\"\n}\n\n$cocoOutcomePath = Join-Path $adapterDir \"outcome.json\"\n$cocoOutcome | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $cocoOutcomePath -Encoding UTF8\n\n$scorecard = [ordered]@{\nschema = \"code-evidence-scorecard.v1\"\nstatus = \"ok\"\nnativeMinimal = $true\nadapters = @($cocoOutcome)\nmetrics = [ordered]@{\nfiles = $fileRowsArray.Count\nsymbols = $symbolsArray.Count\nchunks = $chunksArray.Count\nimports = $importsArray.Count\nsymbolContainmentRate = if ($symbolsArray.Count -gt 0) { 1.0 } else { $null }\nfallbackChunkRate = 1.0\n}\n}\n$scorecardPath = Join-Path $root \"merged\\scorecard.json\"\n$scorecard | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $scorecardPath -Encoding UTF8\n$scorecardMarkdownPath = Join-Path $root \"merged\\scorecard.md\"\n@(\n\"# Code Evidence Scorecard\",\n\"\",\n\"- Status: ok\",\n\"- Native minimal: true\",\n\"- Files: $($fileRowsArray.Count)\",\n\"- Symbols: $($symbolsArray.Count)\",\n\"- Chunks: $($chunksArray.Count)\",\n\"- Imports: $($importsArray.Count)\",\n\"- cocoindex-code: $($cocoOutcome.status) ($($cocoOutcome.reasonCode))\"\n) | Set-Content -LiteralPath $scorecardMarkdownPath -Encoding UTF8\n\n$agentSlices = Write-CodeEvidenceAgentSlices `\n-AgentDir $agentDir `\n-SliceDir $sliceDir `\n-Files $fileRowsArray `\n-Symbols $symbolsArray `\n-Imports $importsArray `\n-CocoOutcome $cocoOutcome\n\nreturn [ordered]@{\nschema = \"code-evidence-summary.v1\"\nstatus = \"ok\"\nfatal = $false\nroot = $root\nagentIndex = $agentSlices.agentIndex\nscorecard = $scorecardPath\nscorecardMarkdown = $scorecardMarkdownPath\nfiles = $fileRowsArray.Count\nsymbols = $symbolsArray.Count\nchunks = $chunksArray.Count\nimports = $importsArray.Count\nadapters = @($cocoOutcome)\n}\n}\n\nfunction ConvertTo-NullableDouble {\n param([object]$Value)\n\n if ($null -eq $Value) { return $null }\n try {\n return [double]$Value\n }\n catch {\n return $null\n }\n}\n\nfunction Get-SentruxMetricPair {\n param(\n [string]$Output,\n [string]$Label\n )\n\n if ([string]::IsNullOrWhiteSpace($Output)) { return $null }\n $pattern = [regex]::Escape($Label) + \":\\s+([0-9.]+)\\s+[^\\r\\n0-9.]+\\s+([0-9.]+)\"\n $match = [regex]::Match($Output, $pattern)\n if (-not $match.Success) { return $null }\n\n return [ordered]@{\n before = ConvertTo-NullableDouble $match.Groups[1].Value\n after = ConvertTo-NullableDouble $match.Groups[2].Value\n }\n}\n\nfunction New-SentruxMetricDelta {\n param(\n [string]$Name,\n [object]$Before,\n [object]$After,\n [ValidateSet(\"higher_is_better\", \"lower_is_better\")]\n [string]$Polarity = \"lower_is_better\"\n )\n\n $beforeValue = ConvertTo-NullableDouble $Before\n $afterValue = ConvertTo-NullableDouble $After\n $delta = $null\n $direction = \"unknown\"\n $regressed = $false\n\n if ($null -ne $beforeValue -and $null -ne $afterValue) {\n $delta = $afterValue - $beforeValue\n if ([math]::Abs($delta) -lt 0.000001) {\n $direction = \"stable\"\n }\n elseif ($delta -gt 0) {\n $direction = \"up\"\n }\n else {\n $direction = \"down\"\n }\n\n if ($Polarity -eq \"higher_is_better\") {\n $regressed = $delta -lt 0\n }\n else {\n $regressed = $delta -gt 0\n }\n }\n\n return [ordered]@{\n name = $Name\n before = $beforeValue\n after = $afterValue\n delta = $delta\n direction = $direction\n polarity = $Polarity\n regressed = $regressed\n }\n}\n\nfunction Test-SentruxGateNoDegradation {\n param([string]$GateOutput)\n\n return (-not [string]::IsNullOrWhiteSpace($GateOutput) -and $GateOutput -match \"No degradation detected\")\n}\n\nfunction Resolve-SentruxMetricRegressions {\n param(\n [object[]]$Metrics,\n [bool]$NoDegradation\n )\n\n foreach ($metric in @($Metrics)) {\n if ($null -eq $metric) {\n continue\n }\n\n $rawRegressed = [bool]$metric.regressed\n $gateAccepted = $NoDegradation -and $rawRegressed\n $metric | Add-Member -NotePropertyName rawRegressed -NotePropertyValue $rawRegressed -Force\n $metric | Add-Member -NotePropertyName gateAccepted -NotePropertyValue $gateAccepted -Force\n if ($gateAccepted) {\n $metric.regressed = $false\n }\n $metric\n }\n}\n\nfunction New-SentruxInsight {\n param(\n [string]$RepoName,\n [string]$TargetPath,\n [string]$BaselinePath,\n [object[]]$Steps\n )\n\n $gateStep = @($Steps | Where-Object { $_.name -like \"sentrux gate*\" } | Select-Object -Last 1)\n $checkStep = @($Steps | Where-Object { $_.name -eq \"sentrux check\" } | Select-Object -First 1)\n $rulesPath = if ([string]::IsNullOrWhiteSpace($TargetPath)) { \"\" } else { Join-Path (Join-Path $TargetPath \".sentrux\") \"rules.toml\" }\n $baseline = Read-JsonFileSafe $BaselinePath\n $gateOutput = if ($gateStep.Count -gt 0) { [string]$gateStep[0].output } else { \"\" }\n $noDegradation = Test-SentruxGateNoDegradation $gateOutput\n\n $qualityPair = Get-SentruxMetricPair $gateOutput \"Quality\"\n $couplingPair = Get-SentruxMetricPair $gateOutput \"Coupling\"\n $cyclesPair = Get-SentruxMetricPair $gateOutput \"Cycles\"\n $godFilesPair = Get-SentruxMetricPair $gateOutput \"God files\"\n $distance = $null\n $distanceMatch = [regex]::Match($gateOutput, \"Distance from Main Sequence:\\s+([0-9.]+)\")\n if ($distanceMatch.Success) {\n $distance = ConvertTo-NullableDouble $distanceMatch.Groups[1].Value\n }\n\n $scan = [ordered]@{}\n $resolveMatch = [regex]::Match($gateOutput, \"\\[resolve\\]\\s+([0-9]+)\\s+resolved,\\s+([0-9]+)\\s+unresolved\")\n if ($resolveMatch.Success) {\n $scan[\"resolvedImports\"] = [int]$resolveMatch.Groups[1].Value\n $scan[\"unresolvedImports\"] = [int]$resolveMatch.Groups[2].Value\n }\n $graphMatch = [regex]::Match($gateOutput, \"\\[build_graphs\\]\\s+([0-9]+)\\s+files.*\\|\\s+([0-9]+)\\s+import,\\s+([0-9]+)\\s+call,\\s+([0-9]+)\\s+inherit edges\")\n if ($graphMatch.Success) {\n $scan[\"files\"] = [int]$graphMatch.Groups[1].Value\n $scan[\"importEdges\"] = [int]$graphMatch.Groups[2].Value\n $scan[\"callEdges\"] = [int]$graphMatch.Groups[3].Value\n $scan[\"inheritEdges\"] = [int]$graphMatch.Groups[4].Value\n }\n\n $metrics = @()\n if ($null -ne $qualityPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"quality\" $qualityPair[\"before\"] $qualityPair[\"after\"] \"higher_is_better\")\n }\n if ($null -ne $couplingPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"coupling\" $couplingPair[\"before\"] $couplingPair[\"after\"] \"lower_is_better\")\n }\n if ($null -ne $cyclesPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"cycles\" $cyclesPair[\"before\"] $cyclesPair[\"after\"] \"lower_is_better\")\n }\n if ($null -ne $godFilesPair) {\n $metrics += [pscustomobject](New-SentruxMetricDelta \"god_files\" $godFilesPair[\"before\"] $godFilesPair[\"after\"] \"lower_is_better\")\n }\n $metrics = @(Resolve-SentruxMetricRegressions -Metrics $metrics -NoDegradation $noDegradation)\n\n $regressions = @($metrics | Where-Object { $_.regressed })\n $nextActions = @()\n $codeNexusHints = @()\n\n if ([string]::IsNullOrWhiteSpace($TargetPath)) {\n $nextActions += \"Sentrux target was not resolved; inspect pipeline configuration.\"\n }\n elseif (-not (Test-Path -LiteralPath $BaselinePath -PathType Leaf)) {\n $nextActions += \"Create an intentional Sentrux baseline for this scope before using it as a gate.\"\n }\n elseif ($gateStep.Count -gt 0 -and $gateStep[0].status -eq \"failed\") {\n $nextActions += \"Inspect the Sentrux gate output before saving any new baseline.\"\n }\n elseif ($regressions.Count -gt 0) {\n $nextActions += \"Investigate regressed structural metrics before accepting this change.\"\n }\n else {\n $nextActions += \"No structural regression detected for this scope.\"\n }\n\n if (-not [string]::IsNullOrWhiteSpace($rulesPath) -and -not (Test-Path -LiteralPath $rulesPath -PathType Leaf)) {\n $nextActions += \"Add .sentrux/rules.toml when this scope needs explicit architecture boundary rules.\"\n }\n\n if (@($regressions | Where-Object { $_.name -in @(\"coupling\", \"cycles\") }).Count -gt 0) {\n $codeNexusHints += \"Use CodeNexus impact/context on symbols in newly coupled modules.\"\n $codeNexusHints += \"Suggested query: gitnexus query `\"cross module import dependency cycle`\" --repo $RepoName\"\n }\n elseif (@($regressions | Where-Object { $_.name -eq \"quality\" }).Count -gt 0) {\n $codeNexusHints += \"Use CodeNexus query to locate the flow behind the quality drop.\"\n $codeNexusHints += \"Suggested query: gitnexus query `\"complex hotspot structural regression`\" --repo $RepoName\"\n }\n else {\n $codeNexusHints += \"If a future gate regresses, start with CodeNexus context/impact on the changed files.\"\n }\n\n return [ordered]@{\n targetPath = $TargetPath\n baselinePath = $BaselinePath\n baselineExists = (-not [string]::IsNullOrWhiteSpace($BaselinePath) -and (Test-Path -LiteralPath $BaselinePath -PathType Leaf))\n rulesPath = $rulesPath\n rulesExists = (-not [string]::IsNullOrWhiteSpace($rulesPath) -and (Test-Path -LiteralPath $rulesPath -PathType Leaf))\n checkStatus = if ($checkStep.Count -gt 0) { $checkStep[0].status } else { \"not_run\" }\n gateStatus = if ($gateStep.Count -gt 0) { $gateStep[0].status } else { \"not_run\" }\n noDegradation = $noDegradation\n metrics = $metrics\n baseline = [ordered]@{\n qualitySignal = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"quality_signal\")\n couplingScore = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"coupling_score\")\n cycleCount = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"cycle_count\")\n complexFnCount = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"complex_fn_count\")\n crossModuleEdges = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"cross_module_edges\")\n totalImportEdges = ConvertTo-NullableDouble (Get-JsonProperty $baseline \"total_import_edges\")\n }\n distanceFromMainSequence = $distance\n scan = $scan\n regressions = $regressions\n nextActions = $nextActions\n codeNexusHints = $codeNexusHints\n }\n}\n\nfunction Get-StepMatch {\n param(\n [object[]]$Steps,\n [string]$Pattern,\n [switch]$Last\n )\n\n $matches = @($Steps | Where-Object { [string]$_.name -like $Pattern })\n if ($matches.Count -eq 0) { return $null }\n if ($Last) { return $matches[-1] }\n return $matches[0]\n}\n\nfunction Get-StepScore {\n param([object]$Step)\n\n if ($null -eq $Step) { return 0 }\n switch ([string]$Step.status) {\n \"passed\" { return 100 }\n default { return 0 }\n }\n}\n\nfunction Get-FailureCount {\n param(\n [object]$FailureCounts,\n [string]$Name\n )\n\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains($Name)) {\n return [int]$FailureCounts[$Name]\n }\n if ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[$Name]) {\n return [int]$FailureCounts.$Name\n }\n\n return 0\n}\n\nfunction Get-FirstLine {\n param([string]$Text)\n\n if ([string]::IsNullOrWhiteSpace($Text)) { return \"\" }\n return (($Text -split \"\\r?\\n\") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1)\n}\n\nfunction New-QualityDimension {\n param(\n [string]$Name,\n [int]$Score,\n [string]$Status,\n [string]$Evidence\n )\n\n return [ordered]@{\n name = $Name\n score = [math]::Max(0, [math]::Min(100, $Score))\n status = $Status\n evidence = $Evidence\n }\n}\n\nfunction New-Modality {\n param(\n [string]$Name,\n [string]$Role,\n [object]$Step,\n [int]$Confidence,\n [string]$Artifact,\n [string]$Finding,\n [string]$Limit\n )\n\n $status = if ($Finding -eq \"not generated\" -and [string]::IsNullOrWhiteSpace($Artifact)) {\n \"missing\"\n }\n elseif ($null -ne $Step) {\n [string]$Step.status\n }\n elseif (-not [string]::IsNullOrWhiteSpace($Artifact)) {\n \"generated\"\n }\n else {\n \"not_run\"\n }\n return [ordered]@{\n name = $Name\n role = $Role\n status = $status\n confidence = [math]::Max(0, [math]::Min(100, $Confidence))\n artifact = $Artifact\n finding = $Finding\n limit = $Limit\n durationMs = if ($null -eq $Step -or $null -eq $Step.durationMs) { $null } else { [int]$Step.durationMs }\n }\n}\n\nfunction New-HospitalProtocol {\n param(\n [string]$Name,\n [string]$Status,\n [string]$Command,\n [string]$ExitCriteria\n )\n\n return [ordered]@{\n name = $Name\n status = $Status\n command = $Command\n exit_criteria = $ExitCriteria\n }\n}\n\nfunction New-StateTransition {\n param(\n [string]$From,\n [string]$To,\n [string]$Guard,\n [bool]$Pass\n )\n\n return [ordered]@{\n from = $From\n to = $To\n guard = $Guard\n pass = $Pass\n }\n}\n\nfunction New-HospitalStateMachine {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [string]$GateStatus,\n [string]$CheckStatus,\n [int]$FailingWhatIfCount,\n [string]$Disposition,\n [string]$NextProtocol,\n [bool]$StructuralEvidenceComplete = $true,\n [string]$SurgeryTarget = \"\",\n [string]$CurrentTopHotspot = \"\"\n )\n\n # Keep this guard self-contained because the state-machine seam is also\n # extracted independently by the regression harness.\n $providerQuotaCount = 0\n $providerUnavailableCount = 0\n $configErrorCount = 0\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains(\"providerQuota\")) {\n $providerQuotaCount = [int]$FailureCounts[\"providerQuota\"]\n }\n elseif ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[\"providerQuota\"]) {\n $providerQuotaCount = [int]$FailureCounts.providerQuota\n }\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains(\"providerUnavailable\")) {\n $providerUnavailableCount = [int]$FailureCounts[\"providerUnavailable\"]\n }\n elseif ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[\"providerUnavailable\"]) {\n $providerUnavailableCount = [int]$FailureCounts.providerUnavailable\n }\n if ($FailureCounts -is [System.Collections.IDictionary] -and $FailureCounts.Contains(\"configError\")) {\n $configErrorCount = [int]$FailureCounts[\"configError\"]\n }\n elseif ($null -ne $FailureCounts -and $null -ne $FailureCounts.PSObject.Properties[\"configError\"]) {\n $configErrorCount = [int]$FailureCounts.configError\n }\n\n $toolsOk = ([int]$FailureCounts.localToolError -eq 0)\n $providerAvailable = ($providerQuotaCount -eq 0 -and $providerUnavailableCount -eq 0 -and $configErrorCount -eq 0)\n $graphOk = ([int]$FailureCounts.graphMissing -eq 0)\n $sentruxOk = ([int]$FailureCounts.sentruxFail -eq 0 -and $RulesExists -and $GateStatus -eq \"passed\" -and $CheckStatus -eq \"passed\")\n $surgeryDebtCleared = ($StructuralEvidenceComplete -and $FailingWhatIfCount -eq 0)\n\n # surgery_plan -> post_op: the surgery target has actually been treated\n # (it no longer shows up as the current top hotspot) and sentrux confirms\n # the governed scope is clean, so it is safe to move on to post-op review.\n $surgeryTargetResolved = (-not [string]::IsNullOrWhiteSpace($SurgeryTarget) -and\n -not [string]::IsNullOrWhiteSpace($CurrentTopHotspot) -and\n ($SurgeryTarget -ne $CurrentTopHotspot))\n $surgeryToPostOpOk = ($sentruxOk -and $StructuralEvidenceComplete -and $surgeryTargetResolved)\n $postOpOk = ($toolsOk -and $providerAvailable -and $graphOk -and $sentruxOk -and $surgeryDebtCleared -and $surgeryTargetResolved)\n\n $currentState = switch ($NextProtocol) {\n \"triage\" { \"triage\" }\n \"diagnose\" { \"diagnose\" }\n \"govern\" { \"govern\" }\n \"surgery_plan\" { \"surgery_plan\" }\n \"post_op\" { if ($Disposition -eq \"discharge_ready\") { \"discharge_ready\" } else { \"post_op\" } }\n default { \"triage\" }\n }\n\n return [ordered]@{\n schema = \"code-intel-hospital-state-machine.v1\"\n current_state = $currentState\n disposition = $Disposition\n next_protocol = $NextProtocol\n states = @(\"triage\", \"diagnose\", \"govern\", \"surgery_plan\", \"post_op\", \"discharge_ready\")\n transitions = @(\n (New-StateTransition \"triage\" \"diagnose\" \"local toolchain is available\" $toolsOk)\n (New-StateTransition \"diagnose\" \"govern\" \"architecture graph exists or graph absence is accepted\" $graphOk)\n (New-StateTransition \"govern\" \"surgery_plan\" \"rules and gate pass, but what-if still has planned debt\" ($sentruxOk -and -not $surgeryDebtCleared))\n (New-StateTransition \"govern\" \"post_op\" \"rules and gate pass, no planned surgery debt remains\" ($sentruxOk -and $surgeryDebtCleared))\n (New-StateTransition \"surgery_plan\" \"post_op\" \"sentrux gate/check pass and the surgery target no longer appears as the current top hotspot\" $surgeryToPostOpOk)\n (New-StateTransition \"post_op\" \"discharge_ready\" \"post-op verification passes with no regressions\" $postOpOk)\n )\n guards = [ordered]@{\n tools_ok = $toolsOk\n provider_available = $providerAvailable\n graph_ok = $graphOk\n rules_exists = $RulesExists\n sentrux_check = $CheckStatus\n sentrux_gate = $GateStatus\n sentrux_ok = $sentruxOk\n failing_what_if = $FailingWhatIfCount\n structural_evidence_complete = $StructuralEvidenceComplete\n surgery_debt_cleared = $surgeryDebtCleared\n surgery_target = $SurgeryTarget\n current_top_hotspot = $CurrentTopHotspot\n surgery_target_resolved = $surgeryTargetResolved\n surgery_to_post_op_ok = $surgeryToPostOpOk\n post_op_ok = $postOpOk\n }\n }\n}\n\nfunction Read-JsonPathIfExists {\n param([string]$Path)\n\n if ([string]::IsNullOrWhiteSpace($Path)) { return $null }\n if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }\n\n return Read-JsonFileSafe $Path\n}\n\nfunction Get-SourceAnchorText {\n param([object]$SourceAnchor)\n\n if ($null -eq $SourceAnchor) { return \"\" }\n if ($SourceAnchor -is [string]) { return [string]$SourceAnchor }\n if ($null -ne $SourceAnchor.label) { return [string]$SourceAnchor.label }\n if ($null -ne $SourceAnchor.path) { return [string]$SourceAnchor.path }\n\n return [string]$SourceAnchor\n}\n\nfunction New-CodeIntelSurgeryPlan {\n param(\n [object]$Hospital,\n [string]$RepoPath,\n [string]$SentruxTargetPath,\n [string]$HotspotsPath,\n [string]$WhatIfPath,\n [string]$CodeNexusPath\n )\n\n $hotspots = Read-JsonPathIfExists $HotspotsPath\n $whatIf = Read-JsonPathIfExists $WhatIfPath\n $codeNexus = Read-JsonPathIfExists $CodeNexusPath\n\n $primaryFunction = $null\n if ($null -ne $hotspots -and $null -ne $hotspots.functions -and @($hotspots.functions).Count -gt 0) {\n $primaryFunction = $hotspots.functions[0]\n }\n $primaryFile = $null\n if ($null -ne $hotspots -and $null -ne $hotspots.files -and @($hotspots.files).Count -gt 0) {\n $primaryFile = $hotspots.files[0]\n }\n $primaryScenario = $null\n $failingScenarios = @()\n if ($null -ne $whatIf -and $null -ne $whatIf.scenarios) {\n $failingScenarios = @($whatIf.scenarios | Where-Object { -not $_.pass })\n if ($failingScenarios.Count -gt 0) { $primaryScenario = $failingScenarios[0] }\n }\n $contextFile = $null\n if ($null -ne $codeNexus -and $null -ne $codeNexus.files -and @($codeNexus.files).Count -gt 0) {\n $contextFile = $codeNexus.files[0]\n }\n\n $targetFile = if ($null -ne $primaryFunction) { [string]$primaryFunction.file } elseif ($null -ne $primaryFile) { [string]$primaryFile.path } else { \"\" }\n $targetName = if ($null -ne $primaryFunction) { [string]$primaryFunction.name } elseif ($null -ne $primaryFile) { [string]$primaryFile.path } else { \"\" }\n $targetAnchor = if ($null -ne $primaryFunction) { Get-SourceAnchorText $primaryFunction.sourceAnchor } elseif ($null -ne $primaryFile) { Get-SourceAnchorText $primaryFile.sourceAnchor } else { \"\" }\n $targetComplexity = if ($null -ne $primaryFunction) { [int]$primaryFunction.complexity } elseif ($null -ne $primaryFile) { [int]$primaryFile.maxComplexity } else { $null }\n $scenarioName = if ($null -ne $primaryScenario) { [string]$primaryScenario.name } else { \"\" }\n $scenarioAction = if ($null -ne $primaryScenario) { [string]$primaryScenario.action } else { \"\" }\n $status = if ([string]$Hospital.triage.next_protocol -eq \"surgery_plan\" -or\n ([string]$Hospital.triage.disposition -eq \"admit\" -and -not [string]::IsNullOrWhiteSpace($targetFile))) {\n \"planned\"\n }\n else {\n \"not_required\"\n }\n\n return [ordered]@{\n schema = \"code-intel-surgery-plan.v1\"\n status = $status\n repo = $RepoPath\n scope = $SentruxTargetPath\n admission = [ordered]@{\n disposition = $Hospital.triage.disposition\n diagnosis = $Hospital.triage.primary_diagnosis\n reason = $Hospital.triage.admission_reason\n }\n primary_target = [ordered]@{\n file = $targetFile\n name = $targetName\n source_anchor = $targetAnchor\n complexity = $targetComplexity\n scenario = $scenarioName\n scenario_action = $scenarioAction\n codenexus_file = if ($null -ne $contextFile) { [string]$contextFile.path } else { \"\" }\n }\n operating_plan = @(\n \"Open the primary target and its CodeNexus context before editing.\",\n \"Reduce the selected hotspot by extraction, boundary clarification, or testable decomposition.\",\n \"Do not raise Sentrux thresholds to make the surgery pass.\",\n \"Add or update the smallest test that proves the behavior stayed intact.\"\n )\n verification = @(\n \"Invoke-SentruxAgentTool.ps1 check_rules `\"$SentruxTargetPath`\"\",\n \"Invoke-SentruxAgentTool.ps1 session_end `\"$SentruxTargetPath`\"\",\n \"test-code-intel-pipeline.ps1 -RepoPath `\"$RepoPath`\" -SentruxPath `\"$((Get-RelativePathSafe $RepoPath $SentruxTargetPath) -replace '\\\\', '/')`\" -SkipRepowise -Mode normal\"\n )\n discharge_criteria = $Hospital.triage.discharge_criteria\n evidence = [ordered]@{\n hotspots = $HotspotsPath\n what_if = $WhatIfPath\n codenexus = $CodeNexusPath\n failing_scenarios = @($failingScenarios | Select-Object -First 5)\n }\n }\n}\n\nfunction Convert-SurgeryPlanToMarkdown {\n param([object]$Plan)\n\n $lines = @(\n \"# Code Intel Surgery Plan\",\n \"\",\n \"- Status: $($Plan.status)\",\n \"- Repo: $($Plan.repo)\",\n \"- Scope: $($Plan.scope)\",\n \"- Diagnosis: $($Plan.admission.diagnosis)\",\n \"- Admission reason: $($Plan.admission.reason)\",\n \"\",\n \"## Primary Target\",\n \"- File: $($Plan.primary_target.file)\",\n \"- Symbol: $($Plan.primary_target.name)\",\n \"- Anchor: $($Plan.primary_target.source_anchor)\",\n \"- Complexity: $($Plan.primary_target.complexity)\",\n \"- Scenario: $($Plan.primary_target.scenario)\",\n \"- Action: $($Plan.primary_target.scenario_action)\",\n \"- CodeNexus file: $($Plan.primary_target.codenexus_file)\",\n \"\",\n \"## Operating Plan\"\n )\n foreach ($item in @($Plan.operating_plan)) {\n $lines += \"- $item\"\n }\n $lines += \"\"\n $lines += \"## Verification\"\n foreach ($item in @($Plan.verification)) {\n $lines += \"- ``$item``\"\n }\n $lines += \"\"\n $lines += \"## Discharge Criteria\"\n foreach ($item in @($Plan.discharge_criteria)) {\n $lines += \"- $item\"\n }\n return $lines\n}\n\nfunction Get-HospitalDiagnosis {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [int]$FailingWhatIfCount\n )\n\n $providerQuotaCount = Get-FailureCount $FailureCounts \"providerQuota\"\n $providerUnavailableCount = Get-FailureCount $FailureCounts \"providerUnavailable\"\n $configErrorCount = Get-FailureCount $FailureCounts \"configError\"\n\n if ($FailureCounts.localToolError -gt 0) {\n return [ordered]@{ severity = \"red\"; primaryDiagnosis = \"local tool failure\" }\n }\n if ($providerQuotaCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"provider quota exhausted\" }\n }\n if ($providerUnavailableCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"provider unavailable\" }\n }\n if ($configErrorCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"provider configuration error\" }\n }\n if ($FailureCounts.sentruxFail -gt 0) {\n return [ordered]@{ severity = \"red\"; primaryDiagnosis = \"architecture gate failure\" }\n }\n if ($FailureCounts.graphMissing -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"architecture graph missing\" }\n }\n if (-not $RulesExists) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"ungoverned structural scope\" }\n }\n if ($FailingWhatIfCount -gt 0) {\n return [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"known modernization debt\" }\n }\n\n return [ordered]@{ severity = \"green\"; primaryDiagnosis = \"clean snapshot\" }\n}\n\nfunction Get-HospitalNextProtocol {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [int]$FailingWhatIfCount,\n [object]$GitHubResearch\n )\n\n $providerQuotaCount = Get-FailureCount $FailureCounts \"providerQuota\"\n $providerUnavailableCount = Get-FailureCount $FailureCounts \"providerUnavailable\"\n $configErrorCount = Get-FailureCount $FailureCounts \"configError\"\n\n if ($FailureCounts.localToolError -gt 0) { return \"triage\" }\n if ($providerQuotaCount -gt 0) { return \"triage\" }\n if ($providerUnavailableCount -gt 0) { return \"triage\" }\n if ($configErrorCount -gt 0) { return \"triage\" }\n if ($null -ne $GitHubResearch -and [bool]$GitHubResearch.required) { return \"github_solution_research\" }\n if ($FailureCounts.graphMissing -gt 0) { return \"diagnose\" }\n if (-not $RulesExists) { return \"govern\" }\n if ($FailingWhatIfCount -gt 0) { return \"surgery_plan\" }\n\n return \"post_op\"\n}\n\nfunction Get-HospitalAdmissionReason {\n param([string]$PrimaryDiagnosis)\n\n switch ($PrimaryDiagnosis) {\n \"clean snapshot\" { return \"No active inpatient issue; ready for discharge after post-op verification.\" }\n \"architecture graph missing\" { return \"Admit for diagnostic imaging: Understand graph is missing or stale.\" }\n \"ungoverned structural scope\" { return \"Admit for governance: rules are missing for the selected scope.\" }\n \"known modernization debt\" { return \"Admit for planned surgery: what-if scenarios show debt that should be scheduled, not ignored.\" }\n \"architecture gate failure\" { return \"Admit for structural treatment: Sentrux gate or rules failed.\" }\n \"provider quota exhausted\" { return \"Admit for triage: provider quota prevented complete evidence collection.\" }\n \"provider unavailable\" { return \"Admit for triage: the configured upstream provider route or model was unavailable.\" }\n \"provider configuration error\" { return \"Admit for triage: provider credentials, endpoint, or model configuration must be corrected.\" }\n \"structural evidence incomplete\" { return \"Admit for diagnosis: required structural summaries are incomplete.\" }\n \"local tool failure\" { return \"Admit for triage: local toolchain failed before diagnosis can be trusted.\" }\n default { return \"Admit until the next protocol clears the diagnosis.\" }\n }\n}\n\nfunction Get-HospitalTreatmentPlan {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [int]$FailingWhatIfCount,\n [string]$UnderstandCommand,\n [string]$TopContextFile\n )\n\n $providerQuotaCount = Get-FailureCount $FailureCounts \"providerQuota\"\n $providerUnavailableCount = Get-FailureCount $FailureCounts \"providerUnavailable\"\n $configErrorCount = Get-FailureCount $FailureCounts \"configError\"\n\n $treatment = @()\n if ($FailureCounts.localToolError -gt 0) { $treatment += \"Fix local tool errors before interpreting architecture signals.\" }\n if ($providerQuotaCount -gt 0) { $treatment += \"Restore provider quota or use a complete local evidence path before interpreting the result.\" }\n if ($providerUnavailableCount -gt 0) { $treatment += \"Verify the provider model catalog and route availability; keep local index-only evidence available.\" }\n if ($configErrorCount -gt 0) { $treatment += \"Correct provider endpoint, model, or credential configuration before retrying provider-backed docs.\" }\n if ($FailureCounts.graphMissing -gt 0) { $treatment += \"Refresh Understand graph with: $UnderstandCommand\" }\n if (-not $RulesExists) { $treatment += \"Add .sentrux/rules.toml for the chosen scope.\" }\n if ($FailingWhatIfCount -gt 0) { $treatment += \"Use what-if failures as the tightening roadmap; start with the first failing scenario.\" }\n if (-not [string]::IsNullOrWhiteSpace($TopContextFile)) { $treatment += \"Start CodeNexus review at $TopContextFile.\" }\n if ($treatment.Count -eq 0) { $treatment += \"Keep this artifact as the current clean snapshot and compare the next session against it.\" }\n\n return $treatment\n}\n\nfunction New-HospitalDecisionBlock {\n param(\n [object]$FailureCounts,\n [bool]$RulesExists,\n [string]$GateStatus,\n [string]$CheckStatus,\n [int]$FailingWhatIfCount,\n [string]$UnderstandCommand,\n [string]$TopContextFile,\n [bool]$StructuralEvidenceComplete = $false,\n [string]$SurgeryTarget = \"\",\n [string]$CurrentTopHotspot = \"\",\n [object]$GitHubResearch\n )\n\n $diagnosis = Get-HospitalDiagnosis $FailureCounts $RulesExists $FailingWhatIfCount\n $nextProtocol = Get-HospitalNextProtocol $FailureCounts $RulesExists $FailingWhatIfCount $GitHubResearch\n $sentruxVerified = ($RulesExists -and $GateStatus -eq \"passed\" -and $CheckStatus -eq \"passed\")\n if ($diagnosis.severity -eq \"green\" -and -not $sentruxVerified) {\n $hasExplicitFailure = ($GateStatus -eq \"failed\" -or $CheckStatus -eq \"failed\")\n $diagnosis = [ordered]@{\n severity = if ($hasExplicitFailure) { \"red\" } else { \"amber\" }\n primaryDiagnosis = if ($hasExplicitFailure) { \"architecture gate failure\" } else { \"architecture verification incomplete\" }\n }\n $nextProtocol = \"govern\"\n }\n elseif ($diagnosis.severity -eq \"green\" -and -not $StructuralEvidenceComplete) {\n $diagnosis = [ordered]@{ severity = \"amber\"; primaryDiagnosis = \"structural evidence incomplete\" }\n $nextProtocol = \"diagnose\"\n }\n $postOpResolved = (-not [string]::IsNullOrWhiteSpace($SurgeryTarget) -and\n -not [string]::IsNullOrWhiteSpace($CurrentTopHotspot) -and\n $SurgeryTarget -ne $CurrentTopHotspot)\n $disposition = if ($diagnosis.severity -ne \"green\") {\n \"admit\"\n }\n elseif ($sentruxVerified -and $StructuralEvidenceComplete -and $postOpResolved) {\n \"discharge_ready\"\n }\n else {\n \"observe\"\n }\n $admissionReason = Get-HospitalAdmissionReason $diagnosis.primaryDiagnosis\n $dischargeCriteria = @(\n \"failure category counters are zero\",\n \"Sentrux check and gate pass for the governed scope\",\n \"hospital triage status is green or explicitly accepted for observation\",\n \"session_end reports no quality regression after Agent edits\"\n )\n if ($null -ne $GitHubResearch -and [bool]$GitHubResearch.required) {\n $dischargeCriteria += \"GitHub evidence linked or GitHub evidence insufficiency recorded in github-solution-research artifacts\"\n }\n $treatment = Get-HospitalTreatmentPlan $FailureCounts $RulesExists $FailingWhatIfCount $UnderstandCommand $TopContextFile\n if (-not $sentruxVerified) {\n $treatment = @($treatment) + \"Obtain passing Sentrux check and gate evidence before discharge.\"\n }\n\n $stateMachine = New-HospitalStateMachine `\n -FailureCounts $FailureCounts `\n -RulesExists $RulesExists `\n -GateStatus $GateStatus `\n -CheckStatus $CheckStatus `\n -FailingWhatIfCount $FailingWhatIfCount `\n -Disposition $disposition `\n -NextProtocol $nextProtocol `\n -StructuralEvidenceComplete $StructuralEvidenceComplete `\n -SurgeryTarget $SurgeryTarget `\n -CurrentTopHotspot $CurrentTopHotspot\n\n return [ordered]@{\n severity = $diagnosis.severity\n primaryDiagnosis = $diagnosis.primaryDiagnosis\n nextProtocol = $nextProtocol\n disposition = $disposition\n admissionReason = $admissionReason\n dischargeCriteria = $dischargeCriteria\n treatment = $treatment\n stateMachine = $stateMachine\n }\n}\n\nfunction New-HospitalFindings {\n param(\n [int]$InventoryFiles,\n [object]$SentruxFileDetailsSummary,\n [string]$TopFunction,\n [string]$TopModule,\n [object]$ResolvedRatio,\n [int]$ResolvedImports,\n [int]$UnresolvedImports,\n [int]$ExcludedFiles\n )\n\n $findings = @()\n if ($InventoryFiles -gt 0) { $findings += \"X-ray inventory found $InventoryFiles files.\" }\n if ($null -ne $SentruxFileDetailsSummary) { $findings += \"CT structural scan found $($SentruxFileDetailsSummary.files) files and $($SentruxFileDetailsSummary.functions) functions.\" }\n if (-not [string]::IsNullOrWhiteSpace($TopFunction)) { $findings += \"Top surgical hotspot: $TopFunction.\" }\n if (-not [string]::IsNullOrWhiteSpace($TopModule)) { $findings += \"Top module hotspot: $TopModule.\" }\n if ($ResolvedRatio -ne $null) { $findings += \"Import resolution ratio is $ResolvedRatio% ($ResolvedImports resolved, $UnresolvedImports unresolved).\" }\n if ($ExcludedFiles -gt 0) { $findings += \"$ExcludedFiles files were quarantined from governed source metrics.\" }\n return $findings\n}\n\nfunction New-HospitalModalities {\n param(\n [object]$InventoryStep,\n [object]$UnderstandStep,\n [object]$RepowiseStep,\n [object]$SentruxCheckStep,\n [object]$SentruxGateStep,\n [int]$GraphScore,\n [int]$MemoryScore,\n [int]$MriScore,\n [string]$MriStatus,\n [int]$CtScore,\n [string]$CtStatus,\n [int]$PetScore,\n [string]$PetStatus,\n [int]$GovernanceScore,\n [string]$RunDir,\n [string]$RepoPath,\n [int]$InventoryFiles,\n [object]$SentruxDsmSummary,\n [object]$SentruxFileDetailsSummary,\n [object]$CodeNexusContextSummary,\n [object]$SentruxWhatIfSummary,\n [string]$GovernanceArtifact,\n [string]$GovernanceFinding\n )\n\n $xrayFinding = if ($InventoryFiles -gt 0) { \"$InventoryFiles files inventoried\" } else { \"no inventory\" }\n $ctArtifact = if ($CtStatus -eq \"available\") { [string]$SentruxDsmSummary.path } else { \"\" }\n $ctFinding = if ($CtStatus -eq \"available\") { \"$($SentruxDsmSummary.modules) modules, $($SentruxFileDetailsSummary.functions) functions\" } else { \"not generated\" }\n $mriArtifact = if ($MriStatus -eq \"available\") { [string]$CodeNexusContextSummary.path } else { \"\" }\n $mriFinding = if ($MriStatus -eq \"available\") { \"$($CodeNexusContextSummary.files) files, $($CodeNexusContextSummary.references) references\" } else { \"not generated\" }\n $petArtifact = if ($PetStatus -eq \"available\") { [string]$SentruxWhatIfSummary.path } else { \"\" }\n $petFinding = if ($PetStatus -eq \"available\") { \"$($SentruxWhatIfSummary.failing) failing what-if scenarios\" } else { \"not generated\" }\n $chartFinding = if ($null -ne $RepowiseStep) { [string]$RepowiseStep.status } else { \"not run\" }\n\n return @(\n (New-Modality \"xray\" \"fast file inventory and repo surface\" $InventoryStep (Get-StepScore $InventoryStep) (Join-Path $RunDir \"files.txt\") $xrayFinding \"Sees files, not semantic impact.\")\n (New-Modality \"anatomy\" \"Understand Anything architecture graph\" $UnderstandStep $GraphScore (Join-Path (Join-Path $RepoPath \".understand-anything\") \"knowledge-graph.json\") (Get-FirstLine ([string]$UnderstandStep.output)) \"Requires a prebuilt graph from the Understand tool.\")\n (New-Modality \"ct\" \"Sentrux DSM, hotspots, and structural slices\" $SentruxGateStep $CtScore $ctArtifact $ctFinding \"Static structure is not runtime truth.\")\n (New-Modality \"mri\" \"CodeNexus context and impact localization\" $null $MriScore $mriArtifact $mriFinding \"Lite mode is local evidence, not a full semantic backend.\")\n (New-Modality \"pet\" \"execution proxy: test gaps, evolution, and what-if risk\" $null $PetScore $petArtifact $petFinding \"No live runtime trace is captured yet.\")\n (New-Modality \"chart\" \"Repowise long-term project memory\" $RepowiseStep $MemoryScore \"\" $chartFinding \"Provider quota and index freshness can limit semantic memory.\")\n (New-Modality \"governance\" \"rules, gate, and session safety rails\" $SentruxCheckStep $GovernanceScore $GovernanceArtifact $GovernanceFinding \"Rules only protect boundaries that have been encoded.\")\n )\n}\n\nfunction New-HospitalQualityDimensions {\n param(\n [int]$SourceCoverageScore,\n [string]$SourceScopeStatus,\n [int]$InventoryFiles,\n [int]$ScanFiles,\n [int]$GraphScore,\n [object]$UnderstandStep,\n [int]$ResolutionScore,\n [string]$ImportResolutionStatus,\n [int]$ResolvedImports,\n [int]$UnresolvedImports,\n [int]$PollutionScore,\n [string]$PollutionStatus,\n [int]$ExcludedFiles,\n [int]$GovernanceScore,\n [string]$GovernanceStatus,\n [string]$GovernanceEvidence,\n [int]$MriScore,\n [string]$LocalizationStatus,\n [string]$TopContextFile,\n [int]$MemoryScore,\n [string]$MemoryStatus,\n [string]$MemoryEvidence\n )\n\n return @(\n (New-QualityDimension \"source_coverage\" $SourceCoverageScore $SourceScopeStatus \"inventory=$InventoryFiles; sentrux_scan=$ScanFiles\")\n (New-QualityDimension \"graph_freshness\" $GraphScore ([string]$UnderstandStep.status) (Get-FirstLine ([string]$UnderstandStep.output)))\n (New-QualityDimension \"import_resolution\" $ResolutionScore $ImportResolutionStatus \"resolved=$ResolvedImports; unresolved=$UnresolvedImports\")\n (New-QualityDimension \"pollution_control\" $PollutionScore $PollutionStatus \"excluded=$ExcludedFiles\")\n (New-QualityDimension \"governance\" $GovernanceScore $GovernanceStatus $GovernanceEvidence)\n (New-QualityDimension \"localization\" $MriScore $LocalizationStatus \"top_file=$TopContextFile\")\n (New-QualityDimension \"memory\" $MemoryScore $MemoryStatus $MemoryEvidence)\n )\n}\n\nfunction Read-HospitalArtifactFile {\n param([object]$Summary)\n\n if ($null -eq $Summary) { return $null }\n\n $path = [string]$Summary.path\n if ([string]::IsNullOrWhiteSpace($path)) { return $null }\n if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }\n\n return Read-JsonFileSafe $path\n}\n\nfunction Read-HospitalArtifacts {\n param(\n [object]$SentruxDsmSummary,\n [object]$SentruxFileDetailsSummary,\n [object]$SentruxHotspotsSummary,\n [object]$SentruxEvolutionSummary,\n [object]$SentruxWhatIfSummary,\n [object]$CodeNexusContextSummary\n )\n\n return [ordered]@{\n dsm = Read-HospitalArtifactFile $SentruxDsmSummary\n file_details = Read-HospitalArtifactFile $SentruxFileDetailsSummary\n hotspots = Read-HospitalArtifactFile $SentruxHotspotsSummary\n evolution = Read-HospitalArtifactFile $SentruxEvolutionSummary\n what_if = Read-HospitalArtifactFile $SentruxWhatIfSummary\n codenexus = Read-HospitalArtifactFile $CodeNexusContextSummary\n }\n}\n\nfunction New-HospitalMeasurements {\n param(\n [object]$InventoryStep,\n [object]$SentruxInsight,\n [object]$DsmObject\n )\n\n $inventoryFiles = 0\n $inventoryMatch = [regex]::Match([string]$InventoryStep.output, \"files=([0-9]+)\")\n if ($inventoryMatch.Success) { $inventoryFiles = [int]$inventoryMatch.Groups[1].Value }\n\n $scan = if ($null -ne $SentruxInsight -and $null -ne $SentruxInsight[\"scan\"]) { $SentruxInsight[\"scan\"] } else { @{} }\n $scanFiles = if ($scan.Contains(\"files\")) { [int]$scan[\"files\"] } else { 0 }\n $unresolvedImports = if ($scan.Contains(\"unresolvedImports\")) { [int]$scan[\"unresolvedImports\"] } else { 0 }\n $resolvedImports = if ($scan.Contains(\"resolvedImports\")) { [int]$scan[\"resolvedImports\"] } else { 0 }\n $totalImports = $resolvedImports + $unresolvedImports\n $resolvedRatio = if ($totalImports -gt 0) { [math]::Round(($resolvedImports * 100.0) / $totalImports, 1) } else { $null }\n $dsmScope = $null\n if ($DsmObject -is [System.Collections.IDictionary] -and $DsmObject.Contains(\"scope\")) {\n $dsmScope = $DsmObject[\"scope\"]\n }\n elseif ($null -ne $DsmObject -and $null -ne $DsmObject.PSObject.Properties[\"scope\"]) {\n $dsmScope = $DsmObject.scope\n }\n\n $excludedFilesValue = $null\n $hasPollutionEvidence = $false\n if ($dsmScope -is [System.Collections.IDictionary] -and $dsmScope.Contains(\"excluded_files\")) {\n $excludedFilesValue = $dsmScope[\"excluded_files\"]\n $hasPollutionEvidence = ($null -ne $excludedFilesValue)\n }\n elseif ($null -ne $dsmScope -and $null -ne $dsmScope.PSObject.Properties[\"excluded_files\"]) {\n $excludedFilesValue = $dsmScope.excluded_files\n $hasPollutionEvidence = ($null -ne $excludedFilesValue)\n }\n\n $excludedFiles = if ($hasPollutionEvidence) { [int]$excludedFilesValue } else { 0 }\n $sourceScopeStatus = if ($inventoryFiles -gt 0 -and $scanFiles -gt 0) { \"measured\" } else { \"unknown\" }\n $pollutionStatus = if (-not $hasPollutionEvidence) { \"unknown\" } elseif ($excludedFiles -gt 0) { \"quarantined\" } else { \"clean\" }\n\n return [ordered]@{\n inventory_files = $inventoryFiles\n scan_files = $scanFiles\n unresolved_imports = $unresolvedImports\n resolved_imports = $resolvedImports\n resolved_ratio = $resolvedRatio\n excluded_files = $excludedFiles\n source_scope_status = $sourceScopeStatus\n pollution_status = $pollutionStatus\n }\n}\n\nfunction Get-ImportResolutionScore {\n param([object]$ResolvedRatio)\n\n if ($null -eq $ResolvedRatio) { return 0 }\n if ($ResolvedRatio -ge 75) { return 100 }\n if ($ResolvedRatio -ge 50) { return 75 }\n if ($ResolvedRatio -ge 25) { return 50 }\n\n return 30\n}\n\nfunction Get-SourceCoverageScore {\n param(\n [int]$ScanFiles,\n [int]$InventoryFiles\n )\n\n if ($ScanFiles -le 0 -or $InventoryFiles -le 0) { return 0 }\n\n return [int][math]::Round([math]::Min(100.0, ($ScanFiles * 100.0) / $InventoryFiles))\n}\n\nfunction New-HospitalScoreBlock {\n param(\n [object]$SentruxInsight,\n [object]$Measurements,\n [object]$UnderstandStep,\n [object]$RepowiseStep,\n [object]$SentruxCheckStep,\n [object]$SentruxGateStep,\n [object]$SentruxDsmObject,\n [object]$SentruxFileDetailsObject,\n [object]$SentruxEvolutionObject,\n [object]$SentruxWhatIfObject,\n [object]$CodeNexusContextObject\n )\n\n $rulesExists = [bool]$SentruxInsight[\"rulesExists\"]\n $rulesScore = if ($rulesExists) { 100 } else { 45 }\n $gateScore = Get-StepScore $SentruxGateStep\n $checkScore = Get-StepScore $SentruxCheckStep\n $graphScore = Get-StepScore $UnderstandStep\n $memoryScore = Get-StepScore $RepowiseStep\n $mriStatus = if ($null -ne $CodeNexusContextObject) { \"available\" } else { \"missing\" }\n $ctStatus = if ($null -ne $SentruxDsmObject -and $null -ne $SentruxFileDetailsObject) { \"available\" } else { \"missing\" }\n $petStatus = if ($null -ne $SentruxWhatIfObject -and $null -ne $SentruxEvolutionObject) { \"available\" } else { \"missing\" }\n $mriScore = if ($mriStatus -eq \"available\") { 100 } else { 0 }\n $ctScore = if ($ctStatus -eq \"available\") { 100 } else { 0 }\n $petScore = if ($petStatus -eq \"available\") { 70 } else { 0 }\n $resolutionScore = Get-ImportResolutionScore $Measurements.resolved_ratio\n $pollutionStatus = [string]$Measurements.pollution_status\n $pollutionScore = if ($pollutionStatus -eq \"unknown\") { 0 } elseif ($Measurements.excluded_files -gt 0) { 100 } else { 80 }\n $governanceScore = [int][math]::Round(($rulesScore + $gateScore + $checkScore) / 3.0)\n $diagnosticScore = [int][math]::Round(($ctScore + $mriScore + $graphScore + $memoryScore) / 4.0)\n $overallScore = [int][math]::Round(($diagnosticScore + $governanceScore + $resolutionScore + $pollutionScore) / 4.0)\n $governanceArtifact = if ($rulesExists) { [string]$SentruxInsight[\"rulesPath\"] } else { \"\" }\n $resolvedRatio = $Measurements.resolved_ratio\n\n return [ordered]@{\n rules_exists = $rulesExists\n gate_status = [string]$SentruxInsight[\"gateStatus\"]\n check_status = [string]$SentruxInsight[\"checkStatus\"]\n graph_score = $graphScore\n memory_score = $memoryScore\n mri_score = $mriScore\n mri_status = $mriStatus\n ct_score = $ctScore\n ct_status = $ctStatus\n pet_score = $petScore\n pet_status = $petStatus\n resolution_score = $resolutionScore\n pollution_score = $pollutionScore\n governance_score = $governanceScore\n diagnostic_score = $diagnosticScore\n overall_score = $overallScore\n source_coverage_score = Get-SourceCoverageScore $Measurements.scan_files $Measurements.inventory_files\n import_resolution_status = if ($null -eq $resolvedRatio) { \"unknown\" } else { \"$resolvedRatio%\" }\n pollution_status = $pollutionStatus\n governance_status = if ($rulesExists) { \"rules_present\" } else { \"rules_missing\" }\n governance_artifact = $governanceArtifact\n governance_finding = \"rules=$($SentruxInsight['rulesExists']); gate=$($SentruxInsight['gateStatus']); check=$($SentruxInsight['checkStatus'])\"\n governance_evidence = \"gate=$($SentruxInsight['gateStatus']); check=$($SentruxInsight['checkStatus'])\"\n localization_status = $mriStatus\n memory_status = if ($null -ne $RepowiseStep) { [string]$RepowiseStep.status } else { \"not_run\" }\n memory_evidence = if ($null -ne $RepowiseStep) { Get-FirstLine ([string]$RepowiseStep.output) } else { \"\" }\n }\n}\n\nfunction New-HospitalEvidenceBlock {\n param(\n [object]$HotspotsObject,\n [object]$WhatIfObject,\n [object]$CodeNexusContextSummary\n )\n\n $failingWhatIf = @()\n if ($null -ne $WhatIfObject -and $null -ne $WhatIfObject.scenarios) {\n $failingWhatIf = @($WhatIfObject.scenarios | Where-Object { -not $_.pass })\n }\n\n $topFunction = \"\"\n if ($null -ne $HotspotsObject -and $null -ne $HotspotsObject.functions -and @($HotspotsObject.functions).Count -gt 0) {\n $topFunction = \"{0} in {1} (cc={2})\" -f $HotspotsObject.functions[0].name, $HotspotsObject.functions[0].file, $HotspotsObject.functions[0].complexity\n }\n\n $topModule = \"\"\n if ($null -ne $HotspotsObject -and $null -ne $HotspotsObject.modules -and @($HotspotsObject.modules).Count -gt 0) {\n $topModule = \"{0} (risk={1})\" -f $HotspotsObject.modules[0].name, $HotspotsObject.modules[0].risk\n }\n\n return [ordered]@{\n failing_what_if = $failingWhatIf\n top_function = $topFunction\n top_module = $topModule\n top_context_file = if ($null -ne $CodeNexusContextSummary) { [string]$CodeNexusContextSummary.topFile } else { \"\" }\n }\n}\n\nfunction New-HospitalProtocolBlock {\n param(\n [bool]$RulesExists,\n [int]$FailingWhatIfCount\n )\n\n $governProtocolStatus = if ($RulesExists) { \"active\" } else { \"needs_rules\" }\n $surgeryProtocolStatus = if ($FailingWhatIfCount -gt 0) { \"available\" } else { \"low_risk\" }\n\n return @(\n (New-HospitalProtocol \"triage\" \"available\" \"run-code-intel.ps1 -RepoPath -Mode lite\" \"Classify provider/tool/graph/Sentrux failure bucket and choose next protocol.\")\n (New-HospitalProtocol \"diagnose\" \"available\" \"run-code-intel.ps1 -RepoPath -Mode normal\" \"Produce summary.md, hospital.md, sentrux artifacts, and codenexus context.\")\n (New-HospitalProtocol \"govern\" $governProtocolStatus \"sentrux check ; sentrux gate \" \"Rules pass and gate reports no degradation.\")\n (New-HospitalProtocol \"surgery_plan\" $surgeryProtocolStatus \"read sentrux-what-if.json and codenexus-context.json\" \"Choose one hotspot, one boundary, and one verification command before editing.\")\n (New-HospitalProtocol \"post_op\" \"available\" \"Invoke-SentruxAgentTool.ps1 session_end \" \"Signal does not drop, rules pass, and touched hotspot is lower risk.\")\n )\n}\n\nfunction Get-PreviousSurgeryTarget {\n param([string]$RunDir)\n\n if ([string]::IsNullOrWhiteSpace($RunDir)) { return \"\" }\n $repoArtifactRoot = Split-Path -Parent $RunDir\n if ([string]::IsNullOrWhiteSpace($repoArtifactRoot) -or -not (Test-Path -LiteralPath $repoArtifactRoot -PathType Container)) { return \"\" }\n\n $currentName = Split-Path -Leaf $RunDir\n $previousRun = Get-ChildItem -LiteralPath $repoArtifactRoot -Directory -ErrorAction SilentlyContinue |\n Where-Object { $_.Name -ne $currentName } |\n Sort-Object Name -Descending |\n Select-Object -First 1\n if ($null -eq $previousRun) { return \"\" }\n\n $previousPlanPath = Join-Path $previousRun.FullName \"surgery-plan.json\"\n if (-not (Test-Path -LiteralPath $previousPlanPath -PathType Leaf)) { return \"\" }\n\n $previousPlan = Read-JsonFileSafe $previousPlanPath\n if ($null -eq $previousPlan -or $null -eq $previousPlan.primary_target) { return \"\" }\n if ([string]::IsNullOrWhiteSpace([string]$previousPlan.primary_target.name)) { return \"\" }\n\n return \"$($previousPlan.primary_target.name) in $($previousPlan.primary_target.file)\"\n}\n\nfunction New-CodeIntelHospitalReport {\n param(\n [string]$RepoPath,\n [string]$Mode,\n [string]$RunDir,\n [string]$ReportPath,\n [string]$SummaryPath,\n [string]$UnderstandingPath,\n [object[]]$Steps,\n [object]$FailureCounts,\n [object]$SentruxInsight,\n [object]$SentruxDsmSummary,\n [object]$SentruxFileDetailsSummary,\n [object]$SentruxHotspotsSummary,\n[object]$SentruxEvolutionSummary,\n[object]$SentruxWhatIfSummary,\n[object]$CodeNexusContextSummary,\n[string]$UnderstandCommand,\n[object]$ToolState,\n[object]$GitHubResearch\n)\n\n $gitStep = Get-StepMatch $Steps \"git status\"\n $inventoryStep = Get-StepMatch $Steps \"rg file inventory\"\n $understandStep = Get-StepMatch $Steps \"understand graph\"\n $repowiseStep = Get-StepMatch $Steps \"repowise*\" -Last\n $sentruxCheckStep = Get-StepMatch $Steps \"sentrux check\"\n $sentruxGateStep = Get-StepMatch $Steps \"sentrux gate*\" -Last\n\n $artifacts = Read-HospitalArtifacts $SentruxDsmSummary $SentruxFileDetailsSummary $SentruxHotspotsSummary $SentruxEvolutionSummary $SentruxWhatIfSummary $CodeNexusContextSummary\n $structuralEvidenceComplete = ($null -ne $artifacts.dsm -and\n $null -ne $artifacts.file_details -and\n $null -ne $artifacts.hotspots -and\n $null -ne $artifacts.evolution -and\n $null -ne $artifacts.what_if)\n $measurements = New-HospitalMeasurements $inventoryStep $SentruxInsight $artifacts.dsm\n $scores = New-HospitalScoreBlock `\n -SentruxInsight $SentruxInsight `\n -Measurements $measurements `\n -UnderstandStep $understandStep `\n -RepowiseStep $repowiseStep `\n -SentruxCheckStep $sentruxCheckStep `\n -SentruxGateStep $sentruxGateStep `\n -SentruxDsmObject $artifacts.dsm `\n -SentruxFileDetailsObject $artifacts.file_details `\n -SentruxEvolutionObject $artifacts.evolution `\n -SentruxWhatIfObject $artifacts.what_if `\n -CodeNexusContextObject $artifacts.codenexus\n $evidence = New-HospitalEvidenceBlock $artifacts.hotspots $artifacts.what_if $CodeNexusContextSummary\n\n $currentTopHotspot = \"\"\n if ($null -ne $artifacts.hotspots -and $null -ne $artifacts.hotspots.functions -and @($artifacts.hotspots.functions).Count -gt 0) {\n $topFn = $artifacts.hotspots.functions[0]\n $currentTopHotspot = \"$($topFn.name) in $($topFn.file)\"\n }\n $surgeryTarget = Get-PreviousSurgeryTarget $RunDir\n\n $decision = New-HospitalDecisionBlock `\n -FailureCounts $FailureCounts `\n -RulesExists $scores.rules_exists `\n -GateStatus $scores.gate_status `\n -CheckStatus $scores.check_status `\n -FailingWhatIfCount @($evidence.failing_what_if).Count `\n -UnderstandCommand $UnderstandCommand `\n -TopContextFile $evidence.top_context_file `\n -StructuralEvidenceComplete $structuralEvidenceComplete `\n -SurgeryTarget $surgeryTarget `\n -CurrentTopHotspot $currentTopHotspot `\n -GitHubResearch $GitHubResearch\n\n $findings = New-HospitalFindings `\n -InventoryFiles $measurements.inventory_files `\n -SentruxFileDetailsSummary $SentruxFileDetailsSummary `\n -TopFunction $evidence.top_function `\n -TopModule $evidence.top_module `\n -ResolvedRatio $measurements.resolved_ratio `\n -ResolvedImports $measurements.resolved_imports `\n -UnresolvedImports $measurements.unresolved_imports `\n -ExcludedFiles $measurements.excluded_files\n\n $modalities = New-HospitalModalities `\n -InventoryStep $inventoryStep `\n -UnderstandStep $understandStep `\n -RepowiseStep $repowiseStep `\n -SentruxCheckStep $sentruxCheckStep `\n -SentruxGateStep $sentruxGateStep `\n -GraphScore $scores.graph_score `\n -MemoryScore $scores.memory_score `\n -MriScore $scores.mri_score `\n -MriStatus $scores.mri_status `\n -CtScore $scores.ct_score `\n -CtStatus $scores.ct_status `\n -PetScore $scores.pet_score `\n -PetStatus $scores.pet_status `\n -GovernanceScore $scores.governance_score `\n -RunDir $RunDir `\n -RepoPath $RepoPath `\n -InventoryFiles $measurements.inventory_files `\n -SentruxDsmSummary $SentruxDsmSummary `\n -SentruxFileDetailsSummary $SentruxFileDetailsSummary `\n -CodeNexusContextSummary $CodeNexusContextSummary `\n -SentruxWhatIfSummary $SentruxWhatIfSummary `\n -GovernanceArtifact $scores.governance_artifact `\n -GovernanceFinding $scores.governance_finding\n\n $quality = New-HospitalQualityDimensions `\n -SourceCoverageScore $scores.source_coverage_score `\n -SourceScopeStatus $measurements.source_scope_status `\n -InventoryFiles $measurements.inventory_files `\n -ScanFiles $measurements.scan_files `\n -GraphScore $scores.graph_score `\n -UnderstandStep $understandStep `\n -ResolutionScore $scores.resolution_score `\n -ImportResolutionStatus $scores.import_resolution_status `\n -ResolvedImports $measurements.resolved_imports `\n -UnresolvedImports $measurements.unresolved_imports `\n -PollutionScore $scores.pollution_score `\n -PollutionStatus $scores.pollution_status `\n -ExcludedFiles $measurements.excluded_files `\n -GovernanceScore $scores.governance_score `\n -GovernanceStatus $scores.governance_status `\n -GovernanceEvidence $scores.governance_evidence `\n -MriScore $scores.mri_score `\n -LocalizationStatus $scores.localization_status `\n -TopContextFile $evidence.top_context_file `\n -MemoryScore $scores.memory_score `\n -MemoryStatus $scores.memory_status `\n -MemoryEvidence $scores.memory_evidence\n\n $protocols = New-HospitalProtocolBlock $scores.rules_exists @($evidence.failing_what_if).Count\n\n return [ordered]@{\n schema = \"code-intel-hospital.v1\"\n generatedAt = (Get-Date).ToString(\"o\")\n repo = $RepoPath\n mode = $Mode\n artifacts = [ordered]@{\n runDir = $RunDir\n report = $ReportPath\n summary = $SummaryPath\n understanding = $UnderstandingPath\n github_solution_research = if ($null -ne $GitHubResearch) { [string]$GitHubResearch.path } else { \"\" }\n github_solution_research_markdown = if ($null -ne $GitHubResearch) { [string]$GitHubResearch.markdown } else { \"\" }\n }\n triage = [ordered]@{\n status = $decision.severity\n disposition = $decision.disposition\n primary_diagnosis = $decision.primaryDiagnosis\n overall_score = $scores.overall_score\n next_protocol = $decision.nextProtocol\n research_status = if ($null -ne $GitHubResearch) { [string]$GitHubResearch.status } else { \"not_applicable\" }\n research_required = if ($null -ne $GitHubResearch) { [bool]$GitHubResearch.required } else { $false }\n exit_criteria = if ($null -ne $GitHubResearch) { @($GitHubResearch.exitCriteria) } else { @() }\n admission_reason = $decision.admissionReason\n discharge_criteria = $decision.dischargeCriteria\n }\n state_machine = $decision.stateMachine\n modalities = $modalities\n policies = [ordered]@{\n admission = [ordered]@{\n admit_when = @(\n \"local toolchain fails\",\n \"architecture graph is missing\",\n \"Sentrux rules are missing\",\n \"Sentrux check or gate fails\",\n \"what-if reports planned modernization debt\"\n )\n current_reason = $decision.admissionReason\n }\n discharge = [ordered]@{\n criteria = $decision.dischargeCriteria\n current_state = $decision.stateMachine.current_state\n }\n }\n report_quality = [ordered]@{\n overall_score = $scores.overall_score\n diagnostic_score = $scores.diagnostic_score\n governance_score = $scores.governance_score\n dimensions = $quality\n }\n diagnosis = [ordered]@{\n findings = $findings\n impression = $decision.primaryDiagnosis\n risk = $decision.severity\n evidence = [ordered]@{\n top_function = $evidence.top_function\n top_module = $evidence.top_module\n top_context_file = $evidence.top_context_file\n failing_what_if = @($evidence.failing_what_if | Select-Object -First 5)\n }\n }\n treatment = [ordered]@{\n plan = $decision.treatment\n follow_up = @(\n \"Rerun normal mode after code changes.\",\n \"Compare hospital-report.json overall_score and Sentrux quality signal.\",\n \"Use session_start/session_end around Agent edits.\"\n )\n }\n protocols = $protocols\n tools = $ToolState\n }\n}\n\nfunction Convert-HospitalReportToMarkdown {\n param([object]$Hospital)\n\n $lines = @(\n \"# Code Intel Hospital Report\",\n \"\",\n \"- Repo: $($Hospital.repo)\",\n \"- Mode: $($Hospital.mode)\",\n \"- Status: $($Hospital.triage.status)\",\n \"- Disposition: $($Hospital.triage.disposition)\",\n \"- Primary diagnosis: $($Hospital.triage.primary_diagnosis)\",\n \"- Admission reason: $($Hospital.triage.admission_reason)\",\n\"- Overall score: $($Hospital.triage.overall_score)\",\n\"- Next protocol: $($Hospital.triage.next_protocol)\",\n\"- Research status: $($Hospital.triage.research_status)\",\n\"- Research required: $($Hospital.triage.research_required)\",\n\"- Current state: $($Hospital.state_machine.current_state)\",\n\"\",\n\"## Imaging Modalities\"\n)\nif ($null -ne $Hospital.triage.exit_criteria -and @($Hospital.triage.exit_criteria).Count -gt 0) {\n $lines += \"\"\n $lines += \"## Exit Criteria\"\n foreach ($criterion in @($Hospital.triage.exit_criteria)) {\n $lines += \"- $criterion\"\n }\n}\nforeach ($item in @($Hospital.modalities)) {\n $lines += \"- $($item.name): $($item.status), confidence=$($item.confidence), finding=$($item.finding)\"\n }\n $lines += \"\"\n $lines += \"## Report Quality\"\n foreach ($dimension in @($Hospital.report_quality.dimensions)) {\n $lines += \"- $($dimension.name): $($dimension.score) ($($dimension.status)) - $($dimension.evidence)\"\n }\n $lines += \"\"\n $lines += \"## Diagnosis\"\n foreach ($finding in @($Hospital.diagnosis.findings)) {\n $lines += \"- $finding\"\n }\n $lines += \"\"\n $lines += \"## Treatment\"\n foreach ($item in @($Hospital.treatment.plan)) {\n $lines += \"- $item\"\n }\n if ($null -ne $Hospital.surgery_plan) {\n $lines += \"\"\n $lines += \"## Surgery Plan\"\n $lines += \"- Status: $($Hospital.surgery_plan.status)\"\n $lines += \"- Report: $($Hospital.surgery_plan.path)\"\n $lines += \"- Markdown: $($Hospital.surgery_plan.markdown)\"\n $lines += \"- Primary target: $($Hospital.surgery_plan.primary_target)\"\n }\n $lines += \"\"\n $lines += \"## Discharge Criteria\"\n foreach ($item in @($Hospital.triage.discharge_criteria)) {\n $lines += \"- $item\"\n }\n $lines += \"\"\n $lines += \"## State Machine\"\n foreach ($transition in @($Hospital.state_machine.transitions)) {\n $lines += \"- $($transition.from) -> $($transition.to): pass=$($transition.pass), guard=$($transition.guard)\"\n }\n $lines += \"\"\n $lines += \"## Protocols\"\n foreach ($protocol in @($Hospital.protocols)) {\n $lines += \"- $($protocol.name): $($protocol.status) - $($protocol.exit_criteria)\"\n }\n return $lines\n}\n\nfunction Get-CodeIntelSentruxStep {\n param(\n [object[]]$Steps,\n [string]$NamePattern,\n [switch]$Last\n )\n\n $matches = @($Steps | Where-Object { [string]$_.name -like $NamePattern })\n if ($matches.Count -eq 0) { return $null }\n if ($Last) { return $matches[-1] }\n return $matches[0]\n}\n\nfunction Get-CodeIntelBoundedExcerpt {\n param(\n [string]$Text,\n [int]$MaxLength = 500\n )\n\n if ([string]::IsNullOrWhiteSpace($Text)) { return \"\" }\n $singleLine = (($Text -split \"`r?`n\") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 8) -join \" | \"\n if ($singleLine.Length -le $MaxLength) { return $singleLine }\n return $singleLine.Substring(0, $MaxLength)\n}\n\nfunction New-CodeIntelSentruxTarget {\n param(\n [ValidateSet(\"resolved\", \"unresolved\", \"aggregate\", \"not_applicable\")]\n [string]$Status,\n [string]$File = \"\",\n [string]$Symbol = \"\"\n )\n\n $target = [ordered]@{ status = $Status }\n if (-not [string]::IsNullOrWhiteSpace($File)) { $target[\"file\"] = $File }\n if (-not [string]::IsNullOrWhiteSpace($Symbol)) { $target[\"symbol\"] = $Symbol }\n return $target\n}\n\nfunction New-CodeIntelSentruxRecord {\n param(\n [string]$Id,\n [string]$Kind,\n [string]$Source,\n [string]$SourceStep,\n [string]$RawOutputPath,\n [string]$Stdout,\n [object]$Target,\n [string]$Metric = \"\",\n [Nullable[int]]$Value = $null,\n [Nullable[int]]$Threshold = $null,\n [Nullable[int]]$Before = $null,\n [Nullable[int]]$After = $null\n )\n\n $record = [ordered]@{\n id = $Id\n kind = $Kind\n source = $Source\n source_step = $SourceStep\n provenance = \"stdout\"\n raw_output_path = $RawOutputPath\n stdout_excerpt = Get-CodeIntelBoundedExcerpt $Stdout\n parsed_at = (Get-Date).ToString(\"o\")\n target = $Target\n }\n if (-not [string]::IsNullOrWhiteSpace($Metric)) { $record[\"metric\"] = $Metric }\n if ($null -ne $Value) { $record[\"value\"] = [int]$Value }\n if ($null -ne $Threshold) { $record[\"threshold\"] = [int]$Threshold }\n if ($null -ne $Before) { $record[\"before\"] = [int]$Before }\n if ($null -ne $After) { $record[\"after\"] = [int]$After }\n return $record\n}\n\nfunction Get-CodeIntelObjectValue {\n param(\n [object]$Object,\n [string]$Name\n )\n\n if ($null -eq $Object) { return $null }\n if ($Object -is [System.Collections.IDictionary] -and $Object.Contains($Name)) {\n return $Object[$Name]\n }\n return Get-JsonProperty $Object $Name\n}\n\nfunction New-CodeIntelSentruxConflict {\n param(\n [object]$Authoritative,\n [object]$Conflicting,\n [string]$ConflictingSource,\n [string]$RawPointer\n )\n\n if ($null -eq $Authoritative -or $null -eq $Conflicting) { return $null }\n $authoritativeValue = ConvertTo-NullableDouble (Get-CodeIntelObjectValue $Authoritative \"value\")\n $conflictingValue = ConvertTo-NullableDouble (Get-CodeIntelObjectValue $Conflicting \"complexity\")\n if ($null -eq $authoritativeValue -or $null -eq $conflictingValue) { return $null }\n if ([int]$authoritativeValue -eq [int]$conflictingValue) { return $null }\n\n $conflictingId = \"{0}:max_cc:{1}:{2}\" -f $ConflictingSource, [string](Get-CodeIntelObjectValue $Conflicting \"file\"), [string](Get-CodeIntelObjectValue $Conflicting \"name\")\n return [ordered]@{\n kind = \"metric_conflict\"\n authoritative_record_id = [string](Get-CodeIntelObjectValue $Authoritative \"id\")\n conflicting_record_id = $conflictingId\n metric = \"cyclomatic_complexity\"\n authoritative_value = [int]$authoritativeValue\n conflicting_value = [int]$conflictingValue\n authoritative_source = [string](Get-CodeIntelObjectValue $Authoritative \"source\")\n conflicting_source = $ConflictingSource\n raw_output_path = $RawPointer\n stdout_excerpt = Get-CodeIntelBoundedExcerpt (\"{0} {1} (cc={2})\" -f [string](Get-CodeIntelObjectValue $Conflicting \"name\"), [string](Get-CodeIntelObjectValue $Conflicting \"file\"), [string](Get-CodeIntelObjectValue $Conflicting \"complexity\"))\n parsed_at = (Get-Date).ToString(\"o\")\n resolution = \"authoritative_stdout_wins\"\n }\n}\n\nfunction New-CodeIntelSentruxFailures {\n param(\n [object[]]$Steps,\n [string]$OutputPath = \"\",\n [string]$HotspotsPath = \"\",\n [string]$FileDetailsPath = \"\"\n )\n\n $checkStep = Get-CodeIntelSentruxStep -Steps $Steps -NamePattern \"sentrux check\"\n $gateStep = Get-CodeIntelSentruxStep -Steps $Steps -NamePattern \"sentrux gate*\" -Last\n $records = [System.Collections.Generic.List[object]]::new()\n $parserNotes = [System.Collections.Generic.List[string]]::new()\n $parserErrors = [System.Collections.Generic.List[string]]::new()\n\n if ($null -ne $checkStep) {\n $checkStatus = [string]$checkStep.status\n $checkText = (([string]$checkStep.output) + \"`n\" + ([string]$checkStep.error)).Trim()\n if ($checkStatus -eq \"failed\" -or $checkStatus -eq \"manual_required\") {\n $namedMatches = @([regex]::Matches($checkText, \"(?im)(?[^\\s:()]+(?:\\.ps1|\\.psm1|\\.ts|\\.tsx|\\.js|\\.jsx|\\.py|\\.rs|\\.go|\\.cs|\\.java|\\.kt|\\.v)):(?[A-Za-z_][A-Za-z0-9_.:-]*)\\s*\\(cc=(?\\d+)\\)\"))\n if ($namedMatches.Count -gt 0) {\n foreach ($match in $namedMatches) {\n $file = [string]$match.Groups[\"file\"].Value\n $symbol = [string]$match.Groups[\"symbol\"].Value\n $value = [int]$match.Groups[\"cc\"].Value\n $records.Add((New-CodeIntelSentruxRecord `\n -Id (\"check:max_cc:{0}:{1}\" -f $file, $symbol) `\n -Kind \"max_cc\" `\n -Source \"sentrux check\" `\n -SourceStep \"sentrux check\" `\n -RawOutputPath \"report.json#/steps/sentrux check/output\" `\n -Stdout $checkText `\n -Metric \"cyclomatic_complexity\" `\n -Value $value `\n -Threshold 70 `\n -Target (New-CodeIntelSentruxTarget -Status \"resolved\" -File $file -Symbol $symbol)))\n }\n }\n elseif ($checkText -match \"(?i)max[_ -]?cc|cyclomatic|complex\") {\n $value = $null\n $valueMatch = [regex]::Match($checkText, \"(?i)(?:max[_ -]?cc|cc|cyclomatic[^0-9]*)(?:\\D+)(?\\d+)\")\n if ($valueMatch.Success) { $value = [int]$valueMatch.Groups[\"cc\"].Value }\n $records.Add((New-CodeIntelSentruxRecord `\n -Id \"check:max_cc:unresolved\" `\n -Kind \"max_cc\" `\n -Source \"sentrux check\" `\n -SourceStep \"sentrux check\" `\n -RawOutputPath \"report.json#/steps/sentrux check/output\" `\n -Stdout $checkText `\n -Metric \"cyclomatic_complexity\" `\n -Value $value `\n -Threshold 70 `\n -Target (New-CodeIntelSentruxTarget -Status \"unresolved\")))\n }\n else {\n $parserErrors.Add(\"sentrux check failed but stdout did not match known max_cc formats.\")\n }\n }\n }\n\n if ($null -ne $gateStep) {\n $gateStatus = [string]$gateStep.status\n $gateText = (([string]$gateStep.output) + \"`n\" + ([string]$gateStep.error)).Trim()\n if ($gateStatus -eq \"failed\" -or $gateStatus -eq \"manual_required\") {\n $gateMatches = @([regex]::Matches($gateText, \"(?im)(?