341 lines
12 KiB
Go
341 lines
12 KiB
Go
package taskloop
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
_ "embed"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const paritySchemaVersion = 1
|
|
|
|
//go:embed testdata/parity.yaml
|
|
var embeddedParityManifest []byte
|
|
|
|
// ParityManifest records the S13 disposition of each retained reference
|
|
// behavior. It is intentionally evidence-only: runtime behavior remains in
|
|
// the shared agenttask and agentpolicy packages.
|
|
type ParityManifest struct {
|
|
SchemaVersion int `yaml:"schema_version"`
|
|
ReferenceRoot string `yaml:"reference_root"`
|
|
DeletionGate string `yaml:"deletion_gate"`
|
|
Behaviors []ParityBehavior `yaml:"behaviors"`
|
|
Disposal ParityDisposal `yaml:"disposal"`
|
|
}
|
|
|
|
type ParityBehavior struct {
|
|
ID string `yaml:"id"`
|
|
ReferenceSources []string `yaml:"reference_sources"`
|
|
Disposition string `yaml:"disposition"`
|
|
GoOwner string `yaml:"go_owner"`
|
|
GoSource []string `yaml:"go_source"`
|
|
GoTest []string `yaml:"go_test"`
|
|
Invariants string `yaml:"invariants"`
|
|
ProductionCaller string `yaml:"production_caller"`
|
|
}
|
|
|
|
type ParityDisposal struct {
|
|
State string `yaml:"state"`
|
|
Procedure string `yaml:"procedure"`
|
|
ReferenceFiles []ParityReferenceFile `yaml:"reference_files"`
|
|
}
|
|
|
|
type ParityReferenceFile struct {
|
|
Path string `yaml:"path"`
|
|
SHA256 string `yaml:"sha256"`
|
|
Disposition string `yaml:"disposition"`
|
|
CoveredBy []string `yaml:"covered_by"`
|
|
}
|
|
|
|
// LoadEmbeddedParityManifest returns the repository-owned S13 manifest.
|
|
func LoadEmbeddedParityManifest() (ParityManifest, error) {
|
|
return ParseParityManifest(embeddedParityManifest)
|
|
}
|
|
|
|
// ValidateEmbeddedParityManifest validates the checked-in manifest against the
|
|
// supplied repository root and returns its stable operator report.
|
|
func ValidateEmbeddedParityManifest(root string) (string, error) {
|
|
manifest, err := LoadEmbeddedParityManifest()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := ValidateParityManifest(root, manifest); err != nil {
|
|
return "", err
|
|
}
|
|
return FormatParityReport(manifest), nil
|
|
}
|
|
|
|
// RepositoryRoot walks upward from start until it finds the module root.
|
|
func RepositoryRoot(start string) (string, error) {
|
|
directory, err := filepath.Abs(start)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for {
|
|
if info, err := os.Stat(filepath.Join(directory, "go.mod")); err == nil && !info.IsDir() {
|
|
return directory, nil
|
|
}
|
|
parent := filepath.Dir(directory)
|
|
if parent == directory {
|
|
return "", fmt.Errorf("repository root not found from %q", start)
|
|
}
|
|
directory = parent
|
|
}
|
|
}
|
|
|
|
func ParseParityManifest(data []byte) (ParityManifest, error) {
|
|
var manifest ParityManifest
|
|
if err := yaml.Unmarshal(data, &manifest); err != nil {
|
|
return ParityManifest{}, fmt.Errorf("parity manifest: decode: %w", err)
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
// ValidateParityManifest proves that the inventory is complete in retained
|
|
// mode, or completely absent in disposed mode. The latter keeps the recorded
|
|
// checksums as historical pre-deletion evidence without requiring deleted
|
|
// files to exist.
|
|
func ValidateParityManifest(root string, manifest ParityManifest) error {
|
|
if manifest.SchemaVersion != paritySchemaVersion {
|
|
return fmt.Errorf("parity manifest: unsupported schema_version %d", manifest.SchemaVersion)
|
|
}
|
|
if strings.TrimSpace(manifest.ReferenceRoot) == "" || strings.TrimSpace(manifest.DeletionGate) == "" {
|
|
return fmt.Errorf("parity manifest: reference_root and deletion_gate are required")
|
|
}
|
|
if len(manifest.Behaviors) == 0 {
|
|
return fmt.Errorf("parity manifest: behaviors are required")
|
|
}
|
|
|
|
behaviorIDs := make(map[string]struct{}, len(manifest.Behaviors))
|
|
behaviorSources := make(map[string]struct{})
|
|
for _, behavior := range manifest.Behaviors {
|
|
if strings.TrimSpace(behavior.ID) == "" {
|
|
return fmt.Errorf("parity manifest: unclassified behavior")
|
|
}
|
|
if _, exists := behaviorIDs[behavior.ID]; exists {
|
|
return fmt.Errorf("parity manifest: duplicate behavior %q", behavior.ID)
|
|
}
|
|
behaviorIDs[behavior.ID] = struct{}{}
|
|
if !allowedDisposition(behavior.Disposition) {
|
|
return fmt.Errorf("parity manifest: behavior %q has unclassified disposition %q", behavior.ID, behavior.Disposition)
|
|
}
|
|
if strings.TrimSpace(behavior.GoOwner) == "" || strings.TrimSpace(behavior.Invariants) == "" || behavior.ProductionCaller != "go-only" {
|
|
return fmt.Errorf("parity manifest: behavior %q lacks Go ownership evidence", behavior.ID)
|
|
}
|
|
if len(behavior.GoSource) == 0 || len(behavior.GoTest) == 0 {
|
|
return fmt.Errorf("parity manifest: behavior %q lacks concrete Go evidence", behavior.ID)
|
|
}
|
|
for _, source := range behavior.GoSource {
|
|
if err := validateLiteralExistingPath(root, source); err != nil {
|
|
return fmt.Errorf("parity manifest: behavior %q: %w", behavior.ID, err)
|
|
}
|
|
}
|
|
for _, source := range behavior.ReferenceSources {
|
|
if err := validateLiteralPath(source); err != nil {
|
|
return fmt.Errorf("parity manifest: behavior %q: %w", behavior.ID, err)
|
|
}
|
|
behaviorSources[source] = struct{}{}
|
|
}
|
|
for _, testName := range behavior.GoTest {
|
|
if strings.TrimSpace(testName) == "" {
|
|
return fmt.Errorf("parity manifest: behavior %q has empty Go test", behavior.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
if strings.TrimSpace(manifest.Disposal.Procedure) == "" {
|
|
return fmt.Errorf("parity manifest: disposal procedure is required")
|
|
}
|
|
if !allowedDisposalState(manifest.Disposal.State) {
|
|
return fmt.Errorf("parity manifest: unknown disposal state %q", manifest.Disposal.State)
|
|
}
|
|
discovered, err := discoverParityReferenceFiles(root, manifest.ReferenceRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
discoveredSet := make(map[string]struct{}, len(discovered))
|
|
for _, path := range discovered {
|
|
discoveredSet[path] = struct{}{}
|
|
}
|
|
disposal := make(map[string]ParityReferenceFile, len(manifest.Disposal.ReferenceFiles))
|
|
for _, reference := range manifest.Disposal.ReferenceFiles {
|
|
if _, exists := disposal[reference.Path]; exists {
|
|
return fmt.Errorf("parity manifest: duplicate disposal path %q", reference.Path)
|
|
}
|
|
if !allowedDisposition(reference.Disposition) || len(reference.CoveredBy) == 0 {
|
|
return fmt.Errorf("parity manifest: disposal entry %q is unclassified", reference.Path)
|
|
}
|
|
if err := validateLiteralPath(reference.Path); err != nil {
|
|
return fmt.Errorf("parity manifest: disposal entry: %w", err)
|
|
}
|
|
for _, behaviorID := range reference.CoveredBy {
|
|
if _, exists := behaviorIDs[behaviorID]; !exists {
|
|
return fmt.Errorf("parity manifest: disposal entry %q covers unknown behavior %q", reference.Path, behaviorID)
|
|
}
|
|
}
|
|
if len(reference.SHA256) != sha256.Size*2 {
|
|
return fmt.Errorf("parity manifest: invalid checksum for %q", reference.Path)
|
|
}
|
|
disposal[reference.Path] = reference
|
|
}
|
|
if err := validateParityInventory(behaviorSources, disposal); err != nil {
|
|
return err
|
|
}
|
|
if manifest.Disposal.State == "disposed" {
|
|
if len(discovered) != 0 {
|
|
return fmt.Errorf("parity manifest: disposed state has retained fixture %q", discovered[0])
|
|
}
|
|
for path := range disposal {
|
|
if _, err := os.Lstat(filepath.Join(root, filepath.FromSlash(path))); err == nil {
|
|
return fmt.Errorf("parity manifest: disposed state has survivor %q", path)
|
|
} else if !os.IsNotExist(err) {
|
|
return fmt.Errorf("parity manifest: inspect disposed entry %q: %w", path, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for _, path := range discovered {
|
|
if _, exists := disposal[path]; !exists {
|
|
return fmt.Errorf("parity manifest: unrecorded retained fixture %q", path)
|
|
}
|
|
if _, exists := behaviorSources[path]; !exists {
|
|
return fmt.Errorf("parity manifest: missing behavior classification for %q", path)
|
|
}
|
|
}
|
|
for path, reference := range disposal {
|
|
if _, exists := discoveredSet[path]; !exists {
|
|
return fmt.Errorf("parity manifest: retained state is missing fixture %q", path)
|
|
}
|
|
contents, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path)))
|
|
if err != nil {
|
|
return fmt.Errorf("parity manifest: read disposal entry %q: %w", path, err)
|
|
}
|
|
digest := sha256.Sum256(contents)
|
|
if !strings.EqualFold(reference.SHA256, hex.EncodeToString(digest[:])) {
|
|
return fmt.Errorf("parity manifest: checksum drift for %q", path)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateParityInventory keeps disposition evidence anchored to the complete
|
|
// classified source inventory even after the files themselves are disposed.
|
|
func validateParityInventory(behaviorSources map[string]struct{}, disposal map[string]ParityReferenceFile) error {
|
|
missing := make([]string, 0)
|
|
for path := range behaviorSources {
|
|
if _, exists := disposal[path]; !exists {
|
|
missing = append(missing, path)
|
|
}
|
|
}
|
|
if len(missing) != 0 {
|
|
sort.Strings(missing)
|
|
return fmt.Errorf("parity manifest: disposal inventory is missing classified reference %q", missing[0])
|
|
}
|
|
extra := make([]string, 0)
|
|
for path := range disposal {
|
|
if _, exists := behaviorSources[path]; !exists {
|
|
extra = append(extra, path)
|
|
}
|
|
}
|
|
if len(extra) != 0 {
|
|
sort.Strings(extra)
|
|
return fmt.Errorf("parity manifest: disposal inventory has unclassified reference %q", extra[0])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// discoverParityReferenceFiles returns every retained Python source or test
|
|
// fixture below the manifest-owned root in deterministic repository-relative
|
|
// order. The manifest must enumerate this result exactly.
|
|
func discoverParityReferenceFiles(root, referenceRoot string) ([]string, error) {
|
|
if strings.TrimSpace(referenceRoot) == "" || filepath.IsAbs(referenceRoot) {
|
|
return nil, fmt.Errorf("parity manifest: invalid reference_root %q", referenceRoot)
|
|
}
|
|
cleaned := filepath.Clean(filepath.FromSlash(referenceRoot))
|
|
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
|
|
return nil, fmt.Errorf("parity manifest: invalid reference_root %q", referenceRoot)
|
|
}
|
|
directory := filepath.Join(root, cleaned)
|
|
info, err := os.Stat(directory)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parity manifest: reference_root %q: %w", referenceRoot, err)
|
|
}
|
|
if !info.IsDir() {
|
|
return nil, fmt.Errorf("parity manifest: reference_root %q must be a directory", referenceRoot)
|
|
}
|
|
var files []string
|
|
err = filepath.WalkDir(directory, func(path string, entry os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() || !entry.Type().IsRegular() {
|
|
return nil
|
|
}
|
|
name := entry.Name()
|
|
if !strings.HasSuffix(name, ".py") && !strings.HasSuffix(name, ".py.orig") {
|
|
return nil
|
|
}
|
|
relative, err := filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, filepath.ToSlash(relative))
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parity manifest: discover retained fixtures: %w", err)
|
|
}
|
|
sort.Strings(files)
|
|
return files, nil
|
|
}
|
|
|
|
// FormatParityReport emits a bounded, stable report for the operator command.
|
|
func FormatParityReport(manifest ParityManifest) string {
|
|
ids := make([]string, 0, len(manifest.Behaviors))
|
|
for _, behavior := range manifest.Behaviors {
|
|
ids = append(ids, behavior.ID)
|
|
}
|
|
sort.Strings(ids)
|
|
return fmt.Sprintf("parity: ok\n schema_version: %d\n behaviors: %d\n disposal_state: %s\n disposal_files: %d\n deletion_gate: %s\n ids: %s\n",
|
|
manifest.SchemaVersion, len(ids), manifest.Disposal.State, len(manifest.Disposal.ReferenceFiles), manifest.DeletionGate, strings.Join(ids, ","))
|
|
}
|
|
|
|
func allowedDisposition(value string) bool {
|
|
return value == "absorb" || value == "replace" || value == "not-applicable"
|
|
}
|
|
|
|
func allowedDisposalState(value string) bool {
|
|
return value == "retained" || value == "disposed"
|
|
}
|
|
|
|
func validateLiteralPath(value string) error {
|
|
if strings.TrimSpace(value) == "" || strings.ContainsAny(value, "*?[") || filepath.IsAbs(value) {
|
|
return fmt.Errorf("invalid literal path %q", value)
|
|
}
|
|
cleaned := filepath.Clean(filepath.FromSlash(value))
|
|
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
|
|
return fmt.Errorf("invalid literal path %q", value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateLiteralExistingPath(root, value string) error {
|
|
if err := validateLiteralPath(value); err != nil {
|
|
return err
|
|
}
|
|
cleaned := filepath.Clean(filepath.FromSlash(value))
|
|
info, err := os.Stat(filepath.Join(root, cleaned))
|
|
if err != nil {
|
|
return fmt.Errorf("stale path %q: %w", value, err)
|
|
}
|
|
if info.IsDir() {
|
|
return fmt.Errorf("path %q must be a file", value)
|
|
}
|
|
return nil
|
|
}
|