@viccydev/pi-fpa 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
- "description": "FP&A 正式预测冻结:消费已审核策略交接与主 Agent 已提交的精确 strategy_decision,生成、合成并冻结 approved_cycle_forecast;Graph 不请求审批、不发布仪表盘。",
3
- "maxSteps": 10,
2
+ "description": "FP&A 正式预测冻结:消费已审核策略交接与主 Agent 已提交的精确 strategy_decision,生成预测决策计划并通过确定性 finalize module 冻结 approved_cycle_forecast;Graph 不请求审批、不发布仪表盘。",
3
+ "maxSteps": 6,
4
4
  "mutationPolicy": "mutating",
5
5
  "name": "fpa-forecast-freeze",
6
6
  "nodes": [
@@ -11,29 +11,29 @@
11
11
  "next": "route_confirmed_strategy",
12
12
  "outputKey": "confirmed_strategy",
13
13
  "parseJson": true,
14
- "prompt": "操作备注:{{goal}}\n\n读取 artifacts/reviewed_strategy_handoff.json、其中绑定的 strategy_proposal 与 strategy_review,以及操作备注明确给出的 strategy_decision path。必须核验 handoff status=ready;scope_id/cycle_id/forecast_role 与 Graph context 一致;proposal、review、handoff 的 strategy_version 一致;decision.kind=fpa.strategy.decision、decision=confirm;decision 的 strategy_version、handoff_fingerprint、decision_fingerprint 与操作备注和当前 handoff 精确一致。任一不一致即 blocked。不得请求或记录第二次审批,不得调用任何 fpa_dashboard_* 工具。\n\n只输出 JSON:{\"status\":\"ready|blocked\",\"strategy_version\":\"...\",\"strategy_decision_path\":\"...\",\"strategy_decision_fingerprint\":\"...\",\"review_conditions\":[],\"blockers\":[]}",
14
+ "prompt": "操作备注:{{goal}}\n\n读取 artifacts/reviewed_strategy_handoff.json、其中绑定的 strategy_proposal 与 strategy_review,以及操作备注明确给出的 strategy_decision path。必须核验 handoff status=ready;scope_id/cycle_id 与 Graph context 一致;proposal、review、handoff 的 strategy_version 一致;decision.kind=fpa.strategy.decision、decision=confirm;decision 的 strategy_version、handoff_fingerprint、decision_fingerprint 与操作备注和当前 handoff 精确一致。handoff 中的旧业务 forecast_role 仅作为兼容元数据,不参与 ledger role 判断;最终角色由冻结 module 按目标周期确定。任一身份或审批不一致即 blocked。不得请求或记录第二次审批,不得调用任何 fpa_dashboard_* 工具。\n\n只输出 JSON:{\"status\":\"ready|blocked\",\"strategy_version\":\"...\",\"strategy_decision_path\":\"...\",\"strategy_decision_fingerprint\":\"...\",\"review_conditions\":[],\"blockers\":[]}",
15
15
  "skills": ["fpa-apply-core-rules"],
16
- "systemPrompt": "你是 FP&A 已确认策略核验 Agent。只读核验主 Agent 已提交的不可变决策,不批准、不预测、不发布仪表盘。权威上下文为 scope_id={{context.scope_id}}、cycle_id={{context.cycle_id}}、forecast_role={{context.forecast_role}}。",
16
+ "systemPrompt": "你是 FP&A 已确认策略核验 Agent。只读核验主 Agent 已提交的不可变决策,不批准、不预测、不发布仪表盘。权威上下文为 scope_id={{context.scope_id}}、cycle_id={{context.cycle_id}}。",
17
17
  "tools": ["read"],
18
18
  "type": "subagent"
19
19
  },
20
20
  {
21
21
  "cases": [
22
22
  { "equals": "ready", "label": "确认有效", "to": "draft_forecast" },
23
- { "equals": "blocked", "label": "确认无效", "to": "report_blocked" }
23
+ { "equals": "blocked", "label": "确认无效", "to": "report_strategy_blocked" }
24
24
  ],
25
- "default": { "label": "其他结果", "to": "report_blocked" },
25
+ "default": { "label": "其他结果", "to": "report_strategy_blocked" },
26
26
  "id": "route_confirmed_strategy",
27
27
  "label": "判断是否可预测",
28
28
  "path": "data.confirmed_strategy.status",
29
29
  "type": "router"
30
30
  },
31
31
  {
32
- "id": "report_blocked",
33
- "label": "报告预测阻断",
32
+ "id": "report_strategy_blocked",
33
+ "label": "报告策略核验阻断",
34
34
  "outputKey": "forecast_blocked",
35
35
  "parseJson": true,
36
- "prompt": "核验结果:{{data.confirmed_strategy}}\n\n只输出 JSON:{\"kind\":\"fpa.graph-handoff\",\"status\":\"blocked\",\"graph\":\"fpa-forecast-freeze\",\"blockers\":[],\"required_action\":\"由原主会话提交与当前 handoff 精确匹配的策略确认\"}",
36
+ "prompt": "策略核验结果:{{data.confirmed_strategy}}\n\n汇总实际阻断原因。只输出 JSON:{\"kind\":\"fpa.graph-handoff\",\"status\":\"blocked\",\"graph\":\"fpa-forecast-freeze\",\"blockers\":[],\"required_action\":\"需要主会话处理的治理或策略动作\"}",
37
37
  "systemPrompt": "用中文简洁报告预测阻断,只输出约定 JSON。",
38
38
  "type": "prompt"
39
39
  },
@@ -41,55 +41,51 @@
41
41
  "agentName": "draft_forecast",
42
42
  "id": "draft_forecast",
43
43
  "label": "正式预测决策计划",
44
- "next": "compose_forecast",
44
+ "next": "route_draft_forecast",
45
45
  "outputKey": "draft_forecast",
46
- "prompt": "操作备注:{{goal}}\n\n已核验策略与决策:{{data.confirmed_strategy}}\n\n读取 reviewed_strategy_handoff、strategy_proposal、strategy_review 和精确 strategy_decision,按 fpa-forecast-approved-strategy 写 artifacts/forecast_plan.json。不得重新优化策略;业务分配、范围、假设或审核条件需变化时必须 blocked 并回到策略规划。只写 allocation 与每个切片的 ROAS assumptions;revenue、汇总、单位和 frozen_at 由下游 fpa_forecast_compose 推导。切片键必须使用 ua_spend 的 app_code/platform/media_source 合法值。不得调用任何 fpa_dashboard_* 工具。",
46
+ "parseJson": true,
47
+ "prompt": "操作备注:{{goal}}\n\n已核验策略与决策:{{data.confirmed_strategy}}\n\n读取 reviewed_strategy_handoff、strategy_proposal、strategy_review 和精确 strategy_decision,按 fpa-forecast-approved-strategy 写 artifacts/forecast_plan.json。不得重新优化策略;业务分配、范围、假设或审核条件需变化时不得写入计划,必须返回 blocked 并回到策略规划。只写 allocation 与每个切片的 ROAS assumptions;revenue、汇总、单位、frozen_at、forecast_role、数据覆盖状态和修复提醒全部由下游 fpa_forecast_finalize 确定。优先使用 ua_spend 的 app_code/platform/media_source 真实组合;若已确认分配中的部分切片与当前上游映射不一致,不得擅自删除、置零、重分配或伪造映射,保留原切片交给 finalize 自动产出 complete_with_limits、覆盖率和数据修复项。不得调用任何 fpa_dashboard_* 工具。成功写入后只输出 JSON:{\"status\":\"ready\",\"plan_path\":\"artifacts/forecast_plan.json\",\"blockers\":[]};需要治理处理时只输出 JSON:{\"status\":\"blocked\",\"plan_path\":null,\"blockers\":[]}。",
47
48
  "skills": ["fpa-apply-core-rules", "fpa-forecast-approved-strategy"],
48
49
  "systemPrompt": "你是 FP&A 正式预测 Agent。只按已确认策略写预测计划,不请求审批、不发布仪表盘、不手算派生值。",
49
50
  "tools": ["read", "write", "fpa_calc"],
50
51
  "type": "subagent"
51
52
  },
52
53
  {
53
- "args": { "plan_path": "artifacts/forecast_plan.json" },
54
- "failure": { "maxAttempts": 2, "onError": "repair_forecast_plan" },
55
- "id": "compose_forecast",
56
- "label": "合成正式预测",
57
- "next": "commit_forecast",
58
- "outputKey": "compose_forecast",
59
- "tool": "fpa_forecast_compose",
60
- "type": "tool"
54
+ "cases": [
55
+ { "equals": "ready", "label": "计划已写入", "to": "finalize_forecast" },
56
+ { "equals": "blocked", "label": "需要治理处理", "to": "report_plan_blocked" }
57
+ ],
58
+ "default": { "label": "其他结果", "to": "report_plan_blocked" },
59
+ "id": "route_draft_forecast",
60
+ "label": "判断预测计划是否可冻结",
61
+ "path": "data.draft_forecast.status",
62
+ "type": "router"
61
63
  },
62
64
  {
63
- "agentName": "repair_forecast_plan",
64
- "id": "repair_forecast_plan",
65
- "label": "定点修正预测计划",
66
- "next": "compose_forecast",
67
- "outputKey": "repair_forecast_plan",
68
- "prompt": "合成失败:{{data.__graphError}}\n\n读取 artifacts/forecast_plan.json、planning_brief 和 reviewed_strategy_handoff,只定点修正格式、合法切片键或 forecast plan 契约问题。不得改变已确认策略的业务分配、范围、假设或条件;若错误要求此类变化,报告 blocked。不得调用任何 fpa_dashboard_* 工具。",
69
- "skills": ["fpa-forecast-approved-strategy"],
70
- "systemPrompt": "你是预测计划修复 Agent,只按确定性合成报错做最小修正。",
71
- "tools": ["read", "write", "edit"],
72
- "type": "subagent"
65
+ "id": "report_plan_blocked",
66
+ "label": "报告预测计划阻断",
67
+ "outputKey": "forecast_blocked",
68
+ "parseJson": true,
69
+ "prompt": "预测计划结果:{{data.draft_forecast}}\n\n汇总实际阻断原因。只输出 JSON:{\"kind\":\"fpa.graph-handoff\",\"status\":\"blocked\",\"graph\":\"fpa-forecast-freeze\",\"blockers\":[],\"required_action\":\"需要主会话处理的治理或策略动作\"}",
70
+ "systemPrompt": "用中文简洁报告预测阻断,只输出约定 JSON。",
71
+ "type": "prompt"
73
72
  },
74
73
  {
75
74
  "args": {
76
- "artifact": "{{data.compose_forecast.details.artifact}}",
77
- "context": {
78
- "cycle_id": "{{context.cycle_id}}",
79
- "forecast_role": "{{context.forecast_role}}",
80
- "scope_id": "{{context.scope_id}}"
81
- }
75
+ "plan_path": "artifacts/forecast_plan.json",
76
+ "scope_id": "{{context.scope_id}}",
77
+ "cycle_id": "{{context.cycle_id}}"
82
78
  },
83
- "id": "commit_forecast",
79
+ "id": "finalize_forecast",
84
80
  "label": "冻结正式预测",
85
81
  "mutates": true,
86
- "outputKey": "commit_forecast",
87
- "tool": "fpa_artifact_commit",
82
+ "outputKey": "finalize_forecast",
83
+ "tool": "fpa_forecast_finalize",
88
84
  "type": "tool"
89
85
  }
90
86
  ],
91
87
  "schemaVersion": 1,
92
88
  "start": "load_confirmed_strategy",
93
89
  "transitionLabels": { "default": "其他情况", "error": "失败", "next": "继续" },
94
- "version": "2.1.0"
90
+ "version": "3.0.0"
95
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viccydev/pi-fpa",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "description": "Full-cycle FP&A planning, strategy, forecast, and review prompts, skills, and data tools for Pi",
6
6
  "license": "UNLICENSED",
@@ -31,9 +31,9 @@
31
31
  "fpa-dashboard-worker": "./bin/fpa-dashboard-worker.mjs"
32
32
  },
33
33
  "scripts": {
34
- "test": "node tests/package-structure.test.mjs && node tests/workflow-routing.test.mjs && node --test tests/workflow-routing-guard.test.mjs tests/graph-contract.test.mjs tests/graph-installer.test.mjs && node tests/extension-unit.test.mjs && node --test tests/catalog-timeout.test.mjs tests/artifact-store.test.mjs tests/artifact-ledger.test.mjs tests/artifact-handoff.test.mjs tests/forecast-compose.test.mjs tests/dashboard-actuals.test.mjs tests/dashboard-projector.test.mjs tests/dashboard-module-publisher.test.mjs tests/dashboard-stage-projector.test.mjs tests/strategy-decision.test.mjs tests/cycle-operating-projection.test.mjs tests/forward-outlook.test.mjs tests/forecast-accuracy.test.mjs tests/dashboard-publisher.test.mjs tests/dashboard-provenance.test.mjs tests/dashboard-coordinator.test.mjs && node tests/pi-loader-smoke.mjs && node tests/worker-loader-smoke.mjs && node tests/publish-workflow.test.mjs",
34
+ "test": "node tests/package-structure.test.mjs && node tests/workflow-routing.test.mjs && node --test tests/workflow-routing-guard.test.mjs tests/graph-contract.test.mjs tests/graph-installer.test.mjs && node tests/extension-unit.test.mjs && node --test tests/catalog-timeout.test.mjs tests/artifact-store.test.mjs tests/artifact-ledger.test.mjs tests/artifact-handoff.test.mjs tests/forecast-compose.test.mjs tests/forecast-finalize.test.mjs tests/dashboard-actuals.test.mjs tests/dashboard-projector.test.mjs tests/dashboard-module-publisher.test.mjs tests/dashboard-stage-projector.test.mjs tests/strategy-decision.test.mjs tests/cycle-operating-projection.test.mjs tests/forward-outlook.test.mjs tests/forecast-accuracy.test.mjs tests/dashboard-publisher.test.mjs tests/dashboard-provenance.test.mjs tests/dashboard-coordinator.test.mjs && node tests/pi-loader-smoke.mjs && node tests/worker-loader-smoke.mjs && node tests/publish-workflow.test.mjs",
35
35
  "test:structure": "node tests/package-structure.test.mjs",
36
- "test:unit": "node tests/extension-unit.test.mjs && node --test tests/catalog-timeout.test.mjs tests/artifact-store.test.mjs tests/artifact-ledger.test.mjs tests/artifact-handoff.test.mjs tests/forecast-compose.test.mjs tests/dashboard-actuals.test.mjs tests/dashboard-projector.test.mjs tests/dashboard-module-publisher.test.mjs tests/dashboard-stage-projector.test.mjs tests/strategy-decision.test.mjs tests/cycle-operating-projection.test.mjs tests/forward-outlook.test.mjs tests/forecast-accuracy.test.mjs tests/dashboard-publisher.test.mjs tests/dashboard-provenance.test.mjs tests/dashboard-coordinator.test.mjs",
36
+ "test:unit": "node tests/extension-unit.test.mjs && node --test tests/catalog-timeout.test.mjs tests/artifact-store.test.mjs tests/artifact-ledger.test.mjs tests/artifact-handoff.test.mjs tests/forecast-compose.test.mjs tests/forecast-finalize.test.mjs tests/dashboard-actuals.test.mjs tests/dashboard-projector.test.mjs tests/dashboard-module-publisher.test.mjs tests/dashboard-stage-projector.test.mjs tests/strategy-decision.test.mjs tests/cycle-operating-projection.test.mjs tests/forward-outlook.test.mjs tests/forecast-accuracy.test.mjs tests/dashboard-publisher.test.mjs tests/dashboard-provenance.test.mjs tests/dashboard-coordinator.test.mjs",
37
37
  "test:loader": "node tests/pi-loader-smoke.mjs && node tests/worker-loader-smoke.mjs",
38
38
  "test:live": "node tests/live-smoke.mjs",
39
39
  "pack:check": "npm pack --dry-run"
@@ -26,8 +26,10 @@ writing artifacts. It applies only when the current agent exposes both
26
26
  | Exact Forecast and Execution refs plus newly arrived comparable Actuals | `fpa-cycle-review` |
27
27
 
28
28
  3. Call `graph_run` with the user's complete goal. Every `context` value must
29
- be a string; provide exact `scope_id`, `cycle_id`, and `forecast_role`, and
30
- keep structured planning details in `goal`. The parent agent must not
29
+ be a string. Provide exact `scope_id` and `cycle_id`; planning additionally
30
+ requires its business `forecast_role`, while forecast freeze derives the
31
+ ledger role from the target period and freeze time. Keep structured planning
32
+ details in `goal`. The parent agent must not
31
33
  reproduce Graph nodes or pre-run their `fpa_*` calls. After the combined
32
34
  planning Graph succeeds, the main Agent publishes `period-review` and then
33
35
  `next-strategy` with their matching `fpa_dashboard_publish_*` tools. Graph
@@ -24,8 +24,11 @@ ready handoff plus the exact confirmed strategy-decision fingerprint:
24
24
 
25
25
  `reviewed_strategy_handoff + strategy_decision -> forecast_plan -> approved_cycle_forecast`
26
26
 
27
- The main Agent owns approval recording; the forecast Graph owns deterministic
28
- composition and artifact freezing. A rejected approval reruns
27
+ The main Agent owns approval recording; the forecast Graph writes the approved
28
+ plan and delegates deterministic composition, lifecycle-role derivation, and
29
+ artifact freezing to `fpa_forecast_finalize`. A business role recorded in an
30
+ older handoff is lineage metadata, not a storage role that the model must
31
+ translate. A rejected approval reruns
29
32
  `fpa-strategy-planning` with the exact feedback and produces a new version
30
33
  rather than modifying the reviewed proposal in place.
31
34
 
@@ -31,6 +31,8 @@ checks:
31
31
  affected_scope: []
32
32
  evidence: string
33
33
  remediation: string
34
+ owner_role: data_steward
35
+ disposition: excluded_from_calculation | warning_only | blocking
34
36
  blocking_issues: []
35
37
  non_blocking_limits: []
36
38
  ```
@@ -36,34 +36,28 @@ from it, return `blocked`; never reconstruct or substitute the handoff.
36
36
  4. Provide downside, base, and upside values for each supported KPI.
37
37
  5. Reconcile allocation totals, formulas, and cross-metric identities.
38
38
  6. Write the forecast plan to `artifacts/forecast_plan.json` using [artifact-contract.md](references/artifact-contract.md).
39
- 7. Derive the artifact with `fpa_forecast_compose`, then freeze it with
40
- `fpa_artifact_commit` and an assigned context containing the explicit
41
- `scope_id`, `cycle_id`, and `forecast_role`. Use `original` only when the
42
- forecast is frozen no later than period start; use `eac` for an in-period
43
- reforecast and `next_plan` only when it is frozen no later than its target
44
- period start and is the approved direct successor of the active cycle. Use
45
- `backtest` for a historical rerun frozen after its target
46
- period; it is immutable evidence, not an operating plan, and it must not
39
+ 7. Call `fpa_forecast_finalize` with the plan path and the exact `scope_id` and
40
+ `cycle_id`. Do not choose or pass `forecast_role`: the tool derives
41
+ `next_plan` before period start, `eac` during the period, and `backtest`
42
+ after period end, then composes and commits the artifact atomically. A
43
+ historical rerun is immutable evidence, not an operating plan, and must not
47
44
  replace the current pointer or enter the automatic operating dashboard
48
45
  queue. It may be displayed from its exact immutable reference only when the
49
- user explicitly requests a backtest dashboard publication. A
50
- `next_plan` is likewise stored as forward lineage and does not replace the
51
- legacy current pointer; its explicit dashboard handoff links it to the
52
- active cycle after successor validation.
46
+ user explicitly requests a backtest dashboard publication.
53
47
 
54
48
  ## Write the decision, not the arithmetic
55
49
 
56
50
  The only judgement in a forecast is the allocation and each slice's ROAS
57
51
  assumption. Revenue, the consolidated roll-up, units, windows, and `frozen_at`
58
- all follow from those, and `fpa_forecast_compose` derives them — which is what
52
+ all follow from those, and `fpa_forecast_finalize` derives them — which is what
59
53
  makes the identities the commit checks true by construction rather than
60
54
  dependent on transcribing several hundred numbers without a slip.
61
55
 
62
56
  So do not hand-compute revenue, totals, or ratios, and do not emit the full
63
57
  artifact as one tool argument: that is the shape that gets cut off at the model
64
- output limit and leaves a turn that reads finished but committed nothing. If a
65
- later node in this graph composes and commits for you, stop after writing the
66
- plan and say so; never claim a fingerprint you were not handed.
58
+ output limit and leaves a turn that reads finished but committed nothing. Stop
59
+ after writing the plan; the finalizer owns composition and commit. Never claim
60
+ a fingerprint you were not handed.
67
61
 
68
62
  ## Status and the publish gate
69
63
 
@@ -84,16 +78,28 @@ tool. `complete_with_limits` remains publishable when its limitations are
84
78
  explicit, while `blocked` is not. Do not hide a real defect to get through the
85
79
  gate.
86
80
 
81
+ Upstream identity or coverage defects are non-blocking data limits. Preserve
82
+ the full approved allocation, leave affected revenue and ROAS values `NULL`,
83
+ and let `fpa_forecast_finalize` emit `data_quality`, a coverage-labelled partial
84
+ `conclusion`, and actionable repair items. Never delete a bad slice, turn it
85
+ into zero spend, or renormalize the remaining allocation. The affected slice
86
+ remains ineligible for Actuals comparison and execution until its mapping is
87
+ fixed. Approval mismatch, duplicate or negative allocation, budget mismatch,
88
+ invalid period/currency/role, and unauthorized execution remain blocking
89
+ governance failures.
90
+
87
91
  ## Boundaries
88
92
 
89
93
  - Do not optimize, revise, or substitute the approved strategy while forecasting.
90
94
  - A required change invalidates the current approval and routes back to recommendation and review.
91
95
  - Unsupported KPIs remain `NULL` with a reason.
96
+ - A partial conclusion must say it covers only calculable slices and must show
97
+ `calculable_spend_pct`; it must never be labelled portfolio ROAS.
92
98
  - Do not execute the strategy and do not start continuous monitoring.
93
99
 
94
100
  ## Completion
95
101
 
96
- The forecast-freeze workflow ends only after `fpa_artifact_commit` returns
102
+ The forecast-freeze workflow ends only after `fpa_forecast_finalize` returns
97
103
  `status: committed`, an `immutable_fingerprint`, and (for assigned artifacts) a
98
104
  `dashboard_handoff`. Commit freezes the business artifact but does not enqueue
99
105
  or publish dashboard work. The calling main Agent owns that explicit handoff.
@@ -41,6 +41,28 @@ calibration_policy:
41
41
  policy_version: string
42
42
  unsupported_metrics: []
43
43
  reconciliation_checks: []
44
+ data_quality:
45
+ status: complete | partial | unavailable
46
+ planned_spend: number
47
+ calculable_spend: number
48
+ calculable_spend_pct: number # 0..1; never renormalized
49
+ excluded_spend: number
50
+ issue_count: number
51
+ issues:
52
+ - issue_id: string
53
+ code: slice_key_not_in_actuals
54
+ severity: warning
55
+ slice: {app_id: string, store: string, channel_group: string}
56
+ affected_metrics: [actuals_comparison, revenue, roas, execution]
57
+ disposition: excluded_from_calculation
58
+ evidence: []
59
+ remediation: string
60
+ owner_role: data_steward
61
+ conclusion:
62
+ status: available | available_with_limits | unavailable
63
+ scope: portfolio | calculable_slices | none
64
+ metrics: {spend: scenario_metric, revenue: scenario_metric, roas: scenario_metric}
65
+ warning: string | null
44
66
  frozen_at: timestamp
45
67
  ```
46
68
 
@@ -51,9 +73,10 @@ Do not include `immutable_fingerprint` in the tool input; a draft that carries o
51
73
  Almost none of the artifact above is a decision. The decision is the allocation
52
74
  and the ROAS assumption behind each slice; revenue, the ROAS write-back, the
53
75
  consolidated roll-up, every unit and window string, and `frozen_at` all follow
54
- from those by arithmetic. Write the plan and let `fpa_forecast_compose` derive
55
- the rest it makes the identities the commit checks true by construction
56
- instead of true if you typed them correctly.
76
+ from those by arithmetic. Write the plan and let `fpa_forecast_finalize` derive
77
+ the rest, choose the lifecycle role from time, and commit it it makes the
78
+ identities the commit checks true by construction instead of true if you typed
79
+ them correctly.
57
80
 
58
81
  ```yaml
59
82
  # artifacts/forecast_plan.json
@@ -89,15 +112,20 @@ Then:
89
112
 
90
113
  ```
91
114
  write artifacts/forecast_plan.json
92
- fpa_forecast_compose { "plan_path": "artifacts/forecast_plan.json" }
93
- fpa_artifact_commit { "artifact": <the artifact compose returned> }
115
+ fpa_forecast_finalize { "plan_path": "artifacts/forecast_plan.json", "scope_id": "<exact scope>", "cycle_id": "<exact cycle>" }
94
116
  ```
95
117
 
118
+ The finalizer derives `next_plan` before the target period, `eac` during it,
119
+ and `backtest` after it. The model does not pass or translate `forecast_role`.
120
+
96
121
  Rules the plan has to respect, because compose enforces them:
97
122
 
98
123
  - **Spend is a decision, not a prediction.** It is identical in all three scenarios, so a slice carries one `approved_spend`. Only revenue moves, through `roas`.
99
124
  - **A stopped slice has no ROAS.** `approved_spend: 0` means revenue 0 and `roas` null in all three scenarios — the contract derives ROAS as revenue/spend and calls a zero denominator null.
100
- - **Slice keys must be values `ua_spend` actually uses.** A plan naming `google_play` where the mart says `android` is rejected with the valid values listed. The single-placeholder App axis for a scoped-out portfolio is the one permitted exception.
125
+ - **Slice keys are checked as real triples.** `app_code`, `platform`, and `media_source` must occur together in `ua_spend`; validating three independent value lists is insufficient. A mismatch is isolated as a data-quality limit: approved spend remains in the allocation and consolidated spend, while affected revenue/ROAS are `NULL`. The single-placeholder App axis for a scoped-out portfolio remains permitted.
126
+ - **Do not hide partiality.** Compose reports planned, calculable, and excluded spend. Its partial conclusion covers only calculable slices; remaining spend is never scaled to 100%, and partial ROAS is never labelled portfolio ROAS.
127
+ - **Data quality does not weaken governance.** Missing/mismatched approval, duplicate or negative allocation, invalid period/currency/forecast role, and budget reconciliation failures still block.
128
+ - **Describe other upstream limits actionably.** For non-identity defects, each `unsupported_metrics` item should name the affected metric/scope, evidence, remediation, and data owner. Dashboard publication keeps these items visible even when `data_quality.status` is otherwise complete.
101
129
  - **`status` and `unsupported_metrics` must agree.** `complete` requires an empty list; `complete_with_limits` requires a non-empty one. Scope excluded upstream is neither.
102
130
  - **Do not hand-compute anything derived.** A ROAS rounded to four decimals misses the commit tolerance and costs a re-draft.
103
131
 
@@ -32,9 +32,12 @@ preserve every other module revision.
32
32
  3. Stop and wait for the dashboard action. The Web host sends a canonical
33
33
  decision message back to the bound original session. In that session call
34
34
  `fpa_strategy_decision_commit` with the exact `action_id`.
35
- 4. On `confirm`, run `fpa-forecast-freeze`; after it returns an exact committed
36
- forecast ref, preview then publish `next-forecast` with
37
- `fpa_dashboard_publish_forecast`.
35
+ 4. On `confirm`, run `fpa-forecast-freeze`. For an operating Forecast, preview
36
+ then publish `next-forecast` with `fpa_dashboard_publish_forecast`. If the
37
+ finalizer returns `forecast_role: backtest` and a `not_applicable` dashboard
38
+ handoff, stop after reporting its immutable ref; do not attempt operating
39
+ publication unless the user explicitly requested the separate backtest
40
+ display path.
38
41
  5. On `request_changes`, pass the verbatim feedback back into strategy planning,
39
42
  publish the replacement `next-strategy` module, and wait for a new decision.
40
43
 
@@ -57,19 +60,21 @@ only part of this tuple or fall back to a mutable current pointer.
57
60
  This path remains only for legacy execution-evidence refreshes. It must not be
58
61
  used to replace review, strategy, or forecast modules.
59
62
 
60
- 1. Take the exact `artifact_ref` from `fpa_artifact_commit.dashboard_handoff`.
63
+ 1. For an operating forecast, take the exact `artifact_ref` from a deferred
64
+ `fpa_forecast_finalize.dashboard_handoff` (or the compatible
65
+ `fpa_artifact_commit.dashboard_handoff` path). A backtest handoff is
66
+ `not_applicable` and must not be queued automatically.
61
67
  2. Call `fpa_dashboard_refresh_queue` with `action: enqueue_artifact` and that
62
68
  ref. Do not restate scope, cycle, role, or upstream refs; the package resolves
63
69
  them from the immutable ledger.
64
70
  3. If the result is `enqueued` or `already_pending` and the current task expects
65
71
  the dashboard immediately, call one bounded `drain`, then verify with
66
72
  `fpa_dashboard_status`.
67
- 4. If the result is `not_applicable` for a `backtest`, do not treat that as a
68
- failed artifact. Automatic post-Graph handoff intentionally leaves the
69
- operating dashboard unchanged. If the user explicitly asked to display or
70
- publish that backtest, continue through the exact backtest display path
71
- below; otherwise report that the frozen historical forecast remains in the
72
- ledger without changing the dashboard.
73
+ 4. A compatible legacy `fpa_artifact_commit` handoff may still return
74
+ `not_applicable` for a `backtest`; do not treat that as a failed artifact or
75
+ retry it. If the user explicitly asked to display that backtest, continue
76
+ through the exact backtest display path below. Otherwise report that it
77
+ remains in the ledger without changing the operating dashboard.
73
78
 
74
79
  ### Human-requested rebuild
75
80