dodil/erp-method

node_modules/.cache/erp-method/v0.1.15/modules/contracts/module.yaml

# Contracts: templates, clauses and our own e-signature. The module source.
#
# PROVENANCE. Derived from a pilot engagement, 2026-10-17 (a creative agency; employment contracts,
# freelance services agreements with IP assignment, NDAs, leaver letters). The code it is rebuilt from is
# an earlier client CRM (its contract and clause domain, public tokens, the contract tables and the
# contract, clause library, versions and record-files pages). How far each part is signed off:
#   - SIGNED OFF BY THE CUSTOMER: nothing yet. The need is "generating the relevant contract from an
#     approved template", in the customer's words; G-03 and G-11 are requirements written from it, not
#     yet reviewed by the customer.
#   - AGREED (boundary review, 2026-10-17): contracts are core; the e-signature level is SIMPLE (SES) with
#     an evidence trail; advanced/qualified signatures are out of scope until a customer needs one; the
#     customer's template wording is an adjustment (invented wording in the demo).
#   - DESIGNED (2026-10-17, DODIL's decisions over contracts best-practice research): everything else.
#     Where this spec differs from that research, the decisions win, notably: the counterparty is ALWAYS
#     a bp_id (never "bp_id or employee_id"), with the business-partner roles each type allows.
#
# What a contracts module usually gets wrong, and this one does not:
#   - editing the library alters signed paper: here a contract holds a COPY of each clause version, frozen
#   - "signed" is a status somebody set: here it is an executed copy backed by an append-only, hash-chained
#     event log, or an uploaded executed copy marked SIGNED_ELSEWHERE, never both confused
#   - overclaiming: this is a simple electronic signature, and the module only ever says "signed electronically"
#
# Extra top-level sections (brick, domain_model, surfaces, provides_capabilities, localisation_packs,
# extension_points) are passed through verbatim in /modules/contracts/spec.yaml for the builders.

id: contracts
name: Contracts & e-signature
short: Contracts
version: 1
maturity: tested          # built and tested, not deployed (pattern §14): rules, both doors, tier 1 and the scenarios pass; live_check ran on DataK3; nothing has run behind the gateway for a real person
summary: >-
  Contracts drafted from your approved templates and clause library, filled from the record rather
  than re-typed, approved when they deviate, signed online with a record of who signed what and
  when, and tracked to their notice deadlines and renewals.

facets:
  domain: legal
  industries: [any]
  customer_words:
    - "generate the contract from an approved template"
    - "where is the signed contract"
    - "renewal dates"
    - "what did we agree"
    - "send it for signature"
    - "our standard clauses"
    - "who still hasn't signed"
  replaces: [a shared drive of Word templates, DocuSign for simple contracts, Ironclad, Juro]
  pillars: [sql]
  first_module_fit: low
  depends_on:
    - module: business-partner
      why: every counterparty is a business partner (an organisation, or a person partner hr writes); its data is read through bp and frozen at send
      required: true
  feeds: [hr, crm]
  owns_master_data: []
  references_master_data: [business_partner, person]

# ---------------------------------------------------------------------------------------------
# The brick: the same shape as brick-pipeline / brick-quote / brick-business-partner / brick-activity
# ---------------------------------------------------------------------------------------------
brick:
  package: brick_contracts
  short_name: contract                # the permission namespace; tables are prefixed ct_ (shared.yaml)
  layout:
    - "src/brick_contracts/domain/        rules in plain Python: Contract, Library (templates, clauses), ContractType; value objects; domain events. No SQLAlchemy, FastAPI, MCP, identity or PDF engine."
    - "src/brick_contracts/application/   use cases declared ONCE with @CT.use_case (an API route AND an MCP tool), `caller` first; row visibility and the sensitivity class decided here; the provided capabilities contracts.draft / send / status"
    - "src/brick_contracts/infrastructure/ tables + one repository per aggregate (erp_platform.repository Aggregate/Part); the PDF renderer, the CapabilitySeam (mail.send) and the public-door client live here"
    - "src/brick_contracts/app.py         app = CT.app()"
    - "src/brick_contracts/schema.sql     inside the package; additive only"
    - "tests/test_rules.py   rules with no database (one test per Given/When/Then sentence)"
    - "tests/test_doors.py   API and MCP over the real app with storage on SQLite / the in-memory repository: same outcome both ways, 404 for another caller's or a sensitive-grade row, 403 without the permission"
    - "tests/test_tier1.py   with the real business-partner and hr bricks in one bucket"
    - "scripts/live_check.py the deployed brick against a dev bucket"
    - "README.md · AGENTS.md · CLAUDE.md, like brick-pipeline's"
  refusals: "Refused(status, reason) in the rule's own words: 409 for a broken rule (\"version 2 is out for signature; recall it to edit\"), 422 for invalid input (missing[] listed), 404 for a row the caller may not see, never 403 for it"
  no_hand_written_doors: "no api/ or mcp/ folder: both doors are generated from the use-case declarations"
  pdf_renderer: "an infrastructure adapter (WeasyPrint in the earlier client CRM, +99 MB of system libraries), so it lives in the PRIVATE app's image only; the public signing page never renders a PDF, it serves the frozen bytes"
  use_cases:
    commands: [keep_contract_type, keep_clause, amend_clause, retire_clause, keep_template, approve_template, retire_template,
               draft_contract, edit_draft, choose_position, override_section, set_variables, add_signer, edit_signer,
               request_approval, decide_approval, send_for_signature, recall, reissue_link, copy_signing_link,
               mark_delivered_by_hand, upload_signed_elsewhere, cancel, amend_contract, renew_contract, terminate_contract,
               record_administrative_change, complete_key_date, run_due, purge_by_retention]
    public_commands: [open_for_signing, consent_and_sign, decline, request_changes]   # through the door only
    internal_commands: [sign_as_us, decline_as_us]                               # our-side signers, signed in
    queries: [list_contracts, show_contract, show_versions, show_certificate, verify_evidence, approvals_queue,
              key_dates, clause_library, show_template, contract_status, fetch_executed, to_sign]
  outside_the_brick:
    - "crm's draft_from_quote is an ADAPTER in the composition (or in crm), never in this brick: it maps quote fields to variables and calls contracts.draft with source_ref 'crm:quote/<quote_id>@v<version>'. The brick has no quote code path."

# ---------------------------------------------------------------------------------------------
# Domain model (DDD)
# ---------------------------------------------------------------------------------------------
domain_model:
  ubiquitous_language:
    contract_type: "what kind of agreement (EMPLOYMENT, FREELANCE_SERVICES, NDA, IP_ASSIGNMENT, LEAVER_LETTER, CUSTOMER_ORDER, SUPPLIER…): a row carrying the allowed counterparty roles, execution (SIMPLE | DEED), esign_allowed, sensitivity, the retention rule and the approval policy"
    counterparty: "ALWAYS a business partner (bp_id), of a role the type allows (employee, worker, supplier, customer)"
    template: "one approved shape per contract type: ordered clause slots and typed variable definitions, versioned"
    clause: "a standing piece of wording, versioned; it belongs to a family (liability cap, IP) with a POSITION: Preferred, Alternate or Fallback (SAP Ariba's vocabulary)"
    position: "Preferred = standard; Alternate = an agreed other wording (deviation 0 or 1 per row); Fallback = acceptable with approval (deviation 1); an override written for one contract = non-standard (deviation 2)"
    slot: "a place in a template: required | optional | alternative-of <slot>, naming a clause family and its default position"
    variable_def: "a typed field a template needs (text, date, money, int, enum, bool, party), with its source (COUNTERPARTY, ENGAGEMENT, PAY, DATES, QUOTE, CALLER), required, validation bounds and conditions that choose clauses"
    variable: "the resolved value of a variable_def on one contract, with its source; frozen at send"
    section: "a COPY of a clause version inside one contract, with its deviation level and whether it was overridden"
    draft_fingerprint: "sha256 over the ordered sections, the resolved variables and the signer list; any edit changes it"
    deviation: "a section or variable away from the template's standard: a Fallback position, an override, or a variable outside its bounds"
    approval: "a decision on one draft_fingerprint by the group a policy row names, with a mandatory fallback group"
    version: "a frozen rendering of the contract (PDF bytes + sha256) made at send; every signature binds to exactly one version"
    signer: "a person who signs for one side (US | THEM), in a signing order, with a title and an authority basis; of kind INTERNAL (our side: signs inside the app as their signed-in pool identity) or EXTERNAL (the counterparty: signs through a public link)"
    signature_event: "an append-only, hash-chained record of what happened to a signer and a version (INVITED, DELIVERED, OPENED, CONSENTED, SIGNED, DECLINED, CHANGES_REQUESTED, REVOKED, REISSUED, SIGNED_ELSEWHERE)"
    executed_copy: "the signed version's PDF plus a certificate page, with its own sha256 kept in the row; or an uploaded executed copy (provenance UPLOADED)"
    signed_electronically: "the ONLY claim this module makes about a signature: a simple electronic signature with its evidence attached"
    amendment: "a change to a signed contract: AMENDMENT, RENEWAL, TERMINATION (each its own versioned, signed contract) or ADMINISTRATIVE (internal, not signed)"
    key_date: "a dated obligation on a contract (START, END, NOTICE_DEADLINE, RENEWAL, PROBATION_END, PRINCIPAL_STATEMENT_DUE, SDS_REVIEW, PAYMENT_MILESTONE) with an owner and reminders"
    source_ref: "the opaque id of what asked for the contract (hr:onboarding/…, crm:quote/…@v3, manual:<form key>); its namespace (hr:, crm:) names the ORIGINATING consumer"
    originator: "the consumer whose capability call created a contract, matched by the source_ref namespace; only it may fetch that contract's executed copy"
  bounded_context:
    inside: "contract types, the clause library, templates, contracts and their sections, variables, versions, signers, approvals, signature evidence, executed copies, amendments, key dates, retention of all of these"
    outside: "the party record (business-partner), why a contract is needed (hr, crm), delivering mail (a provider), the public token (the platform door), the legal judgement of status or validity (the customer and their adviser)"
  context_map:
    - neighbour: business-partner
      relationship: "conformist reader: the counterparty is read through the bp seam at draft (bp:person:read for a person partner) and FROZEN into variables at send; contracts never writes bp"
    - neighbour: hr
      relationship: "customer/supplier: hr is the customer; contracts PROVIDES contracts.draft / send / status / fetch_executed as capabilities; hr polls status and, as the ORIGINATOR, fetches the executed copy of contracts it created; contracts never reads hr tables"
    - neighbour: crm
      relationship: "an ANTI-CORRUPTION LAYER outside the brick: draft_from_quote maps a quote to variables and calls contracts.draft"
    - neighbour: mail provider (none in the first demo)
      relationship: "an ACL through mail.send (capabilities.yaml 0.3); no provider means a manual 'copy the signing link' task"
    - neighbour: the public-link door (erp-platform)
      relationship: "the door holds the signing token (hashed) and calls contracts' public use cases with a service identity scoped to one signer of one contract"
  aggregates:
    - name: Contract
      root: "ct_contract (contract_id)"
      parts: [ct_contract_variable, ct_contract_section, ct_contract_version, ct_signer, ct_signature_event, ct_executed_copy, ct_approval, ct_delivery, ct_key_date, ct_amendment]
      invariants:
        - "the counterparty is one bp_id holding a role the contract type allows, checked on draft"
        - "sections and variables are editable only in DRAFT; at send they are frozen into version N (PDF + sha256)"
        - "every signature event names one version and the sha256 the signer was shown; a SIGN for any version other than the current frozen one is refused (earlier client CRM, defect 3)"
        - "an approval binds to one draft_fingerprint; any edit changes the fingerprint and voids it"
        - "send is refused while missing[] is non-empty, while a required approval is not granted, when execution = DEED or esign_allowed = false, and when both sides are not named"
        - "signature events are append-only and hash-chained (prev_hash → row_hash); a broken chain blocks the executed copy and is shown"
        - "SIGNED requires an executed copy AND a SIGNED event for every signer of the signed version; SIGNED_ELSEWHERE requires an uploaded executed copy and marks signers SIGNED_ELSEWHERE, never SIGNED (earlier client CRM, defect 4)"
        - "the executed copy is a real rendered file with its own sha256, never a placeholder (earlier client CRM, defect 1)"
        - "IP and user agent are structured fields of the event, never free text (earlier client CRM, defect 2)"
        - "an INTERNAL signer signs only inside the app, as their pool identity (sub + session); an EXTERNAL signer only through a public link; neither path can sign for the other kind"
        - "the executed copy is released to a consumer only if it is the originator (source_ref namespace = the caller's brick)"
      enforced_in: domain
    - name: Library
      root: "ct_template (template_id) and ct_clause (clause_id): each its own small aggregate"
      parts: [ct_template_version, ct_template_slot, ct_variable_def, ct_clause_version]
      invariants:
        - "a clause version a contract has taken is frozen; a change is a new version (the earlier client CRM's clause domain)"
        - "a template version is DRAFT → APPROVED → RETIRED; only APPROVED drafts contracts; an approved version is immutable"
        - "a template version pins clause_id@version per slot, so approving a template approves those exact words"
        - "a required clause cannot be retired while a template still requires it"
      enforced_in: domain
    - name: ContractType
      root: "ct_type (type_code)"
      invariants: ["a DEED type has esign_allowed = false", "every type names a retention rule and an approval policy with a fallback group"]
  transactions: "one aggregate per transaction; the tick (run_due) and the capability calls are each idempotent, and cross-aggregate effects (a template retired, a contract still on it) are read, never cascaded"
  value_objects:
    Fingerprint: "sha256 hex of a canonical serialisation: of the draft (sections + variables + signers) for approvals, of PDF bytes for a version, of the executed file"
    VariableValue: "a typed value + its source + resolved_at; money as an exact decimal string with currency"
    KeyDate: "kind + due_on + owner group + reminder cadence; NOTICE_DEADLINE = end − notice period, computed, stored as its own key date"
    SignerRef: "contract_id|signer_no"
    EventHash: "row_hash = sha256(prev_hash || canonical row); prev_hash of the first event = sha256(contract_id)"
    ConsentText: "the exact wording shown, with its version"
    ContractId: "derived: 'ct-' + hex(blake2b(source_ref|type_code, digest_size=8)), so a retried caller makes one contract"
  domain_events:                      # past tense; written as ct_signature_event / state rows, and reported by contracts.status
    Drafted: "a contract was drafted from an approved template version (missing[] and deviations[] computed)"
    Approved: "an approval was granted for one draft fingerprint"
    Sent: "version N was frozen and signing links issued"
    Signed: "every signer of version N signed; the executed copy exists"
    Declined: "a signer declined, with a reason (or requested changes)"
    Expired: "OUT_FOR_SIGNATURE passed its link lifetime unsigned (EXPIRED_UNSIGNED), or an ACTIVE contract passed its end"
    Amended: "an amendment, renewal or termination was signed and the original superseded or terminated"
  domain_services:
    Resolver: "(template version, counterparty bp record, supplied values) → variables + missing[] + clause choices by condition"
    DeviationCheck: "(draft, template version, type policy) → deviations[] with levels, and whether approval is required"
    Renderer: "(frozen sections + variables) → PDF bytes (infrastructure adapter behind a domain port)"
    CertificateBuilder: "(signed version, events, signers) → the certificate page, using only the words 'signed electronically'"
    ChainVerifier: "(events) → valid | broken at seq n"
  policies:
    - "contract types: allowed counterparty roles, execution, esign_allowed, sensitivity, retention rule"
    - "approval policy rows: which deviation or variable bound needs which group, and the mandatory fallback group and timeout"
    - "reminder cadences: signer chasing; key-date reminders (default 120/90/60/30 days for notice deadlines)"
    - "link lifetime and the consent text version"
    - "retention rules per type (term + 6 years; deeds 12)"
  factories:
    ContractFactory: "from contracts.draft (or draft_contract): type + approved template version + counterparty + values → a Contract in DRAFT with sections copied, variables resolved, missing[] and deviations[]"
    AmendmentFactory: "from a signed contract + kind → a new Contract (amends = parent) in DRAFT, prefilled from the parent's frozen variables"
  repositories: "ContractRepository, TemplateRepository, ClauseRepository, ContractTypeRepository: each an Aggregate of Parts whose keys start with the root key; whole-aggregate loads, no lazy loading"

actors:
  - id: contract_admin
    name: Operations / legal (whoever keeps the standard paper)
    does: keeps contract types, the clause library and templates, approves template versions, sets approval policies and settings
    sees: every contract, including employment bodies
    permissions: [contract:type:write, contract:clause:write, contract:template:write, contract:template:approve, contract:contract:read_all,
                  contract:employment:read, contract:contract:write, contract:contract:send, contract:contract:recall,
                  contract:contract:upload_executed, contract:evidence:read, contract:amendment:write, contract:settings:write]
  - id: owner
    name: The person responsible for a contract (account lead, HR, operations)
    does: drafts, edits, sends, chases, recalls, files signed-elsewhere copies, amends and renews their contracts
    sees: contracts they own; employment bodies only with contract:employment:read
    permissions: [contract:contract:read, contract:contract:write, contract:contract:send, contract:contract:recall,
                  contract:contract:upload_executed, contract:evidence:read, contract:amendment:write]
  - id: approver
    name: An approver (legal, finance, HR lead, a director), or the fallback approver
    does: approves or rejects a draft that deviates from the standard
    sees: the drafts in their queue, with the deviations highlighted
    permissions: [contract:approval:decide, contract:contract:read]
  - id: signer
    name: The counterparty's signer (the starter, a freelancer, a client)
    does: reads the frozen version, consents, signs, declines or asks for changes, downloads the signed copy
    sees: one contract version through a link (READ + SIGN), nothing else
    permissions: []                   # holds a public-link token, not a pool role
  - id: our_signatory
    name: Our side's signer (a director, an authorised signatory)
    does: signs or declines inside the app, signed in, from a To sign list
    sees: the contracts where they are a signer, at the version out for signature
    permissions: [contract:contract:sign]
  - id: calling_module
    name: Another module asking for a contract (hr; crm through its adapter), through its service identity
    does: calls contracts.draft / send / status
    sees: the contracts it caused (by source_ref namespace), their state and key dates, and, as their originator, the executed copy; never a body of a contract it did not create, and no broad contract:employment:read
    permissions: [contract:capability:invoke]
  - id: clock
    name: The clock (a service client or the always-on worker)
    does: "calls run_due(now). ONE clock worker (the clock service client) calls every brick's tick route, listed in the composition"
    sees: nothing through people's doors
    permissions: [contract:run:tick]
  - id: system
    name: The system
    does: renders versions and executed copies, chains events, chases signers, reminds on key dates, applies retention
    sees: everything, through its own service account
    permissions: []

stories:
  # person
  - id: CT-01
    kind: person
    actor: contract_admin
    story: As whoever keeps our standard paper, I want a clause library with Preferred, Alternate and Fallback positions, so that negotiation happens inside agreed limits.
    given: the family "IP assignment" with a Preferred clause (full assignment on creation) and a Fallback (assignment on payment)
    when: I amend the Preferred wording
    then: >-
      a new version is cut and old versions stay readable; contracts already holding version 1 are untouched;
      editing a version a contract holds is refused ("version 1 is on 3 contracts, amend it into version 2");
      a required clause cannot be retired while a template requires it
    screen: Clause library
    reference: "partial — the earlier client CRM's clause domain versions and freezes; positions and families are new"
  - id: CT-02
    kind: person
    actor: contract_admin
    story: As whoever keeps our standard paper, I want contract types as rows, so that what may be signed online, with whom and kept how long is ours to set.
    given: the types EMPLOYMENT, FREELANCE_SERVICES, NDA, LEAVER_LETTER and a DEED type
    when: I set FREELANCE_SERVICES to allow the counterparty roles worker and supplier, and the DEED type to execution DEED
    then: >-
      a freelance contract to a partner with only the customer role is refused; the DEED type is saved with
      esign_allowed = false and can never be sent for online signing; every type must name a retention rule and an
      approval policy with a fallback group, or the save is refused
    screen: Contract types
    reference: not built
  - id: CT-03
    kind: person
    actor: contract_admin
    story: As whoever keeps our standard paper, I want one template per type with typed variables, so that a contract is filled from the record and not re-typed.
    given: a draft employment template version
    when: I order its slots (required, optional, alternative-of) and declare variables (start_date DATE from DATES, salary MONEY from PAY with a band, job_title TEXT from ENGAGEMENT, notice_weeks INT with a minimum)
    then: >-
      each slot pins clause_id@version; each variable has a type, a source, whether it is required, its bounds
      and any condition choosing a clause ("engagement_via = PSC → the PSC warranty slot"); the version stays DRAFT until approved
    screen: Templates
    reference: "not built — the earlier client CRM's contract_template is a JSON list of clause ids"
  - id: CT-04
    kind: person
    actor: contract_admin
    story: As whoever keeps our standard paper, I want template versions approved before use, so that nothing goes out on unapproved words.
    given: template version 3 in DRAFT, authored by Sam
    when: Priya approves it
    then: >-
      version 3 is APPROVED and immutable, new drafts use it, contracts on version 2 keep version 2; the author
      approving their own version is refused unless the setting allows it; retiring it stops new drafts only
    screen: Templates
    reference: not built
  - id: CT-05
    kind: person
    actor: owner
    story: As a contract owner, I want to draft a contract for a partner by choosing its type, so that the right template, clauses and values appear at once.
    given: an organisation partner with the supplier role and the NDA type
    when: I draft it (draft_contract), with a form key generated when the form opened
    then: >-
      a DRAFT exists with sections copied from the approved template version, variables resolved from the partner
      (read through bp) and from what I typed, and a readiness panel listing missing[] and deviations[]; a double
      click is one draft (source_ref manual:<form key>)
    screen: Contract editor
    reference: "partial — the earlier client CRM drafts from a quote; type-driven drafting is new"
  - id: CT-06
    kind: person
    actor: owner
    story: As a contract owner, I want to choose an alternative position or rewrite a clause for one deal, so that negotiation is recorded as exactly that.
    given: a draft with the Preferred liability clause
    when: I switch it to the Fallback position, or override its words for this contract
    then: >-
      the section records its position and deviation level (Fallback 1, override 2, marked overridden); the
      library is untouched; deviations[] now requires approval and the send button says so ("liability cap is a
      Fallback: approval by legal needed")
    screen: Contract editor
    reference: "partial — the earlier client CRM overrides sections; deviation levels are new"
  - id: CT-07
    kind: person
    actor: approver
    story: As an approver, I want to approve or reject a deviating draft from a queue, so that margin and risk are protected without email.
    given: a draft whose fingerprint F1 has a Fallback clause and a day rate above its band
    when: I approve it
    then: >-
      the approval binds to F1; any later edit (new fingerprint F2) voids it and the draft needs approval again;
      a rejection returns it to DRAFT with my reason; an approval not decided by its timeout goes to the policy's
      mandatory fallback group, so nothing stalls; an owner cannot approve their own deviation
    screen: Approvals
    reference: not built
  - id: CT-08
    kind: person
    actor: owner
    story: As a contract owner, I want to send for signature in one step, so that each signer gets their own link and the words are frozen.
    given: a draft with nothing missing, its approval granted (if one was needed), signers named on both sides
    when: I send it (send_for_signature)
    then: >-
      version N is rendered to PDF and frozen with its sha256; one signing link per signer (READ + SIGN) is issued
      by the platform door for each EXTERNAL signer only; delivery goes through mail.send in signing order, or, with no
      mail provider, becomes a manual "copy the signing link" task per signer (copying revokes the old token, writes a
      REISSUED event into the chain and shows the new link once); each INTERNAL signer (our side) gets the contract on
      their To sign list instead, with no link. INVITED and DELIVERED events are recorded. Refused, in words, when missing[] is non-empty, an approval is open, the
      type is a deed, or only one side is named.
    screen: Contract editor
    reference: "partial — the earlier client CRM freezes a PDF with sha256 and makes links staff copy by hand; delivery is new"
  - id: CT-09
    kind: person
    actor: signer
    story: As the counterparty's signer, I want to read exactly what I am signing and sign it from a link, so that signing takes a minute and I keep proof.
    given: a link to version 2 of my contract, my turn in the signing order
    when: I open it, read the document and its fingerprint, tick the consent ("I agree to sign electronically"), type my name and sign
    then: >-
      OPENED, CONSENTED and SIGNED events are written with UTC time, version 2 and the sha256 shown to me, my email,
      the token id (never the secret), IP, user agent, the typed name, the consent text and its version, signer kind
      EXTERNAL and auth_method EMAIL_LINK;
      the page states how long the signed copy is kept; signing a version other than the current frozen one is refused
      ("this contract was re-issued: open the new link"); signing out of order is refused naming who it waits on;
      signing twice is one signature
    screen: Signing page
    reference: "partial — the earlier client CRM signs by token; structured evidence, consent text and version binding are new"
  - id: CT-10
    kind: person
    actor: signer
    story: As a signer, I want to decline or ask for changes, so that I am not forced to choose between signing and silence.
    given: my signing link
    when: I decline with a reason, or ask for changes with a note
    then: >-
      DECLINED or CHANGES_REQUESTED is recorded; the contract moves to DECLINED or back to DRAFT (a new version will
      be needed); every other open link is revoked; the owner is told
    screen: Signing page
    reference: "partial — the earlier client CRM has decline; changes requested is new (IWG principle)"
  - id: CT-11
    kind: person
    actor: owner
    story: As a contract owner, I want to see who has opened, signed or still holds it up, so that I chase the right person.
    given: a contract out for signature with three signers
    when: I open its signing panel
    then: >-
      each signer shows seen (first and last opened) apart from acted (signed, declined); whose turn it is; I can
      reissue a link (the old one stops working, REISSUED) or recall the whole contract (every token revoked, the reason recorded)
    screen: Contract editor
    reference: "partial — the earlier client CRM shows signer status and reissues; seen-vs-acted is new"
  - id: CT-12
    kind: person
    actor: owner
    story: As a contract owner, I want to file a copy signed elsewhere, so that a deed or the other side's paper lands on the same record with the same dates.
    given: a deed (online signing not allowed), signed on paper with a witness
    when: I upload the executed copy with where it was signed
    then: >-
      the file is stored with its sha256 as an executed copy (provenance UPLOADED); the contract is SIGNED_ELSEWHERE;
      its signers are SIGNED_ELSEWHERE, not SIGNED, because no event of ours backs a signature; the key dates are
      created as for any signed contract
    screen: Signed-elsewhere upload
    reference: "partial — the earlier client CRM's upload path exists but marks pending signers SIGNED (defect 4)"
  - id: CT-13
    kind: person
    actor: owner
    story: As a contract owner or HR, I want the certificate and the evidence, so that I can show who signed what, when and from where.
    given: a signed contract
    when: I open its certificate
    then: >-
      I see every signer (name, email, side, title, authority basis) with each event's time, IP and user agent, the
      sha256 of the signed version, the sha256 of the executed file, and whether the event chain verifies; the wording is
      "signed electronically. A record of who signed, when, from where, and the fingerprint of the document is
      attached" — never advanced, qualified, legally binding or equivalent to a wet signature
    screen: Certificate
    reference: not built
  - id: CT-14
    kind: person
    actor: owner
    story: As a contract owner, I want to amend, renew or terminate a signed contract, so that the history of what was agreed is one chain.
    given: a signed freelance agreement ending 31 March
    when: I renew it to 30 June
    then: >-
      a new contract (amends = the original, kind RENEWAL) is drafted from the original's frozen variables and goes
      through approval and signing like any contract; when it is signed the original becomes SUPERSEDED and the new key
      dates replace the old; an ADMINISTRATIVE change (a typo in an internal reference) is recorded without signing
    screen: Contract editor
    reference: not built
  - id: CT-15
    kind: person
    actor: owner
    story: As a contract owner, I want a register of key dates with the notice deadline as its own date, so that we never miss the window to give notice.
    given: a supplier contract ending 31 December with 90 days' notice
    when: I open Key dates
    then: >-
      END is 31 December and NOTICE_DEADLINE is 2 October, each with an owner and reminders at 120/90/60/30 days
      before (rows); completing one records who and when; a manager sees only the dates of contracts they may read
    screen: Key dates
    reference: not built
  - id: CT-16
    kind: person
    actor: owner
    story: As a contract owner, I want to cancel a contract before it is signed, so that a dead deal leaves no live links.
    given: a contract in DRAFT or OUT_FOR_SIGNATURE
    when: I cancel it with a reason
    then: it is CANCELLED, every token is revoked (REVOKED events), and a signed contract cannot be cancelled ("terminate it instead")
    screen: Contract editor
    reference: "partial — the earlier client CRM has cancel"
  - id: CT-17
    kind: person
    actor: our_signatory
    story: As our side's signatory, I want to sign inside the app as myself, so that our signature rests on my sign-in and not on whoever holds a link.
    given: a contract out for signature where I am the INTERNAL signer and it is my turn
    when: I open To sign, read version 2 and its fingerprint, consent and sign (sign_as_us)
    then: >-
      OPENED, CONSENTED and SIGNED events with signer kind INTERNAL, auth_method POOL_SESSION, my pool sub and the
      session id, UTC time, version 2 and its sha256 as shown, IP and user agent; no public link exists for me;
      a caller who is not this signer gets 404; out of turn or a stale version is refused as for an external signer
    screen: To sign
    reference: not built

  # system
  - id: CT-30
    kind: system
    actor: system
    trigger: "run_due(now), called by the same clock as hr"
    story: The system chases signers, expires stale signing, and reminds on key dates, so that nobody keeps a list.
    given: a signer whose turn it is and who has not signed for 3 working days; a contract out for signature past its link lifetime; a notice deadline 90 days away
    when: the tick runs
    then: >-
      a reminder to the signer (mail.send, or a manual task); the stale contract becomes EXPIRED_UNSIGNED with its tokens
      revoked; a reminder for the key date to its owner; each at most once per cadence slot (idempotency_key
      contract_id|signer_no|chase|<slot>), so a second tick sends nothing
    reference: not built
  - id: CT-31
    kind: system
    actor: system
    trigger: in the request of the last signature
    story: The system produces the executed copy the moment the last signer signs, so that everyone holds the same evidence.
    given: version 2 signed by every signer, the chain valid
    when: the last SIGNED event commits
    then: >-
      the executed PDF = version 2 + a certificate page is rendered, its sha256 kept in the row (it cannot be printed on
      itself), stored as a DataK3 object, the contract becomes SIGNED, the tokens are used up, and a copy goes to every
      signer (mail.send or manual task); a broken chain refuses to produce it and alerts the owner; a retry produces it once
    reference: "not built — the earlier client CRM's executed file is the placeholder signed-online:<id> (defect 1)"
  - id: CT-32
    kind: system
    actor: system
    trigger: on every signature event
    story: The system writes the evidence as an append-only, hash-chained log, so that a changed row is detectable.
    given: a contract's events seq 1..n
    when: event n+1 is written
    then: >-
      its row_hash = sha256(prev_hash ‖ canonical row) with prev_hash = row n's hash; events are never updated or
      deleted (within retention); the verifier recomputes the chain on every certificate read and names the first seq
      that does not match; IP and user agent are fields, not text (fixes defect 2)
    reference: not built
  - id: CT-33
    kind: system
    actor: system
    trigger: on read (derived)
    story: The system derives the states dates decide (ACTIVE, EXPIRED) and the readiness panel, so that no job sets a flag.
    given: a SIGNED contract starting 1 November and ending 31 March
    when: it is read on 15 November, then on 1 April
    then: ACTIVE, then EXPIRED; readiness (clauses, both sides named, term, nothing missing, approval, signed, executed copy) is computed, each item keyed so the screen can send you to the fix
    reference: "partial — the earlier client CRM's readiness()"
  - id: CT-34
    kind: system
    actor: system
    trigger: daily, in the first tick after 02:00
    story: The system applies the retention rule of each contract type, so that drafts do not pile up and executed contracts are kept exactly as long as they must be.
    given: a declined NDA from 100 days ago, and a simple contract that ended 6 years and 1 day ago
    when: the sweep runs
    then: >-
      the declined draft is purged after the type's short window; the expired contract, its versions and evidence are
      purged at term + 6 years (12 for a deed); every purge is logged; IP and user agent go with the evidence they belong to;
      running twice changes nothing
    reference: not built
  - id: CT-35
    kind: system
    actor: system
    trigger: on draft and on send, for contracts under the UK pack
    story: The system applies the UK checks, so that an employment statement and an IR35 decision cannot be skipped.
    given: an EMPLOYMENT contract, and a FREELANCE_SERVICES contract with client_size_regime MEDIUM_LARGE and ir35_determination OUTSIDE
    when: they are drafted and sent
    then: >-
      the employment draft lists any of the ERA 1996 s.1 day-one particulars missing as missing[], and a
      PRINCIPAL_STATEMENT_DUE key date = start_date is created; contracts.status reports principal_statement_issued_at
      (the duty is to GIVE it, so issued matters as much as signed) and warns when the start date passes without it;
      the freelance contract is refused at send with no sds_issued_on ("an outside determination by a medium or large
      client needs the status determination statement recorded first")
    reference: not built

  # integration
  - id: CT-50
    kind: integration
    actor: calling_module
    trigger: when hr's document step (or crm's adapter) runs
    story: Another module asks for a contract by capability, so that it never writes contract tables and a retry makes one contract.
    given: hr's run step with source_ref hr:onboarding/<run_id>|<step_no>, type FREELANCE_SERVICES, counterparty bp_id, values
    when: it calls contracts.draft, then contracts.send, then polls contracts.status
    then: >-
      one contract (contract_id derived from source_ref|type_code; the same call again replays; the same key with
      different inputs is 409); missing[] with no partial contract when values are absent; status reports state,
      version and signers, and, to the ORIGINATOR only (the caller whose call created it, matched by the source_ref
      namespace hr:), the executed copy's object key and sha256 once signed; hr then streams the file with
      contracts.fetch_executed and files it on the person. Another consumer asking for it gets 404
    owner: contracts
    reference: "not built — the capability entries are proposed below (provides_capabilities)"
  - id: CT-51
    kind: integration
    actor: system
    trigger: at draft, and again at send
    story: The counterparty's details are read through business-partner and frozen at send, so that they are never re-typed and never drift.
    given: a person partner (readable with bp:person:read) or an organisation partner
    when: a contract is drafted and later sent
    then: COUNTERPARTY variables are resolved through the bp seam at draft and refreshed once at send, then frozen in the version; a later change to the partner never alters a sent version
    owner: business-partner
    reference: not built
  - id: CT-52
    kind: integration
    actor: system
    trigger: when a link, a reminder or an executed copy must reach someone
    story: Contracts delivers through the mail.send capability, and with no provider makes a precise manual task.
    given: compose.yaml with or without a mail.send provider
    when: a delivery is due
    then: >-
      with a provider, mail.send with idempotency_key contract_id|signer_no|<kind>|<seq>; with none (rule 8), a manual
      task for the owner carrying the exact message and recipient; for a signing link, "copy the signing link" reissues
      the token and shows the link once, and marking it delivered writes DELIVERED (manual)
    owner: "the capability provider (none in the first demo)"
    reference: not built
  - id: CT-53
    kind: integration
    actor: signer
    trigger: when a signer opens a link
    story: Signers reach the signing page through the platform's public-link door, never the staff app.
    given: the door (a separate app, no pool)
    when: a token is presented
    then: >-
      the door resolves the hash to (contracts, contract_id, signer_no) with grants READ + SIGN, checks expiry and
      revocation, rate-limits, audits the use, and calls contracts' public use cases with a service identity scoped to
      that one signer; a used, revoked or expired token gets the page "this link no longer works, ask <owner>"
    owner: "erp-platform (the public-link door)"
    reference: "not built — platform piece (the earlier client CRM keeps the token unhashed; this must not)"
  - id: CT-54
    kind: integration
    actor: calling_module
    trigger: when crm accepts a quote and wants a contract
    story: crm's quote becomes a contract through an adapter outside the brick, so that contracts never learns what a quote is.
    given: an accepted quote version 3
    when: the adapter maps its fields to variables (QUOTE source) and calls contracts.draft with source_ref crm:quote/<quote_id>@v3
    then: a draft exists like any other; the brick holds the source_ref only; re-running the adapter makes the same contract
    owner: contracts
    reference: "not built — replaces the earlier client CRM's draft_from_quote, which lived inside the contract service"

workflows:
  - entity: contract
    states: [DRAFT, IN_APPROVAL, APPROVED, OUT_FOR_SIGNATURE, SIGNED, SIGNED_ELSEWHERE, ACTIVE, EXPIRED, TERMINATED, SUPERSEDED, DECLINED, CHANGES_REQUESTED, EXPIRED_UNSIGNED, CANCELLED]
    transitions:
      - { from: none, to: DRAFT, by: [owner, calling_module], gate: "an APPROVED template version, a counterparty with a role the type allows" }
      - { from: DRAFT, to: IN_APPROVAL, by: system, gate: "deviations[] needs approval under the type's policy" }
      - { from: IN_APPROVAL, to: APPROVED, by: approver, gate: "the approval's fingerprint equals the draft's" }
      - { from: IN_APPROVAL, to: DRAFT, by: approver, requires: reason, effect: "rejected" }
      - { from: [IN_APPROVAL, APPROVED], to: DRAFT, by: [owner, system], gate: "any edit", effect: "the approval is void" }
      - { from: [DRAFT, APPROVED], to: OUT_FOR_SIGNATURE, by: [owner, calling_module], gate: "missing[] empty; approval granted if needed; both sides named; esign_allowed", effect: "version N frozen (PDF + sha256); one token per signer; INVITED / DELIVERED" }
      - { from: OUT_FOR_SIGNATURE, to: SIGNED, by: system, gate: "a SIGNED event from every signer of version N; chain valid", effect: "executed copy rendered (CT-31)" }
      - { from: OUT_FOR_SIGNATURE, to: DECLINED, by: signer, requires: reason, effect: "every token revoked" }
      - { from: OUT_FOR_SIGNATURE, to: CHANGES_REQUESTED, by: signer, requires: note, effect: "every token revoked; back to DRAFT for a new version" }
      - { from: CHANGES_REQUESTED, to: DRAFT, by: owner }
      - { from: OUT_FOR_SIGNATURE, to: DRAFT, by: owner, requires: reason, effect: "recall: every token revoked, the reason recorded; the next send is version N+1" }
      - { from: OUT_FOR_SIGNATURE, to: EXPIRED_UNSIGNED, by: system, gate: "the link lifetime passed" }
      - { from: [DRAFT, APPROVED], to: SIGNED_ELSEWHERE, by: owner, requires: executed_copy, effect: "provenance UPLOADED; signers SIGNED_ELSEWHERE" }
      - { from: [DRAFT, IN_APPROVAL, APPROVED, OUT_FOR_SIGNATURE], to: CANCELLED, by: owner, requires: reason }
      - { from: [SIGNED, SIGNED_ELSEWHERE], to: SUPERSEDED, by: system, gate: "an AMENDMENT or RENEWAL of it is signed" }
      - { from: [SIGNED, SIGNED_ELSEWHERE], to: TERMINATED, by: system, gate: "a TERMINATION of it is signed" }
    note: "ACTIVE and EXPIRED are DERIVED from starts_on / ends_on on a SIGNED or SIGNED_ELSEWHERE contract; nothing stores them"
  - entity: template_version
    states: [DRAFT, APPROVED, RETIRED]
    transitions:
      - { from: DRAFT, to: APPROVED, by: contract_admin, gate: "every slot pins an active clause version; the approver is not the author unless the setting allows" }
      - { from: APPROVED, to: RETIRED, by: contract_admin, effect: "no new drafts; existing contracts unaffected" }
  - entity: approval
    states: [pending, approved, rejected, void, escalated]
    transitions:
      - { from: pending, to: [approved, rejected], by: approver }
      - { from: [pending, approved], to: void, by: system, gate: "the draft fingerprint changed" }
      - { from: pending, to: escalated, by: system, gate: "the policy's timeout passed", effect: "assigned to the mandatory fallback group" }

entities:
  - id: contract_type
    meaning: a kind of agreement and the rules that come with it
    natural_key: type_code
    key_attributes: [label, allowed_bp_roles, execution (SIMPLE | DEED), esign_allowed, sensitivity (commercial | employment), retention_rule, approval_policy, pack]
    master_data: module-owned (configuration)
    reference_table: ct_type
  - id: clause
    meaning: a standing piece of wording in a family, at a position
    natural_key: clause_id
    key_attributes: [family, position (PREFERRED | ALTERNATE | FALLBACK), deviation_level, category, required, status]
    master_data: module-owned (Library)
    reference_table: ct_clause
  - id: clause_version
    meaning: the words of a clause at one version
    natural_key: "clause_id|version"
    key_attributes: [title, body, created_by, created_at]
    master_data: module-owned (Library)
    reference_table: ct_clause_version
  - id: template
    meaning: the approved shape for one contract type
    natural_key: template_id
    key_attributes: [type_code, name]
    master_data: module-owned (Library)
    reference_table: ct_template
  - id: template_version
    meaning: one version of a template
    natural_key: "template_id|version"
    key_attributes: [status, authored_by, approved_by, approved_at]
    master_data: module-owned (Library)
    reference_table: ct_template_version
  - id: template_slot
    meaning: a place in a template version
    natural_key: "template_id|version|slot_no"
    key_attributes: [family, kind (required | optional | alternative_of), alternative_of_slot, clause_id, clause_version, condition]
    master_data: module-owned (Library)
    reference_table: ct_template_slot
  - id: variable_def
    meaning: a typed field a template version needs
    natural_key: "template_id|version|name"
    key_attributes: [type (text | date | money | int | enum | bool | party), source (COUNTERPARTY | ENGAGEMENT | PAY | DATES | QUOTE | CALLER), required, validation, enum_values, condition]
    master_data: module-owned (Library)
    reference_table: ct_variable_def
  - id: contract
    meaning: one agreement with one counterparty
    natural_key: "contract_id = 'ct-' + hex(blake2b(source_ref|type_code, 8))"
    key_attributes: [type_code, template_id, template_version, bp_id, context_ref, source_ref, owner, state, starts_on, ends_on, notice_days, amends_contract_id, amendment_kind, draft_fingerprint, sensitivity, retention_until, reference]
    relationships: ["-> business_partner (bp_id)", "-> template_version", "0..1 -> contract it amends"]
    master_data: "module-owned; contract_id is SHARED (shared.yaml) because hr stores it"
    reference_table: ct_contract
    note: "context_ref is opaque context (hr: person_id|n) and is never joined by this brick"
  - id: contract_variable
    meaning: a resolved value on one contract
    natural_key: "contract_id|name"
    key_attributes: [value, type, source, resolved_at, frozen_in_version]
    master_data: module-owned (Contract)
    reference_table: ct_contract_variable
  - id: contract_section
    meaning: a copy of a clause version in one contract
    natural_key: "contract_id|position"
    key_attributes: [clause_id, clause_version, position_kind, deviation_level, overridden, title, body]
    master_data: module-owned (Contract)
    reference_table: ct_contract_section
  - id: contract_version
    meaning: a frozen rendering sent for signature
    natural_key: "contract_id|version"
    key_attributes: [object_key, sha256, size_bytes, frozen_at, draft_fingerprint, status (ISSUED | SUPERSEDED | SIGNED)]
    master_data: module-owned (Contract)
    reference_table: ct_contract_version
  - id: signer
    meaning: who signs for which side
    natural_key: "contract_id|signer_no"
    key_attributes: [kind (INTERNAL | EXTERNAL), side (US | THEM), role (SIGNER | APPROVER | CC), signing_order, email, full_name, title, authority_basis, bp_id, pool_sub (INTERNAL), status]
    master_data: module-owned (Contract)
    reference_table: ct_signer
    note: "keyed by number, not email (the earlier client CRM's key), so a mistyped address is corrected without a key change"
  - id: signature_event
    meaning: one thing that happened to a signer and a version, chained
    natural_key: "contract_id|seq (append-only)"
    key_attributes: [kind, signer_no, signer_kind (INTERNAL | EXTERNAL), version, version_sha256_shown, email, token_ref (EXTERNAL), pool_sub and session_id (INTERNAL), ip, user_agent, typed_name, consent_text_version, auth_method (EMAIL_LINK | POOL_SESSION), occurred_at, prev_hash, row_hash]
    master_data: module-owned (Contract)
    reference_table: ct_signature_event
  - id: executed_copy
    meaning: the signed file
    natural_key: contract_id
    key_attributes: [object_key, sha256, provenance (GENERATED | UPLOADED), signed_version, signed_where, filed_by, filed_at]
    master_data: module-owned (Contract)
    reference_table: ct_executed_copy
  - id: approval
    meaning: a decision on one draft fingerprint
    natural_key: "contract_id|seq"
    key_attributes: [draft_fingerprint, deviations, approver_group, fallback_group, due_at, state, decided_by, decided_at, reason]
    master_data: module-owned (Contract)
    reference_table: ct_approval
  - id: delivery
    meaning: something to deliver to a signer (a link, a reminder, the executed copy), by provider or by hand
    natural_key: "contract_id|signer_no|kind|seq"
    key_attributes: [channel (provider | manual), capability_input, status, provider_ref, done_by, done_at]
    master_data: module-owned (Contract)
    reference_table: ct_delivery
  - id: key_date
    meaning: a dated obligation on a contract
    natural_key: "contract_id|kind|seq"
    key_attributes: [due_on, owner_group, reminder_cadence, done_at, done_by]
    master_data: module-owned (Contract)
    reference_table: ct_key_date
  - id: amendment
    meaning: the link from a contract to what changed it
    natural_key: "contract_id|seq"
    key_attributes: [kind (AMENDMENT | RENEWAL | TERMINATION | ADMINISTRATIVE), child_contract_id, note, recorded_by]
    master_data: module-owned (Contract)
    reference_table: ct_amendment
  - id: settings
    meaning: approval policies, reminder cadences, link lifetime, consent texts, retention rules, our signatories
    natural_key: "each by its own code"
    key_attributes: [policy rows, cadence rows, consent_text versions, retention rules]
    master_data: module-owned (configuration)
    reference_table: [ct_approval_policy, ct_reminder_cadence, ct_consent_text, ct_retention_rule, ct_settings]

master_data:
  owns: []
  references:
    - entity: business_partner
      owner: business-partner
      how: "ct_contract.bp_id = business_partner.bp_id (a person partner through bp:person:read)"
      if_absent: "a contract has no counterparty this module can trust; bp is required"
    - entity: person
      owner: hr
      how: "never joined: hr passes context_ref (person_id|n) and the values; contracts reads only the person's partner"
      if_absent: "employment contracts are drafted by hand with CALLER-sourced values"
  warning: >-
    Re-typed counterparty data drifts. Read it through business-partner at draft, refresh it once at
    send, and freeze it in the version. A second contacts list inside contracts is the bug.

jobs:
  - story: CT-30
    trigger: every minute or so (the same clock as hr)
    runs: "POST /run-due (run_due(now)): chasing, EXPIRED_UNSIGNED, key-date reminders"
    where_options: ["the ONE clock worker (the clock service client) that calls every brick's tick route listed in the composition, pinned warm (--reserved 1 --max-replicas 1)", external scheduler with a service client holding contract:run:tick]
    idempotent: "contract_id|signer_no|chase|<slot>; key-date reminders keyed contract_id|kind|seq|<days before>"
  - story: CT-31
    trigger: the last signature
    runs: render and file the executed copy
    where_options: [in the same request, retried by the tick if the render failed]
    idempotent: "one executed copy per contract (key contract_id)"
  - story: CT-32
    trigger: every event
    runs: chain the row
    where_options: [in the same request]
    idempotent: "seq is the next integer; a concurrent duplicate is rejected 23505 and retried after re-reading prev_hash"
  - story: CT-33
    trigger: on read
    runs: nothing stored
    where_options: [derived on read]
    idempotent: "nothing to repeat"
  - story: CT-34
    trigger: daily, first tick after 02:00
    runs: retention sweep
    where_options: [inside the tick (CT-30), external scheduler]
    idempotent: "a purge log keyed run_date|contract_id|category"
  - story: CT-35
    trigger: on draft and send
    runs: UK pack checks
    where_options: [in the same request]
    idempotent: "a check, no write beyond the key date (keyed contract_id|PRINCIPAL_STATEMENT_DUE|1)"

seams:
  - story: CT-50
    with: hr
    owner: contracts
    rule: "hr calls contracts.draft / send / status through its CapabilitySeam; contracts never reads hr tables"
  - story: CT-51
    with: business-partner
    owner: business-partner
    rule: "contracts reads partners through bp's routes (bp:person:read for persons); it never writes bp"
  - story: CT-52
    owner: "the mail.send provider in compose.yaml (none in the first demo)"
    rule: "POST /capabilities/mail.send; no provider = a manual delivery task with the exact input"
  - story: CT-53
    owner: "erp-platform (public-link door)"
    rule: "the door holds the hashed token and calls only open_for_signing / consent_and_sign / decline / request_changes for one signer"
  - story: CT-54
    with: crm
    owner: contracts
    rule: "the draft_from_quote adapter lives outside the brick and calls contracts.draft; the brick holds the source_ref only"

# ---------------------------------------------------------------------------------------------
# Surfaces
# ---------------------------------------------------------------------------------------------
surfaces:
  private:
    app: "the composition's staff app behind the pool"
    screens: [contracts, contract_editor, to_sign, clause_library, templates, contract_types, approvals, versions, certificate, signed_elsewhere, key_dates, settings]
    documents: "versions and executed copies are streamed by use cases after the sensitivity check; the app's service account is the only bucket principal; no object link is ever handed out"
  public:
    app: >-
      the platform's public-link door: a separate app with no pool (public_invoke true), its own narrow
      service account, no PDF engine (it serves the frozen bytes), calling contracts' public use cases through a
      seam scoped to one signer of one contract (an earlier client CRM's public-surface plan is the precedent)
    screens: [signing_page, signed_copy]
    stories: [CT-09, CT-10, CT-53]
    requirement_on_the_door: "several tokens per subject, one per person per grant (a SIGN token and a signed-copy READ token for the same signer) — platform work H2b"
    who_gets_a_link: "EXTERNAL signers only; our side signs inside the private app (CT-17)"
    tokens:
      - { purpose: sign, subject: "(contracts, contract_id, signer_no)", grants: [READ, SIGN], expires: "the link lifetime row", single_use: "SIGN once; READ until expiry" }
      - { purpose: signed_copy, subject: "(contracts, contract_id, signer_no)", grants: [READ], expires: "a settings row (days)", note: "the executed copy, so a signer can download it (IWG principle)" }
    rules: >-
      one token per signer, never per document; stored as a hash; revocable as a column with who and when;
      reissue revokes the previous one; seen (first_seen_at / last_seen_at / seen_count, and an OPENED event) kept
      apart from acted (used_at, and the SIGNED / DECLINED event); an audit row per use; rate-limited; the page shows the
      version, its sha256, the consent text and how long the signed copy is kept

screens:
  - { id: contracts,        name: Contracts,               stories: [], actors: [owner, contract_admin],
      note: "the list: state, counterparty, type, owner, next key date; employment bodies never shown without contract:employment:read" }
  - { id: contract_editor,  name: Contract editor,         stories: [CT-05, CT-06, CT-08, CT-11, CT-14, CT-16], actors: [owner, contract_admin],
      note: "from the earlier client CRM's contract page: header figures (state, signed, term), the sections as clause blocks (position pill, deviation level, overridden marker, 'add a clause'), the variables with their source and what is missing, the readiness panel whose items link to the fix, the signers panel (order up/down, side, title, seen vs acted, reissue, recall)" }
  - { id: clause_library,   name: Clause library,          stories: [CT-01], actors: [contract_admin],
      note: "from the earlier client CRM's clause library page: families with their Preferred / Alternate / Fallback clauses; a clause dialog with tabs Words · Usage (which contracts hold which version) · History (v3 current, v2 on 4 contracts, v1)" }
  - { id: templates,        name: Templates,               stories: [CT-03, CT-04], actors: [contract_admin] }
  - { id: contract_types,   name: Contract types,          stories: [CT-02], actors: [contract_admin] }
  - { id: to_sign,          name: To sign,                 stories: [CT-17], actors: [our_signatory],
      note: "our side's signatories: the contracts waiting on them, the frozen PDF with its fingerprint, consent and sign; private, signed in" }
  - { id: approvals,        name: Approvals,               stories: [CT-07], actors: [approver],
      note: "the queue; each item shows the deviations against the standard and the fingerprint it binds to" }
  - { id: versions,         name: Versions,                stories: [], actors: [owner, contract_admin],
      note: "from the earlier client CRM's versions page: a rail (number, state ISSUED / SUPERSEDED / SIGNED, when, who) and a pane with the PDF and its sha256 on show; a sent version is locked, only superseded" }
  - { id: certificate,      name: Certificate,             stories: [CT-13], actors: [owner, contract_admin],
      note: "signers, every event with time, IP and user agent, the version hash, the executed-file hash, and the chain check (valid, or broken at seq n)" }
  - { id: signed_elsewhere, name: Signed-elsewhere upload, stories: [CT-12], actors: [owner, contract_admin],
      note: "from the earlier client CRM's upload dialog and record-files page: file, filename, where it was signed; downloads are streamed through the app, never an object link" }
  - { id: key_dates,        name: Key dates,               stories: [CT-15], actors: [owner, contract_admin] }
  - { id: signing_page,     name: Signing page,            stories: [CT-09, CT-10], actors: [signer], surface: public,
      served_by: "the platform's public-link door app (no pool)",
      note: "PUBLIC, READ + SIGN: the frozen PDF, its fingerprint, the consent text, the typed name, sign / decline / ask for changes, and how long the copy is kept; no script needed to sign" }
  - { id: signed_copy,      name: Your signed copy,        stories: [], actors: [signer], surface: public,
      served_by: "the platform's public-link door app (no pool)",
      note: "PUBLIC, READ: the executed copy and its certificate, for the signer to download" }
  - { id: settings,         name: Settings,                stories: [], actors: [contract_admin],
      note: "every config_row below lives here" }

permissions:
  namespace: contract
  catalog:
    - contract:type:write
    - contract:clause:write
    - contract:template:write
    - contract:template:approve
    - contract:contract:read
    - contract:contract:read_all
    - contract:employment:read
    - contract:contract:write
    - contract:contract:send
    - contract:contract:recall
    - contract:contract:upload_executed
    - contract:contract:sign
    - contract:approval:decide
    - contract:evidence:read
    - contract:amendment:write
    - contract:settings:write
    - contract:capability:invoke
    - contract:run:tick
  row_visibility:
    rule: >-
      an owner sees the contracts they own; contract:contract:read_all sees every COMMERCIAL contract; a contract
      whose type is employment-sensitive (it holds pay) shows its body, variables, versions, executed copy and
      evidence only to contract:employment:read (hr_admin-grade, finance) — anyone else, including its owner without
      that permission and a manager, sees its existence, state and key dates and gets 404 on the rest; an approver
      sees the drafts in their queue; our signatory sees contracts they sign; a calling module sees state and key dates
      of what its source_refs caused, and only the ORIGINATOR receives the executed copy (contracts.status object key +
      sha256, contracts.fetch_executed); an external signer sees one version through the door
    enforced: "inside every use case from `caller` and the type's sensitivity; the same rule serves API, MCP and the provided capabilities"
    not_found_not_forbidden: true

config_rows:
  - { table: ct_type,             changes: "your contract types: who they may be with, whether they can be signed online, how long they are kept, who approves deviations" }
  - { table: ct_clause,           changes: "your clause families and their Preferred, Alternate and Fallback wording" }
  - { table: ct_template_version, changes: "your templates: which clauses in which order, and the fields each needs" }
  - { table: ct_variable_def,     changes: "each field: its type, where it comes from, whether it is required, its bounds (a day-rate band, a minimum notice)" }
  - { table: ct_approval_policy,  changes: "which deviation needs whose approval, the fallback approver and the timeout" }
  - { table: ct_reminder_cadence, changes: "how often unsigned signers are chased, and the key-date reminders (120/90/60/30 days)" }
  - { table: ct_consent_text,     changes: "the words a signer agrees to before signing electronically, versioned" }
  - { table: ct_retention_rule,   changes: "how long each type's contracts and evidence are kept, and how long dead drafts linger" }
  - { table: ct_settings,         changes: "link lifetime, signed-copy link lifetime, our default signatories per type, whether an author may approve their own template" }
in_code_not_rows:
  - "the lifecycle and who may move it; ACTIVE and EXPIRED derived"
  - "that the counterparty is always a bp_id with an allowed role"
  - "that a contract copies clause versions and freezes them; the library never alters a contract"
  - "that a version is frozen at send with its sha256 and every signature binds to it; a stale version is refused"
  - "that an approval binds to a fingerprint and an edit voids it; a fallback approver is mandatory"
  - "the hash chain and its verification; events are append-only"
  - "that SIGNED needs a complete set of events plus an executed copy, and an upload is SIGNED_ELSEWHERE"
  - "that DEED / esign_allowed=false types can never be sent for online signing"
  - "the claim: 'signed electronically', never advanced, qualified, legally binding or equivalent to a wet signature"
  - "that employment-sensitive bodies need contract:employment:read"
  - "that no provider for mail.send is a manual task with the exact input, and copying a link reissues it"
  - "the UK pack checks (ERA s.1 day-one particulars, PRINCIPAL_STATEMENT_DUE, MEDIUM_LARGE + OUTSIDE needs an SDS)"
  - "money as exact decimals; a money variable carries its currency"

tests:
  acceptance: "one per story id above, named for it (test_ct_09_stale_version_is_refused, …)"
  access:
    - "a caller without contract:employment:read gets 404 on the body, variables, versions, executed copy and evidence of an employment contract, including its owner and a manager, through API, MCP and contracts.status"
    - "an owner opening another owner's commercial contract without read_all gets 404"
    - "an owner cannot decide an approval on their own draft"
    - "a signer token reaches only its own signer's page; READ cannot SIGN; a signed-copy token cannot sign"
    - "the calling module sees only contracts its source_refs caused"
    - "a request with no gateway identity is rejected; a forged X-Dodil-User header is stripped"
  invariants:
    - "a retried contracts.draft with the same source_ref|type_code makes one contract; the same key with different inputs is 409"
    - "a sent version's sha256 never changes; a re-send is version N+1"
    - "the certificate and every page say 'signed electronically' and never 'advanced', 'qualified', 'legally binding' or 'wet signature'"
    - "the executed copy is a real file whose sha256 matches its bytes"
    - "every SIGNED contract has a SIGNED event per signer of its signed version; every SIGNED_ELSEWHERE one has an uploaded copy and no SIGNED event"
    - "IP and user agent are fields on every public event"
  live: "scripts/live_check.py: draft → send with no mail provider (manual link) → sign through the door → executed copy and certificate on a dev bucket"
  scenarios:
    - id: SC-CT-01
      name: a clause deviation needs approval
      stories: [CT-06, CT-07, CT-08]
      given: a draft with the Preferred liability clause and no deviations
      when: the owner switches it to the Fallback position and tries to send
      then: send is refused ("approval by legal needed, liability cap is a Fallback"); an approval request for the current fingerprint appears in legal's queue; after approval the send succeeds
    - id: SC-CT-02
      name: an edit after approval voids it
      stories: [CT-07]
      given: an approved draft with fingerprint F1
      when: the owner changes one variable (fingerprint F2) and tries to send
      then: the approval is void, the draft is back in IN_APPROVAL for F2, and send is refused until F2 is approved
    - id: SC-CT-03
      name: a variable outside its band
      stories: [CT-03, CT-07, CT-50]
      given: a freelance template whose day_rate band is 300–600 GBP, and hr sending 750
      when: contracts.draft runs
      then: the draft is created with deviations [day_rate above band] and approval_required true; hr's run waits on the status; without approval send is refused
    - id: SC-CT-04
      name: an approval nobody answers
      stories: [CT-07]
      given: an approval with a 2-working-day timeout and a fallback group
      when: the timeout passes undecided
      then: it is escalated to the fallback group; the original approver can no longer decide it; the escalation is recorded
    - id: SC-CT-05
      name: a signer looks at a stale version
      stories: [CT-09, CT-11]
      given: version 1 out for signature; the starter opens the link; the owner recalls and sends version 2
      when: the starter submits a signature from the page still showing version 1
      then: refused ("this contract was re-issued, open the new link"); no SIGNED event for version 1; the version-1 token is revoked; the version-2 link works
    - id: SC-CT-06
      name: signing out of order
      stories: [CT-09]
      given: signing order starter (1) then director (2)
      when: the director signs first
      then: refused, naming who it waits on; after the starter signs, the director's signature succeeds
    - id: SC-CT-07
      name: a link expired, revoked or reused
      stories: [CT-53, CT-30]
      given: three signing links
      when: one is used after its lifetime, one after a reissue, one after it already signed
      then: all three get "this link no longer works, ask <owner>"; the door logs each refused use; no event is written to the contract except OPENED attempts recorded by the door; a contract past its lifetime unsigned becomes EXPIRED_UNSIGNED
    - id: SC-CT-08
      name: signed elsewhere
      stories: [CT-12]
      given: a DEED type contract
      when: the owner tries to send it, then uploads the paper copy
      then: send is refused (deeds are not signed online here); the upload makes it SIGNED_ELSEWHERE with the file's sha256; its signers are SIGNED_ELSEWHERE, never SIGNED; key dates exist
    - id: SC-CT-09
      name: amendment and renewal
      stories: [CT-14]
      given: a signed freelance agreement ending 2027-03-31
      when: a RENEWAL to 2027-06-30 is drafted, approved where needed and signed
      then: the original is SUPERSEDED, the renewal is SIGNED, END and NOTICE_DEADLINE move to the renewal's dates, and the original's evidence is unchanged and still verifies
    - id: SC-CT-10
      name: notice deadlines
      stories: [CT-15, CT-30]
      given: a supplier contract ending 2026-12-31 with 90 days' notice and reminders at 120/90/60/30
      when: the clock passes 2026-06-04, 2026-07-04, 2026-08-03 and 2026-09-02
      then: NOTICE_DEADLINE is 2026-10-02; one reminder at each of the four dates to the owner group; a second tick on a reminder date sends nothing more
    - id: SC-CT-11
      name: the hash chain tampered with
      stories: [CT-13, CT-32]
      given: a signed contract with 7 events
      when: someone with bucket access edits the IP on event 4
      then: the certificate's chain check reports "broken at seq 4"; verify_evidence fails; producing an executed copy for a contract in that state is refused
    - id: SC-CT-12
      name: no mail provider
      stories: [CT-08, CT-52]
      given: a composition with no mail.send provider
      when: a contract is sent to two signers
      then: two manual delivery tasks with the exact recipient and message; "copy the signing link" reissues the token and shows the link once; marking it delivered writes DELIVERED (manual); wiring a provider later changes nothing in the template or type
    - id: SC-CT-13
      name: missing variables
      stories: [CT-05, CT-35, CT-50]
      given: an employment template needing the ERA s.1 day-one particulars, and hr supplying no holiday entitlement
      when: contracts.draft runs
      then: missing [holiday_entitlement] is returned and no partial contract exists; a second call with it supplied drafts the contract
    - id: SC-CT-14
      name: IR35 refusal
      stories: [CT-35]
      given: FREELANCE_SERVICES with engagement_via PSC, client_size_regime MEDIUM_LARGE, ir35_determination OUTSIDE, no sds_issued_on
      when: the owner sends it
      then: refused in words; with sds_issued_on and sds_reference set it sends; with client_size_regime SMALL it sends without them
    - id: SC-CT-15
      name: principal statement due on day one
      stories: [CT-35]
      given: an employment contract with start_date 2026-11-02 still OUT_FOR_SIGNATURE on 2026-11-02
      when: the tick runs that morning
      then: the PRINCIPAL_STATEMENT_DUE key date is overdue; contracts.status warns hr ("the written statement has not been given and is due today"), because issued (sent) counts, not only signed
    - id: SC-CT-16
      name: both sides must be named
      stories: [CT-08, CT-33]
      given: a draft with only the counterparty as signer
      when: the owner sends it
      then: refused ("nobody on our side is named, so this cannot be complete"); readiness lists both_sides unmet
    - id: SC-CT-17
      name: decline and ask for changes
      stories: [CT-10]
      given: two contracts out for signature
      when: one signer declines with a reason and another asks for changes
      then: one is DECLINED and one goes back to DRAFT for a new version; every other token on each is revoked; the owner is told
    - id: SC-CT-18
      name: counterparty frozen at send
      stories: [CT-51]
      given: a contract sent with the partner's legal name "Studio Nine Ltd"
      when: the partner is renamed in business-partner afterwards
      then: the sent version and the executed copy still say "Studio Nine Ltd"; a new draft would show the new name
    - id: SC-CT-19
      name: a counterparty of the wrong role
      stories: [CT-02, CT-50]
      given: an EMPLOYMENT type allowing only the employee role
      when: a draft is requested for a partner holding only the supplier role
      then: refused ("an employment contract needs a partner with the employee role")
    - id: SC-CT-20
      name: employment bodies are sensitive-grade
      stories: [CT-13]
      given: a signed employment contract
      when: a manager, an owner without contract:employment:read, and HR each open it through the API and MCP
      then: the first two see its state and key dates and get 404 on the body, versions, executed copy and certificate; HR sees everything
    - id: SC-CT-21
      name: retention and erasure
      stories: [CT-34]
      given: a cancelled draft 40 days old (window 30), an executed simple contract ended 6 years and 1 day ago, a deed ended 7 years ago, and an erasure request for a signer of a live contract
      when: the sweep runs and the erasure is applied
      then: the draft is purged; the simple contract and its evidence are purged; the deed is kept (12 years); the live contract's evidence is kept under the legal-claims exception and the reason is recorded
    - id: SC-CT-22
      name: double send and double sign
      stories: [CT-08, CT-09, CT-50]
      given: a ready draft
      when: send is clicked twice and a signer double-clicks sign
      then: one version, one set of tokens, one SIGNED event per signer
    - id: SC-CT-23
      name: the whole thing through the clock
      stories: [CT-05, CT-07, CT-08, CT-09, CT-31, CT-30]
      given: hr's onboarding run asks for a freelance agreement with a rate above band, no mail provider, demo clock in-process
      when: the clock moves through draft, approval, send, the starter's signature on day 1, a reminder to the director on day 4, the director's signature on day 5
      then: each state appears at its time; the director signs inside the app (INTERNAL, POOL_SESSION); the executed copy with its certificate is produced once on day 5; contracts.status reports it to hr; hr fetches it with contracts.fetch_executed and files it on the person
    - id: SC-CT-24
      name: only the originator gets the executed copy
      stories: [CT-50]
      given: an employment contract created by hr (source_ref hr:onboarding/…) and signed
      when: hr, then crm's adapter identity (another consumer holding contract:capability:invoke), call contracts.status and contracts.fetch_executed
      then: hr receives the object key, the sha256 and the bytes (whose sha256 matches); crm gets executed = null from status and 404 from fetch_executed; neither path needs contract:employment:read
    - id: SC-CT-25
      name: our side signs in the app, not by link
      stories: [CT-17, CT-08]
      given: a contract with an EXTERNAL signer (the starter) and an INTERNAL signer (a director)
      when: it is sent
      then: one public link is issued (the starter's); the director has none and signs from To sign as their pool identity; a SIGN call through the door naming the director's signer_no is refused; the certificate shows EMAIL_LINK for the starter and POOL_SESSION with the pool sub for the director

provides_capabilities:               # PROPOSED entries for erp-platform/capabilities.yaml (to be added there after the platform work)
  contracts.draft:
    summary: Draft a contract of a type for a counterparty from the approved template, filled from the values given.
    input: {type_code: str, counterparty_bp_id: int, context_ref: "str|null", source_ref: str, variables: "map of name → value", signers: "list of {side: US|THEM, full_name, email, title, authority_basis, signing_order}|null"}
    output: {contract_id: str, state: "DRAFT | IN_APPROVAL", missing: "list of variable names", deviations: "list of {what, level, needs: approver group}", approval_required: bool}
    destructive: false
    notes: >-
      idempotency_key MUST equal '<source_ref>|<type_code>' (contract_id is derived from it); the same key with
      different inputs is 409. Missing required variables return missing[] with status failed retryable=false and NO
      partial contract. The caller supplies values, never clause text. A counterparty without a role the type allows is 422.
  contracts.send:
    summary: Freeze the current draft as a new version and send it for signature.
    input: {contract_id: str}
    output: {contract_id: str, state: OUT_FOR_SIGNATURE, version: int, sha256: str, deliveries: "list of {signer_no, channel: provider|manual}"}
    destructive: false
    notes: >-
      Not undoable once delivered (recall revokes the links, it does not unsend mail). Refused (failed, retryable=false,
      message in words) when missing[] is non-empty, an approval is open, the type is a deed or esign_allowed=false, both
      sides are not named, or a UK pack check fails. dry_run returns the version that would be frozen and the deliveries.
  contracts.status:
    summary: Where a contract stands.
    input: {contract_id: str}
    output: {contract_id: str, state: str, version: "int|null", signers: "list of {signer_no, side, status, seen: bool}", executed: "{object_key, sha256, provenance}|null", key_dates: "list of {kind, due_on, done}", principal_statement_issued_at: "iso datetime|null", warnings: "list of str"}
    destructive: false
    notes: >-
      A read, like drive.transfer_status — no idempotency claim. `executed` is returned only to the ORIGINATOR (the
      caller whose contracts.draft created it, matched by the source_ref namespace); for anyone else it is null and
      bodies are never returned.
  contracts.fetch_executed:
    summary: Stream the executed copy of a contract the caller created.
    input: {contract_id: str}
    output: "the file's bytes (application/pdf) with its sha256 in a header; never an object URL"
    destructive: false
    notes: A read. Allowed ONLY to the originator (source_ref namespace = the caller's brick); any other caller gets 404. This is what lets hr file the signed contract on the person without a broad contract:employment:read.

localisation_packs:
  gb:
    variables:
      employment: "the ERA 1996 s.1 day-one particulars as REQUIRED variables: employer and employee names, job title or description, start date and continuous-employment date, pay and its interval, hours and days and how they vary, holiday entitlement, place of work, expected duration or fixed-term end, probation, benefits, obligatory training; sick pay, other paid leave and notice (these three may be in a separate accessible document, named by a variable)"
      freelance: "engagement_via (SOLE_TRADER | PSC | UMBRELLA | AGENCY), intermediary_name, intermediary_company_no, client_size_regime (SMALL | MEDIUM_LARGE | PUBLIC; source CALLER: hr supplies it in contracts.draft from its settings per legal entity, and contracts keeps NO copy), ir35_determination (INSIDE | OUTSIDE | NOT_CLIENT_DECISION), sds_issued_on, sds_reference, substitution_allowed, day_rate or fixed_fee; they choose alternatives (PSC warranty and indemnity, substitution)"
    key_dates: ["PRINCIPAL_STATEMENT_DUE = start_date", "SDS_REVIEW (when a determination is recorded)"]
    checks: ["send refused for MEDIUM_LARGE + OUTSIDE without sds_issued_on", "an IP assignment needs the assignor as a THEM signer (CDPA 1988 s.90(3))", "DEED types are refused for online signing"]
    retention: "term + 6 years (Limitation Act 1980 s.5); deeds 12 years (s.8)"

customer_varies:
  - question: Are you content for contracts to be signed electronically with a link and a typed name?
    changes: which types have esign_allowed, and whether a stronger method is needed (an extension point)
    standard: >-
      Electronic Communications Act 2000 s.7 (an electronic signature is admissible); UK eIDAS (retained
      Regulation (EU) No 910/2014) art. 25 (not denied legal effect solely because it is electronic) and art. 26
      (what an ADVANCED signature needs, which this is not); Law Commission, Electronic execution of documents
      (Law Com No 386, 2019). This module gives a simple electronic signature with its evidence and says only
      "signed electronically".
  - question: Do you sign anything as a deed, or that needs a witness?
    changes: DEED types (esign_allowed = false; signed elsewhere and uploaded)
    standard: "Law Com No 386: its view is that a witness to a deed must be physically present"
  - question: Do your freelancers assign their intellectual property to you?
    changes: the IP clause family and that the assignor must sign
    standard: "Copyright, Designs and Patents Act 1988 s.90(3): an assignment of copyright is not effective unless in writing signed by or on behalf of the assignor"
  - question: Must every employment contract carry the day-one written statement?
    changes: the employment type's required variables and the PRINCIPAL_STATEMENT_DUE key date
    standard: "Employment Rights Act 1996 s.1 (the principal statement on or before the first day; the wider statement within two months)"
  - question: For freelancers through their own companies, how big is your business?
    changes: "the IR35 send check; the size itself is hr's setting per legal entity, passed to contracts.draft as client_size_regime (contracts keeps no copy; non-hr contract types never use it)"
    standard: "Income Tax (Earnings and Pensions) Act 2003 Part 2 Chapter 10 (off-payroll working) and HMRC's size tests (ESM10006); the thresholds rose from 6 April 2025, so check the current figures"
  - question: How long do you keep contracts after they end?
    changes: ct_retention_rule
    standard: "Limitation Act 1980 s.5 (six years for a simple contract) and s.8 (twelve for a deed); UK GDPR art. 5(1)(e) for the personal data in the evidence"
    typical:
      most_businesses: the term plus six years for signed contracts, a month or two for dead drafts
  - question: Who signs for you, and who approves anything off the standard?
    changes: our default signatories per type, approval policies and the fallback approver
    typical:
      small_agency: a director signs; operations approves a rate outside the band, a director anything legal
  - question: How often should we chase an unsigned signer, and how long is a link good for?
    changes: ct_reminder_cadence and the link lifetime
    typical:
      common: a reminder every two or three working days, a link valid for 14 to 30 days
  - question: Do you sign the other side's paper sometimes?
    changes: whether SIGNED_ELSEWHERE uploads are routine for some types

extension_points:
  - { name: signature_provider, what: "an interface for stronger methods: an email one-time code first, then advanced or qualified signatures through a trust-service provider for high-risk types" }
  - { name: propose_changes_inline, what: "a counterparty proposing edits to the text, not only a note (IWG principle)" }
  - { name: obligations, what: "obligations beyond dates (deliverables, payments) tracked against the contract" }
  - { name: trusted_timestamps, what: "RFC 3161 timestamps on version and executed hashes" }
  - { name: multi_language, what: "a template version per language" }

reference:
  # Public repos, anonymous clone, one release tag each.
  repos:
    - name: brick-contracts
      role: brick
      https: https://git.dodil.io/dodil/brick-contracts.git
      ssh: git@git.dodil.io:dodil/brick-contracts.git
      tag: v0.1.0
      read_first: [README.md, AGENTS.md]
    - name: brick-business-partner
      role: brick
      https: https://git.dodil.io/dodil/brick-business-partner.git
      ssh: git@git.dodil.io:dodil/brick-business-partner.git
      tag: v0.3.0
      read_first: [README.md, AGENTS.md]
    - name: brick-hr
      role: brick
      https: https://git.dodil.io/dodil/brick-hr.git
      ssh: git@git.dodil.io:dodil/brick-hr.git
      tag: v0.4.1
      read_first: [README.md, AGENTS.md]
    - name: erp-platform
      role: platform
      https: https://git.dodil.io/dodil/erp-platform.git
      ssh: git@git.dodil.io:dodil/erp-platform.git
      tag: v0.5.3
      read_first: [README.md, AGENTS.md]
  packages: []
  contracts: []
  build_guide: null
  known_gaps:
    - "not deployed: it has not run behind the gateway for a real person, so the self-check through ignite invoke and a sign-in per role are still to do"
    - "no screen (H5): API and MCP only; the signing pages are served by the composition's public app"
    - "a simple electronic signature only; advanced or qualified signatures are the signature_provider extension point, not built"
    - "not built: inline change proposals, obligations beyond dates, RFC 3161 timestamps, multi-language templates"
    - "executed copies are bytes in ct_object_chunk in the bucket until the platform has an object-store helper"
    - "the platform's seam does not say which brick is calling, so the originator is matched by ct_consumer rows (sub, role or email)"
    - "use cases receive no client IP, user agent or session id: our side's events carry the pool sub but no IP, agent or session"
    - "a contracts.draft with missing values answers 422 (claim released), not a stored failure, because a stored failure would make the corrected call a 409"
    - "hr does not poll a contract that needed approval (tests/test_tier1.py xfail)"