{"api_version":"1.0","service":"sprucelab-django","file_formats":["ifc","las","laz","e57","dwg","dxf","pdf","docx","xlsx","pptx","csv","json","xml","svg","other"],"extraction_support":{"full":["docx","dxf","ifc","pdf","pptx","svg","xlsx"],"stub":["e57","las","laz"],"none":["csv","dwg","json","other","xml"]},"mutations_supporting_dry_run":["POST /api/types/type-mappings/bulk-update/","POST /api/types/type-definition-layers/bulk-update/","POST /api/types/claims/{id}/promote/","POST /api/types/claims/{id}/reject/","POST /api/types/claims/{id}/supersede/","PATCH /api/projects/scopes/{id}/","POST /api/automation/webhook-subscriptions/","POST /api/filters/saved/","PATCH /api/filters/saved/{id}/","POST /api/projects/scaffold/","POST /api/v2/classify/","POST /api/files/requeue/","POST /api/models/reconcile-ingests/","POST /api/projects/{id}/delivery/publish/"],"feedback":{"endpoint":"/api/feedback/","purpose":"Reports users file ABOUT SPRUCELAB (bug / idea / question). NOT /api/issues/, which is model-coordination work inside a project.","kinds":["bug","idea","question"],"statuses":["new","seen","in_progress","resolved"],"open_statuses":["in_progress","new","seen"],"reporter_fields":["kind","message","route","user_agent","project_id"],"triage_fields":["status","resolution_note","linked_ref"],"server_owned_fields":["company_id","reporter_email","created_at"],"list_filters":["status","kind","project","company","is_open"],"create_auth":"any approved principal; the report lands in the tenant resolved server-side from the named project, else from the caller's own company. A caller who belongs to NO company still files successfully — the report is stored with company_id null and read by staff and platform-wide triage tokens only, because a report must never be blocked by tenancy shape. A project outside the caller's reach is refused with code feedback_project_out_of_reach.","triage_auth":"staff, or an agent token minted with feedback_triage=true. A reporter reads only the reports they filed and may PATCH nothing (code feedback_triage_required).","refusal_codes":["feedback_triage_required","feedback_project_out_of_reach","feedback_company_ambiguous","feedback_field_is_triage_owned","feedback_method_not_offered"],"linked_ref_note":"The URL of the work a report became. Written by staff or a capable agent when the report is mirrored to a tracker — never by the reporter, who must never be asked to know what a tracker is. That write-back is what closes the loop.","not_supported":["delete","put","attachments","email fan-out"],"not_supported_note":"DELETE and PUT are answered by the permission layer, which runs before the ViewSet resolves the handler — the refusal is code feedback_method_not_offered, not feedback_triage_required, because the true reason is that the surface offers no such method, not that the caller lacks a capability."},"access_model":{"planes":{"data":"projects, files, types, materials, issues, scopes, verification, claims, documents — open to agent tokens per capability scope","control":"scripting/code-exec, pipelines, CDE connectors, token minting, user admin — STAFF ONLY; no agent token can reach it"},"agent_token_scopes":{"read_only":"GET only, data plane","operator":"read + write, data plane","admin":"read + write + admin data ops (still NOT control plane)"},"tenancy":{"root":"per-project membership (ProjectMember), under the company container (Company)","visibility":"Every project-aware list/detail endpoint filters by membership: non-staff principals see projects they are an active ProjectMember of. Staff see all. A HUMAN COMPANY ADMIN (active ROLE_ADMIN CompanyMember) also sees every project under THAT company with no per-project row (gh #381) — the company is the tenancy root and its admin is the tenant owner; strictly within their own company, never another. An active PortfolioMember sees every project in that portfolio with no per-project row and no company membership (gh #393), excluding classified projects. Agent tokens narrow by token subject instead. Cascade applies to /api/projects/, /api/models/, /api/files/, /api/files/extractions/, /api/types/types/, /api/issues/, /api/projects/scopes/, /api/projects/configs/.","writes":"Write-side authz: POST /api/files/, /api/models/upload/, /api/models/get-upload-url/, /api/models/confirm-upload/, /api/models/upload-with-metadata/ all require Project.user_can_write(user, project) = admin or editor. Viewers + non-members + anonymous principals get 403. A PORTFOLIO member resolves to project EDITOR when their PortfolioMember.role is editor or admin, and to no role at all when it is viewer (gh #427). That editor grade is capped exactly like a guest: no project delete, no agent-token mint, no webhook-secret rotation, no member management, no sharing_class change — plus no project roster read and no project-config activate/delete, which administer the hosting firm rather than the work.","roles":["admin","editor","viewer"],"members_endpoint":"GET|POST /api/projects/{id}/members/ · DELETE /api/projects/{id}/members/{member_id}/","members_write_auth":"Project member management (add/remove) requires staff, a ROLE_ADMIN ProjectMember, OR a company-admin principal of the project's OWNING company (gh #381) — human ROLE_ADMIN CompanyMember or an agent token carrying the company_admin capability for that same company. Guests are never project admins. OPTIONS on the endpoint describes the member payload {email, role, is_guest}.","note":"A project creator (POST /api/projects/ or /scaffold/) is auto-granted ROLE_ADMIN — EXCEPT an agent creator, which gets no ProjectMember row (it sees the project through its company subject); the owning company's admins reach it through the company rail instead. Tenant-scoped agent-token minting (gh #34) builds on this — a token never exceeds the minter's own project visibility."},"note":"Platform users hold agent tokens and operate only on data. Only platform staff (the owner) reach code/infra. Minting a token is itself a control-plane action — staff only."},"agent_tokens":{"platform_collection_endpoint":"GET|POST /api/agent-tokens/","platform_detail_endpoint":"DELETE /api/agent-tokens/{id}/","platform_auth":"staff Supabase user (owner)","tenant_collection_endpoint":"GET|POST /api/projects/{id}/agent-tokens/","tenant_detail_endpoint":"DELETE /api/projects/{id}/agent-tokens/{token_id}/","tenant_auth":"HUMAN project admin or editor (or staff). An agent-token principal is refused on POST with code \"agent_token_mint_forbidden\" — no token mints another token (gh #365). GET (audit listing) stays open to writers, agents included.","tenant_scope_cap":"operator (admin scope rejected on tenant mint)","company_collection_endpoint":"GET|POST /api/companies/{id}/agent-tokens/","company_detail_endpoint":"DELETE /api/companies/{id}/agent-tokens/{token_id}/","company_auth":"APPROVED HUMAN company admin (mint/revoke) — an unapproved minter would otherwise hand itself an auto-approved agent principal and skip the waitlist (gh #366); company admin (list)","company_scope_cap":"operator (admin scope rejected on company mint)","company_admin_capability":"company_admin=true at mint grants COMPANY MANAGEMENT (create projects, add/remove/re-role COMPANY members, add/remove PROJECT members on any project under that company, apply project config) for the subject company only; omit it for data operator only. The project-member grant (gh #381) is what lets an agent that created a project hand a human ROLE_ADMIN on it — without it, an agent-created project was writable by the agent and invisible to every human.","sharing_admin_capability":"sharing_admin=true at mint grants the SHARING POSTURE capability (gh #376): set a project's sharing_class — e.g. restricted -> open — for projects under the subject company ONLY, which is what makes the public-delivery publish chain executable end-to-end by an agent. Inert on non-company subjects; widens nothing else (project DELETE stays human-only). Grantable only by a HUMAN company admin at the company mint edge.","feedback_triage_capability":"feedback_triage=true at mint grants the FEEDBACK TRIAGE capability (gh #521): read every UserReport within the token's own tenancy reach and PATCH its triage fields (status, resolution_note, linked_ref). A COMPANY-subject token reaches its own company's reports; a PLATFORM token (staff mint edge, POST /api/agent-tokens/) reaches every tenant. WITHOUT it a token sees only the reports it filed itself and can patch nothing. Grants nothing else — no project data, no company management, no sharing posture. No capability template turns it on; ask for it explicitly.","capability_templates":[{"name":"read-only","description":"Read the data plane; change nothing. Dashboards, monitors, reporting agents. The default when nothing is asked for.","expands_to":{"scope":"read_only","company_admin":false,"sharing_admin":false,"feedback_triage":false}},{"name":"data-operator","description":"Read + write the data plane (ingest, classify, verify). The default working agent; no company management, no sharing posture changes.","expands_to":{"scope":"operator","company_admin":false,"sharing_admin":false,"feedback_triage":false}},{"name":"project-manager","description":"Data operator PLUS company management for the subject company — create projects, apply project config, add/remove/re-role company members, and add/remove PROJECT members on any project under that company (gh #381).","expands_to":{"scope":"operator","company_admin":true,"sharing_admin":false,"feedback_triage":false}},{"name":"delivery-publisher","description":"Project manager PLUS the sharing-posture capability — may set a subject-company project's sharing_class and so run the public-delivery publish chain end to end (gh #345 WP5).","expands_to":{"scope":"operator","company_admin":true,"sharing_admin":true,"feedback_triage":false}}],"capability_template_default":"read-only","capability_template_note":"POST {\"template\": \"<name>\"} to the company mint edge. Templates EXPAND AT MINT TIME into the concrete scope + capability flags stored on the token — the name is never persisted, so authorization never resolves a template and a renamed/retired template can never re-scope an existing token. Explicit scope / company_admin / sharing_admin / feedback_triage in the same body OVERRIDE the template's values. Neither a template nor any explicit value -> read-only (default-deny). Unknown name -> 400 {\"errors\": {\"template\": {\"code\": \"unknown_capability_template\", \"valid_templates\": [...]}}}.","scopes":["read_only","operator","admin"],"subject_kinds":["platform","company","project"],"mint_returns_raw_key_once":true,"cli":"sprucelab auth mint --name <n> --scope operator [--json]","company_cli":"sprucelab companies mint-token --company <id> --name <n> --template <name> [--expires-days N] [--json] — `sprucelab companies templates` lists the bundles below; --scope / --company-admin / --sharing-admin stay available as overrides (gh #377)","note":"Agent tokens map to a synthetic approved user — no email/signup needed. Bootstrap fallback: python manage.py create_agent --name <n> --scope operator. Tenant tokens auto-narrow visibility + writes to their subject project; company tokens narrow to every project under the subject company (classified excluded); platform tokens (staff-minted) keep see-all back-compat."},"project_setup":{"eir_catalog_endpoint":"GET /api/projects/eir-catalog/","eir_presets_endpoint":"GET /api/projects/eir-presets/ (?preset=<id>, ?include_rules=false)","scaffold_endpoint":"POST /api/projects/scaffold/ (?dry_run=true)","presets":["pofin-minimum","baseline"],"default_preset":"pofin-minimum","notes":"Presets are self-contained: they bundle the required singleton rules (CRS, placement, canonical floors, IFC schema, naming, MMI/LOD, geometry rules), so a scaffolded project lands with a complete EIR. Seed your own rules via POST /api/projects/ with 'initial_eir_rules' shaped per the eir-catalog contract."},"extraction_pipelines":{"ifc":"fastapi:/api/v1/ifc/extract","pdf":"django:/api/files/{id}/extract/ → drawing+document extractors","docx":"django:/api/files/{id}/extract/ → document extractor","xlsx":"django:/api/files/{id}/extract/ → document extractor","pptx":"django:/api/files/{id}/extract/ → document extractor","dxf":"django:/api/files/{id}/extract/ → drawing extractor"},"verification":{"engine_endpoint":"GET /api/v2/verify/?project={id}&model={id}","rule_sources":["DEFAULT_RULES","ProjectConfig.config[verification][rules]","ProjectConfig.config[eir][rules]","ProjectConfig.config[delivery_matrix]","twin_resolution.VerificationRule (promoted rule claims)"],"rule_id_prefixes":{"default:":"registered rule run with default params (no operator rule set)","eir.":"translated from an EIR card in ProjectConfig.config[eir][rules]","delivery_matrix.":"discipline×stage MMI floor from the delivery matrix","claim:":"derived from a promoted Claim — see /api/types/types/claim-issues/"},"rule_statuses":{"met":"ran; every target satisfies the rule","unmet":"ran; at least one target violates the rule","not_applicable":"ran; no target matched the rule scope","not_evaluable":"did NOT run — no verify-v2 evaluator for this rule kind, a scope filter v2 cannot honour, the project declared no value to measure against (e.g. no georef CRS/basepoint, no canonical floor list), or no target states a comparable fact. Carries a `reason`. Counted in rules_total, never in rules_met."},"rule_kinds":[{"kind":"artifact_name_pattern","title":"Artifact names must follow the naming convention","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"canonical_floors_match","title":"Model storeys must match the canonical floor list","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"claim_subject_match","title":"Document obligation applies to matching types","default_enabled":false,"tier":1,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"classification_coverage","title":"All instances of {ifc_class} must have classification in {system}","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"declared_disciplines","title":"Model discipline must be one the project declares","default_enabled":false,"tier":2,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"dedup_resolution_progress","title":"All duplicate appearances must be promoted to DedupCredit","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"floor_elevation_plausibility","title":"Canonical floor elevations must be plausible metres","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"geometry_no_orphans","title":"Every product must sit inside a spatial container","default_enabled":false,"tier":2,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"has_base_quantities","title":"Qto_*BaseQuantities required","default_enabled":false,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"has_property","title":"All instances of {ifc_class} must have {pset_name}.{prop_name}","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"has_pset","title":"All instances of {ifc_class} must carry {pset_name}","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"ifc_mvd_allowed","title":"Model MVD must be an allowed view definition","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"ifc_schema_allowed","title":"Model IFC schema must be an allowed version","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"mmi_threshold","title":"Types must reach MMI {threshold} ({pset}.{property})","default_enabled":false,"tier":3,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"predefined_type_required","title":"PredefinedType must be set","default_enabled":false,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"project_crs","title":"Model CRS must match the project CRS","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"project_placement","title":"Model basepoint must match the project basepoint","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"project_rotation","title":"Model rotation must match the project rotation","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"project_vertical_datum","title":"Model vertical datum must match the project vertical datum","default_enabled":false,"tier":3,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"property_in","title":"{pset_name}.{prop_name} must be one of the allowed values","default_enabled":false,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"property_range","title":"{pset_name}.{prop_name} must be within range","default_enabled":false,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"property_regex","title":"{pset_name}.{prop_name} must match /{pattern}/","default_enabled":false,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"quantity_plausibility","title":"Declared quantities must be plausible as a distribution","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"si_units_declared","title":"The model must declare SI units","default_enabled":false,"tier":2,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"spatial_name_pattern","title":"Spatial node names must match the declared pattern","default_enabled":false,"tier":2,"scope_filters":{"honoured":[],"not_evaluable":["classification_codes","ifc_classes","mmi_at_least","type_pattern"]}},{"kind":"type_classification_completeness","title":"Every used IFCType must have at least one classification","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}},{"kind":"type_value_agreement","title":"Instances of one {ifc_class} type must agree on {pset_name}.{prop_name}","default_enabled":true,"tier":2,"scope_filters":{"honoured":["ifc_classes"],"not_evaluable":["classification_codes","mmi_at_least","type_pattern"]}}],"eir_card_kinds":{"classification":{"rule_kinds":["has_property"],"not_evaluable_reasons":[]},"mmi_lod":{"rule_kinds":["has_property"],"not_evaluable_reasons":[]},"custom_properties":{"rule_kinds":["has_property","has_pset","property_in","property_range","property_regex"],"not_evaluable_reasons":[]},"tagging":{"rule_kinds":["has_property","property_regex"],"not_evaluable_reasons":[]},"geometry_rules":{"rule_kinds":["geometry_no_orphans","has_base_quantities","predefined_type_required","si_units_declared"],"not_evaluable_reasons":[]},"naming":{"rule_kinds":["artifact_name_pattern"],"not_evaluable_reasons":["verify v2 has no 'regex' evaluator — the EIR rule constrains a field's text form (e.g. type naming convention)"]},"crs":{"rule_kinds":["project_crs","project_vertical_datum"],"not_evaluable_reasons":[]},"placement":{"rule_kinds":["project_placement","project_rotation"],"not_evaluable_reasons":[]},"canonical_floors":{"rule_kinds":["canonical_floors_match"],"not_evaluable_reasons":[]},"ifc_schema":{"rule_kinds":["ifc_mvd_allowed","ifc_schema_allowed"],"not_evaluable_reasons":["there is no buildingSMART validator run to read a verdict from — needs the IFC delivered to the buildingSMART validation service (or a local ids/mvdXML check) with its verdict persisted per ExtractionRun before this can be answered. Reporting a pass without one would be a fabricated verdict"]},"site_plan":{"rule_kinds":[],"not_evaluable_reasons":["needs a site-model designation on a delivered file — no field marks WHICH SourceFile/Model is the site model (lokasjonsplan / fastmerker / site context), so the card's accepted formats have nothing to be checked against; the KOF and GeoJSON formats it offers are also not ingested today"]},"site_grid":{"rule_kinds":[],"not_evaluable_reasons":["needs a project-grid designation on a delivered file. Half the fact exists — ExtractionRun.discovered_grid holds the IfcGrid axes parsed out of an IFC — but nothing marks which delivery IS the project grid, and the DWG and CSV formats the card offers have no grid extraction"]},"spatial_identifiers":{"rule_kinds":["spatial_name_pattern"],"not_evaluable_reasons":["needs IfcProject / IfcSite / IfcBuilding nodes in the twin substrate. It holds storeys, types, materials and instances and no project-tree node at all (analysis_twin_read_model returns project_name / site_name / building_name as empty strings), so neither an IfcProject.Name pattern nor a required LongName can be measured. The same card's storey_name_pattern IS checked, by spatial_name_pattern"]},"scopes":{"rule_kinds":[],"not_evaluable_reasons":["needs the declared scope names bound to the project's ProjectScope rows. The EIR card and ProjectScope are independent stores with no link between them, so 'is this scope set up' and 'is every model assigned to a declared scope' cannot be answered without inventing a match by name; coordinate_mode has no model-side fact at all"]},"parties":{"rule_kinds":[],"not_evaluable_reasons":["needs a party register. sprucelab records no author, organisation or responsible party per delivery — a Model carries a discipline, not a company — so a declared party cannot be tied to anything a model or a file states. It is also what makes the disciplines card's lead_party unanswerable"]},"disciplines":{"rule_kinds":["declared_disciplines"],"not_evaluable_reasons":[]}}},"events":{"wired":["model.processed","document.processed","claim.extracted","claim.promoted","verification.complete","floor.canonical.changed"],"planned":["types.classified","quantities.extracted"],"signing":"hmac-sha256","signature_header":"X-Webhook-Signature","timestamp_header":"X-Webhook-Timestamp","event_header":"X-Webhook-Event","delivery_id_header":"X-Webhook-Delivery-Id","subscription_endpoint":"/api/automation/webhook-subscriptions/","delivery_log_endpoint":"/api/automation/webhook-deliveries/"},"embed":{"capabilities_endpoint":"/api/embed/capabilities/","instances_endpoint":"/api/embed/instances/","iframe_path":"/embed/{dashboard}?token={scoped_token}","protocol_version":1,"token_endpoints":{"collection":"/api/embed/tokens/","detail":"/api/embed/tokens/{id_or_prefix}/","refresh":"/api/embed/tokens/refresh/"},"auth":{"scheme":"Embed","header":"Authorization","query_param":"token","admin_endpoints_require_staff":true}},"cli_commands":{"admin":["sprucelab admin pending-users [--admin-token <KEY>] [--json]  (staff only; GET /api/admin/dashboard/ — the table is the approval queue, --json the raw dashboard payload with every account in users_list)","sprucelab admin approve <user_id> [--admin-token <KEY>] [--json]  (staff only; opens the waitlist gate for one account and writes a grant row to the access trail; user_id is the Django user id, not the Supabase UUID)","sprucelab admin reject <user_id> [--admin-token <KEY>] [--json]  (staff only; refuses the account at the gate and WIPES approved_by/approved_at)"],"aggregate":"sprucelab aggregate --model <id> --group-by ifc_class|storey|type|predefined_type|material|classification|pset:<Pset>.<Prop> [--where <axis>=<value> ...] [--classification-system <id>] [--json]  (cross-filter instance counts over a model)","auth":["sprucelab auth login [--url <URL>] [--no-browser]  (primary interactive sign-in; browser callback)","sprucelab auth register --url <URL> --token <KEY>","sprucelab auth status","sprucelab auth mint --name <n> --scope operator [--json]  (staff only)","sprucelab auth tokens [--include-inactive] [--json]","sprucelab auth revoke <id>","sprucelab auth personal-tokens list [--json]","sprucelab auth personal-tokens create --name <n> [--scope operator|read_only] [--expires-days N] [--json]  (human session; raw key shown once)","sprucelab auth personal-tokens revoke <token_id> [--json]"],"audit":"sprucelab audit access-events [--mine] [--subject <user-id|email>] [--actor <user-id>] [--action grant|revoke|role_change|invite|accept|expire|posture_change] [--capacity company_admin|portfolio_admin|project_admin|staff|agent|self|system|member] [--target-kind company|portfolio|project|platform] [--company <uuid>] [--portfolio <uuid>] [--project <uuid>] [--sweeping|--no-sweeping] [--since <iso>] [--until <iso>] [--page N] [--page-size N] [--json]  (every grant / re-role / revoke, newest first; is_sweeping marks the estate-wide ones; --mine is the caller's own Art. 15 answer. Read-only by construction)","capabilities":"sprucelab capabilities [--json]","claims":["sprucelab claims list [--project] [--model] [--limit] [--json]","sprucelab claims show <id> [--json]","sprucelab claims promote <id> [--dry-run] [--json]","sprucelab claims reject <id> [--reason] [--dry-run] [--json]","sprucelab claims supersede <id> --by <newer-id> [--dry-run] [--json]","sprucelab claims conflicts <id> [--json]","sprucelab claims diff <claim_id> [<claim_id> ...] [--json]  (prior vs proposed per claim — what promoting it would CHANGE; NOT a model-version diff. One unknown or cross-tenant id 404s the whole batch)"],"companies":["sprucelab companies list [--page-size N] [--admin-token <KEY>] [--json]  (which company am I? — the --company uuid every other command here needs)","sprucelab companies templates [--json]  (capability bundles this backend offers, read live from this manifest)","sprucelab companies mint-token --company <id> --name <n> [--template read-only|data-operator|project-manager|delivery-publisher] [--scope operator|read_only] [--company-admin|--no-company-admin] [--sharing-admin|--no-sharing-admin] [--expires-days N] [--admin-token <KEY>] [--json]","sprucelab companies list-tokens --company <id> [--admin-token <KEY>] [--json]","sprucelab companies revoke-token <token_id> --company <id> [--admin-token <KEY>] [--json]","sprucelab companies members list --company <id> [--admin-token <KEY>] [--json]","sprucelab companies members add --company <id> --email <e> [--role admin|editor|viewer] [--dry-run] [--admin-token <KEY>] [--json]","sprucelab companies members set-role <member_id> --company <id> --role admin|editor|viewer [--dry-run] [--admin-token <KEY>] [--json]","sprucelab companies members remove <member_id> --company <id> [--dry-run] [--admin-token <KEY>] [--json]","sprucelab companies invitations list --company <id> [--status pending] [--admin-token <KEY>] [--json]  (revoked rows hidden unless --status revoked)","sprucelab companies invitations create --company <id> --email <e> [--role admin|editor|viewer] [--no-email] [--dry-run] [--admin-token <KEY>] [--json]","sprucelab companies invitations revoke <invitation_id> --company <id> [--admin-token <KEY>] [--json]  (soft; answers 200 with the updated row)"],"config":["sprucelab config show","sprucelab config set <key> <value>","sprucelab config init"],"dedup":["sprucelab dedup runs [--project <uuid>] [--page N] [--json]  (one run = one execution of the clash/dedup engine over a project's models)","sprucelab dedup show <run_id> [--json]  (header + the engine's own summary block, rendered verbatim)"],"embed":["sprucelab embed pass create --project <uuid> --name <n> [--ttl] [--json]","sprucelab embed pass list [--project] [--include-revoked] [--json]","sprucelab embed pass revoke <id> [--json]","sprucelab embed pass refresh <id> [--json]"],"files":["sprucelab files list [--project] [--format] [--current-only] [--json]","sprucelab files show <id> [--json]","sprucelab files upload <path> --project <uuid> [--on-duplicate ask|use_existing|replace]  (files over upload_limits.multipart_upload.recommended_threshold_bytes route automatically via the multipart presigned path)","sprucelab files download <id> [--out PATH] [--overwrite]","sprucelab files reprocess <id> [--json]","sprucelab files requeue --project <uuid> | --source-file <id> [--dry-run] [--force] [--json]","sprucelab files reconcile [--project <uuid>] [--dry-run] [--timeout-seconds N] [--json]","sprucelab files multipart-uploads --project <uuid> [--json]  (incomplete multipart uploads staged in the bucket; truncated=true means more beyond this page)","sprucelab files multipart-sweep --project <uuid> [--dry-run] [--json]  (aborts every staged upload for the project, in-flight ones included; failed[] is a storage REFUSAL, not a no-op)","sprucelab files versions <id> [--page N] [--page-size N] [--json]  (the filename chain, newest version_number first; 100/page by default, page-size capped at 1000; --json is the raw {count,next,previous,results} envelope)","sprucelab files extraction-log <id> [--run <uuid>] [--level] [--limit N] [--json]","sprucelab files wait <id> [--timeout N] [--json]  (poll one file to a terminal extraction status)"],"ifc":"sprucelab ifc extract <path> [--json] [--output PATH]  (local IFC extraction via the in-repo sprucelab_ifc toolkit, no server round-trip)","instances":["sprucelab instances list --model <uuid> [--ifc-class <c>] [--type <uuid>] [--storey <uuid>] [--predefined-type <p>] [--material <m>] [--classification <code>] [--classification-system <sys>] [--pset <Pset>.<Prop> --value <v>] [--page N] [--page-size N] [--json]","sprucelab instances show <instance_id> [--json]  (the only surface that serves property_sets; a list never inlines them)"],"inventory":["sprucelab inventory properties (--model <uuid> | --project <uuid>) [--pset <substr>] [--max-values N] [--json]  (which psets/properties the model ACTUALLY contains, with value spread)","sprucelab inventory quantities (--model <uuid> | --project <uuid>) [--json]  (authored keys, the slot each resolves to, and the ones that resolve to NOTHING)","sprucelab inventory materials (--model <uuid> | --project <uuid>) [--material <substr>] [--max-values N] [--limit N] [--json]  (palette, per-type string and layer composition, kept separate)"],"issues":["sprucelab issues list [--project] [--status] [--assignee] [--is-open] [--source-kind] [--json]","sprucelab issues summary [--project] [--status] [--assignee] [--is-open] [--source-kind] [--json]  (per-status and per-severity counts, aggregated in the DB over the same filtered queryset as `issues list`)","sprucelab issues show <id> [--json]","sprucelab issues create --project <uuid> --title <t> [--severity] [--priority] [--assignee] [--json]","sprucelab issues set <id> [--status] [--assignee] [--priority] [--json]","sprucelab issues resolve <id> [--json]","sprucelab issues from-verification --model <uuid> [--dry-run] [--json]"],"feedback":["sprucelab feedback list [--status] [--kind] [--project] [--company] [--is-open] [--json]","sprucelab feedback show <id> [--json]","sprucelab feedback create --kind bug|idea|question --message <m> [--project] [--route] [--json]","sprucelab feedback set-status <id> <new|seen|in_progress|resolved> [--json]","sprucelab feedback resolve <id> --note <n> [--ref <url>] [--json]"],"log":"sprucelab log list [--project] [--source-file] [--category] [--json]","materials":["sprucelab materials resolution --project <uuid> [--decided-only] [--include-inactive] [--limit N] [--json]  (one row per distinct authored material; classification is explicitly null for an undecided unit, and the undecided ones ARE the work list)","sprucelab materials classify --project <uuid> --from-json <path|-> [--scheme <key>] [--no-promote] [--dry-run] [--json]  (bulk decisions over the Claim rail; the mapping list is sent verbatim and the endpoint is all-or-nothing, naming the failing index)"],"models":["sprucelab models list [--project] [--page-size N] [--json]","sprucelab models storey-verification <model_id> [--json]  (matched | rename | deviating per storey, plus canonical floors this model is MISSING)","sprucelab models versions <model_id> [--json]  (every version of this model, newest first; the chain is resolved by NAME within the project, and a row with no version_diff is why that version is absent from the project changelog)","sprucelab models compare <model_id> --with <other_model_id> [--json]  (METADATA delta only — stamped counts, file size, schema, status; deltas are TO minus FROM. For the type-grain delta use `projects changelog`)","sprucelab models status <model_id> [--json]  (status, stamped counts, processing_error, task state; Model grain)","sprucelab models reprocess <model_id> [--force] [--json]  (delete this model's parsed data and parse the file again; asynchronous, poll `models status`)"],"projects":["sprucelab projects list [--json]","sprucelab projects show <id> [--json]","sprucelab projects delivery <id> [--json]","sprucelab projects sharing <id> --class open|restricted|classified [--json]  (key #1 of the two-key publish turn; needs project admin or a sharing_admin-capable token)","sprucelab projects publish <id> [--live] [--json]  (DRY RUN unless --live; always reports skipped[] with reasons)","sprucelab projects unpublish <id> [--reclaim] [--json]  (revokes editorially — the public manifest then serves 410)","sprucelab projects create --name <name> [--number] [--owner] [--short-hand] [--description] [--preset pofin-minimum|baseline] [--dry-run] [--json]","sprucelab projects eir-catalog [--json]","sprucelab projects eir-presets [--preset <id>] [--json]","sprucelab projects mint-token <project_id> --name <n> [--scope operator|read_only] [--json]","sprucelab projects list-tokens <project_id> [--json]","sprucelab projects revoke-token <project_id> <token_id> [--json]","sprucelab projects members list <project_id> [--json]","sprucelab projects members add <project_id> --email <e> [--role admin|editor|viewer] [--json]","sprucelab projects members remove <project_id> --member <member_id> [--json]","sprucelab projects eir-rules <project_id> [--json]","sprucelab projects eir-apply-preset <project_id> --preset <id> [--mode merge|replace] [--dry-run] [--json]","sprucelab projects eir-add-rule <project_id> --kind <kind> [--config <json>] [--config-file PATH] [--upsert] [--dry-run] [--json]","sprucelab projects eir-remove-rule <project_id> --rule-id <id> [--json]","sprucelab projects eir-set <project_id> --from <path|-> [--dry-run] [--json]","sprucelab projects update <project_id> [--name] [--short-hand] [--number] [--owner] [--description] [--viewer-choice] [--json]","sprucelab projects guests list <project_id> [--status pending] [--json]","sprucelab projects guests invite <project_id> --email <e> [--role viewer|editor] [--no-email] [--dry-run] [--json]  (a guest is never admin)","sprucelab projects guests revoke <project_id> <invitation_id> [--json]","sprucelab projects configs list <project_id> [--json]","sprucelab projects configs show <config_id> [--json]","sprucelab projects configs activate <config_id> [--json]  (version lifecycle; the others deactivate)","sprucelab projects verification <project_id> [--recompute] [--json]  (rollup from the store; --recompute is the only write)","sprucelab projects verification-timeline <project_id> [--weeks N] [--json]  (verification issues CREATED per ISO week)","sprucelab projects canonical-floors <project_id> [--json]  (read-only ladder; elevation_m is ABSOLUTE, project vertical datum, metres)","sprucelab projects changelog <project_id> [--discipline <code>] [--page N] [--page-size N] [--json]  (TYPE-grain: counts are type signatures, not instances; every row carries grain=type and models never diffed are reported as summary.models_without_diff)","sprucelab projects my-role <project_id> [--json]  (role, discipline, can_approve_scopes, persona_bucket pm|modeler|reviewer for whoever the credential resolves to; an agent token has no team, so discipline is null)","sprucelab projects statistics <project_id> [--json]  (PROJECT-grain model internals; the sources map names which substrate produced each count)"],"portfolios":["sprucelab portfolios list [--company <uuid>] [--page-size N] [--json]","sprucelab portfolios show <id> [--json]","sprucelab portfolios create --name <n> [--company <uuid>] [--description] [--json]  (a company-subject token infers its own company)","sprucelab portfolios update <id> [--name] [--description] [--json]","sprucelab portfolios delete <id> [--json]  (human company-admin session only)","sprucelab portfolios members list <id> [--json]","sprucelab portfolios members add <id> --email <e> [--role admin|editor|viewer] [--dry-run] [--json]","sprucelab portfolios members set-role <id> <member_id> --role <r> [--dry-run] [--json]","sprucelab portfolios members remove <id> <member_id> [--dry-run] [--json]","sprucelab portfolios invitations <id> [--status pending] [--json]","sprucelab portfolios invite <id> --email <e> [--role] [--no-email] [--dry-run] [--json]","sprucelab portfolios projects <id> [--json]","sprucelab portfolios add-project <id> <project_id> [--dry-run] [--json]  (hosting-company admin only)","sprucelab portfolios remove-project <id> <project_id> [--dry-run] [--json]  (hosting-company admin only)"],"qto":"sprucelab qto --project <uuid> --group-by type|class|storey [--model <uuid>] [--include-duplicates] [--measure net|gross] [--sources] [--json]  (sums are PER SLOT; the scalar names its measure and whether it was a fallback; dedup-excluded instances are subtracted and excluded_count says how many)","resolve":"sprucelab resolve --project <uuid> --field <name> [--instance <uuid> ...] [--type <uuid> ...] [--json]  (four-tier resolution instance -> type -> project -> company; each answer carries the winning value, its TIER and the row that supplied it. An unknown --field is a 400 listing every registered field)","scopes":["sprucelab scopes list [--project] [--parent] [--type] [--json]","sprucelab scopes tree --project <uuid> [--json]","sprucelab scopes create --project <uuid> --name <n> [--type] [--parent] [--json]","sprucelab scopes floors <scope_id> [--json]  (read the canonical floor ladder; elevation_m is ABSOLUTE, project vertical datum, metres)","sprucelab scopes set-floors <scope_id> --from <path|-> [--dry-run] [--json]  (ATOMIC FULL REPLACE, not a merge)","sprucelab scopes files <scope_id> [--json]","sprucelab scopes assign-files <scope_id> --file <source_file_id> [--file ...] [--json]"],"types":["sprucelab types list --model <id> [--include-unused] [--json]","sprucelab types classify --model <id> (--type <id> --ns3451 <code> | --from-json <file>) [--dry-run] [--json]","sprucelab types export --model <id> --format excel|reduzer [--out PATH]"],"verify":"sprucelab verify [--project <uuid>] [--model <uuid>] [--json]","vocabulary":["sprucelab vocabulary systems [--json]  (key is the identifier types classify takes; there is no version field — the edition is inside the name)","sprucelab vocabulary codes <system> [--q <substr>] [--parent <code|uuid>] [--level N] [--page N] [--page-size N] [--json]  (<system> is the key, e.g. ns3451, or its UUID; --level 1..8 is the DERIVED depth of the parent chain, not a stored column; an unknown --parent code is a 400, never an empty page)"],"webhooks":["sprucelab webhooks list [--project] [--page-size N] [--json]","sprucelab webhooks create --project <uuid> --url <URL> --events <e,e> [--description] [--secret-out PATH] [--dry-run] [--json]","sprucelab webhooks rotate-secret <id> [--secret-out PATH] [--json]  (new HMAC secret, shown once; the old one stops verifying immediately)","sprucelab webhooks disable <id> [--enable] [--json]","sprucelab webhooks delete <id> [--yes] [--json]","sprucelab webhooks deliveries [--subscription <id>] [--status] [--limit N] [--json]","sprucelab webhooks redeliver <delivery_id> [--json]","sprucelab webhooks test <id> [--json]"],"_control_plane":"Deliberately omitted from the agent contract (staff / control-plane, not part of the headless data surface): dev. Version: `sprucelab --version`. Drive these via `sprucelab <group> --help` if you hold staff auth."},"field_conventions":{"owning_project":{"request_body_and_response_key":"project_id","mcp_tool_arg":"project_id","cli_flag":"--project","list_query_param":"project","note":"The project a resource belongs to is `project_id` in every request/response body and MCP tool arg. The CLI exposes it as the `--project` flag (conventional for flags). List endpoints still filter on the `?project=<uuid>` query parameter."}},"discovery":{"agent_tools_manifest":"/.well-known/agent-tools.json","llms_txt":"/llms.txt","marketing_for_agents":"https://www.sprucelab.io/agents","benchmarks":"https://www.sprucelab.io/benchmarks"},"mcp":{"package_name":"sprucelab","install":"pip install sprucelab","invocation":"sprucelab-mcp","protocol_version":"2024-11-05","transport":"stdio","tools":{"read":["capabilities","agent_tools_manifest","list_projects","list_models","list_types","list_files","list_observations","list_issues","issues_summary","list_scopes","list_members","list_webhooks","list_deliveries","list_tenant_tokens","eir_catalog","eir_presets","eir_rules","show_project","show_file","show_issue","download_file","wait_for_file","model_status","list_multipart_uploads","file_versions","list_feedback","show_feedback","extraction_log","verify_dry_run","list_claims","get_claim","claim_conflicts","list_portfolios","show_portfolio","portfolio_members","portfolio_invitations","portfolio_projects","company_members","company_invitations","project_guest_invitations","list_project_configs","show_project_config","scope_files","project_verification","verification_timeline","canonical_floors","storey_verification","aggregate","list_instances","show_instance","property_inventory","quantity_inventory","material_inventory","qto","resolve","vocabulary_systems","vocabulary_codes","claim_diff","material_resolution","dedup_runs","project_changelog","project_statistics","my_role","model_versions","compare_models"],"write":["scaffold_project","classify_types","verify_model","create_issue","update_issue","issues_from_verification","resolve_issue","create_feedback","resolve_feedback","create_scope","add_member","remove_member","mint_tenant_token","revoke_tenant_token","eir_add_rule","eir_remove_rule","eir_apply_preset","eir_set_rules","upload_file","reprocess_file","reprocess_model","requeue_files","reconcile_files","create_webhook","disable_webhook","delete_webhook","test_webhook","redeliver","promote_claim","reject_claim","supersede_claim","create_portfolio","update_portfolio","add_portfolio_member","set_portfolio_member_role","remove_portfolio_member","invite_to_portfolio","assign_project_to_portfolio","unassign_project_from_portfolio","invite_to_company","invite_project_guest","update_project","assign_files_to_scope"]},"config_example":{"mcpServers":{"sprucelab":{"command":"sprucelab-mcp"}}}},"parser":{"ifc":"ifcfast","ifc_version_min":"0.1.0","fallback":"ifcopenshell","tier_1_speedup_vs_ifcopenshell":"25x-47x","audit_url":"https://www.sprucelab.io/benchmarks","source_repo":"https://github.com/EdvardGK/ifcfast"},"headless_coverage":{"api":182,"cli":149,"mcp":107,"total":184,"pct_api":0.99,"pct_cli":0.81,"pct_mcp":0.58},"upload_limits":{"simple_upload":{"max_bytes":99614720,"endpoints":["POST /api/files/","POST /api/models/upload/","POST /api/models/upload-with-metadata/"],"enforced_by":"cloudflare-edge-request-body-cap (104857600 bytes per request on api.sprucelab.io; surfaces as a bare Vercel 502 via the www.sprucelab.io /api rewrite) + sprucelab-early-gate (structured 413, reason=simple_upload_too_large)"},"multipart_upload":{"max_bytes":5368709120,"recommended_threshold_bytes":47185920,"endpoints":{"init":"POST /api/models/multipart-init/","complete":"POST /api/models/multipart-complete/","abort":"POST /api/models/multipart-abort/","confirm":"POST /api/models/confirm-upload/","single_put_presign":"POST /api/models/get-upload-url/","list_incomplete":"GET /api/models/multipart-uploads/?project_id={uuid}","sweep":"POST /api/models/multipart-uploads/sweep/"},"flow":"multipart-init {project_id, filename, file_size} → PUT each part to its presigned url (raw bytes, NO Content-Type header, read the ETag response header) → multipart-complete {key, upload_id, parts:[{part_number, etag}]} → confirm-upload {project_id, file_path, file_url, file_size, original_filename, checksum?}","enforced_by":"sprucelab-early-gate + supabase-hard-cap (part PUTs go direct to Supabase storage and never traverse the Cloudflare zone in front of the API)"},"max_file_bytes":5368709120,"global_quota_bytes":96636764160,"remaining_quota_bytes":null,"remaining_quota_bytes_visibility":"authenticated-only","enforced_by":"simple: cloudflare-edge-request-body-cap (100 MiB) + sprucelab-early-gate · multipart: sprucelab-early-gate + supabase-hard-cap"}}