From 09ef7fa967e3a9b682042fd43384476646a28d42 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 15 Aug 2026 19:01:45 +0200 Subject: [PATCH] feat: bound AI-plan work to resource-control and stop zeroing missing tokens Record the FIN-WP-0007 split after checking resource-control: they keep intelligence resource identity and provision-level class I metering; session tokens stay with State Hub; work-effectiveness is a reporting join. Missing token counts are now unknown, not zero. --- INTENT.md | 8 + README.md | 2 +- SCOPE.md | 22 ++- docs/ai-plan-token-effectiveness.md | 142 ++++++++++++++++++ src/fin_hub/ingest/anthropic.py | 26 +++- src/fin_hub/models/budget.py | 4 +- tests/test_ingest.py | 39 ++++- tests/test_ledger.py | 14 ++ tests/test_models.py | 8 + ...FIN-WP-0007-ai-plan-token-effectiveness.md | 99 ++++++++---- 10 files changed, 326 insertions(+), 38 deletions(-) create mode 100644 docs/ai-plan-token-effectiveness.md diff --git a/INTENT.md b/INTENT.md index 33516dd..09210a0 100644 --- a/INTENT.md +++ b/INTENT.md @@ -11,6 +11,14 @@ forecasts, commitment candidates, and optimization scenarios from forecast or allocation never becomes booked spend merely because fin-hub stores or reports it. +Intelligence follows the same split. Resource-control owns portfolio +identity and provision-level class `I` metering when Railiance procures a +model API, reserved weights, or inference host. State Hub owns agent-session +token evidence. Fin-hub books the plan invoice, records entitlement, joins +those inputs, and reports effectiveness. It does not invent a `resource_id`, +parse sessions, or emit technical usage or allocation as if it were +resource-control. + Fin-hub does not generate legal invoices, perform bookkeeping, execute or track payments, approve contractual commitments, own client/resource identity, or provision infrastructure. It records, reconciles, reports, projects, and diff --git a/README.md b/README.md index 1ec94fa..b38f2fe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Fin Hub Resource viability hub for the FOS federation — budgets, commitments, burn rate, -runway projection, and token spend tracking. +runway projection, and booked token / AI-plan spend. Fin-hub extends `hub-core` with financial models and read surfaces. Generic hub primitives (domains, repos, messages, progress events) come from hub-core; diff --git a/SCOPE.md b/SCOPE.md index 679c8e5..21997a9 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -2,11 +2,18 @@ ## In Scope -- Budget, commitment, burn rate, runway projection, and token spend models -- Manual CSV import for cloud and API costs (v0.1) +- Budget, commitment, burn rate, runway projection, and booked token / + AI-plan spend +- Manual CSV import for cloud, API, and subscription costs (v0.1) - Runway calculator with alert thresholds - Client-level cost attribution, engagement revenue and margin reporting, and billing-basis exports for external bookkeeping/invoicing systems +- AI-plan entitlements, joins to State Hub session-token evidence, reporting + allocation of booked plan cost onto work, and effectiveness overlays + (work per token and work per euro) that never become booked spend +- Consumption of resource-control planning evidence (forecasts, technical + usage, allocations, optimization, settlement statements) without taking + authority for those records - FOS §9 cross-hub signals: fin→dev (budget pressure), fin→ops (cost attribution), fin→canon (viability alerts) - hub-core generic primitives (domains, repos, messages, progress) @@ -15,4 +22,13 @@ - Legal invoice generation or numbering, bookkeeping, payment execution or tracking, and banking integration - Tax filing or legal entity management -- Authority for client identity or infrastructure resource identity +- Authority for client identity or infrastructure / intelligence resource + identity (`resource:…` remains resource-control) +- Provision-level class `I` token metering against a portfolio resource +- Technical usage, utilization, and allocation-driver evidence for + inventoried resources (resource-control, sourced from platform telemetry) +- Parsing coding-agent session files or storing transcripts (State Hub / + llm-connect) +- Inventing a `resource_id` for an AI plan that resource-control has not + published +- A canonical token↔hour or token↔euro exchange rate diff --git a/docs/ai-plan-token-effectiveness.md b/docs/ai-plan-token-effectiveness.md new file mode 100644 index 0000000..f6d19d6 --- /dev/null +++ b/docs/ai-plan-token-effectiveness.md @@ -0,0 +1,142 @@ +# AI plan cost, token estimates, and effectiveness + +Status: fin-hub design note for `FIN-WP-0007-T00` +Checked against: resource-control `SCOPE.md`, `INTENT.md`, +`docs/TermsOfResourceProcurement.md` §4.1 and §6, +`docs/fin-hub-resource-control-contract-v0.1.md`, +`docs/evidence-basis.md`, +`data/capability/platform-audit-storage.json` (2026-08-15), +and the ratified fin-hub contract +`docs/fin-resource-authority-contract-v0.1.md`. + +This note records the authority split. It does not amend contract v0.1. +A proposed addendum is at the end for resource-control to accept or reject. + +## Why a second check was required + +`FIN-WP-0007` first treated resource-control as the wrong producer for +any AI-token number. That is too coarse. + +Resource-control already names intelligence as a Railiance-procured +resource class — “commercial model APIs, reserved weights, inference +hosts” — and records class `I` consumption on a provision as *their* +gap (`owner: resource-control`), with unknown never stored as zero. +They do not yet inventory any AI plan, and they do not parse coding +sessions. Those two facts have to stay separate. + +## Four quantities, four writers + +| Quantity | Native unit | Authoritative writer | Fin-hub may | +| --- | --- | --- | --- | +| Booked plan / API invoice | currency | fin-hub | Book once as `booked_cost` | +| Recurring plan as commitment | currency / period | fin-hub | Record; not usage | +| Plan entitlement (included tokens or plan label) | token or declared capacity | fin-hub | Commercial fact, same discipline as engagement prices | +| Intelligence *resource* identity | `resource:…` | resource-control | Join when present; never invent | +| Provision-level class `I` usage | token | resource-control, when they meter a named provision | Consume via `FIN-WP-0004` `usage_observation` | +| Session token evidence | token | State Hub (`STATE-WP-0045`) | Join as session-token evidence | +| Technical allocation of a portfolio resource | share of a referenced fact | resource-control | Consume `AllocationEvidence`; do not emit it | +| Work done | task / outcome | State Hub / domain repos | Join key, not a cost driver | +| Reporting allocation of a plan invoice across work | share of a referenced fact | fin-hub | Local reporting derivation (`FIN-WP-0002` family) | +| Implied €/token and work-per-token / work-per-euro | derived | fin-hub | Overlay only; inherit the weakest input basis | + +Session telemetry is not platform telemetry. Contract v0.1 says +technical usage is sourced from platform telemetry and written by +resource-control. Agent-session counts from State Hub therefore must +not be stored as resource-control `usage_observation`. + +Reporting allocation of an invoice across workplans is not technical +allocation of a portfolio resource. Contract v0.1 keeps allocation +method and driver with resource-control. Fin-hub already has a +reporting-only consumer for that evidence (`FIN-WP-0002-T03`). Work +effectiveness uses a *local* reporting allocation and must not be +posted as `AllocationEvidence`. + +## When a `resource_id` exists + +If resource-control later inventories a Railiance-procured +intelligence resource (API account, reserved weights, inference +host, or license): + +- fin-hub books the invoice and may attach the resource-control + `resource_id` they published; +- resource-control owns usage and technical allocation on that + resource, including class `I` tokens against the provision; +- fin-hub still owns entitlement, burn, runway, and the + work-effectiveness report. + +Until that inventory row exists, `resource_id` stays `null`. A +null join is a visible gap, not a license to mint +`resource:anthropic:claude-max`. + +## Financial entity + +Resource-control requires every expense, estimate, and usage +observation to carry exactly one `financial_entity_id`, or an +explicit null with a named gap. Founder-private cards and uncleared +Auslagen stay outside their terms until booked to an entity *and* a +resource. + +Fin-hub may still book those invoices for runway. It must not +silent-default the entity to Railiance or Binky, and it must not +pretend a private plan is a Railiance-procured resource. + +## Measurement layers and estimates + +Keep these records separate, joined by period, provider/plan, and +optional workplan/repo/`resource_id`: + +1. booked plan cost +2. entitlement (or explicit unknown capacity) +3. consumption of class `I`, each row marked with how it was obtained +4. work done + +Reuse resource-control’s evidence-basis vocabulary rather than +inventing a dialect: `invoiced`, `measured`, `quoted`, `derived`, +`projected`, `estimated`, `assumed`, `unknown`. Unknown carries no +quantity. Zero means measured and none. + +State Hub `measurement_kind` maps as: + +| State Hub | Evidence basis used here | +| --- | --- | +| `measured` | `measured` | +| `allocated` | `derived` (from a measured parent; method required) | +| `estimated` | `estimated` | +| `superseded` | excluded from totals | +| no events | `unknown` residual, not a zero observation | + +A derived overlay (implied €/token, work per token) is only as strong +as its weakest input. A month whose coverage is incomplete is +`insufficient` for trend comparison, not interpolated. + +## What fin-hub will not do + +- Invent resource identity or provider-resource identity. +- Meter tokens against a portfolio provision. +- Parse Codex, Claude, Cursor, or Grok session files. +- Emit `usage_observation` or `AllocationEvidence` as if it were + resource-control. +- Collapse tokens into euros at capture time. +- Store missing tokens as `0`. +- Treat a plan entitlement as an invoice, or an implied €/token as a + price. + +## Proposed contract addendum (not ratified) + +Offer to resource-control, without changing v0.1 until they accept: + +1. Intelligence resources remain in their portfolio when Railiance + procures them. Class `I` on a provision stays their meter. +2. Agent-session token events stay with State Hub. Fin-hub may join + them for financial reporting; that join is not technical usage. +3. Work-effectiveness allocation of a booked AI-plan fact is a + fin-hub reporting derivation and is not technical allocation. +4. A booked AI-plan fact may carry `resource_id=null` until they + publish one. Fin-hub will not mint a surrogate. + +`FIN-WP-0004-T05` (Scaleway `platform:audit-storage` booked-cost +round trip) is unchanged and is class `S`, not class `I`. + +Offered to resource-control on 2026-08-15 as State Hub message +`4e75cd00-87f1-46e1-8d17-9169289640a2`. Not assumed until they +accept, reject, or name a conflict. diff --git a/src/fin_hub/ingest/anthropic.py b/src/fin_hub/ingest/anthropic.py index 9128819..5615001 100644 --- a/src/fin_hub/ingest/anthropic.py +++ b/src/fin_hub/ingest/anthropic.py @@ -13,14 +13,28 @@ from fin_hub.ingest._csv import parse_amount, pick, read_csv_rows class TokenSpendRow: provider: str model: str - tokens_in: int - tokens_out: int + tokens_in: int | None + tokens_out: int | None cost: float currency: str session_id: str | None recorded_at: datetime +def parse_optional_token_count(value: str) -> int | None: + """Parse a token quantity. Empty is unknown; 0 is measured none.""" + + if not value: + return None + try: + count = int(value) + except ValueError as error: + raise ValueError(f"token count must be an integer, got {value!r}") from error + if count < 0: + raise ValueError(f"token count cannot be negative: {count}") + return count + + def parse_anthropic_billing_csv(path: Path, *, default_currency: str = "USD") -> list[TokenSpendRow]: rows: list[TokenSpendRow] = [] for row in read_csv_rows(path): @@ -28,8 +42,12 @@ def parse_anthropic_billing_csv(path: Path, *, default_currency: str = "USD") -> cost_raw = pick(row, "cost", "amount", "total_cost", "usage_cost_usd") if not model or not cost_raw: continue - tokens_in = int(pick(row, "input_tokens", "tokens_in", "prompt_tokens") or "0") - tokens_out = int(pick(row, "output_tokens", "tokens_out", "completion_tokens") or "0") + tokens_in = parse_optional_token_count( + pick(row, "input_tokens", "tokens_in", "prompt_tokens") + ) + tokens_out = parse_optional_token_count( + pick(row, "output_tokens", "tokens_out", "completion_tokens") + ) recorded_raw = pick(row, "date", "usage_date", "recorded_at", "timestamp") recorded_at = ( datetime.fromisoformat(recorded_raw.replace("Z", "+00:00")) diff --git a/src/fin_hub/models/budget.py b/src/fin_hub/models/budget.py index 64fb8c5..5ed9208 100644 --- a/src/fin_hub/models/budget.py +++ b/src/fin_hub/models/budget.py @@ -69,8 +69,8 @@ class TokenSpend(Base, TimestampMixin): id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) provider: Mapped[str] = mapped_column(String(32), nullable=False, index=True) model: Mapped[str] = mapped_column(String(64), nullable=False) - tokens_in: Mapped[int] = mapped_column(Integer, nullable=False, default=0) - tokens_out: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + tokens_in: Mapped[int | None] = mapped_column(Integer, nullable=True) + tokens_out: Mapped[int | None] = mapped_column(Integer, nullable=True) cost: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) session_id: Mapped[str | None] = mapped_column(String(128), nullable=True) recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) diff --git a/tests/test_ingest.py b/tests/test_ingest.py index bc17712..0b44df4 100644 --- a/tests/test_ingest.py +++ b/tests/test_ingest.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from fin_hub.ingest.anthropic import parse_anthropic_billing_csv +from fin_hub.ingest.anthropic import parse_anthropic_billing_csv, parse_optional_token_count from fin_hub.ingest.cloud import parse_cloud_cost_csv from fin_hub.ingest.hosteurope import parse_hosteurope_csv @@ -21,6 +21,43 @@ def test_parse_anthropic_billing_csv(): assert len(rows) == 2 assert rows[0].provider == "anthropic" assert rows[0].tokens_in == 12000 + assert rows[0].tokens_out == 3000 + + +def test_parse_optional_token_count_treats_empty_as_unknown(): + assert parse_optional_token_count("") is None + assert parse_optional_token_count("0") == 0 + assert parse_optional_token_count("12") == 12 + with pytest.raises(ValueError, match="integer"): + parse_optional_token_count("n/a") + with pytest.raises(ValueError, match="negative"): + parse_optional_token_count("-1") + + +def test_parse_anthropic_cost_only_row_does_not_invent_zero_tokens(tmp_path: Path): + source = tmp_path / "anthropic-cost-only.csv" + source.write_text( + "model,cost,currency,usage_date\n" + "claude-opus-4-20250514,20.00,USD,2026-08-01\n", + encoding="utf-8", + ) + row = parse_anthropic_billing_csv(source)[0] + assert row.cost == 20.0 + assert row.tokens_in is None + assert row.tokens_out is None + + +def test_parse_anthropic_explicit_zero_tokens_remain_zero(tmp_path: Path): + source = tmp_path / "anthropic-zero-tokens.csv" + source.write_text( + "model,input_tokens,output_tokens,cost,currency,usage_date\n" + "claude-haiku-3-20240307,0,0,0.00,USD,2026-08-02\n", + encoding="utf-8", + ) + row = parse_anthropic_billing_csv(source)[0] + assert row.tokens_in == 0 + assert row.tokens_out == 0 + assert row.cost == 0.0 def test_parse_hosteurope_csv(): diff --git a/tests/test_ledger.py b/tests/test_ledger.py index 9e6184c..d720bb1 100644 --- a/tests/test_ledger.py +++ b/tests/test_ledger.py @@ -20,6 +20,20 @@ from fin_hub.services.ledger import ( FIXTURES = Path(__file__).parent / "fixtures" +def test_ledger_import_anthropic_cost_only_does_not_require_tokens(tmp_path: Path): + ledger = tmp_path / "ledger.db" + source = tmp_path / "anthropic-cost-only.csv" + source.write_text( + "model,cost,currency,usage_date\nclaude-opus-4-20250514,20.00,USD,2026-08-01\n", + encoding="utf-8", + ) + result = import_csv(source, "anthropic", ledger_path=ledger) + assert result.rows_imported == 1 + rollups = monthly_summary(ledger_path=ledger) + assert rollups[0].currency == "USD" + assert rollups[0].total == pytest.approx(20.0) + + def test_ledger_import_and_monthly_summary(tmp_path: Path): ledger = tmp_path / "ledger.db" set_opening_balance(ledger, 12000.0) diff --git a/tests/test_models.py b/tests/test_models.py index 33cf36a..6889d3e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -40,6 +40,14 @@ def test_model_classes_importable(): assert EngagementPrice.__tablename__ == "fin_engagement_prices" +def test_token_spend_token_counts_are_nullable(): + columns = TokenSpend.__table__.columns + assert columns["tokens_in"].nullable is True + assert columns["tokens_out"].nullable is True + assert columns["tokens_in"].default is None + assert columns["tokens_out"].default is None + + def test_service_cost_has_external_attribution_seam(): columns = ServiceCost.__table__.columns assert columns["client_id"].nullable is True diff --git a/workplans/FIN-WP-0007-ai-plan-token-effectiveness.md b/workplans/FIN-WP-0007-ai-plan-token-effectiveness.md index f53aa54..55461a2 100644 --- a/workplans/FIN-WP-0007-ai-plan-token-effectiveness.md +++ b/workplans/FIN-WP-0007-ai-plan-token-effectiveness.md @@ -4,7 +4,7 @@ type: workplan title: "AI plan cost, token estimates, and effectiveness" domain: financials repo: fin-hub -status: proposed +status: active owner: grok topic_slug: financials created: "2026-08-15" @@ -12,7 +12,9 @@ updated: "2026-08-15" related: - FIN-WP-0002 - FIN-WP-0004 + - FIN-WP-0006 - STATE-WP-0045 + - RESOURCE-WP-0003 - ITC-CAP-0.2.0 origin: assessment origin_ref: "info-tech-canon 0.3.0 review — AI token data-gap" @@ -44,22 +46,34 @@ wait for a new commerce or intelligence domain model. ## Boundaries +Checked 2026-08-15 against resource-control SCOPE, INTENT, procurement +terms §4.1/§6, their v0.1 ratification, evidence-basis vocabulary, and +the backup capability restatement. Design note: +`docs/ai-plan-token-effectiveness.md`. Contract v0.1 is not silently +amended; the addendum in that note is offered, not assumed. + | Concept | Authoritative writer | Fin-hub role | | --- | --- | --- | | Provider invoice / plan subscription charge | fin-hub | Book once as `booked_cost` | | Recurring plan as a financial commitment | fin-hub | Record cadence and amount; not usage | | Plan entitlement (included tokens or plan capacity) | fin-hub | Commercial fact, same discipline as engagement prices | -| Session token evidence (`measured` / `allocated` / `estimated` / `superseded`) | State Hub (`STATE-WP-0045`) | Consume aggregates; do not re-parse sessions | -| Technical infrastructure usage and resource identity | resource-control | Out of scope here (class `S`/`C`/`P`, `FIN-WP-0004`) | +| Intelligence / infrastructure `resource_id` | resource-control | Join when they publish one; never invent | +| Provision-level class `I` usage | resource-control (gap they already own) | Consume via `FIN-WP-0004` when emitted | +| Session token evidence (`measured` / `allocated` / `estimated` / `superseded`) | State Hub (`STATE-WP-0045`) | Join as session-token evidence; do not store as `usage_observation` | +| Technical allocation of a portfolio resource | resource-control | Consume `AllocationEvidence`; do not emit it | | Work done (tasks, progress, outcomes) | State Hub / domain repos | Join key for effectiveness; not a cost driver | +| Reporting allocation of a plan invoice across work | fin-hub | Local `FIN-WP-0002`-family derivation | | Implied €/token and work-per-token / work-per-euro | fin-hub | Reporting overlay; never a booked price | | Vendor parsers, transcripts, model names, list prices | State Hub / llm-connect / operators | Not invented here | -Reuse `docs/fin-resource-authority-contract-v0.1.md` shapes -(`booked_cost`, `usage_observation`, `AllocationEvidence`). Resource-control -is the wrong *producer* for AI-session usage; the *envelope* is the right -one. A new AI-plan producer (State Hub aggregates, plus fin-hub's own -entitlement and booked facts) fills that role. +Reuse the *discipline* of contract v0.1 (unknown is not zero, residual +visible, estimates are not booked). Do not impersonate resource-control +record types for agent-session telemetry or work-effectiveness shares. + +Founder-private AI plans may be booked for runway. They stay +`resource_id=null` and must not silent-default `financial_entity_id` +until the spend is booked to an entity and, if Railiance-procured, a +resource. ## Non-goals @@ -67,6 +81,9 @@ entitlement and booked facts) fills that role. - Completing the Scaleway `platform:audit-storage` booked-cost round trip (`FIN-WP-0004-T05`). That is class `S`, not class `I`. - Owning session parsers or storing transcripts. +- Inventing a `resource_id` or metering tokens against a provision. +- Emitting `usage_observation` or `AllocationEvidence` as if this repo + were resource-control. - A canonical token↔hour or token↔euro exchange rate (ITC-CAP forbids asserting one; substitution is observed from the time series). - A new definition of “work” beyond joining to existing task / progress @@ -99,7 +116,7 @@ estimator method starts a new series. ```task id: FIN-WP-0007-T00 -status: todo +status: done priority: high state_hub_task_id: "5e9bccfd-569a-43a2-b016-afed760add49" ``` @@ -117,11 +134,21 @@ Done when the note is committed, `SCOPE.md` matches the boundary, and no task below has an ambiguous owner for booked cost, entitlement, session tokens, or work. +Completed 2026-08-15: boundary-checked against resource-control SCOPE, +INTENT, Terms §4.1/§6, their contract ratification, evidence-basis, and +the backup restatement. They own intelligence *resource* identity and +provision-level class `I` metering (not yet implemented). Session +telemetry and work-effectiveness stay outside that. Note: +`docs/ai-plan-token-effectiveness.md`. `SCOPE.md` and `INTENT.md` +updated. T04/T05 rewritten so fin-hub does not impersonate +`usage_observation` or `AllocationEvidence`. Proposed v0.1 addendum +offered to resource-control; not assumed. + ## Stop treating missing tokens as zero ```task id: FIN-WP-0007-T01 -status: todo +status: done priority: high state_hub_task_id: "aefa287e-0e4d-40b1-adf6-a8e6df7caeb2" ``` @@ -139,6 +166,11 @@ Done when the existing fixture still parses, a missing-token fixture does not emit zeros, and CAP-R8 (`unknown` never stored as zero) holds on the token path. +Completed 2026-08-15: `parse_optional_token_count` treats empty as +unknown and `0` as measured none. `TokenSpend.tokens_in/out` are +nullable with no zero default. Ledger import of a cost-only Anthropic +row still books the amount and does not invent token quantities. + ## Book AI-plan invoices as facts and commitments ```task @@ -186,7 +218,7 @@ invent a token ceiling. Done when a plan month can be reported as booked euros + entitled capacity (or explicit unknown) without reading session logs. -## Consume State Hub token aggregates as usage observations +## Join State Hub session-token aggregates ```task id: FIN-WP-0007-T04 @@ -195,21 +227,28 @@ priority: high state_hub_task_id: "29f1804f-a5ad-46b6-b9d7-8f074e31b04e" ``` -After T00, ingest State Hub token summaries as `usage_observation` -records (or a compatible typed sibling). Preserve `measurement_kind` -(`measured`, `allocated`, `estimated`, `superseded`), period, model, -repo/workplan association, confidence, and coverage. Do not re-parse -Codex/Claude/Cursor/Grok session files in this repo. +After T00, ingest State Hub token summaries as **fin-hub session-token +evidence**, not as resource-control `usage_observation`. Preserve +`measurement_kind` (`measured`, `allocated`, `estimated`, +`superseded`), period, model, repo/workplan association, confidence, +and coverage. Map kinds onto the resource-control evidence-basis +vocabulary (`measured` / `derived` / `estimated` / excluded / +`unknown`). Do not re-parse Codex, Claude, Cursor, or Grok session +files. Do not invent a `resource_id`. + +If resource-control later inventories a Railiance-procured +intelligence resource and emits `usage_observation` for it, consume +that through the existing `FIN-WP-0004` planning-evidence path. Superseded rows must not enter totals. Estimated and measured must remain separable. Months with no State Hub events produce an explicit unknown residual, not a zero-consumption observation. Done when a recorded fixture of mixed measured/estimated/superseded -aggregates stores as usage evidence, reconciles by kind, and cannot be -posted as booked spend. +aggregates stores as session-token evidence, reconciles by kind, and +cannot be posted as booked spend or as resource-control usage. -## Allocate booked plan cost across work +## Reporting-allocate booked plan cost across work ```task id: FIN-WP-0007-T05 @@ -219,17 +258,23 @@ state_hub_task_id: "db098143-90b7-475c-8846-e4c274e50b4a" ``` After T02 and T04, allocate each AI-plan `financial_fact_id` across -repos/workplans using `AllocationEvidence`: named method, shares, and -explicit residual. Allowed first methods: measured-token share, -measured+estimated-token share, session-count share, even split. -Method and estimator version are part of the record identity. +repos/workplans as a **fin-hub reporting allocation** (same family as +`FIN-WP-0002` billing basis). This is not resource-control +`AllocationEvidence` and must not be posted as technical allocation. -Shares plus residual must equal 1. Unattributed and unmeasured shares -stay visible. Allocation never writes a new booked fact. +Allowed first methods: measured-token share, measured+estimated-token +share, session-count share, even split. Method and estimator version +are part of the record identity. Shares plus residual must equal 1. +Unattributed and unmeasured shares stay visible. Allocation never +writes a new booked fact. + +Technical allocation of a portfolio resource remains +resource-control's job and only applies when they have published a +`resource_id`. Done when a one-plan, two-workplan fixture reconciles to the booked -effective amount, names its method, and keeps residual when coverage -is incomplete. +effective amount, names its method, keeps residual when coverage is +incomplete, and is not stored as `AllocationEvidence`. ## Publish an effectiveness report