# GL suite — the skill-of-skills composition manifest.
# It COMPOSES the six base gl/* skills into ONE DataK3 bucket: shared master data
# (accounts/journals/journal_lines/ledger), 14 disjoint tables, and a SINGLE deferred account
# graph (gl_accounts). It is not a fork and it does not re-spec any workflow — the mechanics live
# in the six base contracts; this manifest is the ordered install DAG + the master-data wiring +
# the one-graph rule + THE functional-currency trial-balance rule that resolves the multi-currency seam.
# The human walkthrough is posts/enterprise_software/gl/gl-suite.md (published id gl-suite).
# Lives at skill/enterprise_software/gl/suite.skill.yaml; served over MCP at dodil://skills/suites/gl;
# executed by `dodil skill scaffold suites/gl`. check:skills binds it to its post.

id: suites/gl
version: 1.0.0
summary: >-
  Scaffold a complete, customizable General Ledger on ONE DataK3 bucket — master data
  (accounts / journals / journal_lines / ledger + account_edges) plus six workflows (journal-entry,
  period-close, subledger-reconciliation, multi-currency, financial-reporting) and one account
  hierarchy graph, assembled once. 14 disjoint tables, shared masters joined across every skill,
  no ETL and no second copy. Money is DECIMAL(18,2), FX rates DECIMAL(18,6); every posting is
  idempotent (INSERT … ON CONFLICT (<pk>) DO UPDATE). THE canonical suite trial balance is in the
  FUNCTIONAL currency — foreign-currency ledger balances are converted at the period-end rate — so a
  multi-currency book nets to exactly 0.00 once revaluation has run.
module: gl
category: enterprise-software
pillars: [sql, graph]              # GL is SQL + the account-hierarchy graph; no vector, no Models
overlays: [finserv, manufacturing] # cross-industry base; overlays add regulated-close / cost-accounting diffs

# Binding to the narrative (check:skills verifies these resolve + agree)
post: posts/enterprise_software/gl/gl-suite.md
route: /library/gl-suite

# It composes the six base skills (no overlay by default). An overlay is an additive diff on top.
composes:
  skills: [gl/core, gl/journal-entry, gl/period-close, gl/subledger-reconciliation,
           gl/multi-currency, gl/financial-reporting]
  overlays_apply_on_top: [gl/overlays/finserv, gl/overlays/manufacturing]

# BUILDING A SUBSET (not all six). You do NOT have to install the whole suite to get one workflow.
subsets:
  rule: >-
    To build a SUBSET, install gl/core FIRST — it owns the four masters
    (accounts / journals / journal_lines / ledger) + account_edges + the demo COA + a balanced
    opening-balance journal that seeds the ledger — then install only the workflow skills you want.
    Each workflow skill's `consumes.master` lists its master dependencies (journal-entry, period-close
    and multi-currency post into journals/journal_lines and re-materialize the ledger; financial-reporting
    reads ledger + accounts + the graph; subledger-reconciliation reads ledger + accounts); gl/core
    provides them all.
  standalone_without_core: >-
    Each workflow skill ALSO ships a `when: standalone` stub_masters step that creates just the masters
    IT reads (same column defs core owns, money DECIMAL(18,2), every non-key column nullable:true) — so a
    single workflow runs on an empty bucket with no gl/core. But once you want TWO workflows sharing the
    same masters, install gl/core once instead of letting each stub its own (avoids divergent seed data
    and a double-typed ledger).
  graph: >-
    Skip the graph entirely (no CREATE GRAPH) unless you install gl/financial-reporting — the single
    deferred CREATE GRAPH gl_accounts belongs at the tail (financial-reporting reads it for the
    account-type rollup). gl/core inserts nodes (accounts) + edges (account_edges) but DEFERS the create.
    Every other workflow (journal-entry / period-close / subledger-reconciliation / multi-currency) is
    pure SQL and needs no graph.

# One question set for the whole suite. Only the shared/composition-answered ones collapse here.
params:
  bucket:              { type: string, default: gl,      prompt: "Bucket name? (ONE bucket for all six skills)" }
  functional_currency: { type: string, default: USD,     prompt: "Functional (reporting) currency the ledger is kept in?" }
  coa_preset:          { type: enum,   options: [demo, empty], default: demo }   # shared demo COA + opening balances, drives every skill's Test
  period:              { type: string, default: "2026-09", prompt: "Accounting period to run the close + reports for (YYYY-MM)?" }
  overlay:             { type: enum,   options: [none, finserv, manufacturing], default: none }

# ORDERED install DAG — masters first, the graph created once at the tail by its only reader.
order:
  - gl/core                     # 1. owns accounts/journals/journal_lines/ledger + account_edges; seeds COA + opening balances; DEFERS CREATE GRAPH
  - gl/journal-entry            # 2. the correctness showcase — balance-gated, idempotent ON CONFLICT posting into core's journals/journal_lines; re-materializes the ledger
  - gl/period-close             # 3. trial-balance snapshot (nets 0.00) + period lock (closed period rejects operational journals) + close-to-retained-earnings
  - gl/subledger-reconciliation # 4. AP/AR subledger tied to its GL control account (variance 0.00) over core.ledger + core.accounts
  - gl/multi-currency           # 5. fx_rates + fx_reval; revalues open foreign-currency balances INTO the functional currency — run BEFORE the close so the functional trial balance ties
  - gl/financial-reporting      # 6. LAST — runs the SINGLE CREATE GRAPH gl_accounts, then P&L + balance sheet rolled up by account type over it

# Ordering rule (honest). Unlike ITSM (where the graph owner runs at position 2), GL's graph reader is
# financial-reporting, and reporting must run AFTER multi-currency has revalued — otherwise the balance
# sheet foots at stale FX. So the single CREATE GRAPH gl_accounts is deferred to the TAIL (reporting),
# the last skill, mirroring CRM's account-360 deferral. gl/core loads nodes+edges; nobody creates the
# graph until reporting. If a future skill adds account edges (e.g. a consolidation-mapping skill), the
# CREATE GRAPH moves after it — the snapshot rule is invariant, not the ordinal position.

# Shared master data — owner skill -> the sibling skills that JOIN it (no copy; one row, many readers).
# Derived from each base contract's provides.owns_master -> siblings' consumes.master.
master_wiring:
  accounts:       { owner: gl/core, consumers: [gl/journal-entry, gl/period-close, gl/subledger-reconciliation, gl/multi-currency, gl/financial-reporting] }
  journals:       { owner: gl/core, consumers: [gl/journal-entry, gl/period-close, gl/multi-currency] }
  journal_lines:  { owner: gl/core, consumers: [gl/journal-entry, gl/period-close, gl/multi-currency, gl/financial-reporting] }
  ledger:         { owner: gl/core, consumers: [gl/journal-entry, gl/period-close, gl/subledger-reconciliation, gl/multi-currency, gl/financial-reporting] }
  account_edges:  { owner: gl/core, consumers: [gl/financial-reporting] }   # edges, not a master, but core-owned; the graph's only contributor

# THE single CREATE GRAPH — run exactly once, at the tail, after the only edge contributor (core).
graph_assembly:
  graph: gl_accounts
  rule: >-
    In suite mode gl/core inserts nodes (accounts) + edges (account_edges) but runs NO CREATE GRAPH.
    The snapshot rule (edges added after CREATE are invisible) means the suite defers the single
    CREATE GRAPH gl_accounts NODES (accounts KEY account_id) EDGES (account_edges SRC src DST dst) to
    gl/financial-reporting, its only reader, at the tail. The account tree points child -> parent, so a
    rollup account's descendant posting leaves are the INCOMING direction:
    graph_khop('gl_accounts', 4000, 5, 'in') returns Total Revenue's leaves. LIVE-PROVEN 2026-09-03:
    one CREATE GRAPH; graph_khop('gl_accounts', 1000, 5, 'in') JOIN ledger = the three asset leaves
    (Cash 560000.00, Accounts Receivable 109500.00, Fixed Assets 300000.00 = assets 969500.00), and the
    same rollup over root 4000 folds in 4900 Unrealized FX Gain/Loss (the account multi-currency added).
  contributors:
    - { skill: gl/core, edges: [child_of] }
    - { skill: gl/financial-reporting, edges: [], runs_create_graph: true }
  traversal_note: >-
    graph_khop() resolves ONLY as a top-level SELECT … FROM graph_khop(…) with an integer-LITERAL start
    node (the graph plane intercepts that shape); it CANNOT sit inside INSERT … SELECT or a UNION branch.
    Pattern: run the graph rollup as a verifying SELECT, then materialize the report with a type-grouped
    INSERT … VALUES / SELECT … ON CONFLICT. Bolt agrees: MATCH (root)<-[:account_edges*1..5]-(child)
    WHERE id(root)=4000 RETURN child returns 4100 / 4200 / 4900.

# THE multi-currency trial-balance rule — the seam this suite resolves, stated so builders can't drift.
currency:
  functional_currency: USD           # inherited from gl/core; every downstream skill uses it
  trial_balance: >-
    THE canonical suite trial balance is in the FUNCTIONAL currency. gl/multi-currency carries an open
    foreign-currency account at its TRANSACTION-currency principal in the single-balance ledger (e.g.
    Accounts Receivable = 100000.00 EUR), so a NAIVE SUM(ledger.balance) mixes currencies and is NOT a
    trial balance (LIVE: it read -8000.00, not 0.00). The functional trial balance converts each account's
    ledger balance to the functional currency at the CURRENT period-end fx_rate — functional-currency
    accounts convert 1:1 — via
    SUM(ledger.balance * COALESCE(fx_rate_to_functional(ledger.currency, period_end), 1.0)).
  seam_resolution: >-
    The accounting story, PROVEN LIVE 2026-09-03 in one bucket, in order:
    (a) a EUR sale posts through the balance gate in functional terms (DR AR 108000.00 / CR Revenue
        108000.00 @ 1.08); the ledger materializes AR at its EUR principal 100000.00, currency EUR.
    (b) the functional trial balance at the period-end rate (EUR->USD 1.095000) reads +1500.00 — off by
        EXACTLY the unposted FX movement (100000.00 * (1.095000 - 1.080000) = 1500.00), while the naive
        SUM reads a meaningless -8000.00.
    (c) gl/multi-currency revaluation posts the balanced unrealized FX journal (DR AR 1500.00 / CR
        Unrealized FX Gain/Loss 1500.00; fx_reval gain_loss 1500.00, orig 108000.00 -> revalued 109500.00)
        and re-strikes AR INTO the functional currency in the ledger (109500.00 USD).
    (d) the functional trial balance now nets to 0.00 — AND the naive SUM(ledger.balance) also reads 0.00,
        because every balance is now functional. Revaluation is precisely what makes a multi-currency book
        tie; run it BEFORE period-close so the trial-balance snapshot and the balance sheet foot to 0.00.

# The ordered scaffold plan an agent executes (tools resolve against dodil://commands via check:skills).
steps:
  - id: bucket
    title: One bucket for the whole GL
    when: always
    tools: [data_bucket_create]
    detail: Create the single DataK3 bucket {{bucket}}. All six skills land here — one data plane, cross-skill JOINs are free.
  - id: core
    title: Install gl/core (the system of record)
    when: always
    tools: [data_table_create, data_table_upsert, data_pg]
    detail: >-
      5 master tables (accounts / journals / journal_lines / ledger + account_edges), money DECIMAL(18,2),
      composite PK (journal_id, line_no) on journal_lines. Seed the demo COA (5 roots + leaves; the AR leaf
      1200 is EUR-denominated, plus a 4900 Unrealized FX Gain/Loss account) + the balanced opening-balance
      journal + the materialized ledger. Populate account_edges but DEFER the single CREATE GRAPH.
  - id: journal-entry
    title: Install gl/journal-entry (balance-gated idempotent posting)
    when: always
    tools: [data_table_create, data_pg, data_sql]
    detail: >-
      Adds journal_audit. Every journal posts through the balance gate (SUM(debit)=SUM(credit) BEFORE
      posted) idempotently (INSERT … ON CONFLICT (journal_id,line_no) DO UPDATE) into core's journals /
      journal_lines, then re-materializes the ledger ON CONFLICT (account_id). A EUR sale posts here.
  - id: period-close
    title: Install gl/period-close (trial-balance snapshot + period lock)
    when: always
    tools: [data_table_create, data_pg, data_sql]
    detail: >-
      Adds periods + trial_balance. Snapshot the per-account trial balance for {{period}} (grand total nets
      to 0.00), then lock the period — a guarded posting path rejects an operational journal (source <>
      'close') dated in a closed period. Run AFTER multi-currency so the snapshot is at current FX.
  - id: subledger-reconciliation
    title: Install gl/subledger-reconciliation (control-account tie-out)
    when: always
    tools: [data_table_create, data_pg, data_sql]
    detail: >-
      Adds subledger_items + reconciliation. Ties an AP/AR subledger to its GL control account over
      core.ledger + core.accounts: gl_balance (normalized to the account's natural side) = Σ open
      subledger items, variance 0.00. Pure SQL cross-table JOIN, idempotent ON CONFLICT (recon_id).
  - id: multi-currency
    title: Install gl/multi-currency (FX revaluation — resolves the trial-balance seam)
    when: always
    tools: [data_table_create, data_pg, data_table_upsert]
    detail: >-
      Adds fx_rates (DECIMAL(18,6)) + fx_reval. Revalue each open foreign-currency ledger balance INTO
      {{functional_currency}} at the period-end rate, post the balanced unrealized FX gain/loss journal
      (DR/CR the account vs Unrealized FX Gain/Loss), and re-strike the account into functional currency.
      This is what makes the functional trial balance net to 0.00 — run it BEFORE the close.
  - id: financial-reporting
    title: Install gl/financial-reporting + run the SINGLE graph assembly (LAST)
    when: always
    tools: [data_table_create, data_pg, data_bolt]
    detail: >-
      Adds income_statement + balance_sheet. Run the ONE CREATE GRAPH gl_accounts over core's fully-loaded
      account_edges, then roll up by account TYPE via graph_khop('gl_accounts', <root>, N, 'in'): P&L
      (revenue - expense = net income) and balance sheet (assets = liabilities + equity + current net
      income). The sheet TIES because the functional trial balance is 0.00.
  - id: overlay
    title: Apply an industry overlay (optional)
    when: { overlay: [finserv, manufacturing] }
    tools: [data_pg, data_table_create, data_table_upsert]
    detail: >-
      Apply gl/overlays/{{overlay}} — an ADDITIVE diff on top of the 14 tables (ALTER-add columns via the
      pg wire + maybe one extra table + gate tweaks). finserv adds a segregation-of-duties close sign-off
      (preparer <> approver) + a close checklist; manufacturing adds inventory/WIP/COGS variance journals +
      cost centers. Never rewrites a base skill.

# The union Q&A (the composition removes redundant asks).
customize:
  - param: bucket
    ask: "One bucket name for the whole GL?"
    effects:
      any: "all six skills scaffold into this single bucket — one data plane, cross-skill JOINs are free."
  - param: functional_currency
    ask: "What functional (reporting) currency is the ledger kept in?"
    effects:
      USD: "set ONCE here and every skill inherits it — the currency accounts/ledger rows carry, that period-close snapshots, that multi-currency revalues foreign balances INTO, and that the statements present. THE canonical trial balance is in it."
  - param: coa_preset
    ask: "Load the shared demo chart of accounts + opening balances, or ship empty schemas?"
    effects:
      demo:  "one demo COA + balanced opening journal drives EVERY skill's Test — asked once, not six times. Includes the EUR receivable + the FX gain/loss account for the multi-currency seam."
      empty: "empty schemas across all 14 tables; each skill's Test switches to structural assertions."
  - param: period
    ask: "Which accounting period do you run the close + reports for (YYYY-MM)?"
    effects:
      default: "shared by period-close (trial-balance snapshot + lock), multi-currency (the reval period), and financial-reporting (the P&L + balance-sheet period) — asked once. Default 2026-09."
  - param: overlay
    ask: "Which industry? (drives the overlay diffs at once — finserv segregation-of-duties / manufacturing cost accounting)"
    effects:
      none:          "the cross-industry base suite."
      finserv:       "regulated close — preparer <> approver HARD GATE on every posting + a close checklist sign-off + an audit trail."
      manufacturing: "cost accounting — inventory / WIP / COGS accounts + standard-cost variance journals + plant cost centers."

# Composition-answered asks REMOVED (documented so the union is auditable):
#  - journal-entry / period-close / multi-currency consume accounts/journals/journal_lines/ledger FROM
#    gl/core — no "seed a COA?" or "stub the masters?" re-ask; core owns them (§ subsets).
#  - financial-reporting's account graph comes FROM the single deferred CREATE GRAPH — no "build a
#    hierarchy?" re-ask; core loads the edges, reporting runs the one create.
#  - functional_currency + period are asked ONCE and inherited by every skill (no per-skill re-ask).

# What proves the ASSEMBLY (the suite's Test is about composition + the FX-seam resolution, not
# re-testing each skill). LIVE-VALIDATED 2026-09-03 on ONE throwaway bucket (org IHDIASH), torn down.
tests:
  - "data table list -> 14 tables coexist in ONE bucket, NO name collision (5 core + 1 journal-entry + 2 period-close + 2 subledger-reconciliation + 2 multi-currency + 2 financial-reporting)"
  - "shared masters: 18 accounts + 13 account_edges (gl/core) joined by every workflow; the ledger (11 rows) is written by journal-entry/multi-currency and read by period-close/subledger-reconciliation/financial-reporting — one copy, many readers"
  - "single graph: ONE CREATE GRAPH gl_accounts assembled by financial-reporting at the tail; graph_khop('gl_accounts', 1000, 5, 'in') JOIN ledger -> Cash 560000.00 / Accounts Receivable 109500.00 / Fixed Assets 300000.00 (assets 969500.00); Bolt MATCH (root)<-[:account_edges*1..5]-(child) WHERE id(root)=4000 -> 4100/4200/4900"
  - "FX SEAM (a): a EUR sale posts (DR AR 108000.00 / CR Product Revenue 108000.00 @ 1.08); the ledger carries AR at its EUR principal 100000.00, currency EUR"
  - "FX SEAM (b): naive SUM(ledger.balance) = -8000.00 (mixes currencies, NOT a trial balance); the FUNCTIONAL trial balance at the period-end rate (EUR->USD 1.095000) = +1500.00 — off by EXACTLY the unposted FX movement 100000.00 * (1.095000 - 1.080000)"
  - "FX SEAM (c): gl/multi-currency revaluation posts the balanced FX journal (DR AR 1500.00 / CR Unrealized FX Gain/Loss 1500.00); fx_reval orig 108000.00 -> revalued 109500.00, gain_loss 1500.00 at rate 1.095000; AR re-struck to 109500.00 USD in the ledger"
  - "FX SEAM (d): the FUNCTIONAL trial balance now = 0.00, AND naive SUM(ledger.balance) = 0.00 — revaluation is what makes the multi-currency book tie"
  - "cross-skill: period-close trial_balance snapshot (11 rows) grand total SUM(balance) = 0.00 for 2026-09; period lock rejects operational journal 100 into CLOSED 2026-09 (absent) and accepts journal 101 into OPEN 2026-10 (present)"
  - "cross-skill: financial-reporting P&L revenue 479500.00 - expense 410000.00 = net income 69500.00 (incl. the 1500.00 FX gain); balance sheet assets 969500.00 = liabilities 40000.00 + equity 860000.00 + net income 69500.00 (TIES to 0.00)"
  - "cross-skill: subledger-reconciliation AP control (2100) gl_balance 40000.00 = Σ open subledger_items 40000.00 (Acme 15000 + Globex 12500 + Initech 12500; cleared Umbrella 5000 excluded), variance 0.00, reconciled true"

tested_branches:
  - { coa_preset: demo, functional_currency: USD, period: "2026-09", overlay: none, tested_at: 2026-09-03 }   # default suite — full single-bucket assembly + the FX-seam narrative, live-validated

# Ship-it contract.
# NOTE (2026-09-03): the suite is a COMPOSITION — the six base gl/* skills scaffolded into ONE bucket, the
# single deferred account graph, and THE functional-currency trial-balance rule that resolves the
# multi-currency seam. The data-plane composition (14 tables coexist, shared masters JOIN, one graph, the
# pre-reval-off-by-1500 / post-reval-0.00 narrative) is VALIDATED LIVE END-TO-END (org IHDIASH, throwaway
# bucket, torn down). GL is pure SQL + graph — NO Models anywhere. Each base skill deploys its own IMAGE-mode
# Ignite engine (gl-posting-engine / gl-period-close-engine / gl-reval-engine) in its own tutorial; the
# suite's deploy is SHOWN-AS-CODE here (the dedicated live-deploy stress test — the gl-posting-engine as the
# idempotency-under-load proof — is a later step). A pure-SQL GL engine's service account needs ONLY
# k3.editor + ignite.app-developer (NO ignite.model-user).
deploy:
  app: none            # the suite is a composition of data ops; each engine deploys in its own base tutorial
  mode: none
  status: shown-as-code
  data_plane: "pg-wire pg.uk-lon-1.dodil.io:5432 (db = bucket, user = token, password = login/SA token) — SQL + graph_khop() rollup + Bolt over one connection; idempotent write = INSERT … ON CONFLICT (<pk>) DO UPDATE (a bare re-INSERT of a committed PK raises duplicate-key 23505); no k3.dodil.io HTTP API"
  service_account:
    roles: [k3.editor, ignite.app-developer]   # pure SQL + graph — NO ignite.model-user
    note: >-
      DODIL_SERVICE_ACCOUNT_ID = the cli-… serviceAccountId printed by `auth service-account create`,
      NOT the internal uuid (the uuid fails client_credentials with invalid_client).
  models: []           # GL is pure SQL + graph — no Models anywhere in the suite
