@viccydev/pi-fpa 0.7.2 → 0.8.0

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 CHANGED
@@ -148,12 +148,12 @@ pi list
148
148
  团队分发建议使用固定 Git tag:
149
149
 
150
150
  ```bash
151
- pi install git:github.com/linyqh/pi-fpa@v0.7.2
151
+ pi install git:github.com/linyqh/pi-fpa@v0.8.0
152
152
  ```
153
153
 
154
154
  ## 发布到 npm
155
155
 
156
- 发布动作由 GitHub Release 触发。Release 标签必须严格使用 `v<package.json version>`,例如版本 `0.7.2` 对应 `v0.7.2`。工作流会检出该标签,执行 `npm ci`、`npm test` 和包内容预检,全部通过后发布公开包 `@viccydev/pi-fpa`。普通 Release 发布到 `latest`,Prerelease 发布到 `next`。
156
+ 发布动作由 GitHub Release 触发。Release 标签必须严格使用 `v<package.json version>`,例如版本 `0.8.0` 对应 `v0.8.0`。工作流会检出该标签,执行 `npm ci`、`npm test` 和包内容预检,全部通过后发布公开包 `@viccydev/pi-fpa`。普通 Release 发布到 `latest`,Prerelease 发布到 `next`。
157
157
 
158
158
  发布认证使用 npm Trusted Publishing / OIDC,不使用长期 npm Token。npm 包后台的 Trusted Publisher 配置为:
159
159
 
@@ -0,0 +1,45 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import type { DashboardBuild } from "./projector.ts";
5
+ import type { DashboardProjectorProvenance, DashboardProjectorRuntime } from "./provenance.ts";
6
+ import { publishDashboard, resolveDashboardDir } from "./publisher.ts";
7
+ import { publishDashboardModule, readDashboardModuleManifest } from "./module-publisher.ts";
8
+ import type { DashboardActualsSnapshot } from "./source.ts";
9
+
10
+ interface Options {
11
+ cwd: string;
12
+ build: DashboardBuild;
13
+ projector: DashboardProjectorProvenance;
14
+ runtime: DashboardProjectorRuntime;
15
+ actualsSnapshot?: DashboardActualsSnapshot;
16
+ allowBacktestDisplay?: boolean;
17
+ }
18
+
19
+ export async function publishDashboardProjection(options: Options): Promise<{ dashboardDir: string; fingerprint: string; publishedDatasets: number }> {
20
+ const dashboardDir = await resolveDashboardDir(options.cwd);
21
+ let schemaVersion: number | undefined;
22
+ try {
23
+ schemaVersion = (JSON.parse(await readFile(join(dashboardDir, "manifest.json"), "utf8")) as { schemaVersion?: number }).schemaVersion;
24
+ } catch (error) {
25
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
26
+ }
27
+ if (schemaVersion !== 2) return publishDashboard(options);
28
+ if (options.build.source.forecast_role === "backtest" && options.allowBacktestDisplay !== true) {
29
+ throw new Error("A backtest cannot replace dashboard execution evidence without explicit user authorization.");
30
+ }
31
+ const current = await readDashboardModuleManifest(options.cwd);
32
+ const published = await publishDashboardModule({
33
+ cwd: options.cwd,
34
+ dashboardTitle: options.build.title,
35
+ expectedDashboardRevision: current?.dashboardRevision ?? null,
36
+ module: {
37
+ id: "execution-evidence",
38
+ title: "经营执行与校准证据",
39
+ status: "published",
40
+ source: { ...options.build.source, warnings: options.build.warnings, projector: options.projector, runtime: options.runtime },
41
+ widgets: options.build.widgets,
42
+ },
43
+ });
44
+ return { dashboardDir: published.dashboardDir, fingerprint: published.moduleRevision, publishedDatasets: published.publishedDatasets };
45
+ }
@@ -9,7 +9,8 @@ import {
9
9
  type ArtifactRefV2,
10
10
  type ForecastRole,
11
11
  } from "../fpa-artifacts/store.ts";
12
- import { dashboardBuildFingerprint, publishDashboard, resolveDashboardDir } from "./publisher.ts";
12
+ import { dashboardBuildFingerprint, resolveDashboardDir } from "./publisher.ts";
13
+ import { publishDashboardProjection } from "./compat-publisher.ts";
13
14
  import { buildDashboardProjection } from "./service.ts";
14
15
 
15
16
  const EVENT_MAX_BYTES = 64 * 1024;
@@ -723,7 +724,7 @@ async function defaultWorker(event: DashboardRefreshEvent, projectRoot: string,
723
724
  if (event.desired_generation_id && generationId !== event.desired_generation_id) {
724
725
  throw new ObsoleteDashboardRefreshError("Dashboard inputs changed after polling; the stale desired generation must be replanned.");
725
726
  }
726
- await publishDashboard({ cwd: projectRoot, build: result.build, projector: result.projector, runtime: result.runtime, actualsSnapshot: result.actuals });
727
+ await publishDashboardProjection({ cwd: projectRoot, build: result.build, projector: result.projector, runtime: result.runtime, actualsSnapshot: result.actuals });
727
728
  return {
728
729
  generation_id: generationId,
729
730
  data_as_of: result.build.source.data_as_of,
@@ -2,8 +2,19 @@ import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
4
 
5
- import { readArtifactByRef, type ArtifactRefV2 } from "../fpa-artifacts/store.ts";
6
- import { dashboardBuildFingerprint, publishDashboard } from "./publisher.ts";
5
+ import { readArtifactByRef, readProjectJsonFile, type ArtifactRefV2 } from "../fpa-artifacts/store.ts";
6
+ import { dashboardBuildFingerprint } from "./publisher.ts";
7
+ import { publishDashboardProjection } from "./compat-publisher.ts";
8
+ import {
9
+ dashboardModuleBuildFingerprint,
10
+ publishDashboardModule,
11
+ readDashboardModuleManifest,
12
+ readDashboardModuleSource,
13
+ transitionStrategyModule,
14
+ } from "./module-publisher.ts";
15
+ import { projectForecastModule, projectReviewModule, projectStrategyModule } from "./stage-projector.ts";
16
+ import type { ApprovedCycleForecast } from "../fpa-artifacts/contracts.ts";
17
+ import { commitStrategyDecision, createStrategyDecisionRequest, readCommittedStrategyDecision } from "./strategy-decision.ts";
7
18
  import {
8
19
  enqueueDashboardRefresh,
9
20
  enqueueDashboardRefreshForArtifact,
@@ -28,6 +39,220 @@ export default function fpaDashboardExtension(pi: ExtensionAPI): void {
28
39
  entry_id: Type.String({ pattern: "^[a-f0-9]{64}$" }),
29
40
  body_fingerprint: Type.String({ pattern: "^[a-f0-9]{64}$" }),
30
41
  }, { additionalProperties: false });
42
+ pi.registerTool({
43
+ name: "fpa_dashboard_module_status",
44
+ label: "FP&A Dashboard Module Status",
45
+ description: "Inspect independently published FP&A dashboard modules and their current dashboard revision without changing files.",
46
+ promptSnippet: "Inspect the independently published FP&A dashboard modules",
47
+ parameters: Type.Object({}, { additionalProperties: false }),
48
+ executionMode: "parallel",
49
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
50
+ const manifest = await readDashboardModuleManifest(ctx.cwd);
51
+ return toolResult(manifest ? {
52
+ status: "available",
53
+ dashboard_revision: manifest.dashboardRevision,
54
+ module_order: manifest.moduleOrder,
55
+ modules: Object.fromEntries(Object.entries(manifest.modules).map(([id, module]) => [id, module ? {
56
+ status: module.status,
57
+ revision: module.revision,
58
+ updated_at: module.updatedAt,
59
+ interactive: module.interaction?.kind === "strategy-decision",
60
+ } : null])),
61
+ } : { status: "missing" });
62
+ },
63
+ });
64
+
65
+ pi.registerTool({
66
+ name: "fpa_dashboard_publish_review",
67
+ label: "Publish FP&A Period Review Module",
68
+ description: "Preview or publish only the period-review dashboard module from a project driver_analysis JSON artifact. Other dashboard modules are preserved.",
69
+ promptSnippet: "Publish the completed period analysis as its own dashboard module",
70
+ promptGuidelines: [
71
+ "This tool is called by the main Agent after the analysis Graph returns; never add it to a Graph tool allowlist.",
72
+ "Preview first, then publish with the exact preview fingerprint and dashboard revision.",
73
+ ],
74
+ parameters: Type.Object({
75
+ mode: StringEnum(["preview", "publish"] as const),
76
+ analysis_path: Type.String({ minLength: 1 }),
77
+ expected_preview_fingerprint: Type.Optional(Type.String({ pattern: "^[a-f0-9]{64}$" })),
78
+ expected_dashboard_revision: Type.Optional(Type.Union([Type.String({ pattern: "^[a-f0-9]{64}$" }), Type.Null()])),
79
+ dashboard_title: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
80
+ }, { additionalProperties: false }),
81
+ executionMode: "sequential",
82
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
83
+ const module = projectReviewModule(await readProjectJsonFile(ctx.cwd, params.analysis_path, "analysis_path"));
84
+ const previewFingerprint = dashboardModuleBuildFingerprint(module);
85
+ const current = await readDashboardModuleManifest(ctx.cwd);
86
+ if (params.mode === "preview") return toolResult({
87
+ status: "ready",
88
+ module_id: module.id,
89
+ preview_fingerprint: previewFingerprint,
90
+ dashboard_revision: current?.dashboardRevision ?? null,
91
+ widget_count: module.widgets.length,
92
+ });
93
+ if (params.expected_preview_fingerprint !== previewFingerprint) throw new Error("Review module inputs changed after preview; preview again before publishing.");
94
+ if (params.expected_dashboard_revision === undefined) throw new Error("Publish requires expected_dashboard_revision from preview, including null for a new dashboard.");
95
+ const published = await publishDashboardModule({
96
+ cwd: ctx.cwd,
97
+ module,
98
+ dashboardTitle: params.dashboard_title,
99
+ expectedDashboardRevision: params.expected_dashboard_revision,
100
+ });
101
+ return toolResult({ status: "published", module_id: module.id, preview_fingerprint: previewFingerprint, dashboard_revision: published.dashboardRevision, module_revision: published.moduleRevision });
102
+ },
103
+ });
104
+
105
+ pi.registerTool({
106
+ name: "fpa_dashboard_publish_strategy",
107
+ label: "Publish FP&A Strategy Module",
108
+ description: "Preview or publish only the reviewed next-strategy module and create a server-side decision action bound to the calling main session. Other modules are preserved.",
109
+ promptSnippet: "Publish the reviewed strategy for human confirmation in the dashboard",
110
+ promptGuidelines: [
111
+ "Call this from the main Agent after the strategy Graph returns; Graph nodes must never publish dashboard modules.",
112
+ "Preview first. Publishing binds the confirmation action to this exact main session and reviewed handoff fingerprint.",
113
+ ],
114
+ parameters: Type.Object({
115
+ mode: StringEnum(["preview", "publish"] as const),
116
+ proposal_path: Type.String({ minLength: 1 }),
117
+ handoff_path: Type.String({ minLength: 1 }),
118
+ expected_preview_fingerprint: Type.Optional(Type.String({ pattern: "^[a-f0-9]{64}$" })),
119
+ expected_dashboard_revision: Type.Optional(Type.Union([Type.String({ pattern: "^[a-f0-9]{64}$" }), Type.Null()])),
120
+ dashboard_title: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
121
+ }, { additionalProperties: false }),
122
+ executionMode: "sequential",
123
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
124
+ const proposal = await readProjectJsonFile(ctx.cwd, params.proposal_path, "proposal_path");
125
+ const handoff = await readProjectJsonFile(ctx.cwd, params.handoff_path, "handoff_path");
126
+ const module = projectStrategyModule(proposal, handoff);
127
+ const previewFingerprint = dashboardModuleBuildFingerprint(module);
128
+ const current = await readDashboardModuleManifest(ctx.cwd);
129
+ if (params.mode === "preview") return toolResult({
130
+ status: "ready",
131
+ module_id: module.id,
132
+ preview_fingerprint: previewFingerprint,
133
+ dashboard_revision: current?.dashboardRevision ?? null,
134
+ strategy_version: module.source.strategy_version,
135
+ widget_count: module.widgets.length,
136
+ });
137
+ if (params.expected_preview_fingerprint !== previewFingerprint) throw new Error("Strategy module inputs changed after preview; preview again before publishing.");
138
+ if (params.expected_dashboard_revision === undefined) throw new Error("Publish requires expected_dashboard_revision from preview, including null for a new dashboard.");
139
+ const handoffFingerprint = module.source.handoff_fingerprint as string;
140
+ const request = await createStrategyDecisionRequest(ctx.cwd, {
141
+ sessionId: ctx.sessionManager.getSessionId(),
142
+ strategyVersion: module.source.strategy_version as string,
143
+ handoffFingerprint,
144
+ });
145
+ module.interaction = { kind: "strategy-decision", action_id: request.actionId };
146
+ const published = await publishDashboardModule({
147
+ cwd: ctx.cwd,
148
+ module,
149
+ dashboardTitle: params.dashboard_title,
150
+ expectedDashboardRevision: params.expected_dashboard_revision,
151
+ });
152
+ return toolResult({
153
+ status: "awaiting_decision",
154
+ module_id: module.id,
155
+ preview_fingerprint: previewFingerprint,
156
+ dashboard_revision: published.dashboardRevision,
157
+ module_revision: published.moduleRevision,
158
+ action_id: request.actionId,
159
+ });
160
+ },
161
+ });
162
+
163
+ pi.registerTool({
164
+ name: "fpa_dashboard_publish_forecast",
165
+ label: "Publish FP&A Forecast Module",
166
+ description: "Preview or publish only the next-forecast dashboard module from an exact committed approved forecast. Other dashboard modules are preserved.",
167
+ promptSnippet: "Publish the confirmed strategy forecast as its own dashboard module",
168
+ promptGuidelines: [
169
+ "Call this from the main Agent only after fpa_strategy_decision_commit confirmed the exact strategy and the forecast Graph returned an immutable forecast ref.",
170
+ "Never add this tool to a Graph tool allowlist. Preview first and publish with the exact preview fingerprint and dashboard revision.",
171
+ ],
172
+ parameters: Type.Object({
173
+ mode: StringEnum(["preview", "publish"] as const),
174
+ forecast_ref: artifactRefSchema,
175
+ strategy_decision: Type.Object({
176
+ action_id: Type.String({ pattern: "^[a-f0-9]{64}$" }),
177
+ decision_fingerprint: Type.String({ pattern: "^[a-f0-9]{64}$" }),
178
+ }, { additionalProperties: false }),
179
+ expected_preview_fingerprint: Type.Optional(Type.String({ pattern: "^[a-f0-9]{64}$" })),
180
+ expected_dashboard_revision: Type.Optional(Type.Union([Type.String({ pattern: "^[a-f0-9]{64}$" }), Type.Null()])),
181
+ dashboard_title: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
182
+ }, { additionalProperties: false }),
183
+ executionMode: "sequential",
184
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
185
+ const decision = await readCommittedStrategyDecision(ctx.cwd, params.strategy_decision.action_id, params.strategy_decision.decision_fingerprint);
186
+ if (decision.decision !== "confirm") throw new Error("Forecast publication requires a confirmed strategy decision.");
187
+ const read = await readArtifactByRef(ctx.cwd, params.forecast_ref as ArtifactRefV2);
188
+ if (read.artifact.artifact_type !== "approved_cycle_forecast") throw new Error("Forecast module requires an approved_cycle_forecast artifact ref.");
189
+ const forecast = read.artifact as ApprovedCycleForecast;
190
+ if (forecast.status === "blocked" || !forecast.approval_conditions_satisfied) throw new Error("Forecast module requires a completed forecast whose approval conditions are satisfied.");
191
+ if (forecast.strategy_version !== decision.strategy_version) throw new Error("Forecast strategy_version does not match the confirmed strategy decision.");
192
+ const currentStrategy = (await readDashboardModuleManifest(ctx.cwd))?.modules["next-strategy"];
193
+ if (!currentStrategy || currentStrategy.status !== "approved") throw new Error("Forecast publication requires the current strategy dashboard module to be approved.");
194
+ const strategySource = await readDashboardModuleSource(ctx.cwd, "next-strategy");
195
+ if (strategySource?.strategy_version !== decision.strategy_version || strategySource?.handoff_fingerprint !== decision.handoff_fingerprint) {
196
+ throw new Error("Approved strategy module does not match the confirmed decision lineage.");
197
+ }
198
+ const module = projectForecastModule(forecast, params.forecast_ref as ArtifactRefV2);
199
+ const previewFingerprint = dashboardModuleBuildFingerprint(module);
200
+ const current = await readDashboardModuleManifest(ctx.cwd);
201
+ if (params.mode === "preview") return toolResult({
202
+ status: "ready",
203
+ module_id: module.id,
204
+ preview_fingerprint: previewFingerprint,
205
+ dashboard_revision: current?.dashboardRevision ?? null,
206
+ forecast_version: forecast.forecast_version,
207
+ widget_count: module.widgets.length,
208
+ });
209
+ if (params.expected_preview_fingerprint !== previewFingerprint) throw new Error("Forecast module inputs changed after preview; preview again before publishing.");
210
+ if (params.expected_dashboard_revision === undefined) throw new Error("Publish requires expected_dashboard_revision from preview, including null for a new dashboard.");
211
+ const published = await publishDashboardModule({
212
+ cwd: ctx.cwd,
213
+ module,
214
+ dashboardTitle: params.dashboard_title,
215
+ expectedDashboardRevision: params.expected_dashboard_revision,
216
+ });
217
+ return toolResult({ status: "published", module_id: module.id, preview_fingerprint: previewFingerprint, dashboard_revision: published.dashboardRevision, module_revision: published.moduleRevision });
218
+ },
219
+ });
220
+
221
+ pi.registerTool({
222
+ name: "fpa_strategy_decision_commit",
223
+ label: "Commit FP&A Strategy Decision",
224
+ description: "Commit an idempotent human strategy confirmation or change request from the original main session, then transition the strategy dashboard module.",
225
+ promptSnippet: "Commit the dashboard strategy decision in the original main session",
226
+ parameters: Type.Object({
227
+ action_id: Type.String({ pattern: "^[a-f0-9]{64}$" }),
228
+ decision: StringEnum(["confirm", "request_changes"] as const),
229
+ feedback: Type.Optional(Type.String({ minLength: 1, maxLength: 8000 })),
230
+ }, { additionalProperties: false }),
231
+ executionMode: "sequential",
232
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
233
+ const committed = await commitStrategyDecision(ctx.cwd, {
234
+ actionId: params.action_id,
235
+ sessionId: ctx.sessionManager.getSessionId(),
236
+ decision: params.decision,
237
+ feedback: params.feedback,
238
+ });
239
+ const transitioned = await transitionStrategyModule(ctx.cwd, {
240
+ actionId: params.action_id,
241
+ status: params.decision === "confirm" ? "approved" : "changes_requested",
242
+ });
243
+ return toolResult({
244
+ status: "committed",
245
+ decision: committed.decision,
246
+ decision_fingerprint: committed.decisionFingerprint,
247
+ decision_path: committed.path,
248
+ strategy_version: committed.strategyVersion,
249
+ handoff_fingerprint: committed.handoffFingerprint,
250
+ dashboard_revision: transitioned.dashboardRevision,
251
+ next_stage: params.decision === "confirm" ? "run fpa-forecast-freeze Graph, then publish next-forecast module" : "rerun strategy planning with the exact feedback, then republish next-strategy module",
252
+ });
253
+ },
254
+ });
255
+
31
256
  pi.registerTool({
32
257
  name: "fpa_dashboard_status",
33
258
  label: "FP&A Dashboard Status",
@@ -157,7 +382,7 @@ export default function fpaDashboardExtension(pi: ExtensionAPI): void {
157
382
  if (params.expected_preview_fingerprint !== previewFingerprint) {
158
383
  throw new Error("Dashboard inputs changed after preview; run preview again before publishing.");
159
384
  }
160
- const published = await publishDashboard({
385
+ const published = await publishDashboardProjection({
161
386
  cwd: ctx.cwd,
162
387
  build,
163
388
  projector,