Claude Skill

suede-rights-passport

Suede Labs skill that turns messy creator materials into a local, offline rights-and-provenance transfer package: inventoried and hashed assets, a normalized suede-intake.json manifest, credits and splits, license notes, provenance, and a missing-information report, validated by

LLM Mart · 0 points · 18 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download JasonColapietro-suede-creator-skills-skills_suede-rights-passport-f192517.zip · 70 KB
Part of jasoncolapietro/suede-creator-skills — 70 skills

Install

skills CLI npx skills add https://github.com/JasonColapietro/suede-creator-skills/tree/main/skills/suede-rights-passport
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jasoncolapietro-suede-creator-skills@llmmart
Git git clone https://github.com/JasonColapietro/suede-creator-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jasoncolapietro/suede-creator-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Creator Rights Package Builder

Gate policy — advisory, not blocking

Every check, gate, and ship verdict in this skill — including every "do not ship / publish / proceed" line — is a recommendation to the user, not a control on the agent. Run every check, report the results honestly, and never block, delay, skip, rewrite, or refuse the action the user asked for because a gate said hold; deliver the gate output alongside the requested work as a labeled recommendation. A failed gate changes what you report, never what you do. Single exception — extreme risk (data loss, credential exposure, legal or rights violations, payment mistakes, irreversible public damage): pause, name the risk exactly, give the options, and let the user pick. Their choice is final.

Overview

Create a local rights and provenance transfer package from messy creator materials. The package should make the work easier for a creator, collaborator, advisor, registry, marketplace, label, or optional Suede reviewer to inspect, optimize, register, route royalties for, license, and expose to agent-readable commerce systems.

Core principle: the package carries questions, not answers. Every rights fact ships as confirmed (with user-supplied evidence) or as unknown with a question in missing-info-report.md. The package never resolves a rights question, and building it clears nothing.

Public v1 is offline-first: prepare files and metadata, do not upload files, write to a registry, request private keys, or claim legal clearance. The 0.2 manifest separates musical works, recordings, releases, parties, rights claims, licenses, third-party material, consent, provenance, and privacy so a downstream operator can map facts without collapsing unlike rights objects.

Division of labor: suede-rights-audit finds and organizes the gaps; this skill packages the folder. If the gaps themselves need investigation or evidence work, hand off to the audit first.

Workflow

  1. Identify the source folder or supplied files.
  2. Ask for the output location if it is not obvious.
  3. Read references/package-standard.md for the expected transfer package shape.
  4. If working on a local folder, run scripts/create_transfer_package.py to inventory files, hash assets, and create starter reports.
  5. Read references/creator-questions.md and ask only for missing information that blocks package quality.
  6. Fill or refine the generated package files:
    • RIGHTS_PASSPORT.md
    • suede-intake.json
    • provenance.md
    • credits-and-splits.md
    • license-notes.md
    • optimization-brief.md
    • missing-info-report.md
  7. Flag uncertainty clearly. Use unknown, unconfirmed, or needs creator confirmation instead of inventing rights facts. Never resolve a rights question while packaging: ownership, split, sample, and license statuses move to confirmed only on user-supplied evidence, and every open gap ships as a question in missing-info-report.md.
  8. For an external exchange, read references/ddex-c2pa-crosswalk.md, identify the receiver's exact profile/version, and keep the mapping labeled as a crosswalk until receiver conformance tooling passes.
  9. Run scripts/validate_transfer_package.py with --strict-current against new output folders. A pass confirms schema, evidence-state, reference, and share-bound structure only — it does not mean rights are confirmed.
  10. End with a concise transfer summary: package path, schema version, files found, missing info, risk flags, privacy/redaction posture, and recommended next step.

Quick Start

For a local project folder:

python3 /path/to/suede-rights-passport/scripts/create_transfer_package.py \
  /path/to/source-project \
  --output /path/to/transfer-package \
  --metadata /path/to/source-project/metadata.json \
  --project-title "Project Title" \
  --artist "Artist Name"

To copy media into the transfer package as well as inventory it:

python3 /path/to/suede-rights-passport/scripts/create_transfer_package.py \
  /path/to/source-project \
  --output /path/to/transfer-package \
  --copy-assets

Safety defaults:

  • Hidden files, dependency folders, build outputs, caches, and secret-like files are skipped by default.
  • Symlinked sources, metadata, files, and directories are rejected; the builder hashes or copies only regular files that resolve inside the declared source tree.
  • Unrecognized file types are skipped unless --include-other is passed.
  • Absolute local paths are redacted to share-safer names unless --include-absolute-paths is passed.
  • Existing generated package files are not overwritten unless --force is passed.
  • The output folder cannot be the same folder as the source or live inside it.
  • Public-safe JSON, YAML, or key=value text metadata can prefill known project, rights, contributor, release, wallet, and provenance facts. Do not point metadata at real .env, credential, wallet, or deployment config files. Unknown facts remain flagged. YAML metadata requires PyYAML.

Halt format — material that may not be shareable. Before any --copy-assets run, scan for draft, unreleased, private, or do-not-share files. If any appear: stop, name the specific files and why each one reads as do-not-share, offer the options (exclude and proceed / include with a redaction note / inventory without copying / abort), and wait for the choice. Use the same shape for anything hitting the gate policy's extreme-risk exception. Never guess which way the creator would want it.

Validate A Package

After creating or editing a package, check that it is structurally complete with scripts/validate_transfer_package.py:

python3 /path/to/suede-rights-passport/scripts/validate_transfer_package.py \
  --strict-current /path/to/transfer-package

It is a dependency-free (stdlib-only) check that executes the bundled Draft 2020-12 JSON Schema. It confirms the 7 required report files, that suede-intake.json matches the shape documented in references/intake-schema.md, real 64-hex sha256 digests on every asset, unique IDs with resolving references, evidence on every confirmed record, in-range and non-oversubscribed shares, and explicit privacy/redaction posture — each one mapped to its exact error string in the Completion Checklist below.

It exits non-zero with a specific error list on failure and prints a short pass summary — including a risk-flag count — on success. Run --help for usage, or --quiet to suppress the success summary. Legacy 0.1 packages remain inspectable without --strict-current; new exchanges require 0.2.0.

To migrate an existing 0.1 manifest without modifying it:

python3 /path/to/suede-rights-passport/scripts/migrate_intake_v1_to_v2.py \
  /path/to/transfer-package/suede-intake.json

The migration writes suede-intake.v0.2.json, records the source manifest digest and custody history, preserves open questions and risk flags, maps only roles stated in source data, and never upgrades evidence state or fills missing shares. Review it before replacing any current manifest.

Structural validity is not a rights clearance. The validator checks that a package is shaped correctly and complete, not that the rights facts inside it are confirmed — a project with unconfirmed ownership, unconfirmed splits, or an uncleared sample still passes, because risk_flags[] and missing_information[] are exactly where that uncertainty belongs. Never read a PASS as clearance, and never expect a risk-flagged package to fail.

scripts/fixtures/sample-complete-package/ and sample-blocked-package/ are worked examples at both ends of that range, and both validate. Read scripts/fixtures/README.md when you need a concrete example of what a risk-flagged but structurally valid package looks like, or when changing create_transfer_package.py.

Package Standards

Read each bundled reference at the moment it is needed, not up front:

  • references/package-standard.md: before creating or repairing any package — required output files, folder structure, risk labels, and quality bar.
  • references/intake-schema.md: when filling or validating suede-intake.json.
  • references/ddex-c2pa-crosswalk.md: before external standards mapping or any DDEX/C2PA claim.
  • references/optimization-checklist.md: when writing optimization-brief.md.
  • references/creator-questions.md: when information is missing — ask only the questions that block package quality.
  • references/passport-context.md: when the user asks how the package relates to Suede review or the Suede Creator Passport.

Use the bundled assets as templates when creating or repairing a package:

  • assets/rights-passport.template.md
  • assets/suede-intake.template.json
  • assets/suede-intake.schema.json
  • assets/provenance.template.md
  • assets/credits-and-splits.template.md
  • assets/license-notes.template.md
  • assets/optimization-brief.template.md
  • assets/missing-info-report.template.md

Public Safety Rules

  • Do not say Suede owns, controls, or has cleared a work unless the user provides explicit proof.
  • Do not call the package a legal contract.
  • Do not ask for private keys, seed phrases, unreleased account secrets, or full payment credentials.
  • Do not include private implementation details, private endpoints, internal provider names, or non-public pricing.
  • Do not upload files or call live services unless the user explicitly asks and provides the relevant authenticated workflow.
  • Treat generated reports and transfer packages as private drafts until a creator or operator reviews and redacts them for the intended audience.
  • Do not call a field crosswalk DDEX conformance, and do not call a hash a C2PA Content Credential. Validate the receiver's exact profile separately.
  • Keep composition, recording/master, and release identifiers on their proper objects. ISWC and ISRC are not interchangeable, and neither proves ownership.
  • Unknown voice, likeness, or synthetic-media consent stays unknown; silence is not consent.
  • Keep public positioning focused on broadly reusable creator workflows: rights packaging, provenance, registry readiness, royalty routing, licensing, and agent commerce.

Completion Checklist

Run scripts/validate_transfer_package.py with --strict-current against the output folder first and report the result: it is the evidence behind most of this checklist, and every structural gap it names gets fixed before the package is called ready. Each machine-checked box names the error raised when it is unmet:

  • All 7 required files present — missing required file.
  • Every asset has a stable relative path and a 64-hex SHA-256 — empty or non-string sha256 field.
  • Parties, works, recordings, and releases have distinct IDs that resolve — duplicate id / references unknown id.
  • Every media/document file is inventoried or intentionally excluded, and identifiers (ISWC, ISRC, IPI/CAE, ISNI, UPC/EAN, catalog) sit only on their proper objects — identifiers[…].scheme is unsupported.
  • Claims and licenses are scoped by subject, right/use type, party, territory, term, evidence, and restrictions, with no scope over 100% — share_percent must be null or between 0 and 100 / total … above 100%. Never force unknown shares to total 100.
  • Every confirmed record carries evidence — is confirmed but has no evidence_refs.
  • Privacy classification and redaction posture are explicit — privacy.default_classification is unsupported.

Three boxes the validator cannot check — the human-judgment residue, on which a clean run says nothing:

  • Do-not-share review: no draft, private, or unreleased material was copied in without the user's explicit choice (the halt format above).
  • Redaction review: someone read the sensitive fields before any external share instead of trusting the classification labels.
  • Uncertainty stated: final clearance requires creator/legal confirmation wherever a rights fact is uncertain; contributor, split, license, sample, and ownership facts are confirmed only on user-supplied evidence and unknown when in doubt; missing-info-report.md ships even when empty, and optimization-brief.md ships with concrete next actions.

A validator pass still does not resolve a rights fact.

Red flags — stop

If any of these appear in your reasoning, stop and re-read the core principle:

  • "Fill in the missing split so the total reaches 100." A guessed split is a false rights fact. Record the shortfall and ask.
  • "The artist told me they own it — mark ownership confirmed." Record the claim as claimed; confirmed needs evidence.
  • "Nothing seems missing — skip missing-info-report.md." The report ships even when empty. That is the checklist.
  • "Copy all the assets; sorting is the reviewer's problem." Check for draft and do-not-share files before any --copy-assets run.
  • "Call it registered or cleared since the package looks complete." A complete package is organized, not approved.

Downstream Review Context

Artifacts produced by this skill (RIGHTS_PASSPORT.md, suede-intake.json, provenance.md, credits-and-splits.md, license-notes.md) are portable review materials. They can support a release, registry, licensing conversation, collaborator handoff, marketplace review, label review, advisor review, or Suede review without claiming that any downstream system has accepted, cleared, registered, paid, or approved the work.

Routing

  • Rights gaps that need investigation or evidence organizing → suede-rights-audit (it finds the gaps; this skill packages them).
  • Release-readiness lint before or after packaging → suede-release-linter.
  • Track headed to film/TV/ads once packaged → suede-sync-packaging.
  • The release needs a rollout → suede-campaign-in-a-box.

Family order: suede-release-linter → suede-rights-audit → suede-rights-passport → suede-sync-packaging; this skill is step 3.

Files (suede-creator-skills)
  • agents
    • openai.yaml 480 B
      interface:
        display_name: "Creator Rights Package Builder"
        short_description: "Build validated, evidence-scoped rights packages."
        brand_color: "#D71920"
        default_prompt: "Use $suede-rights-passport to organize this creator project into a validated rights package with separate works, recordings, releases, parties, claims, licenses, consent, provenance, and privacy — preserving unknown or disputed facts without clearing them."
      
      policy:
        allow_implicit_invocation: true
      
  • assets
    • credits-and-splits.template.md 533 B
      # Credits And Splits
      
      ## Contributors
      
      | Name | Role | Master % | Publishing % | Confirmation |
      | --- | --- | ---: | ---: | --- |
      | unknown | unknown | 0 | 0 | needs creator confirmation |
      
      ## Organizations
      
      | Name | Role | Notes |
      | --- | --- | --- |
      | unknown | unknown | none provided |
      
      ## Payment / Wallet Notes
      
      - Creator wallet: unknown
      - Contributor wallets: unknown
      - Royalty routing readiness: not ready until splits are confirmed
      
      ## Blockers
      
      - Contributor list needs confirmation.
      - Split percentages need confirmation.
      
    • license-notes.template.md 488 B
      # License Notes
      
      ## Third-Party Material
      
      - Samples: unknown
      - Loops: unknown
      - Interpolations: unknown
      - Covers: unknown
      - Beat leases: unknown
      
      ## Existing Releases
      
      - DSP release: unknown
      - Social/video platform release: unknown
      - Prior mint/registry/license: unknown
      - Existing takedowns/disputes: unknown
      
      ## Restrictions
      
      No restrictions confirmed. Treat as unknown until creator confirms.
      
      ## Clearance Status
      
      High-confidence licensing should wait for creator/legal confirmation.
      
    • missing-info-report.template.md 882 B
      # Missing Info Report
      
      ## Summary
      
      Missing information must be resolved before Suede can confidently register, license, route royalties for, or expose the work to agent commerce.
      
      ## Questions
      
      | Severity | Topic | Question | Blocks |
      | --- | --- | --- | --- |
      | high | ownership | Who owns the master and publishing rights? | registry, licensing, royalty routing |
      | high | contributors | Who contributed and what are the confirmed splits? | royalty routing, licensing |
      | high | samples | Does the work contain samples, covers, interpolations, loops, or third-party beats? | licensing, registry |
      | medium | files | Which audio file is the final master? | media optimization |
      | medium | release | Has the work already been released, registered, minted, or licensed? | registry, licensing |
      
      ## Status
      
      Not ready for final Suede intake until high-severity questions are answered.
      
    • optimization-brief.template.md 823 B
      # Optimization Brief
      
      ## Goal
      
      Prepare the work for Suede optimization after rights and file review.
      
      ## Recommended Next Actions
      
      1. Rights review - high priority
         Reason: ownership, contributor, and split details are not fully confirmed.
         Blocks: registry, licensing, royalty routing, agent commerce.
      
      2. Media inventory review - normal priority
         Reason: confirm which files are masters, stems, lyrics, artwork, and documents.
         Blocks: downstream optimization quality.
      
      ## Candidate Suede Services
      
      - Mastering / WAV export: unknown
      - Stem separation: unknown
      - Lyric sync: unknown
      - Artwork polish: unknown
      - Registry readiness: unknown
      - Royalty routing: unknown
      - License packaging: unknown
      - Agent commerce packaging: unknown
      
      ## Operator Notes
      
      Review `missing-info-report.md` before beginning optimization.
      
    • provenance.template.md 485 B
      # Provenance
      
      ## Source
      
      - Source folder: unknown
      - Package generated: unknown
      - Submitter: unknown
      
      ## Creation History
      
      Needs creator confirmation.
      
      ## Chain Of Custody
      
      | Date | Event | Actor | Evidence |
      | --- | --- | --- | --- |
      | unknown | Project prepared for Suede intake | unknown | transfer package |
      
      ## File Hashes
      
      See `suede-intake.json` for SHA-256 hashes.
      
      ## Registry Notes
      
      - Registry status: unknown
      - Asset hash selected for registry: unknown
      - Notes: needs review
      
    • rights-passport.template.md 898 B
      # Rights Passport
      
      ## Work Summary
      
      - Title: unknown
      - Artist / creator: unknown
      - Work type: unknown
      - Release status: unknown
      - Package status: draft for Suede intake
      
      ## Intake Readiness
      
      - Overall risk: unknown
      - Registry readiness: unknown
      - Royalty routing readiness: unknown
      - Licensing readiness: unknown
      - Agent commerce readiness: unknown
      
      ## Rights Snapshot
      
      - Owner claim: unknown
      - Ownership status: unknown
      - Contributor list confirmed: no
      - Splits confirmed: no
      - Samples / interpolations / covers: unknown
      - Sample clearance status: unknown
      - Existing licenses or restrictions: unknown
      
      ## Asset Snapshot
      
      | Category | Count | Notes |
      | --- | ---: | --- |
      | Audio | 0 | unknown |
      | Stems | 0 | unknown |
      | Lyrics | 0 | unknown |
      | Artwork | 0 | unknown |
      | Documents | 0 | unknown |
      | Other | 0 | unknown |
      
      ## Suede Next Step
      
      Needs creator confirmation before Suede optimization.
      
    • suede-intake.schema.json 15.2 KB
      {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "$id": "urn:suede:rights-passport:suede-intake:0.2.0",
        "title": "Suede Rights Passport Intake Manifest",
        "description": "Structural schema for a Suede rights package. Validation is not rights clearance, registration, DDEX conformance, or C2PA verification.",
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "package_type",
          "generated_at",
          "project",
          "creator",
          "parties",
          "works",
          "recordings",
          "releases",
          "assets",
          "rights",
          "rights_claims",
          "licenses",
          "third_party_material",
          "consents",
          "provenance",
          "privacy",
          "optimization",
          "missing_information",
          "risk_flags"
        ],
        "properties": {
          "schema_version": { "const": "0.2.0" },
          "package_type": { "const": "suede-transfer-package" },
          "generated_at": { "type": "string", "format": "date-time" },
          "metadata_source": { "type": "string" },
          "project": { "$ref": "#/$defs/project" },
          "creator": { "$ref": "#/$defs/creator" },
          "parties": { "type": "array", "items": { "$ref": "#/$defs/party" } },
          "works": { "type": "array", "items": { "$ref": "#/$defs/work" } },
          "recordings": { "type": "array", "items": { "$ref": "#/$defs/recording" } },
          "releases": { "type": "array", "items": { "$ref": "#/$defs/release" } },
          "assets": { "type": "array", "items": { "$ref": "#/$defs/asset" } },
          "rights": { "$ref": "#/$defs/legacyRightsSummary" },
          "rights_claims": { "type": "array", "items": { "$ref": "#/$defs/rightsClaim" } },
          "licenses": { "type": "array", "items": { "$ref": "#/$defs/license" } },
          "third_party_material": { "type": "array", "items": { "$ref": "#/$defs/thirdPartyMaterial" } },
          "consents": { "type": "array", "items": { "$ref": "#/$defs/consent" } },
          "provenance": { "$ref": "#/$defs/provenance" },
          "privacy": { "$ref": "#/$defs/privacy" },
          "optimization": { "type": "object" },
          "missing_information": { "type": "array", "items": { "$ref": "#/$defs/missingInformation" } },
          "risk_flags": { "type": "array", "items": { "$ref": "#/$defs/riskFlag" } }
        },
        "$defs": {
          "status": {
            "type": "string",
            "enum": ["confirmed", "claimed", "unconfirmed", "disputed", "unknown"]
          },
          "identifier": {
            "type": "object",
            "additionalProperties": false,
            "required": ["scheme", "value", "status", "evidence_refs"],
            "properties": {
              "scheme": {
                "type": "string",
                "enum": ["ISRC", "ISWC", "IPI_CAE", "ISNI", "UPC_EAN", "CATALOG_NUMBER", "PROPRIETARY"]
              },
              "value": { "type": "string", "minLength": 1 },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } }
            }
          },
          "project": {
            "type": "object",
            "required": ["title", "artist_name", "work_type", "description", "release_status", "public_urls"],
            "properties": {
              "title": { "type": "string" },
              "artist_name": { "type": "string" },
              "work_type": { "type": "string" },
              "description": { "type": "string" },
              "release_status": { "type": "string" },
              "public_urls": { "type": "array", "items": { "type": "string" } }
            }
          },
          "creator": {
            "type": "object",
            "required": ["name", "email", "wallet_address", "organization", "confirmation_status"],
            "properties": {
              "name": { "type": "string" },
              "email": { "type": "string" },
              "wallet_address": { "type": "string" },
              "organization": { "type": "string" },
              "confirmation_status": { "type": "string" }
            }
          },
          "party": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "name", "roles", "identifiers", "organization", "status", "evidence_refs", "privacy_classification"],
            "properties": {
              "id": { "type": "string", "pattern": "^party-[A-Za-z0-9._-]+$" },
              "name": { "type": "string", "minLength": 1 },
              "roles": { "type": "array", "items": { "type": "string", "minLength": 1 } },
              "identifiers": { "type": "array", "items": { "$ref": "#/$defs/identifier" } },
              "organization": { "type": "string" },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string" } },
              "privacy_classification": { "$ref": "#/$defs/privacyClass" }
            }
          },
          "work": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "title", "identifiers", "writer_party_ids", "publisher_party_ids", "status", "evidence_refs"],
            "properties": {
              "id": { "type": "string", "pattern": "^work-[A-Za-z0-9._-]+$" },
              "title": { "type": "string", "minLength": 1 },
              "identifiers": { "type": "array", "items": { "$ref": "#/$defs/identifier" } },
              "writer_party_ids": { "type": "array", "items": { "type": "string" } },
              "publisher_party_ids": { "type": "array", "items": { "type": "string" } },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "recording": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "title", "asset_ids", "identifiers", "performer_party_ids", "master_owner_party_ids", "status", "evidence_refs"],
            "properties": {
              "id": { "type": "string", "pattern": "^recording-[A-Za-z0-9._-]+$" },
              "title": { "type": "string", "minLength": 1 },
              "asset_ids": { "type": "array", "items": { "type": "string" } },
              "identifiers": { "type": "array", "items": { "$ref": "#/$defs/identifier" } },
              "performer_party_ids": { "type": "array", "items": { "type": "string" } },
              "master_owner_party_ids": { "type": "array", "items": { "type": "string" } },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "release": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "title", "recording_ids", "identifiers", "label_party_id", "distributor_party_id", "release_date", "territories", "status", "evidence_refs"],
            "properties": {
              "id": { "type": "string", "pattern": "^release-[A-Za-z0-9._-]+$" },
              "title": { "type": "string", "minLength": 1 },
              "recording_ids": { "type": "array", "items": { "type": "string" } },
              "identifiers": { "type": "array", "items": { "$ref": "#/$defs/identifier" } },
              "label_party_id": { "type": ["string", "null"] },
              "distributor_party_id": { "type": ["string", "null"] },
              "release_date": { "type": ["string", "null"], "format": "date" },
              "territories": { "type": "array", "items": { "type": "string" } },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "asset": {
            "type": "object",
            "required": ["id", "relative_path", "category", "role", "mime_guess", "size_bytes", "sha256"],
            "properties": {
              "id": { "type": "string", "minLength": 1 },
              "relative_path": { "type": "string", "minLength": 1 },
              "original_path": { "type": "string" },
              "category": { "type": "string" },
              "role": { "type": "string" },
              "mime_guess": { "type": "string" },
              "size_bytes": { "type": "integer", "minimum": 0 },
              "sha256": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" },
              "notes": { "type": "string" }
            }
          },
          "legacyRightsSummary": {
            "type": "object",
            "required": ["owner_claim", "ownership_status", "contributors_confirmed", "splits_confirmed", "contains_samples", "sample_clearance_status", "cover_or_interpolation", "license_restrictions"],
            "properties": {
              "owner_claim": { "type": "string" },
              "ownership_status": { "type": "string" },
              "contributors": { "type": "array" },
              "contributors_confirmed": { "type": "boolean" },
              "splits_confirmed": { "type": "boolean" },
              "contains_samples": { "type": "string" },
              "sample_clearance_status": { "type": "string" },
              "cover_or_interpolation": { "type": "string" },
              "license_restrictions": { "type": "array", "items": { "type": "string" } }
            }
          },
          "rightsClaim": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "subject_type", "subject_id", "right_type", "party_id", "share_percent", "territories", "start_date", "end_date", "status", "evidence_refs", "conflict_notes"],
            "properties": {
              "id": { "type": "string", "pattern": "^claim-[A-Za-z0-9._-]+$" },
              "subject_type": { "type": "string", "enum": ["work", "recording", "release"] },
              "subject_id": { "type": "string" },
              "right_type": { "type": "string", "enum": ["composition", "publishing", "mechanical", "performance", "synchronization", "master", "neighboring", "distribution", "other"] },
              "party_id": { "type": "string" },
              "share_percent": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
              "territories": { "type": "array", "items": { "type": "string", "minLength": 1 } },
              "start_date": { "type": ["string", "null"], "format": "date" },
              "end_date": { "type": ["string", "null"], "format": "date" },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } },
              "conflict_notes": { "type": "string" }
            }
          },
          "license": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "subject_ids", "licensor_party_ids", "licensee_party_ids", "use_types", "media", "territories", "start_date", "end_date", "exclusive", "sublicensing", "revocable", "status", "restrictions", "evidence_refs"],
            "properties": {
              "id": { "type": "string", "pattern": "^license-[A-Za-z0-9._-]+$" },
              "subject_ids": { "type": "array", "items": { "type": "string" } },
              "licensor_party_ids": { "type": "array", "items": { "type": "string" } },
              "licensee_party_ids": { "type": "array", "items": { "type": "string" } },
              "use_types": { "type": "array", "items": { "type": "string" } },
              "media": { "type": "array", "items": { "type": "string" } },
              "territories": { "type": "array", "items": { "type": "string" } },
              "start_date": { "type": ["string", "null"] },
              "end_date": { "type": ["string", "null"] },
              "exclusive": { "type": ["boolean", "null"] },
              "sublicensing": { "type": "string", "enum": ["allowed", "prohibited", "unknown"] },
              "revocable": { "type": ["boolean", "null"] },
              "status": { "$ref": "#/$defs/status" },
              "restrictions": { "type": "array", "items": { "type": "string" } },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "thirdPartyMaterial": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "type", "source", "subject_ids", "license_id", "status", "evidence_refs"],
            "properties": {
              "id": { "type": "string" },
              "type": { "type": "string", "enum": ["sample", "interpolation", "cover", "beat", "loop", "visual", "other"] },
              "source": { "type": "string" },
              "subject_ids": { "type": "array", "items": { "type": "string" } },
              "license_id": { "type": ["string", "null"] },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "consent": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id", "party_id", "scope", "media", "ai_use", "voice_likeness", "status", "evidence_refs"],
            "properties": {
              "id": { "type": "string" },
              "party_id": { "type": "string" },
              "scope": { "type": "string" },
              "media": { "type": "array", "items": { "type": "string" } },
              "ai_use": { "type": "string", "enum": ["allowed", "prohibited", "limited", "unknown"] },
              "voice_likeness": { "type": "string", "enum": ["allowed", "prohibited", "limited", "unknown"] },
              "status": { "$ref": "#/$defs/status" },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "provenance": {
            "type": "object",
            "required": ["source_root", "creation_notes", "chain_of_custody", "registry_status", "content_credentials"],
            "properties": {
              "source_root": { "type": "string" },
              "metadata_source": { "type": "string" },
              "creation_notes": { "type": "string" },
              "chain_of_custody": { "type": "array" },
              "registry_status": { "type": "string" },
              "content_credentials": { "type": "array", "items": { "$ref": "#/$defs/contentCredential" } }
            }
          },
          "contentCredential": {
            "type": "object",
            "additionalProperties": false,
            "required": ["asset_id", "kind", "manifest_reference", "verification_status", "verified_at", "evidence_refs"],
            "properties": {
              "asset_id": { "type": "string" },
              "kind": { "type": "string", "enum": ["sha256", "c2pa", "other"] },
              "manifest_reference": { "type": ["string", "null"] },
              "verification_status": { "type": "string", "enum": ["verified", "unverified", "failed", "not-checked"] },
              "verified_at": { "type": ["string", "null"] },
              "evidence_refs": { "type": "array", "items": { "type": "string" } }
            }
          },
          "privacyClass": {
            "type": "string",
            "enum": ["public", "shared-with-recipient", "private-draft", "restricted", "do-not-share"]
          },
          "privacy": {
            "type": "object",
            "additionalProperties": false,
            "required": ["default_classification", "field_rules", "redaction_required_before_external_share"],
            "properties": {
              "default_classification": { "$ref": "#/$defs/privacyClass" },
              "field_rules": { "type": "array", "items": { "$ref": "#/$defs/privacyRule" } },
              "redaction_required_before_external_share": { "type": "boolean" }
            }
          },
          "privacyRule": {
            "type": "object",
            "additionalProperties": false,
            "required": ["json_pointer", "classification", "redaction_action", "reason"],
            "properties": {
              "json_pointer": { "type": "string", "pattern": "^/" },
              "classification": { "$ref": "#/$defs/privacyClass" },
              "redaction_action": { "type": "string", "enum": ["keep", "mask", "remove", "review"] },
              "reason": { "type": "string" }
            }
          },
          "missingInformation": {
            "type": "object",
            "required": ["field", "question", "blocks", "severity"],
            "properties": {
              "field": { "type": "string" },
              "question": { "type": "string" },
              "blocks": { "type": "array", "items": { "type": "string" } },
              "severity": { "type": "string", "enum": ["low", "medium", "high"] }
            }
          },
          "riskFlag": {
            "type": "object",
            "required": ["label", "severity", "detail", "recommended_action"],
            "properties": {
              "label": { "type": "string" },
              "severity": { "type": "string", "enum": ["low", "medium", "high", "unknown"] },
              "detail": { "type": "string" },
              "recommended_action": { "type": "string" }
            }
          }
        }
      }
      
    • suede-intake.template.json 2 KB
      {
        "schema_version": "0.2.0",
        "package_type": "suede-transfer-package",
        "generated_at": null,
        "project": {
          "title": "unknown",
          "artist_name": "unknown",
          "work_type": "unknown",
          "description": "",
          "release_status": "unknown",
          "public_urls": []
        },
        "creator": {
          "name": "unknown",
          "email": "",
          "wallet_address": "",
          "organization": "",
          "confirmation_status": "unknown"
        },
        "parties": [],
        "works": [
          {
            "id": "work-001",
            "title": "unknown",
            "identifiers": [],
            "writer_party_ids": [],
            "publisher_party_ids": [],
            "status": "unknown",
            "evidence_refs": []
          }
        ],
        "recordings": [],
        "releases": [],
        "assets": [],
        "rights": {
          "owner_claim": "unknown",
          "ownership_status": "unknown",
          "contributors_confirmed": false,
          "splits_confirmed": false,
          "contains_samples": "unknown",
          "sample_clearance_status": "unknown",
          "cover_or_interpolation": "unknown",
          "license_restrictions": []
        },
        "rights_claims": [],
        "licenses": [],
        "third_party_material": [],
        "consents": [],
        "provenance": {
          "source_root": "",
          "creation_notes": "",
          "chain_of_custody": [],
          "registry_status": "unknown",
          "content_credentials": []
        },
        "privacy": {
          "default_classification": "private-draft",
          "field_rules": [
            {
              "json_pointer": "/creator/email",
              "classification": "restricted",
              "redaction_action": "review",
              "reason": "Contact data should be shared only with the intended recipient."
            },
            {
              "json_pointer": "/creator/wallet_address",
              "classification": "restricted",
              "redaction_action": "review",
              "reason": "Payment-routing data requires recipient and purpose review."
            }
          ],
          "redaction_required_before_external_share": true
        },
        "optimization": {
          "requested_services": [],
          "recommended_services": [],
          "priority": "normal",
          "notes": ""
        },
        "missing_information": [],
        "risk_flags": []
      }
      
  • references
    • creator-questions.md 1.6 KB
      # Creator Intake Questions
      
      Ask only questions that resolve missing or risky information. Prefer grouped questions when many fields are missing.
      
      ## Identity
      
      - What is the official project or song title?
      - What artist/creator name should Suede use publicly?
      - Who is submitting this package, and are they authorized to do so?
      - Is there a public wallet address or payment destination Suede should associate with the creator?
      
      ## Files
      
      - Which file is the final master?
      - Are stems available?
      - Are there separate lyrics, cover art, project files, or split sheets?
      - Are any files drafts that should not be used for registry, licensing, or public cataloging?
      
      ## Contributors And Splits
      
      - Who contributed to the work?
      - What role did each contributor have?
      - Are publishing/master splits confirmed in writing?
      - Are any contributors unresponsive, disputed, or missing?
      - Should any manager, label, publisher, or collective be listed?
      
      ## Rights And Licenses
      
      - Does the work contain samples, loops, interpolations, covers, or third-party beats?
      - Are those materials cleared?
      - Are there beat leases, exclusivity restrictions, sync restrictions, or platform restrictions?
      - Has the work already been distributed, registered, minted, licensed, or sold?
      
      ## Suede Optimization
      
      - What do you want Suede to improve first: mastering, stems, lyrics, artwork, metadata, registry, royalty routing, licensing, or agent commerce?
      - Is this intended for private optimization, public cataloging, licensing, agent commerce, or a future release?
      - Are there deadlines, launch dates, takedown concerns, or confidentiality limits?
      
    • ddex-c2pa-crosswalk.md 5.3 KB
      # Interoperability Crosswalk: DDEX, Identifiers, and C2PA
      
      Use this reference only when a package may be exchanged with a registry,
      label, distributor, publisher, PRO/CMO, marketplace, or provenance system.
      
      ## Boundary
      
      The Suede schema is an intake and evidence model. The mappings below are
      orientation aids, not a DDEX implementation profile, certified message, legal
      opinion, registry submission, or C2PA Content Credential. Do not say a package
      is DDEX- or C2PA-compliant merely because similarly named fields exist.
      
      Before an external exchange:
      
      1. identify the receiver and exact standard/profile/version it accepts;
      2. validate identifiers with the responsible issuing or authoritative party;
      3. map controlled vocabularies using that receiver's rules;
      4. preserve source evidence and unknown/disputed status;
      5. validate the exported message or manifest with the receiver's conformance
         tooling; and
      6. obtain creator/operator approval before transmission.
      
      ## Object and identifier separation
      
      | Suede object | Common identifier | What it identifies | Do not use it for |
      | --- | --- | --- | --- |
      | `works[]` | ISWC | A musical work/composition | A sound recording or release |
      | `recordings[]` | ISRC | A specific sound recording or music video recording | The underlying composition |
      | `releases[]` | UPC/EAN | A marketed release/product | Ownership or party identity |
      | `parties[]` | IPI/CAE | A party in rights-management systems | A work or recording |
      | `parties[]` | ISNI | A public identity for a person or organization | Proof of ownership |
      | `releases[]` | Catalog number | A label/distributor catalog reference | A globally unique registry ID |
      
      Every identifier is paired with `status` and `evidence_refs`. A plausible
      format is not proof that the identifier is valid or belongs to the object.
      
      ## DDEX-oriented mapping
      
      | Suede field | DDEX-oriented concept | Export note |
      | --- | --- | --- |
      | `parties[]` | Party | Map roles and identifiers to the selected DDEX allowed-value sets. |
      | `works[]` | Musical work | Preserve writers, publishers, ISWC, and evidence separately. |
      | `recordings[]` | Resource / sound recording | Preserve ISRC, performers, master controllers, and asset links. |
      | `releases[]` | Release | Map recording membership, release identifiers, label, dates, and territories. |
      | `rights_claims[]` | Right share / right-delegation facts | Scope each share by object, right type, territory, and dates. Never force unknown shares to total 100. |
      | `licenses[]` | Deal or delegated-use facts | Receiver-specific messages may model these differently; retain source agreements as evidence. |
      | `third_party_material[]` | Resource/work dependencies and clearances | Samples, interpolations, covers, loops, and beats need explicit source and clearance state. |
      
      The studio-stage metadata concepts in DDEX Recording Information Notification
      (RIN) are useful for contributor, role, session, and resource capture. RIN
      orientation does not make this package a valid RIN message.
      
      Primary sources:
      
      - [DDEX Recording Information Notification](https://rin.ddex.net/recording-information-notification/)
      - [DDEX studio metadata standards](https://ddex.ddex.net/standards/collection-of-studio-metadata/)
      - [DDEX identifier guidance](https://kb.ddex.net/implementing-each-standard/best-practices-for-all-ddex-standards/guidance-on-identifiers%2C-iso-codes-lists-and-dates/communication-of-identifiers-in-ddex-messages)
      - [IFPI ISRC Handbook](https://isrc.ifpi.org/isrc-standard/isrc-handbook)
      - [ISWC](https://www.iswc.org/iswc)
      
      ## C2PA-oriented mapping
      
      `provenance.content_credentials[]` can record that a C2PA manifest was found
      or checked. It does not create or sign one.
      
      | Suede field | C2PA-oriented concept | Required handling |
      | --- | --- | --- |
      | `asset_id` | Asset associated with a manifest | Resolve to a hashed asset in `assets[]`. |
      | `kind: c2pa` | Content Credential / manifest reference | Store a reference, not private signing material. |
      | `verification_status` | Local verification result | Record `verified_at` and evidence from a named verifier. |
      | `manifest_reference` | Manifest or sidecar location | Apply privacy classification before sharing. |
      | `evidence_refs` | Verification output | Retain the redacted report or digest used for the claim. |
      
      Hashing an asset proves only that the bytes seen later can be compared with
      the bytes inventoried. A SHA-256 digest alone does not establish authorship,
      ownership, consent, chronology, or C2PA authenticity.
      
      Primary source: [C2PA specifications](https://spec.c2pa.org/specifications/).
      
      ## Privacy and synthetic-media consent
      
      - Default packages to `private-draft`.
      - Classify contact, wallet/payment, agreement, unreleased-asset, and identity
        fields before external sharing.
      - Use `consents[]` for voice, likeness, and AI-use scope. Unknown consent stays
        `unknown`; silence is not consent.
      - Never include seed phrases, private keys, account secrets, unredacted IDs,
        or an agreement whose sharing is restricted.
      - Export only the minimum fields required by the intended recipient.
      
      ## Export gate
      
      An external export is blocked when any required identifier, share, territory,
      term, authority, consent, or evidence is disputed or unknown. The package can
      still be structurally valid; the blocking condition belongs in
      `missing_information[]` and `risk_flags[]`.
      
    • intake-schema.md 6.5 KB
      # Suede Intake Manifest Schema 0.2.0
      
      `suede-intake.json` is the agent-readable center of the transfer package.
      The canonical machine-readable contract is
      `assets/suede-intake.schema.json` (JSON Schema draft 2020-12).
      
      Schema validity establishes structure and referential integrity only. It does
      not establish ownership, clearance, registration, identifier validity, DDEX
      conformance, C2PA authenticity, consent, or authority to transact.
      
      ## Top-level model
      
      ```json
      {
        "schema_version": "0.2.0",
        "package_type": "suede-transfer-package",
        "generated_at": "ISO-8601 timestamp",
        "metadata_source": "optional source label",
        "project": {},
        "creator": {},
        "parties": [],
        "works": [],
        "recordings": [],
        "releases": [],
        "assets": [],
        "rights": {},
        "rights_claims": [],
        "licenses": [],
        "third_party_material": [],
        "consents": [],
        "provenance": {},
        "privacy": {},
        "optimization": {},
        "missing_information": [],
        "risk_flags": []
      }
      ```
      
      Version 0.2 retains `project`, `creator`, and the compact `rights` summary for
      0.1 readers. The normalized arrays are the exchange-oriented source of truth.
      Do not infer normalized claims from prose when evidence is absent.
      
      ## Evidence state
      
      Every identifier, party, work, recording, release, rights claim, license,
      third-party-material record, and consent uses one of:
      
      - `confirmed`: supported by a user-supplied evidence reference;
      - `claimed`: asserted by a named party but not independently evidenced;
      - `unconfirmed`: recorded but awaiting confirmation;
      - `disputed`: conflicting evidence or claims exist;
      - `unknown`: not enough information to characterize it.
      
      `confirmed` records must include at least one `evidence_refs[]` item. A filename,
      document ID, registry readback, or redacted verification report is acceptable;
      an agent's conclusion is not.
      
      ## Object model
      
      ### `parties[]`
      
      People and organizations. Each party has a stable `party-*` ID, name, roles,
      optional IPI/CAE or ISNI identifiers, organization, evidence state, evidence
      references, and privacy classification. Roles are descriptive in this schema;
      an external DDEX export must map them to the receiver's controlled vocabulary.
      
      ### `works[]`
      
      Musical works/compositions. Each `work-*` record has a title, optional ISWC,
      writer and publisher party references, evidence state, and evidence references.
      
      ### `recordings[]`
      
      Sound recordings. Each `recording-*` record has associated asset IDs, optional
      ISRC, performer and master-owner party references, evidence state, and evidence
      references. Do not place an ISWC here.
      
      ### `releases[]`
      
      Marketed releases/products. Each `release-*` record links recordings, UPC/EAN
      or catalog identifiers, label/distributor parties, release date, territories,
      evidence state, and evidence references.
      
      ### Identifiers
      
      An identifier object contains `scheme`, `value`, `status`, and
      `evidence_refs[]`. Supported schemes are `ISRC`, `ISWC`, `IPI_CAE`, `ISNI`,
      `UPC_EAN`, `CATALOG_NUMBER`, and `PROPRIETARY`. Format checks do not confirm an
      identifier; retain `claimed` or `unknown` until authoritative evidence exists.
      
      ## Rights and permission model
      
      ### `rights_claims[]`
      
      Each claim includes:
      
      - stable `claim-*` ID;
      - `subject_type` and `subject_id` (`work`, `recording`, or `release`);
      - `right_type` such as `composition`, `publishing`, `mechanical`,
        `performance`, `synchronization`, `master`, `neighboring`, or `distribution`;
      - claimant `party_id`;
      - nullable `share_percent` from 0 through 100;
      - territories and optional start/end dates;
      - evidence state, evidence references, and conflict notes.
      
      The validator rejects a known-share total over 100 for the same subject, right
      type, territory set, and term. Distinct territorial or temporal scopes are
      counted separately. It does not fill a shortfall. A total below 100 remains a
      documented gap unless the creator supplies evidence; potentially overlapping
      but non-identical scopes still require human rights review.
      
      ### `licenses[]`
      
      Record subjects, licensors/licensees, allowed uses and media, territory, term,
      exclusivity, sublicensing, revocability, restrictions, evidence state, and
      evidence references. A summary is not a substitute for the governing agreement.
      
      ### `third_party_material[]`
      
      Record samples, interpolations, covers, beats, loops, visuals, or other
      dependencies; link the affected subjects and supporting license where known.
      
      ### `consents[]`
      
      Record scope, media, AI-use permission, voice/likeness permission, status, and
      evidence. Unknown or absent consent never becomes permission by inference.
      
      ## Assets and provenance
      
      Every `assets[]` item includes a stable ID, relative path, category, role,
      media type, byte size, and SHA-256 digest. `provenance.chain_of_custody[]`
      records events. `provenance.content_credentials[]` may record hash checks or a
      C2PA manifest verification result, but never private signing material.
      
      Hash continuity is not proof of authorship, ownership, chronology, or consent.
      
      ## Privacy
      
      `privacy.default_classification` is `private-draft` by default. Per-field rules
      use JSON Pointers and classifications:
      
      - `public`
      - `shared-with-recipient`
      - `private-draft`
      - `restricted`
      - `do-not-share`
      
      Each rule specifies `keep`, `mask`, `remove`, or `review`. Redaction review is
      required before external sharing unless an authorized operator explicitly sets
      and verifies a narrower policy.
      
      ## Open questions and risk flags
      
      `missing_information[]` records a field, creator question, blocked downstream
      steps, and severity. `risk_flags[]` records a factual label, severity, detail,
      and recommended action. These arrays are the correct place for incomplete or
      conflicting facts; structural validation should not erase them.
      
      ## Migration from 0.1.0
      
      The validator accepts 0.1.0 packages as `legacy` by default so existing creator
      packages remain inspectable. New packages are generated as 0.2.0. Run the
      validator with `--strict-current` to require 0.2.0 before an exchange or release.
      
      Migration steps:
      
      1. keep the original 0.1.0 package immutable;
      2. create a 0.2.0 copy and retain its source-package hash/evidence reference;
      3. map contributors to `parties[]` without upgrading their confirmation state;
      4. separate composition (`works[]`) from master (`recordings[]`) and release;
      5. translate splits into scoped `rights_claims[]` without filling gaps;
      6. record licenses, third-party material, consent, privacy, and provenance;
      7. validate with `--strict-current`; and
      8. obtain creator/operator review before external exchange.
      
      See `references/ddex-c2pa-crosswalk.md` before mapping to an external standard.
      
    • optimization-checklist.md 1.9 KB
      # Suede Optimization Checklist
      
      Use this when writing `optimization-brief.md`.
      
      ## Media Preparation
      
      - `mastering`: Final loudness, balance, WAV export, and platform-ready audio.
      - `stem-separation`: Vocals, drums, bass, instrumental, and other useful stems.
      - `vocal-isolation`: Acapella extraction for remix, sync, or derivative workflows.
      - `lyric-sync`: Timestamped lyrics for video, app, or interactive experiences.
      - `midi-transcription`: MIDI from melodic, harmonic, or rhythmic source material.
      - `artwork-polish`: Cover image cleanup, sizing, crop variants, and metadata alignment.
      
      ## Rights And Registry Preparation
      
      - `rights-review`: Confirm owner, contributors, samples, licenses, and restrictions.
      - `provenance-cleanup`: Improve source notes, file hashes, chain-of-custody, and creation timeline.
      - `registry-readiness`: Prepare for registry-backed provenance and programmable IP flows.
      - `royalty-routing`: Prepare contributor splits, wallets/payment destinations, and confirmation status.
      - `license-packaging`: Create terms, allowed uses, restrictions, and buyer-facing summaries.
      
      ## Agent Commerce Preparation
      
      - `agent-readable-metadata`: Make the work understandable to autonomous agents.
      - `x402-readiness`: Package pricing, usage, and fulfillment notes for payable endpoints.
      - `catalog-discovery`: Prepare titles, descriptions, tags, rights notes, and public-safe summaries.
      - `derivative-controls`: State what agents can remix, cover, extend, sample, or license.
      
      ## Recommendation Format
      
      Use concise bullets:
      
      ```text
      1. Rights review - high priority
         Reason: splits are unknown for two contributors.
         Blocks: registry, licensing, royalty routing.
      
      2. Stem separation - normal priority
         Reason: only a stereo MP3 exists; stems would improve remix/licensing options.
         Blocks: none.
      ```
      
      Never imply Suede can optimize around unclear rights. Rights blockers come first.
      
    • package-standard.md 3.7 KB
      # Suede Transfer Package Standard
      
      ## Goal
      
      Prepare a creator project for clean Suede intake. The package should let a Suede operator or agent quickly understand what the work is, where the files are, what rights data is known, what is missing, and what optimization should happen next.
      
      ## Required Folder Shape
      
      ```text
      suede-transfer-package/
        RIGHTS_PASSPORT.md
        suede-intake.json
        provenance.md
        credits-and-splits.md
        license-notes.md
        optimization-brief.md
        missing-info-report.md
        assets/
          audio/
          stems/
          lyrics/
          artwork/
          docs/
      ```
      
      `assets/` may be empty when the package is manifest-only. A manifest-only package is acceptable if files remain in their original location and `suede-intake.json` records their relative source paths and hashes.
      
      ## Required Reports
      
      `RIGHTS_PASSPORT.md`: Human-readable summary of the work, creator, rights posture, ownership confidence, known contributors, known releases, and Suede intake readiness.
      
      `suede-intake.json`: Agent-readable manifest. New packages use schema 0.2.0,
      the machine contract in `assets/suede-intake.schema.json`, and the field guide
      in `references/intake-schema.md`.
      
      `provenance.md`: Chain-of-custody notes, creation history, source folder, file hashes, upload/export history, and registry/readiness notes.
      
      `credits-and-splits.md`: Contributors, roles, publishers, labels, managers, split percentages, wallet/payment details if provided, and confirmation status.
      
      `license-notes.md`: Samples, interpolations, covers, beat leases, sync/master licenses, platform releases, takedown risks, and usage restrictions.
      
      `optimization-brief.md`: Recommended Suede next actions, such as mastering, stems, lyric sync, metadata cleanup, artwork polish, rights registration, licensing setup, or agent commerce packaging.
      
      `missing-info-report.md`: Blockers and unresolved questions. Include this even when there are no blockers.
      
      ## Risk Labels
      
      Use plain labels consistently:
      
      - `low`: information is complete enough for intake and no obvious rights blockers are visible.
      - `medium`: package can move forward, but some contributor, file, metadata, or release facts need confirmation.
      - `high`: do not optimize/register/license until the user resolves a rights, sample, collaborator, or ownership issue.
      - `unknown`: not enough information to rate.
      
      ## Language Rules
      
      Say "Suede-ready transfer package" or "prepared for Suede intake." Avoid saying "ownership transfer" unless the user is explicitly discussing legal assignment. The public skill prepares materials for Suede's workflow; it does not perform legal transfer or rights clearance.
      
      Use "needs creator confirmation" instead of guessing.
      
      ## Intake Quality Bar
      
      A strong package answers:
      
      - What is the project or work?
      - Who created it?
      - Which files are originals, masters, stems, lyrics, artwork, and documents?
      - Who contributed and what are their roles?
      - What rights are confirmed, disputed, licensed, or unknown?
      - What release history exists?
      - What should Suede optimize first?
      - What facts must be confirmed before registry, licensing, royalty routing, or agent commerce?
      
      It also keeps unlike industry objects separate:
      
      - musical work/composition and its ISWC, writers, publishers, and composition shares;
      - sound recording/master and its ISRC, performers, master controllers, and master shares;
      - release/product and its UPC/EAN or catalog number, recordings, label, distributor, date, and territories;
      - parties and their roles, IPI/CAE or ISNI identifiers, evidence, and privacy state;
      - licenses, third-party material, voice/likeness or AI-use consent, and chain-of-title evidence.
      
      Before an external exchange, run the current validator in strict mode and read
      `references/ddex-c2pa-crosswalk.md`. A crosswalk is not receiver conformance.
      
    • passport-context.md 782 B
      # Suede Creator Passport Context
      
      The Suede Creator Passport is a forward-looking Suede-native record of creator
      work: registered IP, declared rights, release linting, rights packages, signed
      transfers, and other verified Suede activity.
      
      This public skill does not issue stamps, allocate rewards, gate access, write to
      a registry, or promise legal clearance. It creates offline transfer-package
      artifacts that Suede can review later:
      
      - `RIGHTS_PASSPORT.md`
      - `suede-intake.json`
      - `provenance.md`
      - `credits-and-splits.md`
      - `license-notes.md`
      - `optimization-brief.md`
      - `missing-info-report.md`
      
      Use careful language. A completed package means the creator organized the work
      for review. It does not mean the work has been cleared, registered, licensed, or
      approved for payouts.
      
  • scripts
    • fixtures
      • sample-blocked-package
        • assets
          • audio
            • Neon Static - Rough Mix.mp3 2.8 KB · in bundle
          • docs
            • metadata.json 1.6 KB
              {
                "project": {
                  "title": "Neon Static",
                  "artist_name": "Rae Marlowe",
                  "work_type": "song",
                  "description": "Synthetic test fixture: a fictional single with unconfirmed ownership, unconfirmed splits, and an uncleared sample.",
                  "release_status": "unknown",
                  "public_urls": []
                },
                "creator": {
                  "name": "Rae Marlowe",
                  "email": "rae.marlowe.synthetic@example.com",
                  "wallet_address": "",
                  "organization": "",
                  "confirmation_status": "needs-confirmation"
                },
                "rights": {
                  "owner_claim": "Rae Marlowe believes they own it, but a second collaborator (Kai) never signed a split sheet and may have a competing claim",
                  "ownership_status": "disputed",
                  "contributors": [
                    {
                      "name": "Rae Marlowe",
                      "role": "Songwriter/Producer",
                      "master_percent": 70,
                      "publishing_percent": 70,
                      "confirmed": false
                    },
                    {
                      "name": "Kai (last name unknown)",
                      "role": "Co-writer, unconfirmed",
                      "master_percent": 30,
                      "publishing_percent": 30,
                      "confirmed": false
                    }
                  ],
                  "contributors_confirmed": false,
                  "splits_confirmed": false,
                  "contains_samples": "yes",
                  "sample_clearance_status": "uncleared",
                  "cover_or_interpolation": "unknown",
                  "license_restrictions": []
                },
                "provenance": {
                  "creation_notes": "Synthetic test fixture created for validate_transfer_package.py testing. Track includes a vocal chop from an unidentified source; second collaborator's involvement and split were never formally confirmed."
                },
                "suede": {
                  "optimization_notes": "Synthetic fixture: rights review required before any further packaging."
                }
              }
              
            • notes-do-not-share.txt 320 B
              Internal notes (synthetic placeholder):
              
              - Track loops a vocal chop pulled from a random YouTube video, source unknown.
              - Never got a split sheet signed with the second collaborator (Kai).
              - Not sure if Kai's manager already registered a competing claim somewhere.
              - Ownership is genuinely unclear pending legal review.
              
          • lyrics
            • lyrics-draft.txt 244 B
              Neon Static - Lyrics Draft (synthetic placeholder)
              
              [Verse 1]
              This is placeholder lyric text for a synthetic test fixture.
              Contains a chopped vocal sample from an unknown/uncleared source (see notes).
              
              [Hook]
              Synthetic test data, nothing more.
              
        • credits-and-splits.md 772 B
          
          # Credits And Splits
          
          Private draft: contributor, split, wallet, and organization notes may be
          sensitive. Review before sharing.
          
          ## Contributors
          
          | Name | Role | Master % | Publishing % | Confirmation |
          | --- | --- | ---: | ---: | --- |
          | Rae Marlowe | Songwriter/Producer | 70 | 70 | needs creator confirmation |
          | Kai (last name unknown) | Co-writer, unconfirmed | 30 | 30 | needs creator confirmation |
          
          
          ## Organizations
          
          | Name | Role | Notes |
          | --- | --- | --- |
          | unknown | owner / label | needs creator confirmation |
          
          ## Payment / Wallet Notes
          
          - Creator wallet: unknown
          - Contributor wallets: unknown
          - Royalty routing readiness: not ready until splits are confirmed
          
          ## Blockers
          
          - Contributor list needs confirmation.
          - Split percentages need confirmation.
          
        • license-notes.md 665 B
          
          # License Notes
          
          Private draft: rights, restrictions, and third-party material notes may be
          sensitive. Review before sharing.
          
          ## Third-Party Material
          
          - Samples: yes
          - Loops: unknown
          - Interpolations: unknown
          - Covers: unknown
          - Beat leases: unknown
          
          ## Existing Releases
          
          - DSP release: unknown
          - Social/video platform release: unknown
          - Prior mint/registry/license: unknown
          - Existing takedowns/disputes: unknown
          
          ## Restrictions
          
          No restrictions confirmed. Treat as unknown until creator confirms.
          
          ## Clearance Status
          
          Sample clearance status: uncleared.
          High-confidence licensing should wait for creator/legal confirmation when any rights fact is uncertain.
          
        • missing-info-report.md 1.7 KB
          
          # Missing Info Report
          
          Private draft: unresolved rights and creator questions may be sensitive. Review
          before sharing.
          
          ## Summary
          
          Missing information must be resolved before Suede can confidently register, license, route royalties for, or expose the work to agent commerce.
          
          ## Questions
          
          | Severity | Field | Question | Blocks |
          | --- | --- | --- | --- |
          | high | `rights.owner_claim` | Who owns the master and publishing rights? | registry, licensing, royalty-routing, agent-commerce |
          | high | `credits.contributors` | Who contributed to the work, what were their roles, and are splits confirmed? | royalty-routing, licensing |
          | high | `rights.sample_clearance_status` | Are all samples, loops, interpolations, or third-party beats cleared? | licensing, registry |
          | medium | `project.release_status` | Has the work already been released, registered, minted, licensed, or sold? | registry, licensing, catalog-discovery |
          
          ## Risk Flags
          
          | Severity | Label | Detail | Action |
          | --- | --- | --- | --- |
          | high | ownership-unconfirmed | Owner claim has not been confirmed by the creator. | Confirm master and publishing ownership before registry, licensing, or royalty routing. |
          | high | contributors-unconfirmed | Contributor list and splits are not confirmed. | Collect contributor roles and split confirmations. |
          | high | sample-clearance-unconfirmed | Samples or third-party material are indicated, but clearance is not confirmed. | Collect clearance records or remove uncleared material before licensing. |
          | medium | stems-not-found | No stems were detected. | Ask whether stems exist or use Suede stem preparation during optimization. |
          
          ## Status
          
          Not ready for final Suede intake until high-severity questions are answered.
          
        • optimization-brief.md 1.4 KB
          
          # Optimization Brief
          
          Private draft: review rights and creator details before sharing outside the
          intended Suede workflow.
          
          ## Goal
          
          Prepare `Neon Static` for Suede optimization after rights and file review.
          
          ## Recommended Next Actions
          
          1. rights-review - high priority
             Reason: Ownership, contributor, sample, and release facts need confirmation.
             Blocks: registry, licensing, royalty-routing, agent-commerce
          2. provenance-cleanup - high priority
             Reason: File hashes are present, but creation history and chain-of-custody need creator notes.
             Blocks: registry
          3. mastering-or-wav-review - normal priority
             Reason: Primary audio was found and can be reviewed for final delivery quality.
             Blocks: none
          4. stem-separation - normal priority
             Reason: No stems were detected; stems can improve licensing, remix, and derivative workflows.
             Blocks: none
          5. artwork-preparation - low priority
             Reason: No artwork was detected.
             Blocks: none
          
          ## Candidate Suede Services
          
          - Mastering / WAV export: available for review
          - Stem separation: recommended if audio rights are confirmed
          - Lyric sync: lyrics detected
          - Artwork polish: needs artwork
          - Registry readiness: needs rights review
          - Royalty routing: needs split confirmation
          - License packaging: needs restrictions and clearance details
          - Agent commerce packaging: needs usage terms and rights confidence
          
          ## Operator Notes
          
          Review `missing-info-report.md` before beginning optimization.
          
        • provenance.md 1.5 KB
          
          # Provenance
          
          Private draft: file names, hashes, source notes, and creator context may be
          sensitive. Review before sharing.
          
          ## Source
          
          - Source folder: synthetic-blocked-project
          - Metadata source: metadata.json
          - Package generated: 2026-07-03T03:18:00.964804+00:00
          - Generator: suede-rights-passport
          
          ## Creation History
          
          Synthetic test fixture created for validate_transfer_package.py testing. Track includes a vocal chop from an unidentified source; second collaborator's involvement and split were never formally confirmed.
          
          ## Chain Of Custody
          
          | Date | Event | Actor | Evidence |
          | --- | --- | --- | --- |
          | 2026-07-03 | Source folder inventoried for Suede transfer package. | suede-rights-passport | local file hashes |
          
          ## File Hashes
          
          | ID | Category | Role | Path | SHA-256 |
          | --- | --- | --- | --- | --- |
          | asset-001 | audio | mix | `assets/audio/Neon Static - Rough Mix.mp3` | `d2b7ea13578c84c374b8ce5d68a902d15da7c7f9a456e60cdf89a80ff5892e7b` |
          | asset-002 | lyrics | lyrics | `assets/lyrics/lyrics-draft.txt` | `6a83b7adc23c591455cf60dcf23e3e417864c8764b8a6239c0f20d6e05e33ed1` |
          | asset-003 | document | document | `assets/docs/metadata.json` | `11c144f56211206a92c151a7e32e35a9b2b12b39a766e57a4fc1c728f02e9c58` |
          | asset-004 | document | document | `assets/docs/notes-do-not-share.txt` | `23d039a38e52784fa32ce87c28ef2000ac4e923a94345b78537ec87b95c5fcd1` |
          
          ## Registry Notes
          
          - Registry status: ready-for-review
          - Asset hash selected for registry: needs Suede review
          - Notes: do not treat hash inventory as legal rights clearance.
          
        • RIGHTS_PASSPORT.md 2 KB
          
          # Rights Passport
          
          Private draft: review and redact before publishing, committing, or sharing
          outside the intended Suede intake workflow.
          
          ## Work Summary
          
          - Title: Neon Static
          - Artist / creator: Rae Marlowe
          - Work type: song
          - Release status: unknown
          - Package status: draft for Suede intake
          
          ## Intake Readiness
          
          - Overall risk: high until listed risk flags are resolved.
          - Registry readiness: ready for review, not final clearance.
          - Royalty routing readiness: blocked until splits are confirmed.
          - Licensing readiness: blocked until high-severity rights flags are resolved.
          - Agent commerce readiness: blocked until rights and usage terms are confirmed.
          
          ## Rights Snapshot
          
          - Owner claim: Rae Marlowe believes they own it, but a second collaborator (Kai) never signed a split sheet and may have a competing claim
          - Ownership status: disputed
          - Contributor list confirmed: no
          - Splits confirmed: no
          - Samples / interpolations / covers: yes / unknown
          - Sample clearance status: uncleared
          - Existing licenses or restrictions: unknown
          
          ## Asset Snapshot
          
          | Category | Count |
          | --- | ---: |
          | Audio | 1 |
          | Stems | 0 |
          | Lyrics | 1 |
          | Artwork | 0 |
          | Documents | 2 |
          | Video | 0 |
          | Other | 0 |
          
          ## Assets
          
          | ID | Category | Role | Path | SHA-256 |
          | --- | --- | --- | --- | --- |
          | asset-001 | audio | mix | `assets/audio/Neon Static - Rough Mix.mp3` | `d2b7ea13578c84c374b8ce5d68a902d15da7c7f9a456e60cdf89a80ff5892e7b` |
          | asset-002 | lyrics | lyrics | `assets/lyrics/lyrics-draft.txt` | `6a83b7adc23c591455cf60dcf23e3e417864c8764b8a6239c0f20d6e05e33ed1` |
          | asset-003 | document | document | `assets/docs/metadata.json` | `11c144f56211206a92c151a7e32e35a9b2b12b39a766e57a4fc1c728f02e9c58` |
          | asset-004 | document | document | `assets/docs/notes-do-not-share.txt` | `23d039a38e52784fa32ce87c28ef2000ac4e923a94345b78537ec87b95c5fcd1` |
          
          ## Suede Next Step
          
          Resolve the high-severity questions in `missing-info-report.md`, then route the package into Suede rights review and media optimization.
          
        • suede-intake.json 6.8 KB
          {
            "schema_version": "0.1.0",
            "package_type": "suede-transfer-package",
            "generated_at": "2026-07-03T03:18:00.964804+00:00",
            "metadata_source": "metadata.json",
            "project": {
              "title": "Neon Static",
              "artist_name": "Rae Marlowe",
              "work_type": "song",
              "description": "Synthetic test fixture: a fictional single with unconfirmed ownership, unconfirmed splits, and an uncleared sample.",
              "release_status": "unknown",
              "public_urls": []
            },
            "creator": {
              "name": "Rae Marlowe",
              "email": "rae.marlowe.synthetic@example.com",
              "wallet_address": "",
              "organization": "",
              "confirmation_status": "needs-confirmation"
            },
            "assets": [
              {
                "id": "asset-001",
                "relative_path": "assets/audio/Neon Static - Rough Mix.mp3",
                "original_path": "Neon Static - Rough Mix.mp3",
                "category": "audio",
                "role": "mix",
                "mime_guess": "audio/mpeg",
                "size_bytes": 2900,
                "sha256": "d2b7ea13578c84c374b8ce5d68a902d15da7c7f9a456e60cdf89a80ff5892e7b",
                "notes": ""
              },
              {
                "id": "asset-002",
                "relative_path": "assets/lyrics/lyrics-draft.txt",
                "original_path": "lyrics-draft.txt",
                "category": "lyrics",
                "role": "lyrics",
                "mime_guess": "text/plain",
                "size_bytes": 244,
                "sha256": "6a83b7adc23c591455cf60dcf23e3e417864c8764b8a6239c0f20d6e05e33ed1",
                "notes": ""
              },
              {
                "id": "asset-003",
                "relative_path": "assets/docs/metadata.json",
                "original_path": "metadata.json",
                "category": "document",
                "role": "document",
                "mime_guess": "application/json",
                "size_bytes": 1683,
                "sha256": "11c144f56211206a92c151a7e32e35a9b2b12b39a766e57a4fc1c728f02e9c58",
                "notes": ""
              },
              {
                "id": "asset-004",
                "relative_path": "assets/docs/notes-do-not-share.txt",
                "original_path": "notes-do-not-share.txt",
                "category": "document",
                "role": "document",
                "mime_guess": "text/plain",
                "size_bytes": 320,
                "sha256": "23d039a38e52784fa32ce87c28ef2000ac4e923a94345b78537ec87b95c5fcd1",
                "notes": ""
              }
            ],
            "rights": {
              "owner_claim": "Rae Marlowe believes they own it, but a second collaborator (Kai) never signed a split sheet and may have a competing claim",
              "ownership_status": "disputed",
              "contributors": [
                {
                  "name": "Rae Marlowe",
                  "role": "Songwriter/Producer",
                  "master_percent": 70,
                  "publishing_percent": 70,
                  "confirmed": false
                },
                {
                  "name": "Kai (last name unknown)",
                  "role": "Co-writer, unconfirmed",
                  "master_percent": 30,
                  "publishing_percent": 30,
                  "confirmed": false
                }
              ],
              "contributors_confirmed": false,
              "splits_confirmed": false,
              "contains_samples": "yes",
              "sample_clearance_status": "uncleared",
              "cover_or_interpolation": "unknown",
              "license_restrictions": []
            },
            "provenance": {
              "source_root": "synthetic-blocked-project",
              "metadata_source": "metadata.json",
              "creation_notes": "Synthetic test fixture created for validate_transfer_package.py testing. Track includes a vocal chop from an unidentified source; second collaborator's involvement and split were never formally confirmed.",
              "chain_of_custody": [
                {
                  "date": "2026-07-03",
                  "event": "Source folder inventoried for Suede transfer package.",
                  "actor": "suede-rights-passport",
                  "evidence": "local file hashes"
                }
              ],
              "registry_status": "ready-for-review"
            },
            "optimization": {
              "requested_services": [],
              "recommended_services": [
                {
                  "service": "rights-review",
                  "priority": "high",
                  "reason": "Ownership, contributor, sample, and release facts need confirmation.",
                  "blocks": [
                    "registry",
                    "licensing",
                    "royalty-routing",
                    "agent-commerce"
                  ]
                },
                {
                  "service": "provenance-cleanup",
                  "priority": "high",
                  "reason": "File hashes are present, but creation history and chain-of-custody need creator notes.",
                  "blocks": [
                    "registry"
                  ]
                },
                {
                  "service": "mastering-or-wav-review",
                  "priority": "normal",
                  "reason": "Primary audio was found and can be reviewed for final delivery quality.",
                  "blocks": []
                },
                {
                  "service": "stem-separation",
                  "priority": "normal",
                  "reason": "No stems were detected; stems can improve licensing, remix, and derivative workflows.",
                  "blocks": []
                },
                {
                  "service": "artwork-preparation",
                  "priority": "low",
                  "reason": "No artwork was detected.",
                  "blocks": []
                }
              ],
              "priority": "normal",
              "notes": "Synthetic fixture: rights review required before any further packaging."
            },
            "missing_information": [
              {
                "field": "rights.owner_claim",
                "question": "Who owns the master and publishing rights?",
                "blocks": [
                  "registry",
                  "licensing",
                  "royalty-routing",
                  "agent-commerce"
                ],
                "severity": "high"
              },
              {
                "field": "credits.contributors",
                "question": "Who contributed to the work, what were their roles, and are splits confirmed?",
                "blocks": [
                  "royalty-routing",
                  "licensing"
                ],
                "severity": "high"
              },
              {
                "field": "rights.sample_clearance_status",
                "question": "Are all samples, loops, interpolations, or third-party beats cleared?",
                "blocks": [
                  "licensing",
                  "registry"
                ],
                "severity": "high"
              },
              {
                "field": "project.release_status",
                "question": "Has the work already been released, registered, minted, licensed, or sold?",
                "blocks": [
                  "registry",
                  "licensing",
                  "catalog-discovery"
                ],
                "severity": "medium"
              }
            ],
            "risk_flags": [
              {
                "label": "ownership-unconfirmed",
                "severity": "high",
                "detail": "Owner claim has not been confirmed by the creator.",
                "recommended_action": "Confirm master and publishing ownership before registry, licensing, or royalty routing."
              },
              {
                "label": "contributors-unconfirmed",
                "severity": "high",
                "detail": "Contributor list and splits are not confirmed.",
                "recommended_action": "Collect contributor roles and split confirmations."
              },
              {
                "label": "sample-clearance-unconfirmed",
                "severity": "high",
                "detail": "Samples or third-party material are indicated, but clearance is not confirmed.",
                "recommended_action": "Collect clearance records or remove uncleared material before licensing."
              },
              {
                "label": "stems-not-found",
                "severity": "medium",
                "detail": "No stems were detected.",
                "recommended_action": "Ask whether stems exist or use Suede stem preparation during optimization."
              }
            ]
          }
          
      • sample-complete-package
        • assets
          • artwork
            • cover-artwork.png 960 B · in bundle
          • audio
            • Golden Hour Reprise - Master.wav 2.9 KB · in bundle
          • docs
            • metadata.json 1.5 KB
              {
                "project": {
                  "title": "Golden Hour Reprise",
                  "artist_name": "Ari Fontaine",
                  "work_type": "song",
                  "description": "Synthetic test fixture: a fictional single with fully confirmed ownership and splits, no samples.",
                  "release_status": "unreleased",
                  "public_urls": []
                },
                "creator": {
                  "name": "Ari Fontaine",
                  "email": "ari.fontaine.synthetic@example.com",
                  "wallet_address": "",
                  "organization": "",
                  "confirmation_status": "confirmed"
                },
                "rights": {
                  "owner_claim": "Ari Fontaine (master and publishing, per signed split sheet on file)",
                  "ownership_status": "confirmed",
                  "contributors": [
                    {
                      "name": "Ari Fontaine",
                      "role": "Songwriter/Producer",
                      "master_percent": 60,
                      "publishing_percent": 60,
                      "confirmed": true
                    },
                    {
                      "name": "Devon Okafor",
                      "role": "Co-writer/Vocals",
                      "master_percent": 40,
                      "publishing_percent": 40,
                      "confirmed": true
                    }
                  ],
                  "contributors_confirmed": true,
                  "splits_confirmed": true,
                  "contains_samples": "no",
                  "sample_clearance_status": "not-needed",
                  "cover_or_interpolation": "no",
                  "license_restrictions": []
                },
                "provenance": {
                  "creation_notes": "Synthetic test fixture created for validate_transfer_package.py testing. Written and recorded entirely by the two listed contributors; no third-party material used."
                },
                "suede": {
                  "optimization_notes": "Synthetic fixture: ready for rights review pass."
                }
              }
              
            • split-sheet.txt 376 B
              Golden Hour Reprise - Split Sheet (synthetic placeholder)
              
              Ari Fontaine - Songwriter/Producer - Master 60% / Publishing 60% - Confirmed in writing
              Devon Okafor - Co-writer/Vocals - Master 40% / Publishing 40% - Confirmed in writing
              
              Total Master: 100%
              Total Publishing: 100%
              Both parties signed split confirmation on file (synthetic test fixture, no real signature attached).
              
          • lyrics
            • lyrics.txt 213 B
              Golden Hour Reprise - Lyrics (synthetic placeholder)
              
              [Verse 1]
              This is placeholder lyric text for a synthetic test fixture.
              No real creative work is represented here.
              
              [Chorus]
              Synthetic test data, nothing more.
              
          • stems
            • Golden Hour Reprise - Instrumental Stem.wav 1.8 KB · in bundle
            • Golden Hour Reprise - Vocals Stem.wav 1.6 KB · in bundle
        • credits-and-splits.md 685 B
          
          # Credits And Splits
          
          Private draft: contributor, split, wallet, and organization notes may be
          sensitive. Review before sharing.
          
          ## Contributors
          
          | Name | Role | Master % | Publishing % | Confirmation |
          | --- | --- | ---: | ---: | --- |
          | Ari Fontaine | Songwriter/Producer | 60 | 60 | confirmed |
          | Devon Okafor | Co-writer/Vocals | 40 | 40 | confirmed |
          
          
          ## Organizations
          
          | Name | Role | Notes |
          | --- | --- | --- |
          | unknown | owner / label | needs creator confirmation |
          
          ## Payment / Wallet Notes
          
          - Creator wallet: unknown
          - Contributor wallets: unknown
          - Royalty routing readiness: ready for review
          
          ## Blockers
          
          - Contributor list confirmed.
          - Split percentages confirmed.
          
        • license-notes.md 658 B
          
          # License Notes
          
          Private draft: rights, restrictions, and third-party material notes may be
          sensitive. Review before sharing.
          
          ## Third-Party Material
          
          - Samples: no
          - Loops: unknown
          - Interpolations: no
          - Covers: no
          - Beat leases: unknown
          
          ## Existing Releases
          
          - DSP release: unreleased
          - Social/video platform release: unknown
          - Prior mint/registry/license: unknown
          - Existing takedowns/disputes: unknown
          
          ## Restrictions
          
          No restrictions confirmed. Treat as unknown until creator confirms.
          
          ## Clearance Status
          
          Sample clearance status: not-needed.
          High-confidence licensing should wait for creator/legal confirmation when any rights fact is uncertain.
          
        • missing-info-report.md 549 B
          
          # Missing Info Report
          
          Private draft: unresolved rights and creator questions may be sensitive. Review
          before sharing.
          
          ## Summary
          
          Missing information must be resolved before Suede can confidently register, license, route royalties for, or expose the work to agent commerce.
          
          ## Questions
          
          | Severity | Field | Question | Blocks |
          | --- | --- | --- | --- |
          
          ## Risk Flags
          
          | Severity | Label | Detail | Action |
          | --- | --- | --- | --- |
          
          ## Status
          
          No outstanding missing-information questions or risk flags. Ready for final Suede intake review.
          
        • optimization-brief.md 1.2 KB
          
          # Optimization Brief
          
          Private draft: review rights and creator details before sharing outside the
          intended Suede workflow.
          
          ## Goal
          
          Prepare `Golden Hour Reprise` for Suede optimization after rights and file review.
          
          ## Recommended Next Actions
          
          1. rights-review - high priority
             Reason: Ownership, contributor, sample, and release facts need confirmation.
             Blocks: registry, licensing, royalty-routing, agent-commerce
          2. provenance-cleanup - high priority
             Reason: File hashes are present, but creation history and chain-of-custody need creator notes.
             Blocks: registry
          3. mastering-or-wav-review - normal priority
             Reason: Primary audio was found and can be reviewed for final delivery quality.
             Blocks: none
          
          ## Candidate Suede Services
          
          - Mastering / WAV export: available for review
          - Stem separation: stems already detected
          - Lyric sync: lyrics detected
          - Artwork polish: artwork detected
          - Registry readiness: needs rights review
          - Royalty routing: needs split confirmation
          - License packaging: needs restrictions and clearance details
          - Agent commerce packaging: needs usage terms and rights confidence
          
          ## Operator Notes
          
          Review `missing-info-report.md` before beginning optimization.
          
        • provenance.md 1.9 KB
          
          # Provenance
          
          Private draft: file names, hashes, source notes, and creator context may be
          sensitive. Review before sharing.
          
          ## Source
          
          - Source folder: synthetic-complete-project
          - Metadata source: metadata.json
          - Package generated: 2026-07-03T03:17:54.561582+00:00
          - Generator: suede-rights-passport
          
          ## Creation History
          
          Synthetic test fixture created for validate_transfer_package.py testing. Written and recorded entirely by the two listed contributors; no third-party material used.
          
          ## Chain Of Custody
          
          | Date | Event | Actor | Evidence |
          | --- | --- | --- | --- |
          | 2026-07-03 | Source folder inventoried for Suede transfer package. | suede-rights-passport | local file hashes |
          
          ## File Hashes
          
          | ID | Category | Role | Path | SHA-256 |
          | --- | --- | --- | --- | --- |
          | asset-001 | audio | master | `assets/audio/Golden Hour Reprise - Master.wav` | `35fd720d072bd35f4ca163bfad51453782a874659323b2a553dcc6da28d46aa1` |
          | asset-002 | artwork | cover-art | `assets/artwork/cover-artwork.png` | `a8f5976b87a9b8e603f3215266baf6c2fb81167fc6b362f3abbd63a419277872` |
          | asset-003 | lyrics | lyrics | `assets/lyrics/lyrics.txt` | `b70ec5ceaf4ad3828c15849f280a0c8ff68bc569fdf1e5b31504b85da33e5ca4` |
          | asset-004 | document | document | `assets/docs/metadata.json` | `4b794bfee0893a61dde60e2c311f1c0f50fea1d315b789efc009ac37ebe04338` |
          | asset-005 | document | split-sheet | `assets/docs/split-sheet.txt` | `5a3b2848b1292a4f0291e67e2ed1e548fe18f13b3ed37b6f4be66e84e9873e69` |
          | asset-006 | stem | stem | `assets/stems/Golden Hour Reprise - Instrumental Stem.wav` | `0a7505b8256ff3613f2c4baf35cd0e4153f4bbead269af01bbd6ab7e2264c94c` |
          | asset-007 | stem | stem | `assets/stems/Golden Hour Reprise - Vocals Stem.wav` | `b6eb13e6377dedc642b6ec3ce256a4b06387a19f7750a4b267861f960e930280` |
          
          ## Registry Notes
          
          - Registry status: ready-for-review
          - Asset hash selected for registry: needs Suede review
          - Notes: do not treat hash inventory as legal rights clearance.
          
        • RIGHTS_PASSPORT.md 2.3 KB
          
          # Rights Passport
          
          Private draft: review and redact before publishing, committing, or sharing
          outside the intended Suede intake workflow.
          
          ## Work Summary
          
          - Title: Golden Hour Reprise
          - Artist / creator: Ari Fontaine
          - Work type: song
          - Release status: unreleased
          - Package status: draft for Suede intake
          
          ## Intake Readiness
          
          - Overall risk: low.
          - Registry readiness: ready for review, not final clearance.
          - Royalty routing readiness: ready for review.
          - Licensing readiness: ready for review.
          - Agent commerce readiness: blocked until rights and usage terms are confirmed.
          
          ## Rights Snapshot
          
          - Owner claim: Ari Fontaine (master and publishing, per signed split sheet on file)
          - Ownership status: confirmed
          - Contributor list confirmed: yes
          - Splits confirmed: yes
          - Samples / interpolations / covers: no / no
          - Sample clearance status: not-needed
          - Existing licenses or restrictions: unknown
          
          ## Asset Snapshot
          
          | Category | Count |
          | --- | ---: |
          | Audio | 1 |
          | Stems | 2 |
          | Lyrics | 1 |
          | Artwork | 1 |
          | Documents | 2 |
          | Video | 0 |
          | Other | 0 |
          
          ## Assets
          
          | ID | Category | Role | Path | SHA-256 |
          | --- | --- | --- | --- | --- |
          | asset-001 | audio | master | `assets/audio/Golden Hour Reprise - Master.wav` | `35fd720d072bd35f4ca163bfad51453782a874659323b2a553dcc6da28d46aa1` |
          | asset-002 | artwork | cover-art | `assets/artwork/cover-artwork.png` | `a8f5976b87a9b8e603f3215266baf6c2fb81167fc6b362f3abbd63a419277872` |
          | asset-003 | lyrics | lyrics | `assets/lyrics/lyrics.txt` | `b70ec5ceaf4ad3828c15849f280a0c8ff68bc569fdf1e5b31504b85da33e5ca4` |
          | asset-004 | document | document | `assets/docs/metadata.json` | `4b794bfee0893a61dde60e2c311f1c0f50fea1d315b789efc009ac37ebe04338` |
          | asset-005 | document | split-sheet | `assets/docs/split-sheet.txt` | `5a3b2848b1292a4f0291e67e2ed1e548fe18f13b3ed37b6f4be66e84e9873e69` |
          | asset-006 | stem | stem | `assets/stems/Golden Hour Reprise - Instrumental Stem.wav` | `0a7505b8256ff3613f2c4baf35cd0e4153f4bbead269af01bbd6ab7e2264c94c` |
          | asset-007 | stem | stem | `assets/stems/Golden Hour Reprise - Vocals Stem.wav` | `b6eb13e6377dedc642b6ec3ce256a4b06387a19f7750a4b267861f960e930280` |
          
          ## Suede Next Step
          
          Resolve the high-severity questions in `missing-info-report.md`, then route the package into Suede rights review and media optimization.
          
        • suede-intake.json 5.4 KB
          {
            "schema_version": "0.1.0",
            "package_type": "suede-transfer-package",
            "generated_at": "2026-07-03T03:17:54.561582+00:00",
            "metadata_source": "metadata.json",
            "project": {
              "title": "Golden Hour Reprise",
              "artist_name": "Ari Fontaine",
              "work_type": "song",
              "description": "Synthetic test fixture: a fictional single with fully confirmed ownership and splits, no samples.",
              "release_status": "unreleased",
              "public_urls": []
            },
            "creator": {
              "name": "Ari Fontaine",
              "email": "ari.fontaine.synthetic@example.com",
              "wallet_address": "",
              "organization": "",
              "confirmation_status": "confirmed"
            },
            "assets": [
              {
                "id": "asset-001",
                "relative_path": "assets/audio/Golden Hour Reprise - Master.wav",
                "original_path": "Golden Hour Reprise - Master.wav",
                "category": "audio",
                "role": "master",
                "mime_guess": "audio/x-wav",
                "size_bytes": 2950,
                "sha256": "35fd720d072bd35f4ca163bfad51453782a874659323b2a553dcc6da28d46aa1",
                "notes": ""
              },
              {
                "id": "asset-002",
                "relative_path": "assets/artwork/cover-artwork.png",
                "original_path": "cover-artwork.png",
                "category": "artwork",
                "role": "cover-art",
                "mime_guess": "image/png",
                "size_bytes": 960,
                "sha256": "a8f5976b87a9b8e603f3215266baf6c2fb81167fc6b362f3abbd63a419277872",
                "notes": ""
              },
              {
                "id": "asset-003",
                "relative_path": "assets/lyrics/lyrics.txt",
                "original_path": "lyrics.txt",
                "category": "lyrics",
                "role": "lyrics",
                "mime_guess": "text/plain",
                "size_bytes": 213,
                "sha256": "b70ec5ceaf4ad3828c15849f280a0c8ff68bc569fdf1e5b31504b85da33e5ca4",
                "notes": ""
              },
              {
                "id": "asset-004",
                "relative_path": "assets/docs/metadata.json",
                "original_path": "metadata.json",
                "category": "document",
                "role": "document",
                "mime_guess": "application/json",
                "size_bytes": 1528,
                "sha256": "4b794bfee0893a61dde60e2c311f1c0f50fea1d315b789efc009ac37ebe04338",
                "notes": ""
              },
              {
                "id": "asset-005",
                "relative_path": "assets/docs/split-sheet.txt",
                "original_path": "split-sheet.txt",
                "category": "document",
                "role": "split-sheet",
                "mime_guess": "text/plain",
                "size_bytes": 376,
                "sha256": "5a3b2848b1292a4f0291e67e2ed1e548fe18f13b3ed37b6f4be66e84e9873e69",
                "notes": ""
              },
              {
                "id": "asset-006",
                "relative_path": "assets/stems/Golden Hour Reprise - Instrumental Stem.wav",
                "original_path": "stems/Golden Hour Reprise - Instrumental Stem.wav",
                "category": "stem",
                "role": "stem",
                "mime_guess": "audio/x-wav",
                "size_bytes": 1840,
                "sha256": "0a7505b8256ff3613f2c4baf35cd0e4153f4bbead269af01bbd6ab7e2264c94c",
                "notes": ""
              },
              {
                "id": "asset-007",
                "relative_path": "assets/stems/Golden Hour Reprise - Vocals Stem.wav",
                "original_path": "stems/Golden Hour Reprise - Vocals Stem.wav",
                "category": "stem",
                "role": "stem",
                "mime_guess": "audio/x-wav",
                "size_bytes": 1600,
                "sha256": "b6eb13e6377dedc642b6ec3ce256a4b06387a19f7750a4b267861f960e930280",
                "notes": ""
              }
            ],
            "rights": {
              "owner_claim": "Ari Fontaine (master and publishing, per signed split sheet on file)",
              "ownership_status": "confirmed",
              "contributors": [
                {
                  "name": "Ari Fontaine",
                  "role": "Songwriter/Producer",
                  "master_percent": 60,
                  "publishing_percent": 60,
                  "confirmed": true
                },
                {
                  "name": "Devon Okafor",
                  "role": "Co-writer/Vocals",
                  "master_percent": 40,
                  "publishing_percent": 40,
                  "confirmed": true
                }
              ],
              "contributors_confirmed": true,
              "splits_confirmed": true,
              "contains_samples": "no",
              "sample_clearance_status": "not-needed",
              "cover_or_interpolation": "no",
              "license_restrictions": []
            },
            "provenance": {
              "source_root": "synthetic-complete-project",
              "metadata_source": "metadata.json",
              "creation_notes": "Synthetic test fixture created for validate_transfer_package.py testing. Written and recorded entirely by the two listed contributors; no third-party material used.",
              "chain_of_custody": [
                {
                  "date": "2026-07-03",
                  "event": "Source folder inventoried for Suede transfer package.",
                  "actor": "suede-rights-passport",
                  "evidence": "local file hashes"
                }
              ],
              "registry_status": "ready-for-review"
            },
            "optimization": {
              "requested_services": [],
              "recommended_services": [
                {
                  "service": "rights-review",
                  "priority": "high",
                  "reason": "Ownership, contributor, sample, and release facts need confirmation.",
                  "blocks": [
                    "registry",
                    "licensing",
                    "royalty-routing",
                    "agent-commerce"
                  ]
                },
                {
                  "service": "provenance-cleanup",
                  "priority": "high",
                  "reason": "File hashes are present, but creation history and chain-of-custody need creator notes.",
                  "blocks": [
                    "registry"
                  ]
                },
                {
                  "service": "mastering-or-wav-review",
                  "priority": "normal",
                  "reason": "Primary audio was found and can be reviewed for final delivery quality.",
                  "blocks": []
                }
              ],
              "priority": "normal",
              "notes": "Synthetic fixture: ready for rights review pass."
            },
            "missing_information": [],
            "risk_flags": []
          }
          
      • README.md 1.4 KB
        # Transfer-package fixtures
        
        Two reference example packages, generated end-to-end by
        `create_transfer_package.py` against synthetic (non-real) creator projects. All
        names, contributors, splits, and metadata in both are fake — no real personal
        data. They exist to sanity-check `create_transfer_package.py` and
        `validate_transfer_package.py` after any change, and to show both ends of the
        risk range.
        
        ## sample-complete-package/
        
        Confirmed ownership, confirmed contributors with matching split percentages, no
        samples. Zero risk flags, zero open missing-information items, validates
        cleanly.
        
        ## sample-blocked-package/
        
        Disputed ownership, unconfirmed contributors and splits, an uncleared sample.
        Three high-severity and one medium-severity risk flag, four open
        missing-information items — still structurally valid, but clearly not ready for
        registry, licensing, or royalty routing.
        
        ## The point
        
        Both fixtures pass `validate_transfer_package.py`; only their risk posture
        differs. That is the design, not a validator bug: structural validity and
        rights confirmation are two independent checks, and `risk_flags[]` and
        `missing_information[]` are exactly where unresolved rights questions are
        supposed to live.
        
        ```bash
        python3 ../validate_transfer_package.py --strict-current ./sample-complete-package
        python3 ../validate_transfer_package.py --strict-current ./sample-blocked-package
        ```
        
    • create_transfer_package.py 58.8 KB
      #!/usr/bin/env python3
      """Create a Suede-ready rights passport transfer package from a source folder."""
      
      from __future__ import annotations
      
      import argparse
      import hashlib
      import json
      import mimetypes
      import shutil
      from collections import Counter
      from datetime import datetime, timezone
      from pathlib import Path
      from typing import Any
      
      
      AUDIO_EXTS = {".wav", ".mp3", ".aiff", ".aif", ".flac", ".m4a", ".aac", ".ogg", ".opus"}
      MAX_DESTINATION_SUFFIX_ATTEMPTS = 10_000
      VIDEO_EXTS = {".mp4", ".mov", ".m4v", ".webm", ".avi", ".mkv"}
      IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".tif", ".tiff"}
      LYRIC_EXTS = {".lrc", ".srt", ".vtt"}
      DOC_EXTS = {
          ".pdf",
          ".doc",
          ".docx",
          ".rtf",
          ".txt",
          ".md",
          ".csv",
          ".tsv",
          ".json",
          ".xls",
          ".xlsx",
      }
      
      SKIP_NAMES = {".ds_store", "thumbs.db"}
      DENY_DIR_NAMES = {
          ".aws",
          ".azure",
          ".cache",
          ".config",
          ".docker",
          ".gnupg",
          ".gcloud",
          ".git",
          ".hg",
          ".kube",
          ".mypy_cache",
          ".next",
          ".nuxt",
          ".pytest_cache",
          ".ruff_cache",
          ".ssh",
          ".svn",
          ".turbo",
          ".venv",
          "__pycache__",
          "build",
          "coverage",
          "dist",
          "env",
          "node_modules",
          "venv",
      }
      DENY_FILENAMES = {
          ".env",
          ".env.development",
          ".env.local",
          ".env.production",
          ".env.test",
          ".envrc",
          ".netrc",
          ".npmrc",
          ".pypirc",
          "credentials",
          "credentials.json",
          "id_ed25519",
          "id_rsa",
          "kubeconfig",
          "secrets.json",
          "wallet.json",
      }
      DENY_SUFFIXES = {".env", ".key", ".pem", ".p12", ".pfx"}
      SECRET_NAME_TOKENS = {
          "access_token",
          "api_key",
          "apikey",
          "auth_token",
          "client_secret",
          "credential",
          "credentials",
          "password",
          "private_key",
          "private-key",
          "privatekey",
          "refresh_token",
          "secret",
          "seed_phrase",
          "seed-phrase",
          "service_account",
      }
      GENERATED_FILENAMES = {
          "RIGHTS_PASSPORT.md",
          "credits-and-splits.md",
          "license-notes.md",
          "missing-info-report.md",
          "optimization-brief.md",
          "provenance.md",
          "suede-intake.json",
      }
      METADATA_CANDIDATES = {
          "metadata.json",
          "release.json",
          "suede-intake.json",
          "metadata.yaml",
          "metadata.yml",
          "release.yaml",
          "release.yml",
      }
      STEM_KEYWORDS = {
          "stem": "stem",
          "vocals": "vocals",
          "vocal": "vocals",
          "vox": "vocals",
          "drums": "drums",
          "drum": "drums",
          "bass": "bass",
          "guitar": "guitar",
          "keys": "keys",
          "piano": "piano",
          "instrumental": "instrumental",
          "inst": "instrumental",
      }
      
      
      def parse_args() -> argparse.Namespace:
          parser = argparse.ArgumentParser(
              description="Inventory a creator project and create a Suede transfer package."
          )
          parser.add_argument("source", help="Source folder containing creator assets.")
          parser.add_argument(
              "--output",
              default="suede-transfer-package",
              help="Output package folder. Defaults to ./suede-transfer-package. Must be outside the source folder.",
          )
          parser.add_argument("--project-title", default="", help="Project or work title.")
          parser.add_argument("--artist", default="", help="Artist or creator name.")
          parser.add_argument(
              "--metadata",
              default="",
              help="Optional metadata file: JSON, YAML/YML with PyYAML, or key=value text.",
          )
          parser.add_argument(
              "--copy-assets",
              action="store_true",
              help="Copy inventoried files into the transfer package assets/ folders.",
          )
          parser.add_argument(
              "--include-hidden",
              action="store_true",
              help="Include hidden files and folders. Secret-like files are still skipped.",
          )
          parser.add_argument(
              "--include-other",
              action="store_true",
              help="Include unrecognized file types in the inventory and optional asset copy.",
          )
          parser.add_argument(
              "--include-absolute-paths",
              action="store_true",
              help="Write absolute local paths into reports. Off by default for public-safe sharing.",
          )
          parser.add_argument(
              "--force",
              action="store_true",
              help="Overwrite existing generated package files in the output folder.",
          )
          return parser.parse_args()
      
      
      def load_yaml_if_available(text: str) -> dict[str, Any]:
          try:
              import yaml  # type: ignore
          except Exception as exc:  # pragma: no cover - dependency-dependent
              raise ValueError("YAML metadata requires PyYAML") from exc
          data = yaml.safe_load(text)
          return data if isinstance(data, dict) else {}
      
      
      def load_key_value(text: str) -> dict[str, Any]:
          data: dict[str, Any] = {}
          for raw_line in text.splitlines():
              line = raw_line.strip()
              if not line or line.startswith("#"):
                  continue
              if "=" not in line:
                  continue
              key, value = line.split("=", 1)
              data[key.strip()] = value.strip().strip('"').strip("'")
          return data
      
      
      def load_metadata(path: Path | None) -> dict[str, Any]:
          if path is None:
              return {}
          text = path.read_text(encoding="utf-8")
          suffix = path.suffix.lower()
          if suffix == ".json":
              data = json.loads(text)
              if not isinstance(data, dict):
                  raise ValueError(f"Metadata JSON must be an object: {path}")
              return data
          if suffix in {".yaml", ".yml"}:
              return load_yaml_if_available(text)
          return load_key_value(text)
      
      
      def discover_metadata(source: Path, explicit: str) -> Path | None:
          if explicit:
              raw_path = Path(explicit).expanduser()
              if raw_path.is_symlink():
                  raise SystemExit("Refusing symlinked metadata input. Pass the real public-safe file path.")
              path = raw_path.resolve()
              if not path.exists():
                  raise SystemExit(f"Metadata file not found: {path}")
              if is_denied_path(path, source):
                  raise SystemExit("Refusing to use a secret-like metadata path. Use a public-safe metadata JSON, YAML, or key=value text file.")
              return path
          for name in METADATA_CANDIDATES:
              candidate = source / name
              if candidate.is_symlink():
                  raise SystemExit(f"Refusing symlinked metadata input: {candidate}")
              if candidate.exists() and candidate.is_file():
                  return candidate
          return None
      
      
      def scoped_value(metadata: dict[str, Any], scope_name: str, keys: str | list[str], default: Any = None) -> Any:
          aliases = [keys] if isinstance(keys, str) else keys
          scoped = metadata.get(scope_name)
          if isinstance(scoped, dict):
              for key in aliases:
                  if key in scoped and scoped[key] not in (None, ""):
                      return scoped[key]
          for key in aliases:
              if key in metadata and metadata[key] not in (None, ""):
                  return metadata[key]
          return default
      
      
      def project_value(metadata: dict[str, Any], keys: str | list[str], default: Any = None) -> Any:
          return scoped_value(metadata, "project", keys, default)
      
      
      def creator_value(metadata: dict[str, Any], keys: str | list[str], default: Any = None) -> Any:
          return scoped_value(metadata, "creator", keys, default)
      
      
      def rights_value(metadata: dict[str, Any], keys: str | list[str], default: Any = None) -> Any:
          return scoped_value(metadata, "rights", keys, default)
      
      
      def suede_value(metadata: dict[str, Any], keys: str | list[str], default: Any = None) -> Any:
          return scoped_value(metadata, "suede", keys, default)
      
      
      def boolish(value: Any) -> bool:
          if isinstance(value, bool):
              return value
          return str(value).strip().lower() in {"1", "true", "yes", "y", "confirmed", "claimed", "cleared"}
      
      
      def confirmed_status(value: Any) -> bool:
          return boolish(value) or str(value).strip().lower() in {"confirmed", "claimed", "cleared"}
      
      
      def unknown_status(value: Any) -> bool:
          return str(value).strip().lower() in {"", "unknown", "unconfirmed", "needs-confirmation", "needs_confirmation"}
      
      
      def positive_status(value: Any) -> bool:
          return str(value).strip().lower() in {"1", "true", "yes", "y", "contains-samples", "contains_samples", "sampled"}
      
      
      def list_value(value: Any) -> list[Any]:
          if value in (None, ""):
              return []
          if isinstance(value, list):
              return value
          if isinstance(value, tuple):
              return list(value)
          if isinstance(value, str):
              return [item.strip() for item in value.split(",") if item.strip()]
          return [value]
      
      
      def string_list(value: Any) -> list[str]:
          return [str(item) for item in list_value(value)]
      
      
      def nested_value(
          metadata: dict[str, Any], scope_names: list[str], keys: str | list[str], default: Any = None
      ) -> Any:
          """Read the first non-empty value from one of several metadata scopes."""
          aliases = [keys] if isinstance(keys, str) else keys
          for scope_name in scope_names:
              scoped = metadata.get(scope_name)
              if not isinstance(scoped, dict):
                  continue
              for key in aliases:
                  if key in scoped and scoped[key] not in (None, ""):
                      return scoped[key]
          for key in aliases:
              if key in metadata and metadata[key] not in (None, ""):
                  return metadata[key]
          return default
      
      
      def evidence_status(value: Any, evidence: list[str]) -> str:
          """Normalize evidence state without upgrading an unsupported claim."""
          normalized = str(value or "unknown").strip().lower().replace("_", "-")
          if normalized in {"disputed", "conflicted"}:
              return "disputed"
          if normalized in {"unconfirmed", "needs-confirmation", "pending"}:
              return "unconfirmed"
          if normalized in {"claimed", "asserted"}:
              return "claimed"
          if normalized in {"confirmed", "verified", "registered", "true", "yes", "1"}:
              return "confirmed" if evidence else "claimed"
          return "unknown"
      
      
      def make_identifier_records(
          metadata: dict[str, Any], scope_names: list[str], definitions: list[tuple[str, list[str]]]
      ) -> list[dict[str, Any]]:
          records: list[dict[str, Any]] = []
          for scheme, aliases in definitions:
              raw = nested_value(metadata, scope_names, aliases, None)
              for value in list_value(raw):
                  if isinstance(value, dict):
                      identifier_value = str(value.get("value", "")).strip()
                      evidence = string_list(value.get("evidence_refs", value.get("evidence", [])))
                      status = evidence_status(value.get("status", "claimed"), evidence)
                  else:
                      identifier_value = str(value).strip()
                      evidence = string_list(
                          nested_value(
                              metadata,
                              scope_names,
                              [f"{aliases[0]}_evidence_refs", f"{aliases[0]}_evidence"],
                              [],
                          )
                      )
                      status = evidence_status(
                          nested_value(metadata, scope_names, [f"{aliases[0]}_status"], "claimed"),
                          evidence,
                      )
                  if identifier_value:
                      records.append(
                          {
                              "scheme": scheme,
                              "value": identifier_value,
                              "status": status,
                              "evidence_refs": evidence,
                          }
                      )
          return records
      
      
      def normalize_parties(contributors: list[Any]) -> tuple[list[dict[str, Any]], dict[int, str]]:
          parties: list[dict[str, Any]] = []
          party_ids: dict[int, str] = {}
          for index, contributor in enumerate(contributors):
              party_id = f"party-{index + 1:03d}"
              party_ids[index] = party_id
              if isinstance(contributor, dict):
                  evidence = string_list(
                      contributor.get("evidence_refs", contributor.get("confirmation_evidence", []))
                  )
                  raw_status = contributor.get(
                      "confirmation_status", "confirmed" if contributor.get("confirmed") else "unconfirmed"
                  )
                  roles = string_list(contributor.get("roles", contributor.get("role", [])))
                  identifiers = []
                  for scheme, keys in (
                      ("IPI_CAE", ("ipi_cae", "ipi", "cae")),
                      ("ISNI", ("isni",)),
                  ):
                      value = next((contributor.get(key) for key in keys if contributor.get(key)), None)
                      if value:
                          identifiers.append(
                              {
                                  "scheme": scheme,
                                  "value": str(value),
                                  "status": evidence_status(contributor.get("identifier_status", "claimed"), evidence),
                                  "evidence_refs": evidence,
                              }
                          )
                  parties.append(
                      {
                          "id": party_id,
                          "name": str(contributor.get("name", "unknown")),
                          "roles": roles or ["unknown"],
                          "identifiers": identifiers,
                          "organization": str(contributor.get("organization", "")),
                          "status": evidence_status(raw_status, evidence),
                          "evidence_refs": evidence,
                          "privacy_classification": str(
                              contributor.get("privacy_classification", "private-draft")
                          ),
                      }
                  )
              else:
                  parties.append(
                      {
                          "id": party_id,
                          "name": str(contributor),
                          "roles": ["unknown"],
                          "identifiers": [],
                          "organization": "",
                          "status": "unconfirmed",
                          "evidence_refs": [],
                          "privacy_classification": "private-draft",
                      }
                  )
          return parties, party_ids
      
      
      def party_ids_for_roles(
          contributors: list[Any], party_ids: dict[int, str], role_markers: tuple[str, ...]
      ) -> list[str]:
          """Select only relationships supported by contributor role text.
      
          A contributor entry proves that a party participated somehow; it does not
          make that party a writer, performer, publisher, or master owner by default.
          """
          selected: list[str] = []
          for index, contributor in enumerate(contributors):
              if not isinstance(contributor, dict):
                  continue
              roles = " ".join(
                  value.lower().replace("_", "-")
                  for value in string_list(contributor.get("roles", contributor.get("role", [])))
              )
              if any(marker in roles for marker in role_markers):
                  selected.append(party_ids[index])
          return selected
      
      
      def normalize_licenses(metadata: dict[str, Any]) -> list[dict[str, Any]]:
          raw_licenses = list_value(nested_value(metadata, ["rights"], ["licenses"], []))
          licenses: list[dict[str, Any]] = []
          for index, raw in enumerate(raw_licenses):
              item = raw if isinstance(raw, dict) else {"restrictions": [str(raw)]}
              evidence = string_list(item.get("evidence_refs", item.get("evidence", [])))
              licenses.append(
                  {
                      "id": str(item.get("id", f"license-{index + 1:03d}")),
                      "subject_ids": string_list(item.get("subject_ids", [])),
                      "licensor_party_ids": string_list(item.get("licensor_party_ids", [])),
                      "licensee_party_ids": string_list(item.get("licensee_party_ids", [])),
                      "use_types": string_list(item.get("use_types", item.get("uses", []))),
                      "media": string_list(item.get("media", [])),
                      "territories": string_list(item.get("territories", [])),
                      "start_date": item.get("start_date"),
                      "end_date": item.get("end_date"),
                      "exclusive": item.get("exclusive"),
                      "sublicensing": str(item.get("sublicensing", "unknown")),
                      "revocable": item.get("revocable"),
                      "status": evidence_status(item.get("status", "unknown"), evidence),
                      "restrictions": string_list(item.get("restrictions", [])),
                      "evidence_refs": evidence,
                  }
              )
          return licenses
      
      
      def normalize_consents(metadata: dict[str, Any]) -> list[dict[str, Any]]:
          raw_consents = list_value(nested_value(metadata, ["rights"], ["consents"], []))
          consents: list[dict[str, Any]] = []
          for index, raw in enumerate(raw_consents):
              if not isinstance(raw, dict):
                  continue
              evidence = string_list(raw.get("evidence_refs", raw.get("evidence", [])))
              consents.append(
                  {
                      "id": str(raw.get("id", f"consent-{index + 1:03d}")),
                      "party_id": str(raw.get("party_id", "")),
                      "scope": str(raw.get("scope", "unknown")),
                      "media": string_list(raw.get("media", [])),
                      "ai_use": str(raw.get("ai_use", "unknown")),
                      "voice_likeness": str(raw.get("voice_likeness", "unknown")),
                      "status": evidence_status(raw.get("status", "unknown"), evidence),
                      "evidence_refs": evidence,
                  }
              )
          return consents
      
      
      def normalize_third_party_material(
          metadata: dict[str, Any], rights: dict[str, Any], default_subject_ids: list[str]
      ) -> list[dict[str, Any]]:
          raw_items = list_value(
              nested_value(metadata, ["rights"], ["third_party_material", "samples_and_interpolations"], [])
          )
          items: list[dict[str, Any]] = []
          for index, raw in enumerate(raw_items):
              item = raw if isinstance(raw, dict) else {"source": str(raw)}
              evidence = string_list(item.get("evidence_refs", item.get("evidence", [])))
              items.append(
                  {
                      "id": str(item.get("id", f"third-party-{index + 1:03d}")),
                      "type": str(item.get("type", "sample")),
                      "source": str(item.get("source", "unknown")),
                      "subject_ids": string_list(item.get("subject_ids", default_subject_ids)),
                      "license_id": item.get("license_id"),
                      "status": evidence_status(item.get("status", "unknown"), evidence),
                      "evidence_refs": evidence,
                  }
              )
          if not items and positive_status(rights.get("contains_samples", "unknown")):
              items.append(
                  {
                      "id": "third-party-001",
                      "type": "sample",
                      "source": "unknown",
                      "subject_ids": default_subject_ids,
                      "license_id": None,
                      "status": "unconfirmed",
                      "evidence_refs": [],
                  }
              )
          return items
      
      
      def build_rights_claims(
          contributors: list[Any], party_ids: dict[int, str], splits_confirmed: bool
      ) -> list[dict[str, Any]]:
          claims: list[dict[str, Any]] = []
          for index, contributor in enumerate(contributors):
              if not isinstance(contributor, dict):
                  continue
              evidence = string_list(
                  contributor.get("split_evidence_refs", contributor.get("evidence_refs", []))
              )
              contributor_status = contributor.get(
                  "confirmation_status", "confirmed" if contributor.get("confirmed") else "unconfirmed"
              )
              claim_status = evidence_status(
                  "confirmed" if splits_confirmed and confirmed_status(contributor_status) else contributor_status,
                  evidence,
              )
              territories = string_list(contributor.get("territories", []))
              for subject_type, subject_id, right_type, keys in (
                  ("recording", "recording-001", "master", ("master_percent", "master_share")),
                  ("work", "work-001", "publishing", ("publishing_percent", "publishing_share")),
              ):
                  raw_share = next((contributor.get(key) for key in keys if key in contributor), None)
                  if raw_share in (None, ""):
                      continue
                  try:
                      share: float | None = float(raw_share)
                  except (TypeError, ValueError):
                      share = None
                  claims.append(
                      {
                          "id": f"claim-{len(claims) + 1:03d}",
                          "subject_type": subject_type,
                          "subject_id": subject_id,
                          "right_type": right_type,
                          "party_id": party_ids[index],
                          "share_percent": share,
                          "territories": territories,
                          "start_date": contributor.get("start_date"),
                          "end_date": contributor.get("end_date"),
                          "status": claim_status,
                          "evidence_refs": evidence,
                          "conflict_notes": str(contributor.get("conflict_notes", "")),
                      }
                  )
          return claims
      
      
      def contributor_confirmed(contributor: Any) -> bool:
          if not isinstance(contributor, dict):
              return False
          explicit = contributor.get("confirmed")
          if explicit not in (None, ""):
              return boolish(explicit)
          return confirmed_status(contributor.get("confirmation_status", ""))
      
      
      def contributors_all_confirmed(contributors: list[Any]) -> bool:
          return bool(contributors) and all(contributor_confirmed(contributor) for contributor in contributors)
      
      
      def display_path(path: Path, source: Path, include_absolute: bool) -> str:
          if include_absolute:
              return str(path)
          try:
              return path.relative_to(source).as_posix()
          except ValueError:
              return path.name
      
      
      def is_under(child: Path, parent: Path) -> bool:
          try:
              child.resolve().relative_to(parent.resolve())
              return True
          except ValueError:
              return False
      
      
      def is_hidden_path(path: Path, source: Path) -> bool:
          try:
              relative = path.relative_to(source)
          except ValueError:
              relative = path
          return any(part.startswith(".") for part in relative.parts)
      
      
      def is_denied_path(path: Path, source: Path) -> bool:
          try:
              relative = path.relative_to(source)
              within_source = True
          except ValueError:
              relative = path
              within_source = False
          lowered_parts = [part.lower() for part in relative.parts]
          # Only apply the directory-name denylist to ancestors inside the source
          # folder. For an explicit out-of-source --metadata path, the absolute path
          # may pass through benign dirs (.cache, .config, .next, ...) that would
          # otherwise trigger a false "secret-like path" rejection.
          if within_source and any(part in DENY_DIR_NAMES for part in lowered_parts[:-1]):
              return True
          filename = lowered_parts[-1]
          if filename in DENY_FILENAMES or filename.startswith(".env"):
              return True
          if path.suffix.lower() in DENY_SUFFIXES:
              return True
          normalized = filename.replace(".", "_")
          return any(token in normalized for token in SECRET_NAME_TOKENS)
      
      
      def validate_output(source: Path, output: Path, force: bool) -> None:
          if output == source:
              raise SystemExit("Output folder must be separate from the source folder.")
          if is_under(output, source):
              raise SystemExit("Output folder must be outside the source folder.")
          existing = [output / filename for filename in GENERATED_FILENAMES if (output / filename).exists()]
          if existing and not force:
              names = ", ".join(path.name for path in existing)
              raise SystemExit(f"Refusing to overwrite existing package files ({names}); pass --force to replace them.")
      
      
      def sha256_file(path: Path) -> str:
          digest = hashlib.sha256()
          with path.open("rb") as handle:
              for chunk in iter(lambda: handle.read(1024 * 1024), b""):
                  digest.update(chunk)
          return digest.hexdigest()
      
      
      def classify_file(path: Path, source: Path) -> tuple[str, str]:
          name = path.name.lower()
          try:
              context_parts = path.relative_to(source).parts
          except ValueError:
              context_parts = (path.name,)
          context = "/".join(part.lower() for part in context_parts)
          ext = path.suffix.lower()
      
          if ext in AUDIO_EXTS:
              for keyword, role in STEM_KEYWORDS.items():
                  if keyword in context and "master" not in context and "final" not in context:
                      return "stem", role
              if "master" in context or "final" in context:
                  return "audio", "master"
              if "mix" in context:
                  return "audio", "mix"
              return "audio", "audio"
      
          if ext in LYRIC_EXTS or (ext in {".txt", ".md"} and "lyric" in name):
              return "lyrics", "lyrics"
      
          if ext in IMAGE_EXTS:
              if "cover" in context or "artwork" in context or "front" in context:
                  return "artwork", "cover-art"
              return "artwork", "image"
      
          if ext in VIDEO_EXTS:
              return "video", "video"
      
          if ext in DOC_EXTS:
              if "split" in context:
                  return "document", "split-sheet"
              if "license" in context or "licence" in context:
                  return "document", "license"
              if "contract" in context:
                  return "document", "contract"
              return "document", "document"
      
          return "other", "unknown"
      
      
      def asset_subdir(category: str) -> str:
          return {
              "audio": "assets/audio",
              "stem": "assets/stems",
              "lyrics": "assets/lyrics",
              "artwork": "assets/artwork",
              "document": "assets/docs",
              "video": "assets/video",
          }.get(category, "assets/other")
      
      
      def unique_destination(
          directory: Path,
          filename: str,
          *,
          max_suffix_attempts: int = MAX_DESTINATION_SUFFIX_ATTEMPTS,
      ) -> Path:
          candidate = directory / filename
          if not candidate.exists():
              return candidate
          if max_suffix_attempts < 1:
              raise ValueError("max_suffix_attempts must be positive")
          stem = candidate.stem
          suffix = candidate.suffix
          for index in range(2, max_suffix_attempts + 2):
              next_candidate = directory / f"{stem}-{index}{suffix}"
              if not next_candidate.exists():
                  return next_candidate
          raise SystemExit(
              f"Could not find an unused destination for {filename!r} after "
              f"{max_suffix_attempts} suffix attempts."
          )
      
      
      def display_source_root(source: Path, include_absolute: bool) -> str:
          return str(source) if include_absolute else source.name
      
      
      def discover_assets(
          source: Path,
          output: Path,
          copy_assets: bool,
          include_hidden: bool,
          include_other: bool,
      ) -> list[dict]:
          assets = []
          for path in sorted(source.rglob("*")):
              if path.is_symlink():
                  raise SystemExit(
                      f"Refusing symlink inside source tree: {path.relative_to(source)}. "
                      "Replace it with an explicit in-tree file before packaging."
                  )
              if not path.is_file():
                  continue
              try:
                  resolved_path = path.resolve(strict=True)
              except OSError as exc:
                  raise SystemExit(f"Could not resolve source file safely: {path}: {exc}") from exc
              if not is_under(resolved_path, source):
                  raise SystemExit(f"Refusing source file that resolves outside the source tree: {path}")
              if path.name.lower() in SKIP_NAMES:
                  continue
              if is_under(path, output):
                  continue
              if is_denied_path(path, source):
                  continue
              if not include_hidden and is_hidden_path(path, source):
                  continue
      
              category, role = classify_file(path, source)
              if category == "other" and not include_other:
                  continue
              rel_source = path.relative_to(source).as_posix()
              package_rel = rel_source
      
              if copy_assets:
                  dest_dir = output / asset_subdir(category)
                  dest_dir.mkdir(parents=True, exist_ok=True)
                  dest = unique_destination(dest_dir, path.name)
                  shutil.copy2(path, dest)
                  package_rel = dest.relative_to(output).as_posix()
      
              assets.append(
                  {
                      "id": f"asset-{len(assets) + 1:03d}",
                      "relative_path": package_rel,
                      "original_path": rel_source,
                      "category": category,
                      "role": role,
                      "mime_guess": mimetypes.guess_type(path.name)[0] or "application/octet-stream",
                      "size_bytes": path.stat().st_size,
                      "sha256": sha256_file(path),
                      "notes": "",
                  }
              )
          return assets
      
      
      def build_missing_info(title: str, artist: str, counts: Counter, project: dict, rights: dict) -> list[dict]:
          missing = []
          if title == "unknown":
              missing.append(
                  {
                      "field": "project.title",
                      "question": "What is the official project or work title?",
                      "blocks": ["catalog-discovery", "registry"],
                      "severity": "medium",
                  }
              )
          if artist == "unknown":
              missing.append(
                  {
                      "field": "project.artist_name",
                      "question": "What artist or creator name should Suede use publicly?",
                      "blocks": ["catalog-discovery", "registry"],
                      "severity": "medium",
                  }
              )
          if counts["audio"] == 0:
              missing.append(
                  {
                      "field": "assets.audio",
                      "question": "Which file is the final master or primary media asset?",
                      "blocks": ["media-optimization"],
                      "severity": "high",
                  }
              )
      
          owner_claim = rights.get("owner_claim", "unknown")
          ownership_status = rights.get("ownership_status", "unknown")
          contributors = rights.get("contributors", [])
          contains_samples = rights.get("contains_samples", "unknown")
          sample_clearance = rights.get("sample_clearance_status", "unknown")
          cover_or_interpolation = rights.get("cover_or_interpolation", "unknown")
          release_status = project.get("release_status", "unknown")
      
          if unknown_status(owner_claim) or not confirmed_status(ownership_status):
              missing.append(
                  {
                      "field": "rights.owner_claim",
                      "question": "Who owns the master and publishing rights?",
                      "blocks": ["registry", "licensing", "royalty-routing", "agent-commerce"],
                      "severity": "high",
                  }
              )
          if not contributors or not rights.get("contributors_confirmed") or not rights.get("splits_confirmed"):
              missing.append(
                  {
                      "field": "credits.contributors",
                      "question": "Who contributed to the work, what were their roles, and are splits confirmed?",
                      "blocks": ["royalty-routing", "licensing"],
                      "severity": "high",
                  }
              )
          if unknown_status(contains_samples) and unknown_status(cover_or_interpolation):
              missing.append(
                  {
                      "field": "rights.samples",
                      "question": "Does the work contain samples, covers, interpolations, loops, or third-party beats?",
                      "blocks": ["licensing", "registry"],
                      "severity": "high",
                  }
              )
          elif (positive_status(contains_samples) or positive_status(cover_or_interpolation)) and not confirmed_status(sample_clearance):
              missing.append(
                  {
                      "field": "rights.sample_clearance_status",
                      "question": "Are all samples, loops, interpolations, or third-party beats cleared?",
                      "blocks": ["licensing", "registry"],
                      "severity": "high",
                  }
              )
          if unknown_status(release_status):
              missing.append(
                  {
                      "field": "project.release_status",
                      "question": "Has the work already been released, registered, minted, licensed, or sold?",
                      "blocks": ["registry", "licensing", "catalog-discovery"],
                      "severity": "medium",
                  }
              )
          return missing
      
      
      def build_risk_flags(counts: Counter, rights: dict) -> list[dict]:
          flags = []
          if unknown_status(rights.get("owner_claim", "unknown")) or not confirmed_status(
              rights.get("ownership_status", "unknown")
          ):
              flags.append(
                  {
                      "label": "ownership-unconfirmed",
                      "severity": "high",
                      "detail": "Owner claim has not been confirmed by the creator.",
                      "recommended_action": "Confirm master and publishing ownership before registry, licensing, or royalty routing.",
                  }
              )
          contributors_ok = rights.get("contributors_confirmed")
          splits_ok = rights.get("splits_confirmed")
          if not contributors_ok or not splits_ok:
              if not contributors_ok and not splits_ok:
                  detail = "Contributor list and splits are not confirmed."
              elif not contributors_ok:
                  detail = "Contributor list is not confirmed."
              else:
                  detail = "Splits are not confirmed."
              flags.append(
                  {
                      "label": "contributors-unconfirmed",
                      "severity": "high",
                      "detail": detail,
                      "recommended_action": "Collect contributor roles and split confirmations.",
                  }
              )
          contains_samples = rights.get("contains_samples", "unknown")
          sample_clearance = rights.get("sample_clearance_status", "unknown")
          cover_or_interpolation = rights.get("cover_or_interpolation", "unknown")
          if unknown_status(contains_samples) and unknown_status(cover_or_interpolation):
              flags.append(
                  {
                      "label": "sample-status-unknown",
                      "severity": "high",
                      "detail": "Sample, cover, interpolation, loop, and beat lease status is unknown.",
                      "recommended_action": "Ask the creator for source and clearance details.",
                  }
              )
          elif (positive_status(contains_samples) or positive_status(cover_or_interpolation)) and not confirmed_status(sample_clearance):
              flags.append(
                  {
                      "label": "sample-clearance-unconfirmed",
                      "severity": "high",
                      "detail": "Samples or third-party material are indicated, but clearance is not confirmed.",
                      "recommended_action": "Collect clearance records or remove uncleared material before licensing.",
                  }
              )
          if counts["audio"] == 0:
              flags.append(
                  {
                      "label": "no-primary-audio",
                      "severity": "high",
                      "detail": "No primary audio file was detected.",
                      "recommended_action": "Identify the final master or primary media file.",
                  }
              )
          if counts["stem"] == 0:
              flags.append(
                  {
                      "label": "stems-not-found",
                      "severity": "medium",
                      "detail": "No stems were detected.",
                      "recommended_action": "Ask whether stems exist or use Suede stem preparation during optimization.",
                  }
              )
          return flags
      
      
      def build_manifest(
          source: Path,
          title: str,
          artist: str,
          assets: list[dict],
          include_absolute_paths: bool,
          metadata: dict[str, Any],
          metadata_source: str,
      ) -> dict:
          counts = Counter(asset["category"] for asset in assets)
          project = {
              "title": title,
              "artist_name": artist,
              "work_type": str(project_value(metadata, ["work_type", "type"], "unknown")),
              "description": str(project_value(metadata, ["description", "summary", "notes"], "")),
              "release_status": str(
                  project_value(
                      metadata,
                      ["release_status", "release_history", "distribution_status", "registry_status"],
                      rights_value(metadata, ["release_history", "release_status"], "unknown"),
                  )
              ),
              "public_urls": string_list(project_value(metadata, ["public_urls", "urls", "links"], [])),
          }
          contributors = list_value(rights_value(metadata, ["contributors", "credits"], []))
          contributor_confirmation = rights_value(metadata, ["contributors_confirmed", "credits_confirmed"], None)
          rights = {
              "owner_claim": str(
                  rights_value(metadata, ["owner_claim", "owner", "rights_owner", "master_owner", "publishing_owner"], "unknown")
              ),
              "ownership_status": str(
                  rights_value(metadata, ["ownership_status", "owner_status", "ownership_confirmed", "owner_confirmed"], "unknown")
              ),
              "contributors": contributors,
              "contributors_confirmed": (
                  boolish(contributor_confirmation)
                  if contributor_confirmation is not None
                  else contributors_all_confirmed(contributors)
              ),
              "splits_confirmed": boolish(rights_value(metadata, ["splits_confirmed", "split_confirmed"], False)),
              "contains_samples": str(
                  rights_value(metadata, ["contains_samples", "samples", "sample_status", "third_party_material"], "unknown")
              ),
              "sample_clearance_status": str(
                  rights_value(metadata, ["sample_clearance_status", "sample_clearance", "clearance_status"], "unknown")
              ),
              "cover_or_interpolation": str(
                  rights_value(metadata, ["cover_or_interpolation", "cover", "interpolation_status"], "unknown")
              ),
              "license_restrictions": string_list(
                  rights_value(metadata, ["license_restrictions", "restrictions", "usage_restrictions"], [])
              ),
          }
          parties, party_ids = normalize_parties(contributors)
          work_identifiers = make_identifier_records(
              metadata,
              ["work", "composition", "project"],
              [("ISWC", ["iswc"]), ("PROPRIETARY", ["work_id", "composition_id"])],
          )
          recording_identifiers = make_identifier_records(
              metadata,
              ["recording", "master", "project"],
              [("ISRC", ["isrc"]), ("PROPRIETARY", ["recording_id", "master_id"])],
          )
          release_identifiers = make_identifier_records(
              metadata,
              ["release", "project"],
              [
                  ("UPC_EAN", ["upc_ean", "upc", "ean"]),
                  ("CATALOG_NUMBER", ["catalog_number", "catalog_no"]),
              ],
          )
          writer_party_ids = party_ids_for_roles(
              contributors, party_ids, ("writer", "composer", "lyricist", "songwriter", "author")
          )
          publisher_party_ids = party_ids_for_roles(
              contributors, party_ids, ("publisher", "publishing administrator", "publishing admin")
          )
          performer_party_ids = party_ids_for_roles(
              contributors,
              party_ids,
              ("performer", "vocal", "singer", "rapper", "musician", "instrumentalist", "featured artist"),
          )
          master_owner_party_ids = party_ids_for_roles(
              contributors, party_ids, ("master owner", "master rights owner", "record label", "label owner")
          )
          works = [
              {
                  "id": "work-001",
                  "title": title,
                  "identifiers": work_identifiers,
                  "writer_party_ids": writer_party_ids,
                  "publisher_party_ids": publisher_party_ids,
                  "status": "claimed" if title != "unknown" else "unknown",
                  "evidence_refs": [],
              }
          ]
          primary_asset_ids = [
              asset["id"] for asset in assets if asset.get("category") in {"audio", "stem"}
          ]
          has_master_claim = any(
              isinstance(contributor, dict)
              and any(key in contributor for key in ("master_percent", "master_share"))
              for contributor in contributors
          )
          recordings = []
          if primary_asset_ids or recording_identifiers or has_master_claim:
              recordings.append(
                  {
                      "id": "recording-001",
                      "title": title,
                      "asset_ids": primary_asset_ids,
                      "identifiers": recording_identifiers,
                      "performer_party_ids": performer_party_ids,
                      "master_owner_party_ids": master_owner_party_ids,
                      "status": "claimed" if title != "unknown" else "unknown",
                      "evidence_refs": [],
                  }
              )
          release_date = nested_value(metadata, ["release", "project"], ["release_date", "date"], None)
          releases = []
          if release_identifiers or release_date or project["release_status"] not in {"unknown", "unreleased"}:
              releases.append(
                  {
                      "id": "release-001",
                      "title": str(nested_value(metadata, ["release", "project"], ["release_title", "title"], title)),
                      "recording_ids": ["recording-001"] if recordings else [],
                      "identifiers": release_identifiers,
                      "label_party_id": None,
                      "distributor_party_id": None,
                      "release_date": release_date,
                      "territories": string_list(
                          nested_value(metadata, ["release", "project"], ["territories"], [])
                      ),
                      "status": "claimed",
                      "evidence_refs": [],
                  }
              )
          rights_claims = build_rights_claims(contributors, party_ids, rights["splits_confirmed"])
          licenses = normalize_licenses(metadata)
          third_party_material = normalize_third_party_material(
              metadata, rights, ["recording-001"] if recordings else []
          )
          consents = normalize_consents(metadata)
          provenance_notes = str(
              scoped_value(
                  metadata,
                  "provenance",
                  ["creation_notes", "provenance_notes", "chain_of_custody", "source_notes"],
                  suede_value(metadata, ["provenance_notes", "creation_notes", "chain_of_custody", "source_notes"], ""),
              )
          )
          generated_at = datetime.now(timezone.utc).isoformat()
          manifest = {
              "schema_version": "0.2.0",
              "package_type": "suede-transfer-package",
              "generated_at": generated_at,
              "metadata_source": metadata_source,
              "project": project,
              "creator": {
                  "name": str(creator_value(metadata, ["name", "artist", "artist_name"], artist)),
                  "email": str(creator_value(metadata, ["email", "contact_email"], "")),
                  "wallet_address": str(
                      creator_value(
                          metadata,
                          ["wallet_address", "wallet", "payment_destination", "payment_address", "royalty_destination"],
                          suede_value(metadata, ["wallet_address", "wallet", "payment_destination"], ""),
                      )
                  ),
                  "organization": str(creator_value(metadata, ["organization", "company", "label"], "")),
                  "confirmation_status": str(creator_value(metadata, ["confirmation_status", "status"], "needs-confirmation")),
              },
              "parties": parties,
              "works": works,
              "recordings": recordings,
              "releases": releases,
              "assets": assets,
              "rights": rights,
              "rights_claims": rights_claims,
              "licenses": licenses,
              "third_party_material": third_party_material,
              "consents": consents,
              "provenance": {
                  "source_root": display_source_root(source, include_absolute_paths),
                  "metadata_source": metadata_source,
                  "creation_notes": provenance_notes,
                  "chain_of_custody": [
                      {
                          "date": datetime.now(timezone.utc).date().isoformat(),
                          "event": "Source folder inventoried for Suede transfer package.",
                          "actor": "suede-rights-passport",
                          "evidence": "local file hashes",
                      }
                  ],
                  "registry_status": "ready-for-review" if assets else "unknown",
                  "content_credentials": [
                      {
                          "asset_id": asset["id"],
                          "kind": "sha256",
                          "manifest_reference": None,
                          "verification_status": "verified",
                          "verified_at": generated_at,
                          "evidence_refs": [f"sha256:{asset['sha256']}"],
                      }
                      for asset in assets
                  ],
              },
              "privacy": {
                  "default_classification": "private-draft",
                  "field_rules": [
                      {
                          "json_pointer": "/creator/email",
                          "classification": "restricted",
                          "redaction_action": "review",
                          "reason": "Contact data should be shared only with the intended recipient.",
                      },
                      {
                          "json_pointer": "/creator/wallet_address",
                          "classification": "restricted",
                          "redaction_action": "review",
                          "reason": "Payment-routing data requires recipient and purpose review.",
                      },
                  ],
                  "redaction_required_before_external_share": True,
              },
              "optimization": {
                  "requested_services": string_list(suede_value(metadata, ["requested_services", "services"], [])),
                  "recommended_services": recommend_services(counts),
                  "priority": "normal",
                  "notes": str(
                      suede_value(
                          metadata,
                          ["optimization_notes", "notes"],
                          "Resolve high-severity missing information before final Suede optimization.",
                      )
                  ),
              },
          }
          manifest["missing_information"] = build_missing_info(title, artist, counts, project, rights)
          manifest["risk_flags"] = build_risk_flags(counts, rights)
          return manifest
      
      
      def recommend_services(counts: Counter) -> list[dict]:
          services = [
              {
                  "service": "rights-review",
                  "priority": "high",
                  "reason": "Ownership, contributor, sample, and release facts need confirmation.",
                  "blocks": ["registry", "licensing", "royalty-routing", "agent-commerce"],
              },
              {
                  "service": "provenance-cleanup",
                  "priority": "high",
                  "reason": "File hashes are present, but creation history and chain-of-custody need creator notes.",
                  "blocks": ["registry"],
              },
          ]
          if counts["audio"] > 0:
              services.append(
                  {
                      "service": "mastering-or-wav-review",
                      "priority": "normal",
                      "reason": "Primary audio was found and can be reviewed for final delivery quality.",
                      "blocks": [],
                  }
              )
          if counts["stem"] == 0 and counts["audio"] > 0:
              services.append(
                  {
                      "service": "stem-separation",
                      "priority": "normal",
                      "reason": "No stems were detected; stems can improve licensing, remix, and derivative workflows.",
                      "blocks": [],
                  }
              )
          if counts["lyrics"] == 0:
              services.append(
                  {
                      "service": "lyric-capture-or-sync",
                      "priority": "low",
                      "reason": "No lyric files were detected.",
                      "blocks": [],
                  }
              )
          if counts["artwork"] == 0:
              services.append(
                  {
                      "service": "artwork-preparation",
                      "priority": "low",
                      "reason": "No artwork was detected.",
                      "blocks": [],
                  }
              )
          return services
      
      
      def write_json(path: Path, data: dict) -> None:
          path.write_text(json.dumps(data, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
      
      
      def write_text(path: Path, content: str) -> None:
          path.write_text(content.rstrip() + "\n", encoding="utf-8")
      
      
      def asset_table(assets: list[dict]) -> str:
          if not assets:
              return "| ID | Category | Role | Path | SHA-256 |\n| --- | --- | --- | --- | --- |\n"
          rows = ["| ID | Category | Role | Path | SHA-256 |", "| --- | --- | --- | --- | --- |"]
          for asset in assets:
              rows.append(
                  f"| {asset['id']} | {asset['category']} | {asset['role']} | "
                  f"`{asset['relative_path']}` | `{asset['sha256']}` |"
              )
          return "\n".join(rows) + "\n"
      
      
      def contributors_table(contributors: list[Any]) -> str:
          rows = ["| Name | Role | Master % | Publishing % | Confirmation |", "| --- | --- | ---: | ---: | --- |"]
          if not contributors:
              rows.append("| unknown | unknown | 0 | 0 | needs creator confirmation |")
              return "\n".join(rows) + "\n"
          for contributor in contributors:
              if isinstance(contributor, dict):
                  name = contributor.get("name", "unknown")
                  role = contributor.get("role", "unknown")
                  master = contributor.get("master_percent", contributor.get("master_share", 0))
                  publishing = contributor.get("publishing_percent", contributor.get("publishing_share", 0))
                  confirmation = "confirmed" if contributor_confirmed(contributor) else "needs creator confirmation"
              else:
                  name = str(contributor)
                  role = "unknown"
                  master = 0
                  publishing = 0
                  confirmation = "needs creator confirmation"
              rows.append(f"| {name} | {role} | {master} | {publishing} | {confirmation} |")
          return "\n".join(rows) + "\n"
      
      
      def restrictions_text(restrictions: list[str]) -> str:
          if not restrictions:
              return "No restrictions confirmed. Treat as unknown until creator confirms."
          return "\n".join(f"- {restriction}" for restriction in restrictions)
      
      
      def write_reports(output: Path, manifest: dict) -> None:
          assets = manifest["assets"]
          counts = Counter(asset["category"] for asset in assets)
          project = manifest["project"]
          creator = manifest["creator"]
          rights = manifest["rights"]
          provenance = manifest["provenance"]
          missing = manifest["missing_information"]
          flags = manifest["risk_flags"]
          rights_risk = "low" if not flags else "high until listed risk flags are resolved"
          intake_status_text = (
              "No outstanding missing-information questions or risk flags. Ready for final Suede intake review."
              if not missing and not flags
              else "Not ready for final Suede intake until high-severity questions are answered."
          )
          contributor_ready = "yes" if rights["contributors_confirmed"] else "no"
          splits_ready = "yes" if rights["splits_confirmed"] else "no"
          restrictions = rights.get("license_restrictions", [])
      
          write_text(
              output / "RIGHTS_PASSPORT.md",
              f"""
      # Rights Passport
      
      Private draft: review and redact before publishing, committing, or sharing
      outside the intended Suede intake workflow.
      
      ## Work Summary
      
      - Title: {project['title']}
      - Artist / creator: {project['artist_name']}
      - Work type: {project['work_type']}
      - Release status: {project['release_status']}
      - Manifest schema: {manifest['schema_version']}
      - Package status: draft for Suede intake
      
      ## Intake Readiness
      
      - Overall risk: {rights_risk}.
      - Registry readiness: ready for review, not final clearance.
      - Royalty routing readiness: {'ready for review' if rights['splits_confirmed'] else 'blocked until splits are confirmed'}.
      - Licensing readiness: {'ready for review' if not flags else 'blocked until high-severity rights flags are resolved'}.
      - Agent commerce readiness: blocked until rights and usage terms are confirmed.
      
      ## Rights Snapshot
      
      - Owner claim: {rights['owner_claim']}
      - Ownership status: {rights['ownership_status']}
      - Contributor list confirmed: {contributor_ready}
      - Splits confirmed: {splits_ready}
      - Samples / interpolations / covers: {rights['contains_samples']} / {rights['cover_or_interpolation']}
      - Sample clearance status: {rights['sample_clearance_status']}
      - Existing licenses or restrictions: {', '.join(restrictions) if restrictions else 'unknown'}
      
      ## Normalized Records
      
      - Parties: {len(manifest['parties'])}
      - Musical works: {len(manifest['works'])}
      - Recordings: {len(manifest['recordings'])}
      - Releases: {len(manifest['releases'])}
      - Scoped rights claims: {len(manifest['rights_claims'])}
      - Licenses: {len(manifest['licenses'])}
      - Third-party material records: {len(manifest['third_party_material'])}
      - Consent records: {len(manifest['consents'])}
      - Privacy default: {manifest['privacy']['default_classification']}
      - External sharing: redaction review required
      
      These records are evidence-scoped intake facts, not DDEX/C2PA conformance or
      legal clearance.
      
      ## Asset Snapshot
      
      | Category | Count |
      | --- | ---: |
      | Audio | {counts['audio']} |
      | Stems | {counts['stem']} |
      | Lyrics | {counts['lyrics']} |
      | Artwork | {counts['artwork']} |
      | Documents | {counts['document']} |
      | Video | {counts['video']} |
      | Other | {counts['other']} |
      
      ## Assets
      
      {asset_table(assets)}
      ## Suede Next Step
      
      Resolve the high-severity questions in `missing-info-report.md`, then route the package into Suede rights review and media optimization.
      """,
          )
      
          write_text(
              output / "provenance.md",
              f"""
      # Provenance
      
      Private draft: file names, hashes, source notes, and creator context may be
      sensitive. Review before sharing.
      
      ## Source
      
      - Source folder: {provenance['source_root']}
      - Metadata source: {provenance['metadata_source'] or 'none'}
      - Package generated: {manifest['generated_at']}
      - Generator: suede-rights-passport
      
      ## Creation History
      
      {provenance['creation_notes'] or 'Needs creator confirmation.'}
      
      ## Chain Of Custody
      
      | Date | Event | Actor | Evidence |
      | --- | --- | --- | --- |
      | {provenance['chain_of_custody'][0]['date']} | Source folder inventoried for Suede transfer package. | suede-rights-passport | local file hashes |
      
      ## File Hashes
      
      {asset_table(assets)}
      ## Registry Notes
      
      - Registry status: {manifest['provenance']['registry_status']}
      - Asset hash selected for registry: needs Suede review
      - Notes: do not treat hash inventory as legal rights clearance.
      """,
          )
      
          write_text(
              output / "credits-and-splits.md",
              f"""
      # Credits And Splits
      
      Private draft: contributor, split, wallet, and organization notes may be
      sensitive. Review before sharing.
      
      ## Contributors
      
      {contributors_table(rights.get('contributors', []))}
      
      ## Organizations
      
      | Name | Role | Notes |
      | --- | --- | --- |
      | {creator['organization'] or 'unknown'} | owner / label | needs creator confirmation |
      
      ## Payment / Wallet Notes
      
      - Creator wallet: {creator['wallet_address'] or 'unknown'}
      - Contributor wallets: unknown
      - Royalty routing readiness: {'ready for review' if rights['splits_confirmed'] else 'not ready until splits are confirmed'}
      
      ## Blockers
      
      - {'Contributor list confirmed.' if rights['contributors_confirmed'] else 'Contributor list needs confirmation.'}
      - {'Split percentages confirmed.' if rights['splits_confirmed'] else 'Split percentages need confirmation.'}
      """,
          )
      
          write_text(
              output / "license-notes.md",
              f"""
      # License Notes
      
      Private draft: rights, restrictions, and third-party material notes may be
      sensitive. Review before sharing.
      
      ## Third-Party Material
      
      - Samples: {rights['contains_samples']}
      - Loops: unknown
      - Interpolations: {rights['cover_or_interpolation']}
      - Covers: {rights['cover_or_interpolation']}
      - Beat leases: unknown
      
      ## Existing Releases
      
      - DSP release: {project['release_status']}
      - Social/video platform release: unknown
      - Prior mint/registry/license: unknown
      - Existing takedowns/disputes: unknown
      
      ## Restrictions
      
      {restrictions_text(restrictions)}
      
      ## Clearance Status
      
      Sample clearance status: {rights['sample_clearance_status']}.
      High-confidence licensing should wait for creator/legal confirmation when any rights fact is uncertain.
      """,
          )
      
          recommendation_rows = []
          for index, service in enumerate(manifest["optimization"]["recommended_services"], start=1):
              blocks = ", ".join(service["blocks"]) if service["blocks"] else "none"
              recommendation_rows.append(
                  f"{index}. {service['service']} - {service['priority']} priority\n"
                  f"   Reason: {service['reason']}\n"
                  f"   Blocks: {blocks}"
              )
      
          write_text(
              output / "optimization-brief.md",
              f"""
      # Optimization Brief
      
      Private draft: review rights and creator details before sharing outside the
      intended Suede workflow.
      
      ## Goal
      
      Prepare `{project['title']}` for Suede optimization after rights and file review.
      
      ## Recommended Next Actions
      
      {chr(10).join(recommendation_rows)}
      
      ## Candidate Suede Services
      
      - Mastering / WAV export: {'available for review' if counts['audio'] else 'needs primary audio'}
      - Stem separation: {'stems already detected' if counts['stem'] else 'recommended if audio rights are confirmed'}
      - Lyric sync: {'lyrics detected' if counts['lyrics'] else 'needs lyrics or transcription'}
      - Artwork polish: {'artwork detected' if counts['artwork'] else 'needs artwork'}
      - Registry readiness: needs rights review
      - Royalty routing: needs split confirmation
      - License packaging: needs restrictions and clearance details
      - Agent commerce packaging: needs usage terms and rights confidence
      
      ## Operator Notes
      
      Review `missing-info-report.md` before beginning optimization.
      """,
          )
      
          question_rows = ["| Severity | Field | Question | Blocks |", "| --- | --- | --- | --- |"]
          for item in missing:
              question_rows.append(
                  f"| {item['severity']} | `{item['field']}` | {item['question']} | "
                  f"{', '.join(item['blocks'])} |"
              )
      
          flag_rows = ["| Severity | Label | Detail | Action |", "| --- | --- | --- | --- |"]
          for flag in flags:
              flag_rows.append(
                  f"| {flag['severity']} | {flag['label']} | {flag['detail']} | {flag['recommended_action']} |"
              )
      
          write_text(
              output / "missing-info-report.md",
              f"""
      # Missing Info Report
      
      Private draft: unresolved rights and creator questions may be sensitive. Review
      before sharing.
      
      ## Summary
      
      Missing information must be resolved before Suede can confidently register, license, route royalties for, or expose the work to agent commerce.
      
      ## Questions
      
      {chr(10).join(question_rows)}
      
      ## Risk Flags
      
      {chr(10).join(flag_rows)}
      
      ## Status
      
      {intake_status_text}
      """,
          )
      
      
      def create_directories(output: Path) -> None:
          for rel in [
              "assets/audio",
              "assets/stems",
              "assets/lyrics",
              "assets/artwork",
              "assets/docs",
              "assets/video",
              "assets/other",
          ]:
              (output / rel).mkdir(parents=True, exist_ok=True)
      
      
      def main() -> int:
          args = parse_args()
          source_input = Path(args.source).expanduser()
          if source_input.is_symlink():
              raise SystemExit("Refusing a symlinked source folder. Pass the real source directory path.")
          source = source_input.resolve()
          output = Path(args.output).expanduser().resolve()
      
          if not source.exists() or not source.is_dir():
              raise SystemExit(f"Source folder not found: {source}")
      
          validate_output(source, output, args.force)
          output.mkdir(parents=True, exist_ok=True)
          create_directories(output)
      
          metadata_path = discover_metadata(source, args.metadata)
          try:
              metadata = load_metadata(metadata_path)
          except Exception as exc:
              raise SystemExit(f"Could not load metadata: {exc}") from exc
          metadata_source = display_path(metadata_path, source, args.include_absolute_paths) if metadata_path else ""
      
          title = (
              args.project_title.strip()
              or str(project_value(metadata, ["title", "project_title", "work_title", "name"], "")).strip()
              or source.name
              or "unknown"
          )
          artist = (
              args.artist.strip()
              or str(project_value(metadata, ["artist_name", "artist"], "")).strip()
              or str(creator_value(metadata, ["name", "artist", "artist_name"], "")).strip()
              or "unknown"
          )
          assets = discover_assets(source, output, args.copy_assets, args.include_hidden, args.include_other)
          manifest = build_manifest(source, title, artist, assets, args.include_absolute_paths, metadata, metadata_source)
      
          write_json(output / "suede-intake.json", manifest)
          write_reports(output, manifest)
      
          print(f"Created Suede transfer package: {output}")
      
    • migrate_intake_v1_to_v2.py 6.6 KB
      #!/usr/bin/env python3
      """Migrate a Suede intake manifest from schema 0.1.0 to 0.2.0.
      
      The migration is evidence-preserving and non-destructive: it writes a sibling
      file by default, never upgrades confirmation state, never fills shares, and
      records a digest of the source manifest in the new chain of custody.
      """
      
      from __future__ import annotations
      
      import argparse
      import hashlib
      import json
      import sys
      from pathlib import Path
      from typing import Any
      
      from create_transfer_package import build_manifest
      from validate_transfer_package import (
          check_assets,
          check_list_shaped_sections,
          check_missing_information,
          check_nested_shape,
          check_published_json_schema,
          check_risk_flags,
          check_top_level_shape,
          check_v2_interoperability,
      )
      
      
      def parse_args() -> argparse.Namespace:
          parser = argparse.ArgumentParser(
              description=(
                  "Migrate a suede-intake.json manifest from 0.1.0 to 0.2.0 without "
                  "modifying the source or upgrading any rights fact."
              )
          )
          parser.add_argument("input", help="Path to a schema 0.1.0 suede-intake.json file.")
          parser.add_argument(
              "--output",
              help="Output JSON path. Defaults to suede-intake.v0.2.json beside the input.",
          )
          parser.add_argument("--force", action="store_true", help="Replace the output file if it exists.")
          return parser.parse_args()
      
      
      def read_manifest(path: Path) -> tuple[dict[str, Any], str]:
          raw = path.read_bytes()
          try:
              data = json.loads(raw.decode("utf-8"))
          except (UnicodeDecodeError, json.JSONDecodeError) as exc:
              raise ValueError(f"Input is not valid UTF-8 JSON: {exc}") from exc
          if not isinstance(data, dict):
              raise ValueError("Input manifest must contain a JSON object.")
          if data.get("schema_version") != "0.1.0":
              raise ValueError("Input schema_version must be 0.1.0; no migration was performed.")
          return data, hashlib.sha256(raw).hexdigest()
      
      
      def migrate(data: dict[str, Any], source_path: Path, source_digest: str) -> dict[str, Any]:
          project = data.get("project") if isinstance(data.get("project"), dict) else {}
          creator = data.get("creator") if isinstance(data.get("creator"), dict) else {}
          rights = data.get("rights") if isinstance(data.get("rights"), dict) else {}
          provenance = data.get("provenance") if isinstance(data.get("provenance"), dict) else {}
          optimization = data.get("optimization") if isinstance(data.get("optimization"), dict) else {}
          metadata: dict[str, Any] = {
              "project": project,
              "creator": creator,
              "rights": rights,
              "provenance": provenance,
              "suede": {
                  "requested_services": optimization.get("requested_services", []),
                  "optimization_notes": optimization.get("notes", ""),
              },
          }
          title = str(project.get("title", "unknown"))
          artist = str(project.get("artist_name", creator.get("name", "unknown")))
          assets = data.get("assets") if isinstance(data.get("assets"), list) else []
          migrated = build_manifest(
              source_path.parent,
              title,
              artist,
              assets,
              include_absolute_paths=False,
              metadata=metadata,
              metadata_source=f"migration:{source_path.name}",
          )
          migrated["missing_information"] = (
              data.get("missing_information")
              if isinstance(data.get("missing_information"), list)
              else migrated["missing_information"]
          )
          migrated["risk_flags"] = (
              data.get("risk_flags") if isinstance(data.get("risk_flags"), list) else migrated["risk_flags"]
          )
          if isinstance(provenance.get("source_root"), str):
              migrated["provenance"]["source_root"] = provenance["source_root"]
          if isinstance(provenance.get("metadata_source"), str):
              migrated["provenance"]["metadata_source"] = provenance["metadata_source"]
          if isinstance(provenance.get("creation_notes"), str):
              migrated["provenance"]["creation_notes"] = provenance["creation_notes"]
          if isinstance(provenance.get("registry_status"), str):
              migrated["provenance"]["registry_status"] = provenance["registry_status"]
          legacy_chain = provenance.get("chain_of_custody")
          migrated["provenance"]["chain_of_custody"] = (
              list(legacy_chain) if isinstance(legacy_chain, list) else []
          )
          migrated["provenance"]["chain_of_custody"].append(
              {
                  "date": migrated["generated_at"][:10],
                  "event": "Schema 0.1.0 manifest migrated to 0.2.0 without changing the source.",
                  "actor": "suede-rights-passport",
                  "evidence": f"sha256:{source_digest}",
              },
          )
          return migrated
      
      
      def validate_migrated(data: dict[str, Any]) -> list[str]:
          errors: list[str] = []
          errors += check_top_level_shape(data, strict_current=True)
          errors += check_nested_shape(data)
          errors += check_list_shaped_sections(data)
          errors += check_assets(data)
          errors += check_missing_information(data)
          errors += check_risk_flags(data)
          errors += check_published_json_schema(data)
          errors += check_v2_interoperability(data)
          return errors
      
      
      def main() -> int:
          args = parse_args()
          source_path = Path(args.input).expanduser().resolve()
          output_path = (
              Path(args.output).expanduser().resolve()
              if args.output
              else source_path.with_name("suede-intake.v0.2.json")
          )
          if not source_path.is_file():
              print(f"Input manifest not found: {source_path}", file=sys.stderr)
              return 2
          if output_path == source_path:
              print("Refusing in-place migration; choose a separate --output path.", file=sys.stderr)
              return 2
          if output_path.exists() and not args.force:
              print(f"Refusing to overwrite existing output: {output_path}", file=sys.stderr)
              return 2
          try:
              data, digest = read_manifest(source_path)
              migrated = migrate(data, source_path, digest)
          except (OSError, ValueError) as exc:
              print(f"Migration failed: {exc}", file=sys.stderr)
              return 1
          errors = validate_migrated(migrated)
          if errors:
              print("Migration produced an invalid 0.2.0 manifest:", file=sys.stderr)
              for error in errors:
                  print(f"- {error}", file=sys.stderr)
              return 1
          output_path.parent.mkdir(parents=True, exist_ok=True)
          output_path.write_text(json.dumps(migrated, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
          print(f"Migrated schema 0.1.0 -> 0.2.0: {output_path}")
          print(f"Source preserved: {source_path}")
          print(f"Source manifest digest: sha256:{digest}")
          print("Review normalized records and privacy rules before external exchange.")
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • validate_json_schema.py 7.1 KB
      #!/usr/bin/env python3
      """Dependency-free validator for the JSON Schema keywords used by this skill.
      
      This module executes the bundled Draft 2020-12 schema rather than maintaining a
      second hand-written shape contract. It intentionally supports the complete
      keyword subset present in `assets/suede-intake.schema.json`; adding another
      schema keyword requires adding support here and a regression test.
      """
      
      from __future__ import annotations
      
      import re
      from datetime import date, datetime
      from typing import Any
      
      
      SUPPORTED_KEYWORDS = {
          "$schema",
          "$id",
          "$defs",
          "$ref",
          "title",
          "description",
          "type",
          "additionalProperties",
          "required",
          "properties",
          "items",
          "const",
          "enum",
          "pattern",
          "minLength",
          "minimum",
          "maximum",
          "format",
      }
      
      
      def _pointer(root: dict[str, Any], reference: str) -> dict[str, Any]:
          if not reference.startswith("#/"):
              raise ValueError(f"Only local JSON Schema references are supported: {reference}")
          value: Any = root
          for raw_part in reference[2:].split("/"):
              part = raw_part.replace("~1", "/").replace("~0", "~")
              if not isinstance(value, dict) or part not in value:
                  raise ValueError(f"Unresolvable JSON Schema reference: {reference}")
              value = value[part]
          if not isinstance(value, dict):
              raise ValueError(f"JSON Schema reference does not resolve to an object: {reference}")
          return value
      
      
      def _is_type(value: Any, expected: str) -> bool:
          if expected == "object":
              return isinstance(value, dict)
          if expected == "array":
              return isinstance(value, list)
          if expected == "string":
              return isinstance(value, str)
          if expected == "integer":
              return isinstance(value, int) and not isinstance(value, bool)
          if expected == "number":
              return isinstance(value, (int, float)) and not isinstance(value, bool)
          if expected == "boolean":
              return isinstance(value, bool)
          if expected == "null":
              return value is None
          raise ValueError(f"Unsupported JSON Schema type: {expected}")
      
      
      def _validate_format(value: str, format_name: str) -> bool:
          try:
              if format_name == "date":
                  date.fromisoformat(value)
                  return True
              if format_name == "date-time":
                  parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
                  return parsed.tzinfo is not None
          except ValueError:
              return False
          raise ValueError(f"Unsupported JSON Schema format: {format_name}")
      
      
      def _join(path: str, child: str | int) -> str:
          return f"{path}/{child}" if path else f"/{child}"
      
      
      def assert_supported_schema(schema: dict[str, Any], path: str = "#") -> None:
          """Fail closed if the published schema introduces an unsupported keyword."""
          unsupported = sorted(set(schema) - SUPPORTED_KEYWORDS)
          if unsupported:
              raise ValueError(f"{path}: unsupported JSON Schema keyword(s): {', '.join(unsupported)}")
          for container_key in ("$defs", "properties"):
              container = schema.get(container_key, {})
              if not isinstance(container, dict):
                  raise ValueError(f"{path}/{container_key}: must be an object")
              for name, child in container.items():
                  if not isinstance(child, dict):
                      raise ValueError(f"{path}/{container_key}/{name}: must be an object")
                  assert_supported_schema(child, f"{path}/{container_key}/{name}")
          if "items" in schema:
              child = schema["items"]
              if not isinstance(child, dict):
                  raise ValueError(f"{path}/items: must be an object")
              assert_supported_schema(child, f"{path}/items")
      
      
      def validate_instance(
          instance: Any,
          schema: dict[str, Any],
          *,
          root_schema: dict[str, Any] | None = None,
          path: str = "",
      ) -> list[str]:
          """Return human-readable schema violations for one JSON value."""
          root = root_schema or schema
          unsupported = sorted(set(schema) - SUPPORTED_KEYWORDS)
          if unsupported:
              raise ValueError(f"Unsupported JSON Schema keyword(s): {', '.join(unsupported)}")
          if "$ref" in schema:
              target = _pointer(root, str(schema["$ref"]))
              return validate_instance(instance, target, root_schema=root, path=path)
      
          label = path or "/"
          errors: list[str] = []
          expected = schema.get("type")
          if expected is not None:
              allowed_types = expected if isinstance(expected, list) else [expected]
              if not any(_is_type(instance, str(type_name)) for type_name in allowed_types):
                  errors.append(f"{label}: expected type {' or '.join(map(str, allowed_types))}")
                  return errors
      
          if "const" in schema and instance != schema["const"]:
              errors.append(f"{label}: must equal {schema['const']!r}")
          if "enum" in schema and instance not in schema["enum"]:
              errors.append(f"{label}: value is not in the allowed enum")
      
          if isinstance(instance, str):
              if len(instance) < int(schema.get("minLength", 0)):
                  errors.append(f"{label}: string is shorter than minLength {schema['minLength']}")
              if "pattern" in schema and re.search(str(schema["pattern"]), instance) is None:
                  errors.append(f"{label}: string does not match pattern {schema['pattern']!r}")
              if "format" in schema and not _validate_format(instance, str(schema["format"])):
                  errors.append(f"{label}: string is not a valid {schema['format']}")
      
          if isinstance(instance, (int, float)) and not isinstance(instance, bool):
              if "minimum" in schema and instance < schema["minimum"]:
                  errors.append(f"{label}: number is below minimum {schema['minimum']}")
              if "maximum" in schema and instance > schema["maximum"]:
                  errors.append(f"{label}: number is above maximum {schema['maximum']}")
      
          if isinstance(instance, dict):
              required = schema.get("required", [])
              for key in required:
                  if key not in instance:
                      errors.append(f"{_join(path, key)}: required property is missing")
              properties = schema.get("properties", {})
              if not isinstance(properties, dict):
                  raise ValueError(f"{label}: schema properties must be an object")
              for key, value in instance.items():
                  if key in properties:
                      child_schema = properties[key]
                      if not isinstance(child_schema, dict):
                          raise ValueError(f"{label}: schema for property {key!r} must be an object")
                      errors += validate_instance(
                          value, child_schema, root_schema=root, path=_join(path, key)
                      )
                  elif schema.get("additionalProperties") is False:
                      errors.append(f"{_join(path, key)}: additional property is not allowed")
      
          if isinstance(instance, list) and "items" in schema:
              item_schema = schema["items"]
              if not isinstance(item_schema, dict):
                  raise ValueError(f"{label}: schema items must be an object")
              for index, value in enumerate(instance):
                  errors += validate_instance(
                      value, item_schema, root_schema=root, path=_join(path, index)
                  )
          return errors
      
    • validate_transfer_package.py 30 KB
      #!/usr/bin/env python3
      """Validate the structure of a Suede rights transfer package.
      
      Checks that a package folder produced by create_transfer_package.py (or
      hand-assembled to the same standard) is structurally complete:
      
      - All 7 required report files are present.
      - suede-intake.json is valid JSON and matches the documented top-level and
        nested shape from references/intake-schema.md.
      - Every asset entry in suede-intake.json carries a sha256 hash.
      
      This is a structural/completeness check only. It does not evaluate rights
      facts, ownership claims, split confirmations, or risk-flag severity. A
      package can be structurally valid while still carrying open, high-severity
      risk flags (unconfirmed ownership, unconfirmed splits, unknown sample
      status, etc.) -- that is expected and correct for a package documenting a
      project with real open questions. See SKILL.md and
      references/package-standard.md for what "valid" does and does not mean
      here.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import sys
      from pathlib import Path
      from typing import Any
      
      from validate_json_schema import assert_supported_schema, validate_instance
      
      
      REQUIRED_FILES = [
          "RIGHTS_PASSPORT.md",
          "suede-intake.json",
          "provenance.md",
          "credits-and-splits.md",
          "license-notes.md",
          "optimization-brief.md",
          "missing-info-report.md",
      ]
      
      # Top-level keys documented in references/intake-schema.md.
      REQUIRED_TOP_LEVEL_KEYS = [
          "schema_version",
          "package_type",
          "generated_at",
          "project",
          "creator",
          "assets",
          "rights",
          "provenance",
          "optimization",
          "missing_information",
          "risk_flags",
      ]
      
      CURRENT_SCHEMA_VERSION = "0.2.0"
      LEGACY_SCHEMA_VERSIONS = {"0.1.0"}
      V2_REQUIRED_TOP_LEVEL_KEYS = [
          "parties",
          "works",
          "recordings",
          "releases",
          "rights_claims",
          "licenses",
          "third_party_material",
          "consents",
          "privacy",
      ]
      EVIDENCE_STATUSES = {"confirmed", "claimed", "unconfirmed", "disputed", "unknown"}
      SCHEMA_PATH = Path(__file__).resolve().parents[1] / "assets" / "suede-intake.schema.json"
      V2_REQUIRED_ITEM_KEYS: dict[str, set[str]] = {
          "parties": {
              "id", "name", "roles", "identifiers", "organization", "status", "evidence_refs", "privacy_classification"
          },
          "works": {
              "id", "title", "identifiers", "writer_party_ids", "publisher_party_ids", "status", "evidence_refs"
          },
          "recordings": {
              "id", "title", "asset_ids", "identifiers", "performer_party_ids", "master_owner_party_ids", "status", "evidence_refs"
          },
          "releases": {
              "id", "title", "recording_ids", "identifiers", "label_party_id", "distributor_party_id",
              "release_date", "territories", "status", "evidence_refs"
          },
          "rights_claims": {
              "id", "subject_type", "subject_id", "right_type", "party_id", "share_percent", "territories",
              "start_date", "end_date", "status", "evidence_refs", "conflict_notes"
          },
          "licenses": {
              "id", "subject_ids", "licensor_party_ids", "licensee_party_ids", "use_types", "media", "territories",
              "start_date", "end_date", "exclusive", "sublicensing", "revocable", "status", "restrictions", "evidence_refs"
          },
          "third_party_material": {"id", "type", "source", "subject_ids", "license_id", "status", "evidence_refs"},
          "consents": {"id", "party_id", "scope", "media", "ai_use", "voice_likeness", "status", "evidence_refs"},
      }
      
      # Nested object/array keys, keyed by their parent top-level field.
      REQUIRED_NESTED_KEYS: dict[str, list[str]] = {
          "project": ["title", "artist_name", "work_type", "description", "release_status", "public_urls"],
          "creator": ["name", "email", "wallet_address", "organization", "confirmation_status"],
          "rights": [
              "owner_claim",
              "ownership_status",
              "contributors_confirmed",
              "splits_confirmed",
              "contains_samples",
              "sample_clearance_status",
              "cover_or_interpolation",
              "license_restrictions",
          ],
          "provenance": ["source_root", "creation_notes", "chain_of_custody", "registry_status"],
          "optimization": ["requested_services", "recommended_services", "priority", "notes"],
      }
      
      # Required fields per-item for list-shaped sections.
      REQUIRED_ASSET_KEYS = [
          "id",
          "relative_path",
          "category",
          "role",
          "mime_guess",
          "size_bytes",
          "sha256",
      ]
      REQUIRED_MISSING_INFO_KEYS = ["field", "question", "blocks", "severity"]
      REQUIRED_RISK_FLAG_KEYS = ["label", "severity", "detail", "recommended_action"]
      
      
      class ValidationError(Exception):
          """Raised for a structural problem; message is shown to the user."""
      
      
      def parse_args() -> argparse.Namespace:
          parser = argparse.ArgumentParser(
              description=(
                  "Validate that a Suede rights transfer package folder is "
                  "structurally complete: all 7 required report files are present, "
                  "suede-intake.json is valid JSON matching the documented schema, "
                  "and every asset entry has a sha256 hash. This does not evaluate "
                  "rights facts or risk-flag severity -- a structurally valid "
                  "package can still carry open, high-severity risk flags."
              )
          )
          parser.add_argument(
              "package",
              help="Path to a transfer package folder (e.g. output of create_transfer_package.py).",
          )
          parser.add_argument(
              "--quiet",
              action="store_true",
              help="Suppress the pass summary; print nothing on success.",
          )
          parser.add_argument(
              "--strict-current",
              action="store_true",
              help=(
                  f"Require schema_version {CURRENT_SCHEMA_VERSION}. Without this flag, "
                  "legacy 0.1.0 packages remain structurally inspectable."
              ),
          )
          return parser.parse_args()
      
      
      def check_required_files(package: Path) -> list[str]:
          missing = [name for name in REQUIRED_FILES if not (package / name).is_file()]
          if missing:
              return [f"Missing required file: {name}" for name in missing]
          return []
      
      
      def load_intake_json(package: Path) -> tuple[dict[str, Any] | None, list[str]]:
          intake_path = package / "suede-intake.json"
          if not intake_path.is_file():
              # Already reported by check_required_files; do not double-report.
              return None, []
          text = intake_path.read_text(encoding="utf-8")
          try:
              data = json.loads(text)
          except json.JSONDecodeError as exc:
              return None, [f"suede-intake.json is not valid JSON: {exc}"]
          if not isinstance(data, dict):
              return None, ["suede-intake.json must contain a JSON object at the top level."]
          return data, []
      
      
      def check_top_level_shape(data: dict[str, Any], strict_current: bool = False) -> list[str]:
          errors = []
          for key in REQUIRED_TOP_LEVEL_KEYS:
              if key not in data:
                  errors.append(f"suede-intake.json missing top-level field: {key}")
          version = data.get("schema_version")
          if version == CURRENT_SCHEMA_VERSION:
              if data.get("package_type") != "suede-transfer-package":
                  errors.append("suede-intake.json package_type must be 'suede-transfer-package'.")
              for key in V2_REQUIRED_TOP_LEVEL_KEYS:
                  if key not in data:
                      errors.append(f"suede-intake.json schema 0.2.0 missing top-level field: {key}")
              allowed = set(REQUIRED_TOP_LEVEL_KEYS) | set(V2_REQUIRED_TOP_LEVEL_KEYS) | {"metadata_source"}
              for key in sorted(set(data) - allowed):
                  errors.append(f"suede-intake.json schema 0.2.0 has unsupported top-level field: {key}")
          elif version in LEGACY_SCHEMA_VERSIONS:
              if strict_current:
                  errors.append(
                      f"suede-intake.json uses legacy schema {version}; --strict-current requires {CURRENT_SCHEMA_VERSION}."
                  )
          else:
              errors.append(
                  f"suede-intake.json schema_version must be {CURRENT_SCHEMA_VERSION}"
                  f" or a supported legacy version ({', '.join(sorted(LEGACY_SCHEMA_VERSIONS))})."
              )
          return errors
      
      
      def check_nested_shape(data: dict[str, Any]) -> list[str]:
          errors = []
          for parent, keys in REQUIRED_NESTED_KEYS.items():
              section = data.get(parent)
              if not isinstance(section, dict):
                  # Missing-top-level-field already reported; skip nested check
                  # rather than raising a redundant/confusing error.
                  if parent in data:
                      errors.append(f"suede-intake.json field '{parent}' must be a JSON object.")
                  continue
              for key in keys:
                  if key not in section:
                      errors.append(f"suede-intake.json '{parent}.{key}' is missing.")
          return errors
      
      
      def check_list_shaped_sections(data: dict[str, Any]) -> list[str]:
          errors = []
          for field in ("assets", "missing_information", "risk_flags"):
              if field in data and not isinstance(data[field], list):
                  errors.append(f"suede-intake.json field '{field}' must be a JSON array.")
          return errors
      
      
      def check_assets(data: dict[str, Any]) -> list[str]:
          errors = []
          assets = data.get("assets")
          if not isinstance(assets, list):
              return errors  # already reported above
          for index, asset in enumerate(assets):
              label = f"assets[{index}]"
              if not isinstance(asset, dict):
                  errors.append(f"suede-intake.json {label} must be a JSON object.")
                  continue
              asset_id = asset.get("id")
              if isinstance(asset_id, str) and asset_id:
                  label = f"assets[{index}] ({asset_id})"
              for key in REQUIRED_ASSET_KEYS:
                  if key not in asset:
                      errors.append(f"suede-intake.json {label} missing field: {key}")
              sha = asset.get("sha256")
              if "sha256" in asset:
                  if not isinstance(sha, str) or not sha.strip():
                      errors.append(f"suede-intake.json {label} has an empty or non-string sha256 field.")
                  elif len(sha) != 64 or any(c not in "0123456789abcdefABCDEF" for c in sha):
                      errors.append(
                          f"suede-intake.json {label} sha256 field does not look like a 64-char hex digest: {sha!r}"
                      )
          return errors
      
      
      def check_missing_information(data: dict[str, Any]) -> list[str]:
          errors = []
          items = data.get("missing_information")
          if not isinstance(items, list):
              return errors
          for index, item in enumerate(items):
              if not isinstance(item, dict):
                  errors.append(f"suede-intake.json missing_information[{index}] must be a JSON object.")
                  continue
              for key in REQUIRED_MISSING_INFO_KEYS:
                  if key not in item:
                      errors.append(f"suede-intake.json missing_information[{index}] missing field: {key}")
          return errors
      
      
      def check_risk_flags(data: dict[str, Any]) -> list[str]:
          errors = []
          items = data.get("risk_flags")
          if not isinstance(items, list):
              return errors
          for index, item in enumerate(items):
              if not isinstance(item, dict):
                  errors.append(f"suede-intake.json risk_flags[{index}] must be a JSON object.")
                  continue
              for key in REQUIRED_RISK_FLAG_KEYS:
                  if key not in item:
                      errors.append(f"suede-intake.json risk_flags[{index}] missing field: {key}")
          return errors
      
      
      def _object_list(data: dict[str, Any], field: str, errors: list[str]) -> list[dict[str, Any]]:
          value = data.get(field)
          if not isinstance(value, list):
              errors.append(f"suede-intake.json field '{field}' must be a JSON array.")
              return []
          objects: list[dict[str, Any]] = []
          for index, item in enumerate(value):
              if not isinstance(item, dict):
                  errors.append(f"suede-intake.json {field}[{index}] must be a JSON object.")
              else:
                  for key in sorted(V2_REQUIRED_ITEM_KEYS.get(field, set())):
                      if key not in item:
                          errors.append(f"suede-intake.json {field}[{index}] missing field: {key}")
                  objects.append(item)
          return objects
      
      
      def _ids_for(records: list[dict[str, Any]], field: str, errors: list[str]) -> set[str]:
          ids: set[str] = set()
          for index, record in enumerate(records):
              record_id = record.get("id")
              if not isinstance(record_id, str) or not record_id:
                  errors.append(f"suede-intake.json {field}[{index}] requires a non-empty id.")
                  continue
              if record_id in ids:
                  errors.append(f"suede-intake.json {field} contains duplicate id: {record_id}")
              ids.add(record_id)
          return ids
      
      
      def _check_evidence_state(record: dict[str, Any], label: str, errors: list[str]) -> None:
          status = record.get("status")
          evidence = record.get("evidence_refs")
          if status not in EVIDENCE_STATUSES:
              errors.append(
                  f"suede-intake.json {label}.status must be one of: {', '.join(sorted(EVIDENCE_STATUSES))}."
              )
          if not isinstance(evidence, list):
              errors.append(f"suede-intake.json {label}.evidence_refs must be a JSON array.")
          elif status == "confirmed" and not any(isinstance(item, str) and item for item in evidence):
              errors.append(
                  f"suede-intake.json {label} is confirmed but has no evidence_refs; downgrade the state or add evidence."
              )
      
      
      def _check_identifiers(record: dict[str, Any], label: str, errors: list[str]) -> None:
          identifiers = record.get("identifiers")
          if not isinstance(identifiers, list):
              errors.append(f"suede-intake.json {label}.identifiers must be a JSON array.")
              return
          allowed = {"ISRC", "ISWC", "IPI_CAE", "ISNI", "UPC_EAN", "CATALOG_NUMBER", "PROPRIETARY"}
          for index, identifier in enumerate(identifiers):
              item_label = f"{label}.identifiers[{index}]"
              if not isinstance(identifier, dict):
                  errors.append(f"suede-intake.json {item_label} must be a JSON object.")
                  continue
              if identifier.get("scheme") not in allowed:
                  errors.append(f"suede-intake.json {item_label}.scheme is unsupported.")
              if not isinstance(identifier.get("value"), str) or not identifier.get("value"):
                  errors.append(f"suede-intake.json {item_label}.value must be a non-empty string.")
              _check_evidence_state(identifier, item_label, errors)
      
      
      def _check_refs(
          record: dict[str, Any], field: str, allowed: set[str], label: str, errors: list[str], nullable: bool = False
      ) -> None:
          value = record.get(field)
          if nullable and value is None:
              return
          values = value if isinstance(value, list) else [value]
          for item in values:
              if not isinstance(item, str) or item not in allowed:
                  errors.append(f"suede-intake.json {label}.{field} references unknown id: {item!r}")
      
      
      def _check_array_fields(record: dict[str, Any], fields: tuple[str, ...], label: str, errors: list[str]) -> None:
          for field in fields:
              if not isinstance(record.get(field), list):
                  errors.append(f"suede-intake.json {label}.{field} must be a JSON array.")
      
      
      def check_v2_interoperability(data: dict[str, Any]) -> list[str]:
          """Check v0.2 evidence state and cross-object references without legal inference."""
          if data.get("schema_version") != CURRENT_SCHEMA_VERSION:
              return []
      
          errors: list[str] = []
          parties = _object_list(data, "parties", errors)
          works = _object_list(data, "works", errors)
          recordings = _object_list(data, "recordings", errors)
          releases = _object_list(data, "releases", errors)
          claims = _object_list(data, "rights_claims", errors)
          licenses = _object_list(data, "licenses", errors)
          third_party = _object_list(data, "third_party_material", errors)
          consents = _object_list(data, "consents", errors)
      
          party_ids = _ids_for(parties, "parties", errors)
          work_ids = _ids_for(works, "works", errors)
          recording_ids = _ids_for(recordings, "recordings", errors)
          release_ids = _ids_for(releases, "releases", errors)
          _ids_for(claims, "rights_claims", errors)
          license_ids = _ids_for(licenses, "licenses", errors)
          _ids_for(third_party, "third_party_material", errors)
          _ids_for(consents, "consents", errors)
          asset_ids = {
              item.get("id") for item in data.get("assets", []) if isinstance(item, dict) and isinstance(item.get("id"), str)
          }
          subject_ids = work_ids | recording_ids | release_ids
      
          for index, party in enumerate(parties):
              label = f"parties[{index}]"
              _check_evidence_state(party, label, errors)
              _check_identifiers(party, label, errors)
              _check_array_fields(party, ("roles",), label, errors)
              if party.get("privacy_classification") not in {
                  "public", "shared-with-recipient", "private-draft", "restricted", "do-not-share"
              }:
                  errors.append(f"suede-intake.json {label}.privacy_classification is unsupported.")
      
          for field, records in (("works", works), ("recordings", recordings), ("releases", releases)):
              for index, record in enumerate(records):
                  label = f"{field}[{index}]"
                  _check_evidence_state(record, label, errors)
                  _check_identifiers(record, label, errors)
      
          for index, work in enumerate(works):
              label = f"works[{index}]"
              _check_array_fields(work, ("writer_party_ids", "publisher_party_ids", "evidence_refs"), label, errors)
              _check_refs(work, "writer_party_ids", party_ids, label, errors)
              _check_refs(work, "publisher_party_ids", party_ids, label, errors)
          for index, recording in enumerate(recordings):
              label = f"recordings[{index}]"
              _check_array_fields(
                  recording,
                  ("asset_ids", "performer_party_ids", "master_owner_party_ids", "evidence_refs"),
                  label,
                  errors,
              )
              _check_refs(recording, "asset_ids", asset_ids, label, errors)
              _check_refs(recording, "performer_party_ids", party_ids, label, errors)
              _check_refs(recording, "master_owner_party_ids", party_ids, label, errors)
          for index, release in enumerate(releases):
              label = f"releases[{index}]"
              _check_array_fields(release, ("recording_ids", "territories", "evidence_refs"), label, errors)
              _check_refs(release, "recording_ids", recording_ids, label, errors)
              _check_refs(release, "label_party_id", party_ids, label, errors, nullable=True)
              _check_refs(release, "distributor_party_id", party_ids, label, errors, nullable=True)
      
          share_totals: dict[tuple[str, str, tuple[str, ...], Any, Any], float] = {}
          allowed_right_types = {
              "composition", "publishing", "mechanical", "performance", "synchronization",
              "master", "neighboring", "distribution", "other"
          }
          for index, claim in enumerate(claims):
              label = f"rights_claims[{index}]"
              _check_evidence_state(claim, label, errors)
              _check_array_fields(claim, ("territories", "evidence_refs"), label, errors)
              if claim.get("subject_type") not in {"work", "recording", "release"}:
                  errors.append(f"suede-intake.json {label}.subject_type is unsupported.")
              if claim.get("right_type") not in allowed_right_types:
                  errors.append(f"suede-intake.json {label}.right_type is unsupported.")
              _check_refs(claim, "subject_id", subject_ids, label, errors)
              _check_refs(claim, "party_id", party_ids, label, errors)
              share = claim.get("share_percent")
              if share is not None:
                  if not isinstance(share, (int, float)) or isinstance(share, bool) or not 0 <= share <= 100:
                      errors.append(f"suede-intake.json {label}.share_percent must be null or between 0 and 100.")
                  else:
                      territories = claim.get("territories")
                      territory_scope = tuple(
                          sorted(str(item) for item in territories)
                      ) if isinstance(territories, list) else ()
                      key = (
                          str(claim.get("subject_id")),
                          str(claim.get("right_type")),
                          territory_scope,
                          claim.get("start_date"),
                          claim.get("end_date"),
                      )
                      share_totals[key] = share_totals.get(key, 0.0) + float(share)
          for (subject_id, right_type, territories, start_date, end_date), total in share_totals.items():
              if total > 100.000001:
                  scope = ",".join(territories) or "unspecified-territory"
                  term = f"{start_date or 'open'}..{end_date or 'open'}"
                  errors.append(
                      f"suede-intake.json rights claims for {subject_id}/{right_type} "
                      f"scope {scope} {term} total {total:g}%, above 100%."
                  )
      
          for index, license_record in enumerate(licenses):
              label = f"licenses[{index}]"
              _check_evidence_state(license_record, label, errors)
              _check_array_fields(
                  license_record,
                  (
                      "subject_ids", "licensor_party_ids", "licensee_party_ids", "use_types", "media",
                      "territories", "restrictions", "evidence_refs"
                  ),
                  label,
                  errors,
              )
              if license_record.get("sublicensing") not in {"allowed", "prohibited", "unknown"}:
                  errors.append(f"suede-intake.json {label}.sublicensing is unsupported.")
              for field in ("exclusive", "revocable"):
                  if license_record.get(field) is not None and not isinstance(license_record.get(field), bool):
                      errors.append(f"suede-intake.json {label}.{field} must be boolean or null.")
              _check_refs(license_record, "subject_ids", subject_ids, label, errors)
              _check_refs(license_record, "licensor_party_ids", party_ids, label, errors)
              _check_refs(license_record, "licensee_party_ids", party_ids, label, errors)
          for index, item in enumerate(third_party):
              label = f"third_party_material[{index}]"
              _check_evidence_state(item, label, errors)
              _check_array_fields(item, ("subject_ids", "evidence_refs"), label, errors)
              if item.get("type") not in {"sample", "interpolation", "cover", "beat", "loop", "visual", "other"}:
                  errors.append(f"suede-intake.json {label}.type is unsupported.")
              _check_refs(item, "subject_ids", subject_ids, label, errors)
              _check_refs(item, "license_id", license_ids, label, errors, nullable=True)
          for index, consent in enumerate(consents):
              label = f"consents[{index}]"
              _check_evidence_state(consent, label, errors)
              _check_array_fields(consent, ("media", "evidence_refs"), label, errors)
              for field in ("ai_use", "voice_likeness"):
                  if consent.get(field) not in {"allowed", "prohibited", "limited", "unknown"}:
                      errors.append(f"suede-intake.json {label}.{field} is unsupported.")
              _check_refs(consent, "party_id", party_ids, label, errors)
      
          provenance = data.get("provenance")
          if isinstance(provenance, dict):
              credentials = provenance.get("content_credentials")
              if not isinstance(credentials, list):
                  errors.append("suede-intake.json provenance.content_credentials must be a JSON array.")
              else:
                  for index, credential in enumerate(credentials):
                      label = f"provenance.content_credentials[{index}]"
                      if not isinstance(credential, dict):
                          errors.append(f"suede-intake.json {label} must be a JSON object.")
                          continue
                      for key in (
                          "asset_id", "kind", "manifest_reference", "verification_status", "verified_at", "evidence_refs"
                      ):
                          if key not in credential:
                              errors.append(f"suede-intake.json {label} missing field: {key}")
                      if credential.get("kind") not in {"sha256", "c2pa", "other"}:
                          errors.append(f"suede-intake.json {label}.kind is unsupported.")
                      if credential.get("verification_status") not in {"verified", "unverified", "failed", "not-checked"}:
                          errors.append(f"suede-intake.json {label}.verification_status is unsupported.")
                      if not isinstance(credential.get("evidence_refs"), list):
                          errors.append(f"suede-intake.json {label}.evidence_refs must be a JSON array.")
                      _check_refs(credential, "asset_id", asset_ids, label, errors)
      
          privacy = data.get("privacy")
          if not isinstance(privacy, dict):
              errors.append("suede-intake.json privacy must be a JSON object.")
          else:
              allowed_privacy = {"public", "shared-with-recipient", "private-draft", "restricted", "do-not-share"}
              if privacy.get("default_classification") not in allowed_privacy:
                  errors.append("suede-intake.json privacy.default_classification is unsupported.")
              if not isinstance(privacy.get("field_rules"), list):
                  errors.append("suede-intake.json privacy.field_rules must be a JSON array.")
              else:
                  for index, rule in enumerate(privacy["field_rules"]):
                      label = f"privacy.field_rules[{index}]"
                      if not isinstance(rule, dict):
                          errors.append(f"suede-intake.json {label} must be a JSON object.")
                          continue
                      for key in ("json_pointer", "classification", "redaction_action", "reason"):
                          if key not in rule:
                              errors.append(f"suede-intake.json {label} missing field: {key}")
                      if not isinstance(rule.get("json_pointer"), str) or not rule.get("json_pointer", "").startswith("/"):
                          errors.append(f"suede-intake.json {label}.json_pointer must start with '/'.")
                      if rule.get("classification") not in allowed_privacy:
                          errors.append(f"suede-intake.json {label}.classification is unsupported.")
                      if rule.get("redaction_action") not in {"keep", "mask", "remove", "review"}:
                          errors.append(f"suede-intake.json {label}.redaction_action is unsupported.")
              if not isinstance(privacy.get("redaction_required_before_external_share"), bool):
                  errors.append(
                      "suede-intake.json privacy.redaction_required_before_external_share must be a boolean."
                  )
          return errors
      
      
      def check_published_json_schema(data: dict[str, Any]) -> list[str]:
          if data.get("schema_version") != CURRENT_SCHEMA_VERSION:
              return []
          try:
              schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
          except (OSError, json.JSONDecodeError) as exc:
              return [f"Could not load published JSON Schema {SCHEMA_PATH}: {exc}"]
          if not isinstance(schema, dict):
              return [f"Published JSON Schema must be a JSON object: {SCHEMA_PATH}"]
          try:
              assert_supported_schema(schema)
              return [f"JSON Schema {error}" for error in validate_instance(data, schema)]
          except ValueError as exc:
              return [f"Published JSON Schema uses an unsupported or invalid contract: {exc}"]
      
      
      def summarize_risk_posture(data: dict[str, Any]) -> str:
          """Describe risk-flag posture for the pass summary. Informational only --
          never affects the exit code. Structural validity and business-logic
          confirmation status are independent axes; see module docstring."""
          flags = data.get("risk_flags")
          if not isinstance(flags, list) or not flags:
              return "no risk flags recorded"
          high = sum(1 for f in flags if isinstance(f, dict) and f.get("severity") == "high")
          medium = sum(1 for f in flags if isinstance(f, dict) and f.get("severity") == "medium")
          low = sum(1 for f in flags if isinstance(f, dict) and f.get("severity") == "low")
          parts = []
          if high:
              parts.append(f"{high} high")
          if medium:
              parts.append(f"{medium} medium")
          if low:
              parts.append(f"{low} low")
          other = len(flags) - high - medium - low
          if other:
              parts.append(f"{other} other")
          return f"{len(flags)} risk flag(s) recorded ({', '.join(parts)}) -- structural validity is unaffected"
      
      
      def main() -> int:
          args = parse_args()
          package = Path(args.package).expanduser().resolve()
      
          if not package.exists() or not package.is_dir():
              print(f"Package folder not found: {package}", file=sys.stderr)
              return 2
      
          errors: list[str] = []
          errors += check_required_files(package)
      
          data, load_errors = load_intake_json(package)
          errors += load_errors
      
          if data is not None:
              errors += check_top_level_shape(data, strict_current=args.strict_current)
              errors += check_nested_shape(data)
              errors += check_list_shaped_sections(data)
              errors += check_assets(data)
              errors += check_missing_information(data)
              errors += check_risk_flags(data)
              errors += check_published_json_schema(data)
              errors += check_v2_interoperability(data)
      
          if errors:
              print(f"FAIL: {package} is not a structurally valid transfer package.", file=sys.stderr)
              print("", file=sys.stderr)
              for error in errors:
                  print(f"- {error}", file=sys.stderr)
              print("", file=sys.stderr)
              print(
                  "Structural validity means the required files exist and "
                  "suede-intake.json matches the documented schema. It says "
                  "nothing about whether rights facts are confirmed.",
                  file=sys.stderr,
              )
              return 1
      
          if not args.quiet:
              asset_count = len(data.get("assets", [])) if data else 0
              missing_count = len(data.get("missing_information", [])) if data else 0
              print(f"PASS: {package}")
              version = data.get("schema_version", "unknown") if data else "unknown"
              posture = "current" if version == CURRENT_SCHEMA_VERSION else "legacy"
              print(f"- Schema {version} ({posture}).")
              print(f"- All {len(REQUIRED_FILES)} required report files present.")
              if version == CURRENT_SCHEMA_VERSION:
                  print("- suede-intake.json passes the published Draft 2020-12 JSON Schema contract.")
              else:
                  print("- suede-intake.json is valid JSON and matches the supported legacy shape.")
              print(f"- {asset_count} asset(s) inventoried, each with a sha256 hash.")
              print(f"- {missing_count} open missing-information item(s) recorded.")
              if data is not None:
                  print(f"- Risk posture: {summarize_risk_posture(data)}.")
              print(
                  "\nThis confirms structural completeness only. Confirm rights facts "
                  "with the creator before registry, licensing, or royalty routing."
              )
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • CARD.md 5.5 KB
    # Skill Card — Creator Rights Package Builder
    
    <!-- Generated by scripts/build-skill-cards.mjs — do not hand-edit. -->
    <!-- Regenerate with: npm run build:cards -->
    
    Release record for the `suede-rights-passport` skill, following the NVIDIA skill-card template (<https://docs.nvidia.com/skills/skill-cards>). It tells a reviewer what the skill does, who owns it, what it needs, what could go wrong, and what evidence backs the release — without requiring them to open the source first.
    
    ## Description
    
    Suede Labs skill that turns messy creator materials into a local, offline rights-and-provenance transfer package: inventoried and hashed assets, a normalized suede-intake.json manifest, credits and splits, license notes, provenance, and a missing-information report, validated by a bundled stdlib script.
    
    Status: production. Ships in the `suede-skills` plugin (the full pack) at release 0.19.0; loads as a Claude Code / Codex agent skill from this directory's [SKILL.md](./SKILL.md).
    
    ## Owner
    
    Jason Colapietro, Suede Labs AI (<https://github.com/JasonColapietro>). Security contact: `info@suedeai.ai` per [SECURITY.md](../../SECURITY.md).
    
    ## License / Terms of Use
    
    MIT ([LICENSE](../../LICENSE)). The pack's combined license expression is `MIT AND BSD-3-Clause`; this skill bundles no third-party licensed material of its own.
    
    ## Use Case
    
    Target users: developers and creators running the skill inside a Claude Code or Codex CLI session.
    
    Use when a creator needs to hand a song, release, or project to a collaborator, advisor, registry, marketplace, or label; when someone asks for a rights package, intake package, or handoff folder; or when a validated manifest is needed before licensing or royalty-routing review. Carries questions, not answers: building the package clears nothing and uploads nothing.
    
    Out of scope — finding or investigating the rights gaps in the first place (use suede-rights-audit); linting a release folder's files and metadata (use suede-release-linter); a sync one-sheet (use suede-sync-packaging).
    
    ## Deployment Geography
    
    Global. The skill is a prompt-and-script package that runs locally inside the invoking agent session; it pins no region-specific service of its own.
    
    ## Requirements / Dependencies
    
    - A Claude Code or Codex CLI session with the `suede-skills` plugin installed (install options: <https://skills.suedeai.ai/>).
    - Bundled files loaded relative to this directory: `agents/` (1 file), `scripts/` (30 files), `references/` (6 files), `assets/` (8 files).
    - Credentials: none are bundled or required by the skill files. Any tool or API credentials come from the host session; never paste credentials into skill files, prompts, or outputs.
    
    ## Known Risks and Mitigations
    
    - Risk: an agent treats a quality gate as autonomous authority. Mitigation: every gate in the pack is advisory — it changes what is reported, never what the user decided; only extreme-risk findings (data loss, credential exposure, legal/rights violations, payment mistakes, irreversible public damage) pause for the user's explicit choice.
    - Risk: a skill instruction is used to act outside its mandate. Mitigation: the hard limits in the skill body's "Public Safety Rules" section, quoted below.
    
    From "Public Safety Rules":
    
    - Do not say Suede owns, controls, or has cleared a work unless the user provides explicit proof.
    - Do not call the package a legal contract.
    - Do not ask for private keys, seed phrases, unreleased account secrets, or full payment credentials.
    - Do not include private implementation details, private endpoints, internal provider names, or non-public pricing.
    - Do not upload files or call live services unless the user explicitly asks and provides the relevant authenticated workflow.
    - Treat generated reports and transfer packages as private drafts until a creator or operator reviews and redacts them for the intended audience.
    - Do not call a field crosswalk DDEX conformance, and do not call a hash a C2PA Content Credential. Validate the receiver's exact profile separately.
    - Keep composition, recording/master, and release identifiers on their proper objects. ISWC and ISRC are not interchangeable, and neither proves ownership.
    - Unknown voice, likeness, or synthetic-media consent stays unknown; silence is not consent.
    - Keep public positioning focused on broadly reusable creator workflows: rights packaging, provenance, registry readiness, royalty routing, licensing, and agent commerce.
    
    ## References
    
    - Skill source: [`skills/suede-rights-passport/SKILL.md`](./SKILL.md)
    - Rendered reference page: <https://skills.suedeai.ai/skills/suede-rights-passport.html>
    - Security policy and reviewed scanner exceptions: [SECURITY.md](../../SECURITY.md) and [`.plugin-scanner.toml`](../../.plugin-scanner.toml) at the repo root
    
    ## Skill Output
    
    Markdown analysis and recommendations returned in the agent's response. The skill publishes, posts, and sends nothing without the user's explicit authorization; delivery decisions stay with the user.
    
    ## Skill Version
    
    0.19.0 — the pack is single-versioned, so every skill releases together; see [VERSION](../../VERSION) and [CITATION.cff](../../CITATION.cff) for the release identifier this card describes.
    
    ## Ethical Considerations
    
    - The skill produces recommendations for a human decision-maker. Publishing, sending, payment, and rights decisions stay with the user.
    - Its gates require verifiable claims and honest reporting; do not use the skill to fabricate claims, evidence, metrics, or attribution.
    - Report suspected misuse or a security concern privately per [SECURITY.md](../../SECURITY.md); do not open a public issue for it.
    
  • SKILL.md 14.7 KB
    ---
    name: suede-rights-passport
    description: "Suede Labs skill that turns messy creator materials into a local, offline rights-and-provenance transfer package: inventoried and hashed assets, a normalized suede-intake.json manifest, credits and splits, license notes, provenance, and a missing-information report, validated by a bundled stdlib script. Use when a creator needs to hand a song, release, or project to a collaborator, advisor, registry, marketplace, or label; when someone asks for a rights package, intake package, or handoff folder; or when a validated manifest is needed before licensing or royalty-routing review. Carries questions, not answers: building the package clears nothing and uploads nothing. NOT FOR: finding or investigating the rights gaps in the first place (use suede-rights-audit); linting a release folder's files and metadata (use suede-release-linter); a sync one-sheet (use suede-sync-packaging)."
    ---
    
    # Creator Rights Package Builder
    
    ## Gate policy — advisory, not blocking
    
    Every check, gate, and ship verdict in this skill — including every "do not
    ship / publish / proceed" line — is a **recommendation to the user, not a
    control on the agent**. Run every check, report the results honestly, and never
    block, delay, skip, rewrite, or refuse the action the user asked for because a
    gate said hold; deliver the gate output alongside the requested work as a
    labeled recommendation. A failed gate changes what you report, never what you
    do. Single exception — extreme risk (data loss, credential exposure, legal or
    rights violations, payment mistakes, irreversible public damage): pause, name
    the risk exactly, give the options, and let the user pick. Their choice is final.
    
    ## Overview
    
    Create a local rights and provenance transfer package from messy creator materials. The package should make the work easier for a creator, collaborator, advisor, registry, marketplace, label, or optional Suede reviewer to inspect, optimize, register, route royalties for, license, and expose to agent-readable commerce systems.
    
    **Core principle:** the package carries questions, not answers. Every rights fact ships as confirmed (with user-supplied evidence) or as unknown with a question in `missing-info-report.md`. The package never resolves a rights question, and building it clears nothing.
    
    Public v1 is offline-first: prepare files and metadata, do not upload files, write to a registry, request private keys, or claim legal clearance. The 0.2 manifest separates musical works, recordings, releases, parties, rights claims, licenses, third-party material, consent, provenance, and privacy so a downstream operator can map facts without collapsing unlike rights objects.
    
    Division of labor: `suede-rights-audit` finds and organizes the gaps; this skill packages the folder. If the gaps themselves need investigation or evidence work, hand off to the audit first.
    
    ## Workflow
    
    1. Identify the source folder or supplied files.
    2. Ask for the output location if it is not obvious.
    3. Read `references/package-standard.md` for the expected transfer package shape.
    4. If working on a local folder, run `scripts/create_transfer_package.py` to inventory files, hash assets, and create starter reports.
    5. Read `references/creator-questions.md` and ask only for missing information that blocks package quality.
    6. Fill or refine the generated package files:
       - `RIGHTS_PASSPORT.md`
       - `suede-intake.json`
       - `provenance.md`
       - `credits-and-splits.md`
       - `license-notes.md`
       - `optimization-brief.md`
       - `missing-info-report.md`
    7. Flag uncertainty clearly. Use `unknown`, `unconfirmed`, or `needs creator confirmation` instead of inventing rights facts. Never resolve a rights question while packaging: ownership, split, sample, and license statuses move to confirmed only on user-supplied evidence, and every open gap ships as a question in `missing-info-report.md`.
    8. For an external exchange, read `references/ddex-c2pa-crosswalk.md`, identify the receiver's exact profile/version, and keep the mapping labeled as a crosswalk until receiver conformance tooling passes.
    9. Run `scripts/validate_transfer_package.py` with `--strict-current` against new output folders. A pass confirms schema, evidence-state, reference, and share-bound structure only — it does not mean rights are confirmed.
    10. End with a concise transfer summary: package path, schema version, files found, missing info, risk flags, privacy/redaction posture, and recommended next step.
    
    ## Quick Start
    
    For a local project folder:
    
    ```bash
    python3 /path/to/suede-rights-passport/scripts/create_transfer_package.py \
      /path/to/source-project \
      --output /path/to/transfer-package \
      --metadata /path/to/source-project/metadata.json \
      --project-title "Project Title" \
      --artist "Artist Name"
    ```
    
    To copy media into the transfer package as well as inventory it:
    
    ```bash
    python3 /path/to/suede-rights-passport/scripts/create_transfer_package.py \
      /path/to/source-project \
      --output /path/to/transfer-package \
      --copy-assets
    ```
    
    Safety defaults:
    
    - Hidden files, dependency folders, build outputs, caches, and secret-like files are skipped by default.
    - Symlinked sources, metadata, files, and directories are rejected; the builder
      hashes or copies only regular files that resolve inside the declared source tree.
    - Unrecognized file types are skipped unless `--include-other` is passed.
    - Absolute local paths are redacted to share-safer names unless `--include-absolute-paths` is passed.
    - Existing generated package files are not overwritten unless `--force` is passed.
    - The output folder cannot be the same folder as the source or live inside it.
    - Public-safe JSON, YAML, or key=value text metadata can prefill known project,
      rights, contributor, release, wallet, and provenance facts. Do not point
      metadata at real `.env`, credential, wallet, or deployment config files.
      Unknown facts remain flagged. YAML metadata requires PyYAML.
    
    **Halt format — material that may not be shareable.** Before any `--copy-assets`
    run, scan for draft, unreleased, private, or do-not-share files. If any appear:
    stop, name the specific files and why each one reads as do-not-share, offer the
    options (exclude and proceed / include with a redaction note / inventory without
    copying / abort), and wait for the choice. Use the same shape for anything
    hitting the gate policy's extreme-risk exception. Never guess which way the
    creator would want it.
    
    ## Validate A Package
    
    After creating or editing a package, check that it is structurally complete
    with `scripts/validate_transfer_package.py`:
    
    ```bash
    python3 /path/to/suede-rights-passport/scripts/validate_transfer_package.py \
      --strict-current /path/to/transfer-package
    ```
    
    It is a dependency-free (stdlib-only) check that executes the bundled Draft
    2020-12 JSON Schema. It confirms the 7 required report files, that
    `suede-intake.json` matches the shape documented in
    `references/intake-schema.md`, real 64-hex `sha256` digests on every asset,
    unique IDs with resolving references, evidence on every `confirmed` record,
    in-range and non-oversubscribed shares, and explicit privacy/redaction posture —
    each one mapped to its exact error string in the Completion Checklist below.
    
    It exits non-zero with a specific error list on failure and prints a short pass
    summary — including a risk-flag count — on success. Run `--help` for usage, or
    `--quiet` to suppress the success summary. Legacy 0.1 packages remain
    inspectable without `--strict-current`; new exchanges require 0.2.0.
    
    To migrate an existing 0.1 manifest without modifying it:
    
    ```bash
    python3 /path/to/suede-rights-passport/scripts/migrate_intake_v1_to_v2.py \
      /path/to/transfer-package/suede-intake.json
    ```
    
    The migration writes `suede-intake.v0.2.json`, records the source manifest
    digest and custody history, preserves open questions and risk flags, maps only
    roles stated in source data, and never upgrades evidence state or fills missing
    shares. Review it before replacing any current manifest.
    
    **Structural validity is not a rights clearance.** The validator checks that a
    package is shaped correctly and complete, not that the rights facts inside it
    are confirmed — a project with unconfirmed ownership, unconfirmed splits, or an
    uncleared sample still passes, because `risk_flags[]` and
    `missing_information[]` are exactly where that uncertainty belongs. Never read a
    PASS as clearance, and never expect a risk-flagged package to fail.
    
    `scripts/fixtures/sample-complete-package/` and `sample-blocked-package/` are
    worked examples at both ends of that range, and both validate. Read
    `scripts/fixtures/README.md` when you need a concrete example of what a
    risk-flagged but structurally valid package looks like, or when changing
    `create_transfer_package.py`.
    
    ## Package Standards
    
    Read each bundled reference at the moment it is needed, not up front:
    
    - `references/package-standard.md`: before creating or repairing any package — required output files, folder structure, risk labels, and quality bar.
    - `references/intake-schema.md`: when filling or validating `suede-intake.json`.
    - `references/ddex-c2pa-crosswalk.md`: before external standards mapping or any DDEX/C2PA claim.
    - `references/optimization-checklist.md`: when writing `optimization-brief.md`.
    - `references/creator-questions.md`: when information is missing — ask only the questions that block package quality.
    - `references/passport-context.md`: when the user asks how the package relates to Suede review or the Suede Creator Passport.
    
    Use the bundled assets as templates when creating or repairing a package:
    
    - `assets/rights-passport.template.md`
    - `assets/suede-intake.template.json`
    - `assets/suede-intake.schema.json`
    - `assets/provenance.template.md`
    - `assets/credits-and-splits.template.md`
    - `assets/license-notes.template.md`
    - `assets/optimization-brief.template.md`
    - `assets/missing-info-report.template.md`
    
    ## Public Safety Rules
    
    - Do not say Suede owns, controls, or has cleared a work unless the user provides explicit proof.
    - Do not call the package a legal contract.
    - Do not ask for private keys, seed phrases, unreleased account secrets, or full payment credentials.
    - Do not include private implementation details, private endpoints, internal provider names, or non-public pricing.
    - Do not upload files or call live services unless the user explicitly asks and provides the relevant authenticated workflow.
    - Treat generated reports and transfer packages as private drafts until a
      creator or operator reviews and redacts them for the intended audience.
    - Do not call a field crosswalk DDEX conformance, and do not call a hash a C2PA
      Content Credential. Validate the receiver's exact profile separately.
    - Keep composition, recording/master, and release identifiers on their proper
      objects. ISWC and ISRC are not interchangeable, and neither proves ownership.
    - Unknown voice, likeness, or synthetic-media consent stays unknown; silence is
      not consent.
    - Keep public positioning focused on broadly reusable creator workflows: rights packaging, provenance, registry readiness, royalty routing, licensing, and agent commerce.
    
    ## Completion Checklist
    
    Run `scripts/validate_transfer_package.py` with `--strict-current` against the output
    folder first and report the result: it is the evidence behind most of this
    checklist, and every structural gap it names gets fixed before the package is
    called ready. Each machine-checked box names the error raised when it is unmet:
    
    - All 7 required files present — *missing required file*.
    - Every asset has a stable relative path and a 64-hex SHA-256 — *empty or
      non-string sha256 field*.
    - Parties, works, recordings, and releases have distinct IDs that resolve —
      *duplicate id* / *references unknown id*.
    - Every media/document file is inventoried or intentionally excluded, and
      identifiers (ISWC, ISRC, IPI/CAE, ISNI, UPC/EAN, catalog) sit only on their
      proper objects — *identifiers[…].scheme is unsupported*.
    - Claims and licenses are scoped by subject, right/use type, party, territory,
      term, evidence, and restrictions, with no scope over 100% — *share_percent
      must be null or between 0 and 100* / *total … above 100%*. Never force
      unknown shares to total 100.
    - Every `confirmed` record carries evidence — *is confirmed but has no
      evidence_refs*.
    - Privacy classification and redaction posture are explicit —
      *privacy.default_classification is unsupported*.
    
    Three boxes the validator cannot check — the human-judgment residue, on which a
    clean run says nothing:
    
    - **Do-not-share review**: no draft, private, or unreleased material was copied
      in without the user's explicit choice (the halt format above).
    - **Redaction review**: someone read the sensitive fields before any external
      share instead of trusting the classification labels.
    - **Uncertainty stated**: final clearance requires creator/legal confirmation
      wherever a rights fact is uncertain; contributor, split, license, sample, and
      ownership facts are confirmed only on user-supplied evidence and `unknown`
      when in doubt; `missing-info-report.md` ships even when empty, and
      `optimization-brief.md` ships with concrete next actions.
    
    A validator pass still does not resolve a rights fact.
    
    ## Red flags — stop
    
    If any of these appear in your reasoning, stop and re-read the core principle:
    
    - "Fill in the missing split so the total reaches 100." A guessed split is a
      false rights fact. Record the shortfall and ask.
    - "The artist told me they own it — mark ownership confirmed." Record the
      claim as `claimed`; `confirmed` needs evidence.
    - "Nothing seems missing — skip missing-info-report.md." The report ships even
      when empty. That is the checklist.
    - "Copy all the assets; sorting is the reviewer's problem." Check for draft
      and do-not-share files before any `--copy-assets` run.
    - "Call it registered or cleared since the package looks complete." A complete
      package is organized, not approved.
    
    ## Downstream Review Context
    
    Artifacts produced by this skill (`RIGHTS_PASSPORT.md`, `suede-intake.json`,
    `provenance.md`, `credits-and-splits.md`, `license-notes.md`) are portable
    review materials. They can support a release, registry, licensing conversation,
    collaborator handoff, marketplace review, label review, advisor review, or
    Suede review without claiming that any downstream system has accepted, cleared,
    registered, paid, or approved the work.
    
    ## Routing
    
    - Rights gaps that need investigation or evidence organizing →
      **suede-rights-audit** (it finds the gaps; this skill packages them).
    - Release-readiness lint before or after packaging → **suede-release-linter**.
    - Track headed to film/TV/ads once packaged → **suede-sync-packaging**.
    - The release needs a rollout → **suede-campaign-in-a-box**.
    
    Family order: suede-release-linter → suede-rights-audit → suede-rights-passport
    → suede-sync-packaging; this skill is step 3.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related