CC: prepare generic VISION package candidate

This commit is contained in:
Svrnty
2026-06-06 08:25:14 -04:00
parent 8045f46b06
commit d62c5eb744
23 changed files with 1406 additions and 9 deletions
+37
View File
@@ -0,0 +1,37 @@
# Svrnty Vision Context
`svrnty-vision` is a child-local Cortex OS VISION package candidate for visual
perception and visual production tools. It is not Core authority, not Seed
installed, not Runtime, not Profile Exposure, and not provider admission.
## Terms
Visual Perception Package Candidate
: A child-local candidate package that owns pixel/media perception surfaces under
canonical sense `VISION`.
VISION Sense Family
: The canonical sense family for seeing. It can include textual/source reading
through `/research` and pixel/media perception through `/vision`.
Visual Evidence
: A normalized output record produced by a VISION tool. It discloses package,
tool, source, provider mode, retention, observed content, extracted claims,
confidence, caveats, timestamp, and validation status.
Vision Tool Candidate
: A granular tool inside the visual perception package. Granting the package does
not grant every tool.
Current Route Adapter
: An existing HTTP route that can be mapped to a future Cortex OS tool id without
changing the implementation stack.
Research Handoff
: Research may cite or synthesize Visual Evidence that `/vision` already
produced, but `/vision` does not perform research synthesis, web search, page
fetch, PDF extraction, or deep research workflows.
BTE Compatibility Surface
: Existing route names and BTE-shaped behavior stay usable while the package
candidate is documented for Cortex OS.
+14 -5
View File
@@ -31,12 +31,21 @@ libraries — Pillow/colorthief and rembg respectively. They land in 4b.
## Status
**Phase 4a (this commit):** scaffold only. All four endpoints return
HTTP 501 Not Implemented. `/healthz` returns 200.
Current implementation: all four listed endpoints exist. `/vlm/analyze` proxies
to Qwen3-VL through the configured VLM endpoint, `/flux/render` proxies to
ComfyUI FLUX, `/palette/extract` runs in process, `/rembg/cutout` runs in
process, and `/healthz` returns 200.
Phase 4b will port the real logic. Phase 4c deletes the corresponding
.NET code from BTE. Phase 4d wires BTE to call svrnty-vision over HTTP.
See `/home/svrnty/workspaces/hermes/sot/01-ROADMAP/BTE-REFACTOR-EXECUTION-PLAN.md`.
## Cortex OS Package Candidate
`svrnty-vision` is also documented as a child-local Visual Perception Package
Candidate under canonical sense `VISION`. Existing BTE-shaped HTTP routes remain
current route adapters; exact Cortex OS candidate tool ids, grant rules, host
adapter candidates, and Visual Evidence contract live in `docs/` and
`candidate-manifests/`.
This does not grant Core promotion, Seed installation, Runtime startup, Profile
Exposure, provider admission, or wildcard tool access.
## Run
+10
View File
@@ -4,3 +4,13 @@ items:
status: candidate
source: README.md
owner: jp
- id: SVRNTY-VISION-WORK-002
title: Generic VISION Package Candidate And Visual Evidence Proof
status: complete
source: outputs/2026-06-06-svrnty-vision-work-002-package-candidate-validation.md
owner: jp
- id: SVRNTY-VISION-WORK-003
title: Vision Package Candidate Sandcastle
status: ready-for-agent
source: outputs/sandcastle-vision-package-candidate/README.md
owner: jp
@@ -0,0 +1,60 @@
{
"schema_version": "vision.host_adapter_candidate.v1",
"adapter_id": "vision-claude-code-adapter",
"authority": "child-local",
"status": "candidate_only_not_seed_installed",
"host_runtime": "claude-code",
"host_runtime_class": "claude-code",
"package_id": "visual-perception-package-candidate",
"adapter_role": "thin_access_adapter",
"seed_artifact_target": "host-adapters/vision-claude-code-adapter.json",
"wildcard_grant_allowed": false,
"current_tool_candidates": [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout"
],
"planned_tool_candidates": [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit"
],
"disclosure_contract": {
"provider_mode_required": true,
"retention_required": true,
"visual_evidence_required": true
},
"allowed_effects": [
"analyze_image_input",
"generate_image_output",
"extract_palette",
"remove_background",
"return_visual_evidence"
],
"forbidden_effects": [
"research_synthesis",
"textual_web_search",
"textual_page_fetch",
"profile_exposure",
"runtime_start",
"provider_admission",
"wildcard_tool_exposure"
],
"non_authorization": {
"core_promotion": false,
"seed_installation": false,
"runtime_start": false,
"profile_exposure": false,
"provider_admission": false
}
}
@@ -0,0 +1,60 @@
{
"schema_version": "vision.host_adapter_candidate.v1",
"adapter_id": "vision-codex-cli-adapter",
"authority": "child-local",
"status": "candidate_only_not_seed_installed",
"host_runtime": "codex-cli",
"host_runtime_class": "codex-cli",
"package_id": "visual-perception-package-candidate",
"adapter_role": "thin_access_adapter",
"seed_artifact_target": "host-adapters/vision-codex-cli-adapter.json",
"wildcard_grant_allowed": false,
"current_tool_candidates": [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout"
],
"planned_tool_candidates": [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit"
],
"disclosure_contract": {
"provider_mode_required": true,
"retention_required": true,
"visual_evidence_required": true
},
"allowed_effects": [
"analyze_image_input",
"generate_image_output",
"extract_palette",
"remove_background",
"return_visual_evidence"
],
"forbidden_effects": [
"research_synthesis",
"textual_web_search",
"textual_page_fetch",
"profile_exposure",
"runtime_start",
"provider_admission",
"wildcard_tool_exposure"
],
"non_authorization": {
"core_promotion": false,
"seed_installation": false,
"runtime_start": false,
"profile_exposure": false,
"provider_admission": false
}
}
@@ -0,0 +1,60 @@
{
"schema_version": "vision.host_adapter_candidate.v1",
"adapter_id": "vision-pi-code-adapter",
"authority": "child-local",
"status": "candidate_only_not_seed_installed",
"host_runtime": "pi-code",
"host_runtime_class": "pi-code",
"package_id": "visual-perception-package-candidate",
"adapter_role": "thin_access_adapter",
"seed_artifact_target": "host-adapters/vision-pi-code-adapter.json",
"wildcard_grant_allowed": false,
"current_tool_candidates": [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout"
],
"planned_tool_candidates": [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit"
],
"disclosure_contract": {
"provider_mode_required": true,
"retention_required": true,
"visual_evidence_required": true
},
"allowed_effects": [
"analyze_image_input",
"generate_image_output",
"extract_palette",
"remove_background",
"return_visual_evidence"
],
"forbidden_effects": [
"research_synthesis",
"textual_web_search",
"textual_page_fetch",
"profile_exposure",
"runtime_start",
"provider_admission",
"wildcard_tool_exposure"
],
"non_authorization": {
"core_promotion": false,
"seed_installation": false,
"runtime_start": false,
"profile_exposure": false,
"provider_admission": false
}
}
@@ -0,0 +1,84 @@
{
"schema_version": "vision.package_candidate.v1",
"package_id": "visual-perception-package-candidate",
"workspace": "svrnty-vision",
"authority": "child-local",
"status": "candidate_only_not_seed_installed",
"canonical_sense": "VISION",
"package_role": "generic_visual_perception_and_production",
"current_route_adapters": [
{
"route": "POST /vlm/analyze",
"tool_id": "vision.image_analyze",
"capability": "vlm_image_analysis"
},
{
"route": "POST /flux/render",
"tool_id": "vision.image_generate",
"capability": "image_generation"
},
{
"route": "POST /palette/extract",
"tool_id": "vision.palette_extract",
"capability": "palette_extraction"
},
{
"route": "POST /rembg/cutout",
"tool_id": "vision.background_cutout",
"capability": "background_cutout"
}
],
"current_tool_candidates": [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout"
],
"planned_tool_candidates": [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit"
],
"owned_capability_classes": [
"pixel_media_perception",
"visual_evidence_production",
"image_generation",
"image_editing",
"screenshot_and_browser_visual_observation",
"layout_chart_table_diagram_visual_reading"
],
"forbidden_research_owned_capabilities": [
"web_search",
"fetch_page",
"extract_pdf",
"deep_research",
"research_synthesis",
"capsule_writing"
],
"research_handoff": {
"may_produce_visual_evidence_for_research": true,
"may_perform_research_synthesis": false,
"handoff_contract": "candidate-manifests/visual-evidence-contract.json"
},
"grant_policy": {
"wildcard_grant_allowed": false,
"package_default_grants_all_tools": false,
"tool_grants_are_granular": true
},
"non_authorization": {
"core_promotion": false,
"seed_installation": false,
"runtime_start": false,
"profile_exposure": false,
"provider_admission": false
}
}
@@ -0,0 +1,69 @@
{
"schema_version": "vision.tool_grants.v1",
"package_id": "visual-perception-package-candidate",
"authority": "child-local",
"status": "candidate_only_not_seed_installed",
"canonical_sense": "VISION",
"wildcard_grant_allowed": false,
"package_default_grants_all_tools": false,
"current_tool_candidates": [
{
"tool_id": "vision.image_analyze",
"route": "POST /vlm/analyze",
"grant_default": false,
"requires_visual_evidence": true
},
{
"tool_id": "vision.image_generate",
"route": "POST /flux/render",
"grant_default": false,
"requires_visual_evidence": false
},
{
"tool_id": "vision.palette_extract",
"route": "POST /palette/extract",
"grant_default": false,
"requires_visual_evidence": true
},
{
"tool_id": "vision.background_cutout",
"route": "POST /rembg/cutout",
"grant_default": false,
"requires_visual_evidence": true
}
],
"planned_tool_candidates": [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit"
],
"required_disclosures": {
"producing_package_id": true,
"producing_tool_id": true,
"capability_surface": true,
"provider_mode": true,
"retention_disclosure": true,
"validation_status": true
},
"forbidden_effects": [
"research_synthesis",
"textual_web_search",
"textual_page_fetch",
"pdf_text_extraction",
"deep_research_workflow",
"capsule_writing",
"runtime_start",
"profile_exposure",
"provider_admission",
"wildcard_tool_exposure"
]
}
@@ -0,0 +1,37 @@
{
"schema_version": "vision.visual_evidence_contract.v1",
"package_id": "visual-perception-package-candidate",
"authority": "child-local",
"status": "candidate_only_not_seed_installed",
"required_fields": [
"producing_package_id",
"producing_tool_id",
"capability_surface",
"source_reference",
"provider_mode",
"retention_disclosure",
"observed_content_summary",
"extracted_claims",
"confidence",
"caveats",
"timestamp",
"validation_status"
],
"first_vertical_proof": {
"source_route": "POST /vlm/analyze",
"source_tool_id": "vision.image_analyze",
"module": "src/svrnty_vision/visual_evidence.py",
"test": "tests/test_visual_evidence.py",
"live_provider_call_required": false
},
"research_handoff": {
"research_may_consume_visual_evidence": true,
"vision_may_write_research_capsules": false,
"vision_may_perform_research_synthesis": false
},
"disclosure_contract": {
"provider_mode_required": true,
"retention_required": true,
"validation_status_required": true
}
}
+29
View File
@@ -0,0 +1,29 @@
# VISION Host Adapter Candidates
Status: candidate only. These adapter manifests do not install into Seed and do
not grant host access.
## Intent
Claude Code, Codex CLI, and Pi-Code should expose the same VISION package
capabilities when Seed accepts the package. Each host adapter is thin: it maps
host-specific command shape to the same package tool ids and the same disclosure
contract.
## Host Targets
- `claude-code`
- `codex-cli`
- `pi-code`
## Parity Rule
Each host adapter candidate must expose the same current tool ids, the same
planned tool ids, no wildcard grant, and the same Visual Evidence disclosure
contract.
## Candidate Manifests
- `candidate-manifests/host-adapters/vision-claude-code-adapter.json`
- `candidate-manifests/host-adapters/vision-codex-cli-adapter.json`
- `candidate-manifests/host-adapters/vision-pi-code-adapter.json`
+70
View File
@@ -0,0 +1,70 @@
# VISION Package Candidate
Status: child-local candidate only. No Core promotion, Seed installation,
Runtime start, Profile Exposure, or provider admission is authorized. No
wildcard grant is authorized by this document.
## Intent
`svrnty-vision` is the generic visual-perception package candidate for the
canonical Cortex OS sense `VISION`. It owns tools that inspect or produce pixels,
images, screenshots, browser observations, layouts, charts, diagrams, grounded
regions, segmentations, video frames, or generated/edited images.
`research` is also under the `VISION` sense family, but it owns textual/source
reading and research workflows. The boundary is by capability, not by sense name:
Research reads sources; Vision sees media.
## Current Route Adapters
| Current route | Candidate tool id | Capability |
| --- | --- | --- |
| `POST /vlm/analyze` | `vision.image_analyze` | Analyze image input with a VLM and return a normalized observation. |
| `POST /flux/render` | `vision.image_generate` | Generate image output through the existing FLUX route. |
| `POST /palette/extract` | `vision.palette_extract` | Extract dominant colors from image input. |
| `POST /rembg/cutout` | `vision.background_cutout` | Remove image background and return cutout output. |
## Planned Tool Candidates
The complete VISION visual-perception package should cover:
- `vision.ocr_read`
- `vision.screenshot_observe`
- `vision.browser_observe`
- `vision.document_layout_read`
- `vision.chart_read`
- `vision.table_read`
- `vision.diagram_read`
- `vision.object_detect`
- `vision.visual_ground`
- `vision.segment`
- `vision.video_read`
- `vision.image_edit`
These are not implemented or granted by this slice. They are named so future
work has a canonical target and does not duplicate Research capabilities.
## Boundary
Owned here:
- Pixel/media perception.
- Visual evidence production.
- Image generation or editing.
- Visual extraction from screenshots, browser views, image files, video frames,
charts, diagrams, and layouts.
Not owned here:
- Web search.
- Page fetch.
- PDF text extraction.
- Research synthesis.
- Deep research planning.
- Capsule writing.
- Profile Exposure.
- Runtime startup.
- Provider admission.
Research can consume Visual Evidence only through an explicit handoff contract.
Vision never becomes a research synthesizer by returning evidence.
+42
View File
@@ -0,0 +1,42 @@
# Visual Evidence Contract
Status: candidate contract. This is route-only evidence for Cortex OS and Seed
review. It does not grant tools or promote the package.
## Required Fields
Every Visual Evidence record must include:
- `producing_package_id`
- `producing_tool_id`
- `capability_surface`
- `source_reference`
- `provider_mode`
- `retention_disclosure`
- `observed_content_summary`
- `extracted_claims`
- `confidence`
- `caveats`
- `timestamp`
- `validation_status`
## First Vertical Proof
The first proof adapts a raw-mode `POST /vlm/analyze` response into Visual
Evidence through a pure Python adapter. It does not call a live provider.
Proof module: `src/svrnty_vision/visual_evidence.py`
Proof test: `tests/test_visual_evidence.py`
## Research Handoff Rule
Research may cite Visual Evidence as an input source if the record includes the
required fields and validation status. Research owns synthesis and capsule writing.
Vision owns the visual observation record only.
## Provider And Retention Disclosure
Provider mode and retention are mandatory because host agents must be able to
disclose how the visual observation was produced. Missing disclosure invalidates
the evidence record.
@@ -0,0 +1,55 @@
# SVRNTY-VISION-WORK-002 Validation Evidence
Date: 2026-06-06
Scope: generic Cortex OS VISION package candidate and Visual Evidence proof for
`svrnty-vision`.
## What Changed
- Added child-local VISION package candidate docs and manifests.
- Mapped current HTTP routes to candidate tool ids.
- Added granular tool-grant candidate manifest with no wildcard grants.
- Added Visual Evidence contract and a pure VLM response adapter proof.
- Added host adapter candidate manifests for Claude Code, Codex CLI, and
Pi-Code.
- Added a local sandcastle handoff for the next Seed acceptance route.
- Fixed package dependency declaration from `rembg` to `rembg[cpu]` so the
background cutout route has the required CPU backend on fresh installs.
## Gates Passed
- `python3 tools/validate_vision_package_candidate.py`
- `python3 tools/validate_vision_tool_grants.py`
- `python3 tools/validate_visual_evidence_contract.py`
- `python3 tools/validate_vision_host_adapter_candidates.py`
- `python3 tools/validate_svrnty_vision_child.py`
- `.venv/bin/python tools/validate_svrnty_vision_child.py`
- `pytest --noconftest tests/test_visual_evidence.py`
- `.venv/bin/pytest tests/ -m 'not integration'`
- `python3 -m py_compile src/svrnty_vision/visual_evidence.py tools/validate_vision_package_candidate.py tools/validate_vision_tool_grants.py tools/validate_visual_evidence_contract.py tools/validate_vision_host_adapter_candidates.py tools/validate_svrnty_vision_child.py`
- `.venv/bin/python -m py_compile src/svrnty_vision/visual_evidence.py tools/validate_vision_package_candidate.py tools/validate_vision_tool_grants.py tools/validate_visual_evidence_contract.py tools/validate_vision_host_adapter_candidates.py tools/validate_svrnty_vision_child.py`
- `git diff --check`
## Test Result
Non-integration suite after `rembg[cpu]` dependency fix:
- 30 passed.
- 10 integration tests deselected.
- 1 Starlette/httpx deprecation warning.
## Boundaries Preserved
- No Core mutation.
- No Seed installation.
- No Runtime start.
- No Profile Exposure.
- No provider admission.
- No wildcard grant.
- No Research synthesis in Vision.
## Follow-Up
Follow-up, optional: update the FastAPI/TestClient dependency path when the
Starlette/httpx deprecation becomes a compatibility issue.
@@ -0,0 +1,34 @@
# Vision Package Candidate Sandcastle
Status: ready for the next agent. This sandcastle belongs inside
`svrnty-vision`; it is not an umbrella repo and not a Core route.
## End State
Make `svrnty-vision` Seed-acceptable as the generic Cortex OS VISION
visual-perception package candidate, with host parity for Claude Code, Codex CLI,
and Pi-Code, granular grants, and Visual Evidence disclosure.
## Current Vertical Slice
- Package boundary documented.
- Current BTE routes mapped to Cortex OS candidate tool ids.
- Planned SOTA visual tool inventory named without implementing all tools.
- Visual Evidence contract documented.
- VLM raw-mode output adapted into Visual Evidence through a pure proof.
- Host adapter candidate parity documented.
## Stop Conditions
- Do not mutate Core from this workspace.
- Do not install into Seed from this workspace.
- Do not start Runtime.
- Do not grant Profile Exposure.
- Do not admit providers.
- Do not wildcard expose all tools.
## Next ROI
Build the first Seed route outside this workspace that can accept these manifests
as candidate artifacts, then prove one host adapter can expose exactly one tool
grant without wildcard access.
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies = [
"httpx>=0.27,<1.0",
"Pillow>=11,<13",
"colorthief>=0.2.1",
"rembg>=2.0,<3.0",
"rembg[cpu]>=2.0,<3.0",
]
[project.optional-dependencies]
+1 -1
View File
@@ -5,7 +5,7 @@ pydantic-settings>=2.6,<3.0
httpx>=0.27,<1.0
Pillow>=11,<13
colorthief>=0.2.1
rembg>=2.0,<3.0
rembg[cpu]>=2.0,<3.0
# Test deps
pytest>=8.3,<9.0
+148
View File
@@ -0,0 +1,148 @@
"""Visual Evidence adapter for Cortex OS VISION package candidates."""
from __future__ import annotations
import json
from typing import Any, Protocol
from pydantic import BaseModel, Field
PACKAGE_ID = "visual-perception-package-candidate"
DEFAULT_VALIDATION_STATUS = "candidate_validated"
class VlmAnalysisResponse(Protocol):
justification: str
model_id: str
raw_scores_json: str
class VisualEvidence(BaseModel):
"""Normalized visual observation record for host disclosure and handoff."""
producing_package_id: str = PACKAGE_ID
producing_tool_id: str
capability_surface: str
source_reference: str
provider_mode: str
retention_disclosure: str
observed_content_summary: str
extracted_claims: list[str] = Field(default_factory=list)
confidence: str
caveats: list[str] = Field(default_factory=list)
timestamp: str
validation_status: str
model_id: str | None = None
raw_observation_ref: str | None = None
def visual_evidence_from_vlm_response(
response: VlmAnalysisResponse,
*,
source_reference: str,
provider_mode: str,
retention_disclosure: str,
timestamp: str,
producing_tool_id: str = "vision.image_analyze",
capability_surface: str = "vision.image_analyze",
confidence: str = "medium",
caveats: list[str] | None = None,
validation_status: str = DEFAULT_VALIDATION_STATUS,
) -> VisualEvidence:
"""Adapt the current VLM response into a Cortex OS Visual Evidence record."""
_require_non_empty("source_reference", source_reference)
_require_non_empty("provider_mode", provider_mode)
_require_non_empty("retention_disclosure", retention_disclosure)
_require_non_empty("timestamp", timestamp)
raw_text = response.raw_scores_json.strip()
parsed = _parse_json_object(raw_text)
summary = _summary_from_response(parsed, response.justification, raw_text)
claims = _claims_from_raw(parsed, raw_text)
if not claims and summary:
claims = [f"summary: {summary}"]
return VisualEvidence(
producing_tool_id=producing_tool_id,
capability_surface=capability_surface,
source_reference=source_reference,
provider_mode=provider_mode,
retention_disclosure=retention_disclosure,
observed_content_summary=summary,
extracted_claims=claims,
confidence=confidence,
caveats=caveats or [],
timestamp=timestamp,
validation_status=validation_status,
model_id=response.model_id,
raw_observation_ref="AnalyzeResponse.raw_scores_json",
)
def _require_non_empty(field_name: str, value: str) -> None:
if not value or not value.strip():
raise ValueError(f"{field_name} is required for Visual Evidence")
def _parse_json_object(raw_text: str) -> dict[str, Any] | None:
if not raw_text:
return None
try:
parsed = json.loads(raw_text)
except json.JSONDecodeError:
return None
return parsed if isinstance(parsed, dict) else None
def _summary_from_response(
parsed: dict[str, Any] | None, justification: str, raw_text: str
) -> str:
if parsed:
for key in (
"observed_content_summary",
"summary",
"description",
"caption",
"text",
):
value = parsed.get(key)
if isinstance(value, str) and value.strip():
return _compact(value)
if justification.strip():
return _compact(justification)
return _compact(raw_text)
def _claims_from_raw(parsed: dict[str, Any] | None, raw_text: str) -> list[str]:
if not parsed:
return [f"raw: {_compact(raw_text)}"] if raw_text else []
claims: list[str] = []
for key, value in parsed.items():
claims.append(f"{key}: {_format_claim_value(value)}")
return claims
def _format_claim_value(value: Any) -> str:
if isinstance(value, list):
if not value:
return "[]"
if all(
isinstance(item, str | int | float | bool) or item is None
for item in value
):
return ", ".join(str(item) for item in value)
return json.dumps(value, sort_keys=True, separators=(",", ":"))
if isinstance(value, dict):
return json.dumps(value, sort_keys=True, separators=(",", ":"))
if value is None:
return "null"
return str(value)
def _compact(text: str, limit: int = 500) -> str:
compacted = " ".join(text.split())
if len(compacted) <= limit:
return compacted
return compacted[: limit - 3].rstrip() + "..."
+57
View File
@@ -0,0 +1,57 @@
from __future__ import annotations
from types import SimpleNamespace
from svrnty_vision.visual_evidence import visual_evidence_from_vlm_response
def test_vlm_raw_mode_response_becomes_visual_evidence() -> None:
response = SimpleNamespace(
rubric_mode="raw",
justification="",
model_id="qwen-test",
raw_scores_json='{"description":"solid red square","objects":["red square"],"detected_text":[]}',
)
evidence = visual_evidence_from_vlm_response(
response,
source_reference="fixture://red-square.png",
provider_mode="sovereign",
retention_disclosure="synchronous_no_async_persistence",
timestamp="2026-06-06T00:00:00Z",
)
assert evidence.producing_package_id == "visual-perception-package-candidate"
assert evidence.producing_tool_id == "vision.image_analyze"
assert evidence.capability_surface == "vision.image_analyze"
assert evidence.source_reference == "fixture://red-square.png"
assert evidence.provider_mode == "sovereign"
assert evidence.retention_disclosure == "synchronous_no_async_persistence"
assert evidence.observed_content_summary == "solid red square"
assert "description: solid red square" in evidence.extracted_claims
assert "objects: red square" in evidence.extracted_claims
assert "detected_text: []" in evidence.extracted_claims
assert evidence.validation_status == "candidate_validated"
assert "research_synthesis" not in evidence.model_dump()
def test_non_json_vlm_response_gets_safe_raw_fallback() -> None:
response = SimpleNamespace(
rubric_mode="raw",
justification="",
model_id="qwen-test",
raw_scores_json="a screenshot of a pricing page with two columns",
)
evidence = visual_evidence_from_vlm_response(
response,
source_reference="fixture://pricing.png",
provider_mode="sovereign",
retention_disclosure="synchronous_no_async_persistence",
timestamp="2026-06-06T00:00:00Z",
)
assert evidence.observed_content_summary == "a screenshot of a pricing page with two columns"
assert evidence.extracted_claims == [
"raw: a screenshot of a pricing page with two columns"
]
+46 -2
View File
@@ -3,20 +3,42 @@
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
REQUIRED = ["AGENTS.md", "README.md", "WORKBOARD.yaml"]
REQUIRED = [
"AGENTS.md",
"README.md",
"WORKBOARD.yaml",
"CONTEXT.md",
"docs/VISION-PACKAGE-CANDIDATE.md",
"docs/VISUAL-EVIDENCE-CONTRACT.md",
"docs/VISION-HOST-ADAPTER-CANDIDATES.md",
"candidate-manifests/vision-package-candidate.json",
"candidate-manifests/vision-tool-grants.json",
"candidate-manifests/visual-evidence-contract.json",
]
VALIDATORS = [
"tools/validate_vision_package_candidate.py",
"tools/validate_vision_tool_grants.py",
"tools/validate_visual_evidence_contract.py",
"tools/validate_vision_host_adapter_candidates.py",
]
def main() -> int:
errors: list[str] = []
validator_outputs: dict[str, str] = {}
for rel in REQUIRED:
if not (ROOT / rel).exists():
errors.append(f"missing:{rel}")
checks = {
"AGENTS.md": ["child-local", "not Cortex OS Core authority", "python3 tools/validate_svrnty_vision_child.py"],
"WORKBOARD.yaml": ["SVRNTY-VISION-WORK-001", "status: candidate", "owner: jp"],
"CONTEXT.md": ["Visual Perception Package Candidate", "Research Handoff"],
"docs/VISION-PACKAGE-CANDIDATE.md": ["Research reads sources; Vision sees media", "wildcard grant"],
}
for rel, snippets in checks.items():
path = ROOT / rel
@@ -25,7 +47,29 @@ def main() -> int:
for snippet in snippets:
if snippet not in text:
errors.append(f"{rel}:missing:{snippet}")
result = {"ok": not errors, "validator": "svrnty-vision-child-v1", "checked": REQUIRED, "errors": errors, "warnings": []}
for rel in VALIDATORS:
path = ROOT / rel
if not path.exists():
errors.append(f"missing:{rel}")
continue
completed = subprocess.run(
[sys.executable, str(path)],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
validator_outputs[rel] = completed.stdout.strip()
if completed.returncode != 0:
errors.append(f"validator_failed:{rel}:{completed.stderr.strip()}")
result = {
"ok": not errors,
"validator": "svrnty-vision-child-v1",
"checked": REQUIRED + VALIDATORS,
"errors": errors,
"warnings": [],
"validator_outputs": validator_outputs,
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["ok"] else 1
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Validate VISION host adapter candidate parity."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
HOSTS = {
"claude-code": "candidate-manifests/host-adapters/vision-claude-code-adapter.json",
"codex-cli": "candidate-manifests/host-adapters/vision-codex-cli-adapter.json",
"pi-code": "candidate-manifests/host-adapters/vision-pi-code-adapter.json",
}
CURRENT_TOOLS = [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout",
]
PLANNED_TOOLS = [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit",
]
def main() -> int:
errors: list[str] = []
checked = list(HOSTS.values()) + ["docs/VISION-HOST-ADAPTER-CANDIDATES.md"]
manifests: dict[str, dict] = {}
for host, rel in HOSTS.items():
manifest = _read_json(ROOT / rel, errors)
if manifest:
manifests[host] = manifest
for host, manifest in manifests.items():
_expect(manifest.get("host_runtime") == host, f"host_runtime_mismatch:{host}", errors)
_expect(manifest.get("package_id") == "visual-perception-package-candidate", f"package_mismatch:{host}", errors)
_expect(manifest.get("adapter_role") == "thin_access_adapter", f"adapter_role_mismatch:{host}", errors)
_expect(manifest.get("wildcard_grant_allowed") is False, f"wildcard_grant_allowed:{host}", errors)
_expect(manifest.get("current_tool_candidates") == CURRENT_TOOLS, f"current_tools_mismatch:{host}", errors)
_expect(manifest.get("planned_tool_candidates") == PLANNED_TOOLS, f"planned_tools_mismatch:{host}", errors)
disclosures = manifest.get("disclosure_contract", {})
for key in ("provider_mode_required", "retention_required", "visual_evidence_required"):
if disclosures.get(key) is not True:
errors.append(f"disclosure_missing:{host}:{key}")
forbidden = set(manifest.get("forbidden_effects", []))
for effect in ("research_synthesis", "textual_web_search", "runtime_start", "wildcard_tool_exposure"):
if effect not in forbidden:
errors.append(f"forbidden_effect_missing:{host}:{effect}")
for key, value in manifest.get("non_authorization", {}).items():
if value is not False:
errors.append(f"non_authorization_enabled:{host}:{key}")
if len(manifests) == len(HOSTS):
current_sets = {tuple(m["current_tool_candidates"]) for m in manifests.values()}
planned_sets = {tuple(m["planned_tool_candidates"]) for m in manifests.values()}
_expect(len(current_sets) == 1, "current_tool_parity_failed", errors)
_expect(len(planned_sets) == 1, "planned_tool_parity_failed", errors)
_check_snippets(
"docs/VISION-HOST-ADAPTER-CANDIDATES.md",
["Claude Code", "Codex CLI", "Pi-Code", "no wildcard grant"],
errors,
)
result = {
"ok": not errors,
"validator": "vision-host-adapter-candidates-v1",
"checked": checked,
"errors": errors,
"warnings": [],
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["ok"] else 1
def _read_json(path: Path, errors: list[str]) -> dict | None:
if not path.exists():
errors.append(f"missing:{path.relative_to(ROOT)}")
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
errors.append(f"{path.relative_to(ROOT)}:json:{exc}")
return None
def _check_snippets(rel: str, snippets: list[str], errors: list[str]) -> None:
path = ROOT / rel
if not path.exists():
errors.append(f"missing:{rel}")
return
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
errors.append(f"{rel}:missing:{snippet}")
def _expect(condition: bool, error: str, errors: list[str]) -> None:
if not condition:
errors.append(error)
if __name__ == "__main__":
raise SystemExit(main())
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Validate the child-local VISION package candidate manifest and docs."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CURRENT_TOOLS = [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout",
]
PLANNED_TOOLS = [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit",
]
ROUTE_TOOLS = {
"POST /vlm/analyze": "vision.image_analyze",
"POST /flux/render": "vision.image_generate",
"POST /palette/extract": "vision.palette_extract",
"POST /rembg/cutout": "vision.background_cutout",
}
def main() -> int:
errors: list[str] = []
required = [
"CONTEXT.md",
"docs/VISION-PACKAGE-CANDIDATE.md",
"candidate-manifests/vision-package-candidate.json",
]
for rel in required:
if not (ROOT / rel).exists():
errors.append(f"missing:{rel}")
manifest_path = ROOT / "candidate-manifests/vision-package-candidate.json"
manifest = _read_json(manifest_path, errors)
if manifest:
_expect(manifest.get("canonical_sense") == "VISION", "canonical_sense_not_vision", errors)
_expect(
manifest.get("status") == "candidate_only_not_seed_installed",
"status_not_candidate_only",
errors,
)
_expect(manifest.get("current_tool_candidates") == CURRENT_TOOLS, "current_tools_mismatch", errors)
_expect(manifest.get("planned_tool_candidates") == PLANNED_TOOLS, "planned_tools_mismatch", errors)
route_map = {
item.get("route"): item.get("tool_id")
for item in manifest.get("current_route_adapters", [])
}
_expect(route_map == ROUTE_TOOLS, "route_tool_map_mismatch", errors)
grant_policy = manifest.get("grant_policy", {})
_expect(grant_policy.get("wildcard_grant_allowed") is False, "wildcard_grant_allowed", errors)
_expect(
grant_policy.get("package_default_grants_all_tools") is False,
"package_default_grants_all_tools",
errors,
)
_expect(grant_policy.get("tool_grants_are_granular") is True, "tool_grants_not_granular", errors)
for key, value in manifest.get("non_authorization", {}).items():
if value is not False:
errors.append(f"non_authorization_enabled:{key}")
forbidden = set(manifest.get("forbidden_research_owned_capabilities", []))
for capability in ("web_search", "fetch_page", "extract_pdf", "deep_research", "research_synthesis"):
if capability not in forbidden:
errors.append(f"missing_forbidden_research_capability:{capability}")
_check_snippets(
"docs/VISION-PACKAGE-CANDIDATE.md",
[
"Research reads sources; Vision sees media",
"No Core promotion",
"wildcard grant",
"`vision.image_analyze`",
"`vision.video_read`",
],
errors,
)
_check_snippets(
"CONTEXT.md",
["Visual Evidence", "Research Handoff", "BTE Compatibility Surface"],
errors,
)
result = {
"ok": not errors,
"validator": "vision-package-candidate-v1",
"checked": required,
"errors": errors,
"warnings": [],
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["ok"] else 1
def _read_json(path: Path, errors: list[str]) -> dict | None:
if not path.exists():
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
errors.append(f"{path.relative_to(ROOT)}:json:{exc}")
return None
def _check_snippets(rel: str, snippets: list[str], errors: list[str]) -> None:
path = ROOT / rel
if not path.exists():
return
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
errors.append(f"{rel}:missing:{snippet}")
def _expect(condition: bool, error: str, errors: list[str]) -> None:
if not condition:
errors.append(error)
if __name__ == "__main__":
raise SystemExit(main())
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Validate granular VISION tool grant candidate manifest."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CURRENT_TOOLS = [
"vision.image_analyze",
"vision.image_generate",
"vision.palette_extract",
"vision.background_cutout",
]
PLANNED_TOOLS = [
"vision.ocr_read",
"vision.screenshot_observe",
"vision.browser_observe",
"vision.document_layout_read",
"vision.chart_read",
"vision.table_read",
"vision.diagram_read",
"vision.object_detect",
"vision.visual_ground",
"vision.segment",
"vision.video_read",
"vision.image_edit",
]
def main() -> int:
errors: list[str] = []
path = ROOT / "candidate-manifests/vision-tool-grants.json"
manifest = _read_json(path, errors)
if manifest:
_expect(manifest.get("canonical_sense") == "VISION", "canonical_sense_not_vision", errors)
_expect(manifest.get("wildcard_grant_allowed") is False, "wildcard_grant_allowed", errors)
_expect(
manifest.get("package_default_grants_all_tools") is False,
"package_default_grants_all_tools",
errors,
)
current = manifest.get("current_tool_candidates", [])
current_tool_ids = [item.get("tool_id") for item in current]
_expect(current_tool_ids == CURRENT_TOOLS, "current_tool_ids_mismatch", errors)
_expect(manifest.get("planned_tool_candidates") == PLANNED_TOOLS, "planned_tools_mismatch", errors)
for item in current:
if item.get("grant_default") is not False:
errors.append(f"grant_default_enabled:{item.get('tool_id')}")
disclosures = manifest.get("required_disclosures", {})
for field in (
"producing_package_id",
"producing_tool_id",
"capability_surface",
"provider_mode",
"retention_disclosure",
"validation_status",
):
if disclosures.get(field) is not True:
errors.append(f"required_disclosure_missing:{field}")
forbidden = set(manifest.get("forbidden_effects", []))
for effect in (
"research_synthesis",
"textual_web_search",
"textual_page_fetch",
"deep_research_workflow",
"wildcard_tool_exposure",
):
if effect not in forbidden:
errors.append(f"forbidden_effect_missing:{effect}")
result = {
"ok": not errors,
"validator": "vision-tool-grants-v1",
"checked": ["candidate-manifests/vision-tool-grants.json"],
"errors": errors,
"warnings": [],
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["ok"] else 1
def _read_json(path: Path, errors: list[str]) -> dict | None:
if not path.exists():
errors.append(f"missing:{path.relative_to(ROOT)}")
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
errors.append(f"{path.relative_to(ROOT)}:json:{exc}")
return None
def _expect(condition: bool, error: str, errors: list[str]) -> None:
if not condition:
errors.append(error)
if __name__ == "__main__":
raise SystemExit(main())
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Validate the Visual Evidence contract and pure adapter proof."""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
REQUIRED_FIELDS = [
"producing_package_id",
"producing_tool_id",
"capability_surface",
"source_reference",
"provider_mode",
"retention_disclosure",
"observed_content_summary",
"extracted_claims",
"confidence",
"caveats",
"timestamp",
"validation_status",
]
def main() -> int:
errors: list[str] = []
required = [
"docs/VISUAL-EVIDENCE-CONTRACT.md",
"candidate-manifests/visual-evidence-contract.json",
"src/svrnty_vision/visual_evidence.py",
"tests/test_visual_evidence.py",
]
for rel in required:
if not (ROOT / rel).exists():
errors.append(f"missing:{rel}")
manifest = _read_json(ROOT / "candidate-manifests/visual-evidence-contract.json", errors)
if manifest:
_expect(manifest.get("required_fields") == REQUIRED_FIELDS, "required_fields_mismatch", errors)
proof = manifest.get("first_vertical_proof", {})
_expect(proof.get("source_tool_id") == "vision.image_analyze", "proof_tool_mismatch", errors)
_expect(proof.get("live_provider_call_required") is False, "live_provider_required", errors)
handoff = manifest.get("research_handoff", {})
_expect(handoff.get("research_may_consume_visual_evidence") is True, "research_handoff_missing", errors)
_expect(handoff.get("vision_may_write_research_capsules") is False, "vision_writes_capsules", errors)
_expect(handoff.get("vision_may_perform_research_synthesis") is False, "vision_research_synthesis", errors)
_check_snippets(
"docs/VISUAL-EVIDENCE-CONTRACT.md",
[
"`producing_package_id`",
"Research owns synthesis and capsule writing",
"does not call a live provider",
],
errors,
)
_validate_adapter(errors)
result = {
"ok": not errors,
"validator": "visual-evidence-contract-v1",
"checked": required,
"errors": errors,
"warnings": [],
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["ok"] else 1
def _validate_adapter(errors: list[str]) -> None:
try:
from types import SimpleNamespace
from svrnty_vision.visual_evidence import visual_evidence_from_vlm_response
except Exception as exc: # pragma: no cover - validator import report
errors.append(f"adapter_import_failed:{type(exc).__name__}:{exc}")
return
response = SimpleNamespace(
rubric_mode="raw",
justification="",
model_id="qwen-test",
raw_scores_json='{"description":"solid red square","objects":["red square"],"detected_text":[]}',
)
evidence = visual_evidence_from_vlm_response(
response,
source_reference="fixture://red-square.png",
provider_mode="sovereign",
retention_disclosure="synchronous_no_async_persistence",
timestamp="2026-06-06T00:00:00Z",
)
dumped = evidence.model_dump()
for field in REQUIRED_FIELDS:
if field not in dumped:
errors.append(f"adapter_missing_field:{field}")
_expect(dumped.get("producing_package_id") == "visual-perception-package-candidate", "adapter_package_mismatch", errors)
_expect(dumped.get("producing_tool_id") == "vision.image_analyze", "adapter_tool_mismatch", errors)
_expect(dumped.get("observed_content_summary") == "solid red square", "adapter_summary_mismatch", errors)
_expect("description: solid red square" in dumped.get("extracted_claims", []), "adapter_claim_missing", errors)
def _read_json(path: Path, errors: list[str]) -> dict | None:
if not path.exists():
return None
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
errors.append(f"{path.relative_to(ROOT)}:json:{exc}")
return None
def _check_snippets(rel: str, snippets: list[str], errors: list[str]) -> None:
path = ROOT / rel
if not path.exists():
return
text = path.read_text(encoding="utf-8")
for snippet in snippets:
if snippet not in text:
errors.append(f"{rel}:missing:{snippet}")
def _expect(condition: bool, error: str, errors: list[str]) -> None:
if not condition:
errors.append(error)
if __name__ == "__main__":
raise SystemExit(main())