This commit is contained in:
orin
2026-07-30 08:45:38 +08:00
commit 25b24ac4cd
1333 changed files with 170092 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Tools
- `validate_repository.py`:校验公共 Schema、配置文档、领域边界和引用关系。
- 后续建议增加 `scaffold_domain.py`、Schema 到 Python/TypeScript 模型生成器和 golden artifact 更新工具。
Binary file not shown.
Binary file not shown.
+133
View File
@@ -0,0 +1,133 @@
[CmdletBinding()]
param(
[ValidateSet('all', 'combined', 'heater', 'fan', 'move')]
[string]$Profile = 'combined',
[string]$Configuration = 'rev_d',
[string]$SdkRoot = 'C:\Users\D30042075206\Desktop\T2G_Sample_Driver_Library_7.9.0',
[string]$RepositoryRoot = '',
[string]$IarBuild = 'D:\Program Files\IAR Systems\Embedded Workbench 9.2\common\bin\iarbuild.exe'
)
$ErrorActionPreference = 'Stop'
$resolvedSdk = (Resolve-Path -LiteralPath $SdkRoot).Path
$repositoryCandidate = if ($RepositoryRoot) {
$RepositoryRoot
} else {
Join-Path $PSScriptRoot '..'
}
$resolvedRepository = (Resolve-Path -LiteralPath $repositoryCandidate).Path
$resolvedIarBuild = (Resolve-Path -LiteralPath $IarBuild).Path
$projects = [ordered]@{
combined = 'Seat_Controller_demo'
heater = 'Seat_Backrest_Heater_demo'
fan = 'Seat_Fan_demo'
move = 'Seat_Move_demo'
}
$selected = if ($Profile -eq 'all') { @($projects.Keys) } else { @($Profile) }
function Invoke-IarBuild {
param(
[string]$ProjectName,
[string]$ProjectPath
)
Write-Host "Building $ProjectName ($Configuration)..."
& $resolvedIarBuild $ProjectPath -build $Configuration -log all
if ($LASTEXITCODE -ne 0) {
throw "IAR build failed for $ProjectName with exit code $LASTEXITCODE"
}
}
function Write-ImageSummary {
param(
[string]$ProjectName,
[string]$ImagePath
)
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
throw "Build completed but HEX image was not found: $ImagePath"
}
$hash = Get-FileHash -LiteralPath $ImagePath -Algorithm SHA256
Write-Host "$ProjectName HEX: $ImagePath"
Write-Host "$ProjectName SHA256: $($hash.Hash.ToLowerInvariant())"
}
foreach ($name in $selected) {
$projectName = $projects[$name]
if ($name -ne 'combined') {
$project = Join-Path $resolvedSdk "$projectName\IAR\$projectName.ewp"
if (-not (Test-Path -LiteralPath $project -PathType Leaf)) {
throw "IAR project not found: $project"
}
Invoke-IarBuild -ProjectName $projectName -ProjectPath $project
$image = Join-Path $resolvedSdk "$projectName\IAR\$Configuration\Exe\$projectName.hex"
Write-ImageSummary -ProjectName $projectName -ImagePath $image
continue
}
$repositoryFirmware = Join-Path $resolvedRepository 'firmware\t2g\Seat_Controller_demo'
$repositoryProject = Join-Path $repositoryFirmware 'IAR\Seat_Controller_demo.ewp'
if (-not (Test-Path -LiteralPath $repositoryProject -PathType Leaf)) {
throw "Repository IAR project not found: $repositoryProject"
}
# The IAR project intentionally keeps the vendor-library relative layout.
# Stage the repository-owned project beside junctions to the external SDK so
# common/ and tviibh8m/ remain vendor dependencies rather than copied source.
$temporaryParent = Join-Path $resolvedRepository '.codex_tmp'
New-Item -ItemType Directory -Path $temporaryParent -Force | Out-Null
$stagingRoot = Join-Path $temporaryParent ("seat-firmware-build-" + [guid]::NewGuid().ToString('N'))
$stagingRootFull = [System.IO.Path]::GetFullPath($stagingRoot)
$temporaryParentFull = [System.IO.Path]::GetFullPath($temporaryParent)
if (-not $stagingRootFull.StartsWith(
$temporaryParentFull + [System.IO.Path]::DirectorySeparatorChar,
[System.StringComparison]::OrdinalIgnoreCase)) {
throw "Firmware staging directory escaped repository temporary root: $stagingRootFull"
}
try {
New-Item -ItemType Directory -Path $stagingRootFull | Out-Null
$stagingProjectRoot = Join-Path $stagingRootFull 'Seat_Controller_demo'
Copy-Item -LiteralPath $repositoryFirmware -Destination $stagingProjectRoot -Recurse
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'common') -Target (Join-Path $resolvedSdk 'common') | Out-Null
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'tviibh8m') -Target (Join-Path $resolvedSdk 'tviibh8m') | Out-Null
$stagedProject = Join-Path $stagingProjectRoot 'IAR\Seat_Controller_demo.ewp'
Invoke-IarBuild -ProjectName $projectName -ProjectPath $stagedProject
$stagedExe = Join-Path $stagingProjectRoot "IAR\$Configuration\Exe"
$repositoryExe = Join-Path $repositoryFirmware "IAR\$Configuration\Exe"
New-Item -ItemType Directory -Path $repositoryExe -Force | Out-Null
foreach ($artifact in @('Seat_Controller_demo.hex', 'Seat_Controller_demo.out')) {
$sourceArtifact = Join-Path $stagedExe $artifact
if (-not (Test-Path -LiteralPath $sourceArtifact -PathType Leaf)) {
throw "IAR build did not produce $sourceArtifact"
}
Copy-Item -LiteralPath $sourceArtifact -Destination (Join-Path $repositoryExe $artifact) -Force
}
Write-ImageSummary -ProjectName $projectName -ImagePath (Join-Path $repositoryExe 'Seat_Controller_demo.hex')
}
finally {
if (Test-Path -LiteralPath $stagingRootFull) {
foreach ($dependencyLink in @('common', 'tviibh8m')) {
$linkPath = Join-Path $stagingRootFull $dependencyLink
if (Test-Path -LiteralPath $linkPath) {
$link = Get-Item -LiteralPath $linkPath -Force
if (($link.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -eq 0) {
throw "Refusing to remove non-junction dependency path: $linkPath"
}
[System.IO.Directory]::Delete($linkPath, $false)
}
}
$remainingLinks = @(
Get-ChildItem -LiteralPath $stagingRootFull -Recurse -Force -Attributes ReparsePoint -ErrorAction SilentlyContinue
)
if ($remainingLinks.Count -ne 0) {
throw "Refusing recursive cleanup because a reparse point remains in $stagingRootFull"
}
[System.IO.Directory]::Delete($stagingRootFull, $true)
}
}
}
Write-Host 'Build complete. This script never flashes or starts a physical actuator.'
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""Validate repository contracts and isolated domain deliverables."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
try:
import yaml
from jsonschema import Draft202012Validator, FormatChecker, RefResolver
except ImportError as exc: # pragma: no cover - dependency bootstrap path
raise SystemExit(
f"Missing validation dependency: {exc.name}. Install with `pip install -e '.[test]'`."
) from exc
ROOT = Path(__file__).resolve().parents[1]
CONTRACT_SCHEMA_DIR = ROOT / "contracts" / "schemas"
KIND_TO_SCHEMA = {
"IntentIR": "intent-ir.schema.json",
"FunctionConfig": "function-config.schema.json",
"FunctionPreset": "function-preset.schema.json",
"SceneConfig": "scene-config.schema.json",
"CommandPlan": "command-plan.schema.json",
"TraceEvent": "trace-event.schema.json",
"ExecutionReport": "execution-report.schema.json",
"DomainManifest": "domain-manifest.schema.json",
"CapabilityCatalog": "capability-catalog.schema.json",
"SignalMapping": "signal-mapping.schema.json",
"PlatformManifest": "platform-manifest.schema.json",
}
def relative(path: Path) -> str:
return str(path.relative_to(ROOT))
def load_json(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise ValueError("root must be an object")
return value
def load_yaml(path: Path) -> dict[str, Any]:
with path.open(encoding="utf-8") as handle:
value = yaml.safe_load(handle)
if not isinstance(value, dict):
raise ValueError("root must be a mapping")
return value
def collect_schemas(
errors: list[str],
) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
schemas: dict[str, dict[str, Any]] = {}
schema_store: dict[str, dict[str, Any]] = {}
for path in sorted(CONTRACT_SCHEMA_DIR.glob("*.json")):
try:
schema = load_json(path)
Draft202012Validator.check_schema(schema)
schema_id = schema.get("$id")
if not isinstance(schema_id, str):
errors.append(f"{relative(path)}: missing string $id")
continue
schemas[path.name] = schema
schema_store[schema_id] = schema
except Exception as exc: # noqa: BLE001 - report every repository error together
errors.append(f"{relative(path)}: invalid JSON Schema: {exc}")
return schemas, schema_store
def public_documents() -> list[Path]:
paths = list((ROOT / "contracts" / "examples").glob("*.yaml"))
paths.extend((ROOT / "platforms").glob("*/manifest.yaml"))
for domain_dir in sorted((ROOT / "domains").iterdir()):
if not domain_dir.is_dir():
continue
paths.extend(
path
for path in [
domain_dir / "manifest.yaml",
domain_dir / "config" / "capabilities.yaml",
domain_dir / "can" / "signal-mapping.yaml",
]
if path.exists()
)
paths.extend((domain_dir / "config" / "presets").glob("*.yaml"))
return sorted(paths)
def validate_documents(
schemas: dict[str, dict[str, Any]],
schema_store: dict[str, dict[str, Any]],
errors: list[str],
) -> int:
count = 0
checker = FormatChecker()
for path in public_documents():
count += 1
try:
document = load_yaml(path)
except Exception as exc: # noqa: BLE001
errors.append(f"{relative(path)}: invalid YAML: {exc}")
continue
kind = document.get("kind")
schema_name = KIND_TO_SCHEMA.get(kind)
if schema_name is None:
errors.append(f"{relative(path)}: unsupported or missing kind {kind!r}")
continue
schema = schemas.get(schema_name)
if schema is None:
errors.append(f"{relative(path)}: required schema {schema_name} was not loaded")
continue
resolver = RefResolver.from_schema(schema, store=schema_store)
validator = Draft202012Validator(
schema, resolver=resolver, format_checker=checker
)
for issue in sorted(validator.iter_errors(document), key=lambda item: list(item.path)):
location = ".".join(str(part) for part in issue.absolute_path) or "<root>"
errors.append(f"{relative(path)}:{location}: {issue.message}")
if kind in {"FunctionConfig", "FunctionPreset"}:
domain = document.get("spec", {}).get("domain")
domain_schema_path = ROOT / "domains" / str(domain) / "schemas" / "function-parameters.schema.json"
if domain_schema_path.exists():
try:
domain_schema = load_json(domain_schema_path)
Draft202012Validator.check_schema(domain_schema)
domain_validator = Draft202012Validator(domain_schema, format_checker=checker)
for issue in sorted(
domain_validator.iter_errors(document), key=lambda item: list(item.path)
):
location = ".".join(str(part) for part in issue.absolute_path) or "<root>"
errors.append(
f"{relative(path)}:{location}: domain validation: {issue.message}"
)
except Exception as exc: # noqa: BLE001
errors.append(f"{relative(domain_schema_path)}: invalid domain schema: {exc}")
return count
def validate_domains(errors: list[str]) -> int:
count = 0
for domain_dir in sorted((ROOT / "domains").iterdir()):
if not domain_dir.is_dir() or domain_dir.name.startswith("_"):
continue
count += 1
domain = domain_dir.name
required_paths = [
domain_dir / "README.md",
domain_dir / "OWNERS.yaml",
domain_dir / "manifest.yaml",
domain_dir / "config" / "capabilities.yaml",
domain_dir / "schemas" / "function-parameters.schema.json",
domain_dir / "can" / "signal-mapping.yaml",
domain_dir / "tests" / "unit",
domain_dir / "tests" / "contract",
domain_dir / "tests" / "integration",
]
for path in required_paths:
if not path.exists():
errors.append(f"domains/{domain}: missing {relative(path)}")
try:
manifest = load_yaml(domain_dir / "manifest.yaml")
catalog = load_yaml(domain_dir / "config" / "capabilities.yaml")
mapping = load_yaml(domain_dir / "can" / "signal-mapping.yaml")
except Exception as exc: # noqa: BLE001
errors.append(f"domains/{domain}: cannot perform semantic checks: {exc}")
continue
for label, document in [("manifest", manifest), ("catalog", catalog), ("mapping", mapping)]:
actual_domain = document.get("spec", {}).get("domain")
if actual_domain != domain:
errors.append(
f"domains/{domain}: {label} spec.domain is {actual_domain!r}, expected {domain!r}"
)
manifest_spec = manifest.get("spec", {})
for field in ["function_schema", "capabilities", "signal_mapping"]:
value = manifest_spec.get(field)
if isinstance(value, str) and not (domain_dir / value).resolve().is_file():
errors.append(f"domains/{domain}: manifest {field} does not exist: {value}")
capabilities = catalog.get("spec", {}).get("capabilities", [])
capability_names = [item.get("name") for item in capabilities if isinstance(item, dict)]
if len(capability_names) != len(set(capability_names)):
errors.append(f"domains/{domain}: duplicate capability names")
for name in capability_names:
if not isinstance(name, str) or not name.startswith(f"{domain}."):
errors.append(f"domains/{domain}: capability must start with '{domain}.': {name!r}")
for preset_path in sorted((domain_dir / "config" / "presets").glob("*.yaml")):
preset = load_yaml(preset_path)
if preset.get("spec", {}).get("domain") != domain:
errors.append(f"{relative(preset_path)}: preset domain must be {domain}")
for operation in preset.get("spec", {}).get("operations", []):
capability = operation.get("capability")
if capability not in capability_names:
errors.append(
f"{relative(preset_path)}: unknown capability {capability!r}"
)
mapping_spec = mapping.get("spec", {})
for item in mapping_spec.get("mappings", []):
capability = item.get("capability")
if capability not in capability_names:
errors.append(
f"domains/{domain}/can/signal-mapping.yaml: unknown capability {capability!r}"
)
for platform in manifest_spec.get("supported_platforms", []):
if not (ROOT / "platforms" / platform / "manifest.yaml").is_file():
errors.append(f"domains/{domain}: unsupported platform reference {platform!r}")
return count
def validate_platform_resources(errors: list[str], warnings: list[str]) -> int:
count = 0
for manifest_path in sorted((ROOT / "platforms").glob("*/manifest.yaml")):
count += 1
manifest = load_yaml(manifest_path)
spec = manifest.get("spec", {})
platform_dir = manifest_path.parent
execution_enabled = spec.get("execution_enabled") is True
refs = list(spec.get("dbc_files", [])) + list(spec.get("calibrations", {}).values())
for ref in refs:
target = platform_dir / str(ref)
if target.is_file():
continue
message = f"{relative(manifest_path)}: referenced platform resource is missing: {ref}"
if execution_enabled:
errors.append(message)
else:
warnings.append(message + " (allowed because execution_enabled is false)")
return count
def main() -> int:
errors: list[str] = []
warnings: list[str] = []
schemas, schema_store = collect_schemas(errors)
document_count = validate_documents(schemas, schema_store, errors)
domain_count = validate_domains(errors)
platform_count = validate_platform_resources(errors, warnings)
for warning in warnings:
print(f"WARNING: {warning}")
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
print(f"Validation failed with {len(errors)} error(s).", file=sys.stderr)
return 1
print(
"Repository validation passed: "
f"{len(schemas)} schemas, {document_count} documents, "
f"{domain_count} domains, {platform_count} platform(s)."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())