{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://stackcut.io/schemas/stack-manifest-1.json",
  "title": "Stackcut stack manifest",
  "description": "What a user's own agent learns about how a team uses paid software, summarized for Stackcut. The agent builds it locally, shows the user a readable version, and sends only this summary: vendor ids, counts, feature names, job labels, prices and dates. Never raw exports, invoices, file contents, customer data, secrets, or people's names and emails (use roles). Unknown values are omitted or null, never guessed; list them in `unknowns`. Stackcut validates the manifest and answers with the missing fields that would change its recommendation, ranked, so the agent only fetches what matters.",
  "type": "object",
  "required": ["manifest_version", "goal", "services"],
  "additionalProperties": false,
  "properties": {
    "manifest_version": { "const": "1" },
    "captured_at": { "$ref": "#/$defs/date", "description": "When the agent collected this." },
    "goal": {
      "type": "object",
      "additionalProperties": false,
      "required": ["decision"],
      "properties": {
        "decision": { "enum": ["cut_spend", "consolidate", "replace_one", "choose_stack", "check_in"], "description": "What the user wants decided. consolidate = several apps' used features into fewer tools or one build." },
        "focus_vendors": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 80 }, "description": "Vendor ids or names the decision is about, if narrower than all services." },
        "monthly_budget_usd": { "type": ["number", "null"], "minimum": 0 },
        "risk_tolerance": { "enum": ["low", "medium", "high", null], "description": "low = only moves with an official migration path and a rollback." },
        "must_keep": { "type": "array", "maxItems": 30, "items": { "type": "string", "maxLength": 80 }, "description": "Vendors or capabilities the user won't give up, with no reason needed." },
        "deadline": { "$ref": "#/$defs/date_or_null", "description": "e.g. the first renewal they want to avoid." }
      }
    },
    "org": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "team_size": { "type": ["integer", "null"], "minimum": 1 },
        "roles": { "type": "array", "maxItems": 30, "items": { "type": "object", "additionalProperties": false, "required": ["role", "count"], "properties": { "role": { "type": "string", "maxLength": 60 }, "count": { "type": "integer", "minimum": 1 } } } },
        "compliance": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 60 }, "description": "e.g. SOC 2, HIPAA, GDPR, data residency EU, 7-year retention." }
      }
    },
    "builder": {
      "type": "object",
      "description": "Who would build and maintain a replacement, and what they already pay for. Lets Stackcut cost a build at its marginal cost and target the builder's own stack.",
      "additionalProperties": false,
      "properties": {
        "coding_agent": { "type": ["string", "null"], "maxLength": 80, "description": "e.g. Claude Code (Max 20x), Codex, Cursor Pro." },
        "already_paid": { "type": "array", "maxItems": 30, "items": { "type": "string", "maxLength": 80 }, "description": "Vendor ids for hosting, database, auth, AI plans and agent platforms already paid for (e.g. vercel, convex, supabase, claude)." },
        "hourly_value_usd": { "type": ["number", "null"], "minimum": 0, "description": "What an hour of the builder's time is worth to them. Replaces Stackcut's default." },
        "build_hours_budget": { "type": ["number", "null"], "minimum": 0, "description": "Most hours they'd spend building." },
        "upkeep_hours_per_month": { "type": ["number", "null"], "minimum": 0, "description": "Most hours a month they'd spend maintaining." },
        "maintainer_role": { "type": ["string", "null"], "maxLength": 60 },
        "agent_access": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 80 }, "description": "Agents that must read or write the tools (e.g. Claude, Grok, a Slack bot)." }
      }
    },
    "services": {
      "type": "array",
      "minItems": 1,
      "maxItems": 100,
      "description": "One entry per bill (or per project on a shared bill; see account and shared_with). Field names match audit_stack's services, so the list can be passed straight through.",
      "items": { "$ref": "#/$defs/service" }
    },
    "workflows": {
      "type": "array",
      "maxItems": 40,
      "description": "How work actually flows through the tools. These become the acceptance tests of any replacement.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "apps"],
        "properties": {
          "name": { "type": "string", "maxLength": 120, "description": "e.g. 'client onboarding', 'weekly content plan'." },
          "job": { "type": ["string", "null"], "maxLength": 60, "description": "Job id from Stackcut's jobs list when one fits (list_stack_jobs)." },
          "apps": { "type": "array", "minItems": 1, "maxItems": 12, "items": { "type": "string", "maxLength": 80 }, "description": "Vendor ids the workflow touches, in order." },
          "steps": { "type": "array", "maxItems": 20, "items": { "type": "string", "maxLength": 200 } },
          "roles": { "type": "array", "maxItems": 10, "items": { "type": "string", "maxLength": 60 } },
          "frequency": { "type": ["string", "null"], "maxLength": 40, "description": "e.g. daily, weekly, per new client." },
          "pain": { "type": ["string", "null"], "maxLength": 300 }
        }
      }
    },
    "unknowns": {
      "type": "array",
      "maxItems": 60,
      "description": "Facts the agent looked for and couldn't get, and what it would take.",
      "items": { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "vendor": { "type": ["string", "null"], "maxLength": 80 }, "field": { "type": "string", "maxLength": 80 }, "why": { "type": ["string", "null"], "maxLength": 200 } } }
    }
  },
  "$defs": {
    "date": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
    "date_or_null": { "anyOf": [{ "$ref": "#/$defs/date" }, { "type": "null" }] },
    "source": {
      "enum": ["user_said", "invoice", "admin_console", "sso_or_directory", "vendor_export", "repo_scan", "stackcut_plan", "estimate"],
      "description": "Where a fact came from. estimate = the agent's own guess; Stackcut treats it as unconfirmed."
    },
    "service": {
      "type": "object",
      "additionalProperties": false,
      "required": ["vendor"],
      "properties": {
        "vendor": { "type": "string", "maxLength": 80, "description": "Vendor id or name (Notion, notion)." },
        "product": { "type": ["string", "null"], "maxLength": 80, "description": "When a vendor sells several products (Vercel hosting vs Vercel Blob; Google Workspace vs Gemini)." },
        "project": { "type": ["string", "null"], "maxLength": 80, "description": "For a portfolio: which product this line belongs to." },
        "account": { "type": ["string", "null"], "maxLength": 80, "description": "Lines with the same account share one bill." },
        "shared_with": { "type": ["integer", "null"], "minimum": 1, "description": "How many products use this shared account." },
        "plan": { "type": ["string", "null"], "maxLength": 80 },
        "monthly_spend_usd": { "type": ["number", "null"], "minimum": 0, "description": "The whole bill per month, all seats and add-ons. Omit when unknown; never 0 for unknown." },
        "confirmed": { "type": "boolean", "description": "True when the user or an invoice confirms the plan and price." },
        "billing_period": { "enum": ["monthly", "annual", "usage", null] },
        "renewal_date": { "$ref": "#/$defs/date_or_null" },
        "notice_days": { "type": ["integer", "null"], "minimum": 0, "description": "Cancellation notice the contract requires." },
        "committed_until": { "$ref": "#/$defs/date_or_null", "description": "Prepaid or contracted until; charges before this date are sunk." },
        "seats": { "type": ["integer", "null"], "minimum": 0, "description": "Seats paid for." },
        "active_users": { "type": ["integer", "null"], "minimum": 0, "description": "People who used it in the last 30 days." },
        "last_active": { "$ref": "#/$defs/date_or_null" },
        "usage": { "type": "object", "maxProperties": 30, "additionalProperties": { "type": "number", "minimum": 0 }, "description": "Numbers in Stackcut's units, e.g. {\"emails_per_month\": 40000, \"db_gb\": 3, \"MAU\": 1200}." },
        "features_used": { "type": "array", "maxItems": 60, "items": { "type": "string", "maxLength": 80 }, "description": "Must-haves the team actually uses." },
        "feature_usage": {
          "type": "array", "maxItems": 60,
          "items": { "type": "object", "additionalProperties": false, "required": ["feature"], "properties": {
            "feature": { "type": "string", "maxLength": 80 },
            "job": { "type": ["string", "null"], "maxLength": 60, "description": "What the team uses it for (a job id when one fits)." },
            "users": { "type": ["integer", "null"], "minimum": 0 },
            "last_used": { "$ref": "#/$defs/date_or_null" }
          } }
        },
        "integrations": { "type": "array", "maxItems": 30, "items": { "type": "string", "maxLength": 80 }, "description": "What it connects to (vendor ids, 'webhooks', 'SSO', 'Zapier')." },
        "data_volume": {
          "type": "object", "additionalProperties": false,
          "properties": {
            "records": { "type": ["integer", "null"], "minimum": 0 },
            "files_gb": { "type": ["number", "null"], "minimum": 0 },
            "history_years": { "type": ["number", "null"], "minimum": 0 },
            "export_format": { "type": ["string", "null"], "maxLength": 60, "description": "What the vendor's export produced (CSV, JSON, Markdown)." }
          }
        },
        "compliance": { "type": "array", "maxItems": 10, "items": { "type": "string", "maxLength": 60 } },
        "owner_role": { "type": ["string", "null"], "maxLength": 60 },
        "revenue_usd_per_month": { "type": ["number", "null"], "minimum": 0, "description": "Revenue of the product this service supports, when known (drives sanity checks)." },
        "sources": {
          "type": "object", "maxProperties": 40,
          "additionalProperties": { "$ref": "#/$defs/source" },
          "description": "Where each field came from, keyed by field name: {\"seats\": \"admin_console\", \"monthly_spend_usd\": \"invoice\"}."
        }
      }
    }
  }
}
