@viccydev/pi-fpa 0.9.0 → 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.
- package/README.md +3 -3
- package/extensions/fpa-artifacts/compose.ts +40 -15
- package/extensions/fpa-artifacts/finalize.ts +99 -0
- package/extensions/fpa-artifacts/index.ts +34 -0
- package/extensions/fpa-routing-guard/index.ts +8 -3
- package/graphs/fpa-forecast-freeze.json +34 -50
- package/package.json +3 -3
- package/skills/fpa-apply-core-rules/SKILL.md +4 -2
- package/skills/fpa-apply-core-rules/references/core-rules.md +5 -2
- package/skills/fpa-forecast-approved-strategy/SKILL.md +12 -18
- package/skills/fpa-forecast-approved-strategy/references/artifact-contract.md +8 -5
- package/skills/fpa-refresh-dashboard/SKILL.md +15 -10
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ Extension 内置的关键防护:
|
|
|
80
80
|
|
|
81
81
|
## Artifact 与看板 Extension
|
|
82
82
|
|
|
83
|
-
`fpa-artifacts` 提供 `fpa_artifact_commit
|
|
83
|
+
`fpa-artifacts` 提供 `fpa_forecast_finalize`,把紧凑 Forecast plan 一次性合成、校验、按目标周期推导生命周期角色并冻结;模型无需在 Graph handoff 与 ledger 枚举之间转译 `forecast_role`。通用的 `fpa_artifact_commit` 仍负责 `approved_cycle_forecast` 和 `execution_receipt` 的严格字段校验、对账、稳定指纹和原子落盘。只有工具返回成功后的 JSON 才是冻结产物,Markdown 不是正式数据源。
|
|
84
84
|
|
|
85
85
|
`fpa-dashboard` 提供分模块发布工具和兼容的闭环刷新工具:
|
|
86
86
|
|
|
@@ -159,12 +159,12 @@ pi list
|
|
|
159
159
|
团队分发建议使用固定 Git tag:
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
pi install git:github.com/linyqh/pi-fpa@v0.9.
|
|
162
|
+
pi install git:github.com/linyqh/pi-fpa@v0.9.1
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
## 发布到 npm
|
|
166
166
|
|
|
167
|
-
发布动作由 GitHub Release 触发。Release 标签必须严格使用 `v<package.json version>`,例如版本 `0.9.
|
|
167
|
+
发布动作由 GitHub Release 触发。Release 标签必须严格使用 `v<package.json version>`,例如版本 `0.9.1` 对应 `v0.9.1`。工作流会检出该标签,执行 `npm ci`、`npm test` 和包内容预检,全部通过后发布公开包 `@viccydev/pi-fpa`。普通 Release 发布到 `latest`,Prerelease 发布到 `next`。
|
|
168
168
|
|
|
169
169
|
发布认证使用 npm Trusted Publishing / OIDC,不使用长期 npm Token。npm 包后台的 Trusted Publisher 配置为:
|
|
170
170
|
|
|
@@ -34,11 +34,10 @@ import {
|
|
|
34
34
|
// derived. So the plan carries just those, and this module computes the rest —
|
|
35
35
|
// which makes the identities true by construction rather than true if checked.
|
|
36
36
|
//
|
|
37
|
-
// Deliberately returns the artifact instead of writing it
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// while the irreversible freeze stays in its own node.
|
|
37
|
+
// Deliberately returns the artifact instead of writing it, so isolated callers
|
|
38
|
+
// can inspect diagnostics without mutation. The forecast-freeze Graph uses the
|
|
39
|
+
// finalizer as its one mutation seam; malformed plans fail once with the full
|
|
40
|
+
// actionable limitation report instead of entering a repair loop.
|
|
42
41
|
// ============================================================================
|
|
43
42
|
|
|
44
43
|
/** Distinct dimension values a plan's slice keys must be drawn from. */
|
|
@@ -328,19 +327,45 @@ export async function composeApprovedForecast(plan: unknown, options: ComposeOpt
|
|
|
328
327
|
);
|
|
329
328
|
}
|
|
330
329
|
if (status === "complete_with_limits") {
|
|
330
|
+
const limitationErrors: string[] = [];
|
|
331
|
+
const capture = (check: () => void): void => {
|
|
332
|
+
try {
|
|
333
|
+
check();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
limitationErrors.push(error instanceof Error ? error.message : String(error));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
331
338
|
for (const [index, value] of unsupported.entries()) {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
const path = `plan.unsupported_metrics[${index}]`;
|
|
340
|
+
let item: Record<string, unknown>;
|
|
341
|
+
try {
|
|
342
|
+
item = record(value, path);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
limitationErrors.push(error instanceof Error ? error.message : String(error));
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
capture(() => { text(item.metric, `${path}.metric`); });
|
|
348
|
+
capture(() => { text(item.reason, `${path}.reason`); });
|
|
335
349
|
if (!Array.isArray(item.affected_scope) || item.affected_scope.length === 0) {
|
|
336
|
-
|
|
350
|
+
limitationErrors.push(`${path}.affected_scope must be a non-empty array.`);
|
|
351
|
+
} else {
|
|
352
|
+
for (const [scopeIndex, scope] of item.affected_scope.entries()) {
|
|
353
|
+
capture(() => { text(scope, `${path}.affected_scope[${scopeIndex}]`); });
|
|
354
|
+
}
|
|
337
355
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
356
|
+
const evidence = item.evidence === undefined ? [] : Array.isArray(item.evidence) ? item.evidence : [item.evidence];
|
|
357
|
+
if (evidence.length === 0) {
|
|
358
|
+
limitationErrors.push(`${path}.evidence must not be empty.`);
|
|
359
|
+
} else {
|
|
360
|
+
for (const [evidenceIndex, entry] of evidence.entries()) {
|
|
361
|
+
capture(() => { text(entry, `${path}.evidence[${evidenceIndex}]`); });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
capture(() => { text(item.remediation, `${path}.remediation`); });
|
|
365
|
+
capture(() => { text(item.owner_role, `${path}.owner_role`); });
|
|
366
|
+
}
|
|
367
|
+
if (limitationErrors.length > 0) {
|
|
368
|
+
throw new Error(`Forecast plan limitations are invalid:\n- ${limitationErrors.join("\n- ")}`);
|
|
344
369
|
}
|
|
345
370
|
}
|
|
346
371
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { composeApprovedForecast, type ComposeOptions, type ComposeResult } from "./compose.ts";
|
|
2
|
+
import {
|
|
3
|
+
commitArtifact,
|
|
4
|
+
type ArtifactCommitContext,
|
|
5
|
+
type CommitArtifactResult,
|
|
6
|
+
type ForecastRole,
|
|
7
|
+
} from "./store.ts";
|
|
8
|
+
|
|
9
|
+
export interface ForecastFinalizeContext {
|
|
10
|
+
scope_id: string;
|
|
11
|
+
cycle_id: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ForecastFinalizeResult extends ComposeResult {
|
|
15
|
+
forecastRole: Extract<ForecastRole, "eac" | "next_plan" | "backtest">;
|
|
16
|
+
committed: CommitArtifactResult;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function withoutCallerFrozenAt(plan: unknown): unknown {
|
|
20
|
+
if (plan === null || typeof plan !== "object" || Array.isArray(plan)) return plan;
|
|
21
|
+
const { frozen_at: _ignored, ...decisionPlan } = plan as Record<string, unknown>;
|
|
22
|
+
return decisionPlan;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The forecast-freeze workflow owns next-cycle planning, in-period reforecast,
|
|
27
|
+
* and historical reruns. Its ledger role follows from when the immutable body
|
|
28
|
+
* is frozen relative to its own target period; callers do not translate a
|
|
29
|
+
* business label into storage vocabulary.
|
|
30
|
+
*/
|
|
31
|
+
export function deriveFreezeForecastRole(
|
|
32
|
+
targetPeriod: { start_inclusive: string; end_exclusive: string },
|
|
33
|
+
frozenAt: string,
|
|
34
|
+
): ForecastFinalizeResult["forecastRole"] {
|
|
35
|
+
const start = Date.parse(targetPeriod.start_inclusive);
|
|
36
|
+
const end = Date.parse(targetPeriod.end_exclusive);
|
|
37
|
+
const frozen = Date.parse(frozenAt);
|
|
38
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(frozen) || end <= start) {
|
|
39
|
+
throw new Error("Cannot derive forecast role from an invalid target period or frozen_at timestamp.");
|
|
40
|
+
}
|
|
41
|
+
if (frozen >= end) return "backtest";
|
|
42
|
+
if (frozen >= start) return "eac";
|
|
43
|
+
return "next_plan";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function finalizeApprovedForecast(
|
|
47
|
+
projectRoot: string,
|
|
48
|
+
plan: unknown,
|
|
49
|
+
context: ForecastFinalizeContext,
|
|
50
|
+
options: ComposeOptions = {},
|
|
51
|
+
): Promise<ForecastFinalizeResult> {
|
|
52
|
+
const composed = await composeApprovedForecast(withoutCallerFrozenAt(plan), options);
|
|
53
|
+
if (composed.artifact.status === "blocked") {
|
|
54
|
+
throw new Error("A blocked forecast plan cannot be finalized or committed.");
|
|
55
|
+
}
|
|
56
|
+
const forecastRole = deriveFreezeForecastRole(
|
|
57
|
+
composed.artifact.target_period,
|
|
58
|
+
composed.artifact.frozen_at,
|
|
59
|
+
);
|
|
60
|
+
const commitContext: ArtifactCommitContext = {
|
|
61
|
+
scope_id: context.scope_id,
|
|
62
|
+
cycle_id: context.cycle_id,
|
|
63
|
+
forecast_role: forecastRole,
|
|
64
|
+
};
|
|
65
|
+
const committed = await commitArtifact(projectRoot, composed.artifact, { context: commitContext });
|
|
66
|
+
return { ...composed, forecastRole, committed };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Keep the Graph result actionable without returning the full canonical artifact. */
|
|
70
|
+
export function forecastFinalizeDetails(result: ForecastFinalizeResult): Record<string, unknown> {
|
|
71
|
+
const artifactRef = result.committed.artifactRef;
|
|
72
|
+
const dashboardHandoff = result.forecastRole === "backtest"
|
|
73
|
+
? {
|
|
74
|
+
status: "not_applicable",
|
|
75
|
+
reason: "backtest forecasts are immutable evidence and are not enqueued for an operating dashboard",
|
|
76
|
+
}
|
|
77
|
+
: artifactRef
|
|
78
|
+
? {
|
|
79
|
+
status: "deferred",
|
|
80
|
+
tool: "fpa_dashboard_refresh_queue",
|
|
81
|
+
action: "enqueue_artifact",
|
|
82
|
+
artifact_ref: artifactRef,
|
|
83
|
+
}
|
|
84
|
+
: undefined;
|
|
85
|
+
return {
|
|
86
|
+
status: "committed",
|
|
87
|
+
artifact_type: result.committed.artifactType,
|
|
88
|
+
forecast_role: result.forecastRole,
|
|
89
|
+
immutable_fingerprint: result.committed.fingerprint,
|
|
90
|
+
path: result.committed.path,
|
|
91
|
+
ledger_status: result.committed.ledgerStatus,
|
|
92
|
+
diagnostics: result.diagnostics as unknown as Record<string, unknown>,
|
|
93
|
+
data_quality: result.artifact.data_quality,
|
|
94
|
+
unsupported_metrics: result.artifact.unsupported_metrics,
|
|
95
|
+
conclusion: result.artifact.conclusion,
|
|
96
|
+
...(artifactRef ? { artifact_ref: artifactRef } : {}),
|
|
97
|
+
...(dashboardHandoff ? { dashboard_handoff: dashboardHandoff } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
|
|
6
6
|
import { composeApprovedForecast } from "./compose.ts";
|
|
7
|
+
import { finalizeApprovedForecast, forecastFinalizeDetails } from "./finalize.ts";
|
|
7
8
|
import {
|
|
8
9
|
commitArtifact,
|
|
9
10
|
commitArtifactFromPath,
|
|
@@ -170,4 +171,37 @@ export default function fpaArtifactsExtension(pi: ExtensionAPI): void {
|
|
|
170
171
|
});
|
|
171
172
|
},
|
|
172
173
|
});
|
|
174
|
+
|
|
175
|
+
pi.registerTool({
|
|
176
|
+
name: "fpa_forecast_finalize",
|
|
177
|
+
label: "Finalize Approved Forecast",
|
|
178
|
+
description:
|
|
179
|
+
"Compose, validate, assign the lifecycle role, and atomically commit an approved forecast from its compact plan. " +
|
|
180
|
+
"The role is derived from frozen_at relative to the target period: future is next_plan, in-period is eac, and ended periods are backtest. " +
|
|
181
|
+
"Use this as the single mutation seam for fpa-forecast-freeze; callers do not pass or translate forecast_role.",
|
|
182
|
+
promptSnippet: "Finalize and freeze a compact approved forecast plan",
|
|
183
|
+
promptGuidelines: [
|
|
184
|
+
"Pass only the compact plan path and immutable scope/cycle identity; forecast_role is derived deterministically.",
|
|
185
|
+
"A historical target is committed as backtest evidence and never promoted as an operating forecast.",
|
|
186
|
+
],
|
|
187
|
+
parameters: Type.Object({
|
|
188
|
+
plan_path: Type.String({
|
|
189
|
+
minLength: 1,
|
|
190
|
+
description: "Project-relative path to the compact forecast plan JSON file.",
|
|
191
|
+
}),
|
|
192
|
+
scope_id: Type.String({ minLength: 1, maxLength: 256 }),
|
|
193
|
+
cycle_id: Type.String({ minLength: 1, maxLength: 256 }),
|
|
194
|
+
}, { additionalProperties: false }),
|
|
195
|
+
executionMode: "sequential",
|
|
196
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
197
|
+
const plan = await readProjectJsonFile(ctx.cwd, params.plan_path, "plan_path");
|
|
198
|
+
const finalized = await finalizeApprovedForecast(
|
|
199
|
+
ctx.cwd,
|
|
200
|
+
plan,
|
|
201
|
+
{ scope_id: params.scope_id, cycle_id: params.cycle_id },
|
|
202
|
+
{ signal },
|
|
203
|
+
);
|
|
204
|
+
return toolResult(forecastFinalizeDetails(finalized));
|
|
205
|
+
},
|
|
206
|
+
});
|
|
173
207
|
}
|
|
@@ -168,9 +168,11 @@ function graphContextError(event: ToolCallEvent, graph: FpaGraph): string | unde
|
|
|
168
168
|
return "graph_run requires a context object whose values are strings.";
|
|
169
169
|
}
|
|
170
170
|
const values = context as Record<string, unknown>;
|
|
171
|
-
const required = graph === "fpa-strategy-planning"
|
|
171
|
+
const required = graph === "fpa-strategy-planning"
|
|
172
172
|
? ["scope_id", "cycle_id", "forecast_role"]
|
|
173
|
-
:
|
|
173
|
+
: graph === "fpa-forecast-freeze"
|
|
174
|
+
? ["scope_id", "cycle_id"]
|
|
175
|
+
: [];
|
|
174
176
|
const missing = required.filter((key) => !(key in values) || (typeof values[key] === "string" && values[key].trim().length === 0));
|
|
175
177
|
if (missing.length > 0) return `graph_run requires non-empty string context keys: ${missing.join(", ")}.`;
|
|
176
178
|
const nonStrings = Object.entries(values)
|
|
@@ -188,9 +190,12 @@ function routingInstruction(graph: FpaGraph, resumeCompletedGraph = false): stri
|
|
|
188
190
|
"Call graph_list first, then graph_run with that exact graph. All graph_run context values must be strings; never pass arrays, objects, numbers, booleans, or null.",
|
|
189
191
|
"Graph nodes own business phase execution. After a successful Graph handoff, only the main Agent may call the matching fpa_dashboard_publish_* tool; Graphs must never publish dashboard data.",
|
|
190
192
|
];
|
|
191
|
-
if (graph === "fpa-strategy-planning"
|
|
193
|
+
if (graph === "fpa-strategy-planning") {
|
|
192
194
|
instructions.push("Provide the immutable non-empty string context keys scope_id, cycle_id, and forecast_role. Put structured planning details in goal, not context.");
|
|
193
195
|
}
|
|
196
|
+
if (graph === "fpa-forecast-freeze") {
|
|
197
|
+
instructions.push("Provide only the immutable non-empty string context keys scope_id and cycle_id. The ledger forecast_role is derived from the frozen forecast target period; do not translate or pass a business role label.");
|
|
198
|
+
}
|
|
194
199
|
if (graph === "fpa-strategy-planning") {
|
|
195
200
|
instructions.push(resumeCompletedGraph
|
|
196
201
|
? "A completed combined Graph handoff was restored from this session. Continue modular publication from its persisted artifacts; do not rerun the Graph unless those artifacts are unavailable or the user changed the planning requirements. Publish period-review before next-strategy."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "FP&A 正式预测冻结:消费已审核策略交接与主 Agent 已提交的精确 strategy_decision
|
|
3
|
-
"maxSteps":
|
|
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
|
|
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}}
|
|
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": "
|
|
23
|
+
{ "equals": "blocked", "label": "确认无效", "to": "report_strategy_blocked" }
|
|
24
24
|
],
|
|
25
|
-
"default": { "label": "其他结果", "to": "
|
|
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": "
|
|
33
|
-
"label": "
|
|
32
|
+
"id": "report_strategy_blocked",
|
|
33
|
+
"label": "报告策略核验阻断",
|
|
34
34
|
"outputKey": "forecast_blocked",
|
|
35
35
|
"parseJson": true,
|
|
36
|
-
"prompt": "策略核验结果:{{data.confirmed_strategy}}\n
|
|
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,67 +41,51 @@
|
|
|
41
41
|
"agentName": "draft_forecast",
|
|
42
42
|
"id": "draft_forecast",
|
|
43
43
|
"label": "正式预测决策计划",
|
|
44
|
-
"next": "
|
|
44
|
+
"next": "route_draft_forecast",
|
|
45
45
|
"outputKey": "draft_forecast",
|
|
46
|
-
"
|
|
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
|
-
"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"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"agentName": "repair_forecast_plan",
|
|
64
|
-
"id": "repair_forecast_plan",
|
|
65
|
-
"label": "定点修正预测计划",
|
|
66
|
-
"next": "route_repair_forecast_plan",
|
|
67
|
-
"outputKey": "repair_forecast_plan",
|
|
68
|
-
"parseJson": true,
|
|
69
|
-
"prompt": "合成失败:{{data.__graphError}}\n\n读取 artifacts/forecast_plan.json、planning_brief 和 reviewed_strategy_handoff,只定点修正格式或 forecast plan 契约问题。不得通过删除、置零、重分配或伪造映射来修正数据质量问题;这类问题应由 compose 降级为 complete_with_limits。不得改变已确认策略的业务分配、范围、假设或条件;若错误要求此类变化,返回 blocked。不得调用任何 fpa_dashboard_* 工具。只输出 JSON:{\"status\":\"repaired|blocked\",\"changes\":[],\"blockers\":[]}",
|
|
70
|
-
"skills": ["fpa-forecast-approved-strategy"],
|
|
71
|
-
"systemPrompt": "你是预测计划修复 Agent,只按确定性合成报错做最小修正。",
|
|
72
|
-
"tools": ["read", "write", "edit"],
|
|
73
|
-
"type": "subagent"
|
|
74
|
-
},
|
|
75
53
|
{
|
|
76
54
|
"cases": [
|
|
77
|
-
{ "equals": "
|
|
78
|
-
{ "equals": "blocked", "label": "需要治理处理", "to": "
|
|
55
|
+
{ "equals": "ready", "label": "计划已写入", "to": "finalize_forecast" },
|
|
56
|
+
{ "equals": "blocked", "label": "需要治理处理", "to": "report_plan_blocked" }
|
|
79
57
|
],
|
|
80
|
-
"default": { "label": "其他结果", "to": "
|
|
81
|
-
"id": "
|
|
82
|
-
"label": "
|
|
83
|
-
"path": "data.
|
|
58
|
+
"default": { "label": "其他结果", "to": "report_plan_blocked" },
|
|
59
|
+
"id": "route_draft_forecast",
|
|
60
|
+
"label": "判断预测计划是否可冻结",
|
|
61
|
+
"path": "data.draft_forecast.status",
|
|
84
62
|
"type": "router"
|
|
85
63
|
},
|
|
64
|
+
{
|
|
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"
|
|
72
|
+
},
|
|
86
73
|
{
|
|
87
74
|
"args": {
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
"forecast_role": "{{context.forecast_role}}",
|
|
92
|
-
"scope_id": "{{context.scope_id}}"
|
|
93
|
-
}
|
|
75
|
+
"plan_path": "artifacts/forecast_plan.json",
|
|
76
|
+
"scope_id": "{{context.scope_id}}",
|
|
77
|
+
"cycle_id": "{{context.cycle_id}}"
|
|
94
78
|
},
|
|
95
|
-
"id": "
|
|
79
|
+
"id": "finalize_forecast",
|
|
96
80
|
"label": "冻结正式预测",
|
|
97
81
|
"mutates": true,
|
|
98
|
-
"outputKey": "
|
|
99
|
-
"tool": "
|
|
82
|
+
"outputKey": "finalize_forecast",
|
|
83
|
+
"tool": "fpa_forecast_finalize",
|
|
100
84
|
"type": "tool"
|
|
101
85
|
}
|
|
102
86
|
],
|
|
103
87
|
"schemaVersion": 1,
|
|
104
88
|
"start": "load_confirmed_strategy",
|
|
105
89
|
"transitionLabels": { "default": "其他情况", "error": "失败", "next": "继续" },
|
|
106
|
-
"version": "
|
|
90
|
+
"version": "3.0.0"
|
|
107
91
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viccydev/pi-fpa",
|
|
3
|
-
"version": "0.9.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
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
|
|
28
|
-
|
|
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
|
|
|
@@ -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.
|
|
40
|
-
`
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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 `
|
|
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.
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
|
@@ -86,7 +80,7 @@ gate.
|
|
|
86
80
|
|
|
87
81
|
Upstream identity or coverage defects are non-blocking data limits. Preserve
|
|
88
82
|
the full approved allocation, leave affected revenue and ROAS values `NULL`,
|
|
89
|
-
and let `
|
|
83
|
+
and let `fpa_forecast_finalize` emit `data_quality`, a coverage-labelled partial
|
|
90
84
|
`conclusion`, and actionable repair items. Never delete a bad slice, turn it
|
|
91
85
|
into zero spend, or renormalize the remaining allocation. The affected slice
|
|
92
86
|
remains ineligible for Actuals comparison and execution until its mapping is
|
|
@@ -105,7 +99,7 @@ governance failures.
|
|
|
105
99
|
|
|
106
100
|
## Completion
|
|
107
101
|
|
|
108
|
-
The forecast-freeze workflow ends only after `
|
|
102
|
+
The forecast-freeze workflow ends only after `fpa_forecast_finalize` returns
|
|
109
103
|
`status: committed`, an `immutable_fingerprint`, and (for assigned artifacts) a
|
|
110
104
|
`dashboard_handoff`. Commit freezes the business artifact but does not enqueue
|
|
111
105
|
or publish dashboard work. The calling main Agent owns that explicit handoff.
|
|
@@ -73,9 +73,10 @@ Do not include `immutable_fingerprint` in the tool input; a draft that carries o
|
|
|
73
73
|
Almost none of the artifact above is a decision. The decision is the allocation
|
|
74
74
|
and the ROAS assumption behind each slice; revenue, the ROAS write-back, the
|
|
75
75
|
consolidated roll-up, every unit and window string, and `frozen_at` all follow
|
|
76
|
-
from those by arithmetic. Write the plan and let `
|
|
77
|
-
the rest
|
|
78
|
-
instead of true if you typed
|
|
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.
|
|
79
80
|
|
|
80
81
|
```yaml
|
|
81
82
|
# artifacts/forecast_plan.json
|
|
@@ -111,10 +112,12 @@ Then:
|
|
|
111
112
|
|
|
112
113
|
```
|
|
113
114
|
write artifacts/forecast_plan.json
|
|
114
|
-
|
|
115
|
-
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>" }
|
|
116
116
|
```
|
|
117
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
|
+
|
|
118
121
|
Rules the plan has to respect, because compose enforces them:
|
|
119
122
|
|
|
120
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`.
|
|
@@ -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
|
|
36
|
-
|
|
37
|
-
`
|
|
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.
|
|
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.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|