@viccydev/pi-fpa 0.9.2 → 0.9.4
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
|
@@ -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.4
|
|
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.4` 对应 `v0.9.4`。工作流会检出该标签,执行 `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
|
|
|
@@ -76,12 +76,34 @@ if (args.includes("--once")) {
|
|
|
76
76
|
if (!abortController.signal.aborted) throw error;
|
|
77
77
|
}
|
|
78
78
|
} else {
|
|
79
|
+
// A failure that persists — a missing ledger, an artifact a subscription
|
|
80
|
+
// still references — repeats on every tick. Reprinting its stack every
|
|
81
|
+
// interval buries everything else in the log without adding information, so
|
|
82
|
+
// an unchanged failure is reported once in full and then only counted.
|
|
83
|
+
let lastFailure = null;
|
|
84
|
+
let repeatCount = 0;
|
|
79
85
|
while (!stopping) {
|
|
80
86
|
try {
|
|
81
87
|
await runOnce();
|
|
88
|
+
if (lastFailure) {
|
|
89
|
+
process.stderr.write(`${new Date().toISOString()} recovered after ${repeatCount + 1} consecutive failure(s)\n`);
|
|
90
|
+
lastFailure = null;
|
|
91
|
+
repeatCount = 0;
|
|
92
|
+
}
|
|
82
93
|
} catch (error) {
|
|
83
94
|
if (abortController.signal.aborted) break;
|
|
84
|
-
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
if (message === lastFailure) {
|
|
97
|
+
repeatCount += 1;
|
|
98
|
+
// Back off the log, not the work: still retried every interval.
|
|
99
|
+
if (repeatCount === 1 || repeatCount % 20 === 0) {
|
|
100
|
+
process.stderr.write(`${new Date().toISOString()} still failing (${repeatCount + 1}x): ${message}\n`);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
lastFailure = message;
|
|
104
|
+
repeatCount = 0;
|
|
105
|
+
process.stderr.write(`${new Date().toISOString()} ${error instanceof Error ? error.stack ?? message : message}\n`);
|
|
106
|
+
}
|
|
85
107
|
}
|
|
86
108
|
if (!stopping) await new Promise((resolveWait) => {
|
|
87
109
|
const timer = setTimeout(() => {
|
|
@@ -144,7 +144,21 @@ async function existingLedgerDirectories(projectRoot: string): Promise<{ root: s
|
|
|
144
144
|
const objects = join(root, "objects");
|
|
145
145
|
const entries = join(root, "entries");
|
|
146
146
|
for (const [path, label] of [[root, ".ledger"], [objects, ".ledger/objects"], [entries, ".ledger/entries"]] as const) {
|
|
147
|
-
|
|
147
|
+
let stat;
|
|
148
|
+
try {
|
|
149
|
+
stat = await lstat(path);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// A project with no ledger is a state a caller can act on — the
|
|
152
|
+
// artifacts were never committed through it, or the directory was
|
|
153
|
+
// removed out from under a subscription that still holds refs.
|
|
154
|
+
// Letting a raw ENOENT escape turns that into an unreadable stack
|
|
155
|
+
// trace in the coordinator's last_error, which the operator then
|
|
156
|
+
// has to reverse-engineer.
|
|
157
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
158
|
+
throw new Error(`This project has no artifact ledger (${label} is missing), so artifacts cannot be resolved by ref. Commit an artifact through the ledger before referencing one.`);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
148
162
|
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`${label} must be a regular directory, not a symlink.`);
|
|
149
163
|
}
|
|
150
164
|
return { root, objects, entries };
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../fpa-artifacts/store.ts";
|
|
12
12
|
import { dashboardBuildFingerprint, resolveDashboardDir } from "./publisher.ts";
|
|
13
13
|
import { publishDashboardProjection } from "./compat-publisher.ts";
|
|
14
|
+
import { readDashboardModuleManifest } from "./module-publisher.ts";
|
|
14
15
|
import { buildDashboardProjection } from "./service.ts";
|
|
15
16
|
|
|
16
17
|
const EVENT_MAX_BYTES = 64 * 1024;
|
|
@@ -178,6 +179,13 @@ export interface DashboardCoordinatorStatus {
|
|
|
178
179
|
scope_id: string | null;
|
|
179
180
|
cycle_id: string | null;
|
|
180
181
|
generation_id: string | null;
|
|
182
|
+
/**
|
|
183
|
+
* The modular manifest's current `dashboardRevision`, read from disk rather
|
|
184
|
+
* than from the subscription. `generation_id` only ever identifies a legacy
|
|
185
|
+
* whole-dashboard generation, so a consumer on a v2 manifest has nothing in
|
|
186
|
+
* the same hash space to compare against without this.
|
|
187
|
+
*/
|
|
188
|
+
dashboard_revision: string | null;
|
|
181
189
|
pending: number;
|
|
182
190
|
processing: number;
|
|
183
191
|
dead: number;
|
|
@@ -384,6 +392,13 @@ async function writeCoordinatorStatus(
|
|
|
384
392
|
await ensureDirectory(dashboardDir, "Dashboard directory");
|
|
385
393
|
const previous = await readCoordinatorStatus(cwd);
|
|
386
394
|
const queue = options.queue ?? await inspectDashboardRefreshQueue(cwd);
|
|
395
|
+
// Read the revision off the manifest instead of tracking it in the
|
|
396
|
+
// subscription: every publish path (legacy refresh, module publish,
|
|
397
|
+
// strategy transition) rewrites the manifest, so this is the one value that
|
|
398
|
+
// is correct no matter who wrote last.
|
|
399
|
+
const dashboardRevision = await readDashboardModuleManifest(cwd)
|
|
400
|
+
.then((manifest) => manifest?.dashboardRevision ?? null)
|
|
401
|
+
.catch(() => previous?.dashboard_revision ?? null);
|
|
387
402
|
const status: DashboardCoordinatorStatus = {
|
|
388
403
|
kind: "fpa.dashboard.coordinator-status",
|
|
389
404
|
schema_version: 1,
|
|
@@ -397,6 +412,7 @@ async function writeCoordinatorStatus(
|
|
|
397
412
|
scope_id: options.subscription?.scope_id ?? null,
|
|
398
413
|
cycle_id: options.subscription?.cycle_id ?? null,
|
|
399
414
|
generation_id: options.subscription?.last_generation_id ?? null,
|
|
415
|
+
dashboard_revision: dashboardRevision,
|
|
400
416
|
pending: queue.pending,
|
|
401
417
|
processing: queue.processing,
|
|
402
418
|
dead: queue.dead,
|
|
@@ -6,7 +6,14 @@ import { stableJson } from "../fpa-artifacts/store.ts";
|
|
|
6
6
|
import type { DashboardWidget } from "./projector.ts";
|
|
7
7
|
import { validateDatasetForWidget } from "./schema.ts";
|
|
8
8
|
|
|
9
|
-
export type DashboardModuleId =
|
|
9
|
+
export type DashboardModuleId =
|
|
10
|
+
| "portfolio-overview"
|
|
11
|
+
| "period-review"
|
|
12
|
+
| "next-strategy"
|
|
13
|
+
| "next-forecast"
|
|
14
|
+
| "forecast-accuracy"
|
|
15
|
+
| "decision-ledger"
|
|
16
|
+
| "execution-evidence";
|
|
10
17
|
export type DashboardModuleStatus = "published" | "awaiting_decision" | "approved" | "changes_requested" | "generating" | "superseded";
|
|
11
18
|
|
|
12
19
|
export interface DashboardModuleInteraction {
|
|
@@ -20,6 +27,14 @@ export interface DashboardModuleBuild {
|
|
|
20
27
|
status: DashboardModuleStatus;
|
|
21
28
|
source: Record<string, unknown>;
|
|
22
29
|
widgets: DashboardWidget[];
|
|
30
|
+
/**
|
|
31
|
+
* Decision limits the projector found while building this module — an
|
|
32
|
+
* unverified execution receipt, approved slices with no Actuals, an
|
|
33
|
+
* incomplete close. They belong to the module they were computed for, so
|
|
34
|
+
* the consumer can show them next to the numbers they qualify instead of
|
|
35
|
+
* pooling every module's caveats into one banner nobody reads.
|
|
36
|
+
*/
|
|
37
|
+
warnings?: string[];
|
|
23
38
|
interaction?: DashboardModuleInteraction;
|
|
24
39
|
}
|
|
25
40
|
|
|
@@ -64,7 +79,15 @@ export interface DashboardManifestV2 {
|
|
|
64
79
|
modules: Partial<Record<DashboardModuleId, DashboardModuleManifestEntry>>;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
|
-
const MODULE_IDS: DashboardModuleId[] = [
|
|
82
|
+
const MODULE_IDS: DashboardModuleId[] = [
|
|
83
|
+
"portfolio-overview",
|
|
84
|
+
"period-review",
|
|
85
|
+
"next-strategy",
|
|
86
|
+
"next-forecast",
|
|
87
|
+
"forecast-accuracy",
|
|
88
|
+
"decision-ledger",
|
|
89
|
+
"execution-evidence",
|
|
90
|
+
];
|
|
68
91
|
const MODULE_ID_SET = new Set<string>(MODULE_IDS);
|
|
69
92
|
const DATASET_FILE_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,191}\.json$/;
|
|
70
93
|
|
|
@@ -136,6 +159,12 @@ function assertModule(module: DashboardModuleBuild): void {
|
|
|
136
159
|
if (!MODULE_ID_SET.has(module.id)) throw new Error(`Unsupported dashboard module id "${module.id}".`);
|
|
137
160
|
if (!module.title?.trim()) throw new Error("Dashboard module title is required.");
|
|
138
161
|
if (!Array.isArray(module.widgets) || module.widgets.length === 0 || module.widgets.length > 64) throw new Error("Dashboard module must contain between 1 and 64 widgets.");
|
|
162
|
+
if (module.warnings !== undefined) {
|
|
163
|
+
if (!Array.isArray(module.warnings) || module.warnings.length > 128) throw new Error("Dashboard module warnings must be an array of at most 128 entries.");
|
|
164
|
+
for (const warning of module.warnings) {
|
|
165
|
+
if (typeof warning !== "string" || warning.length > 4_096) throw new Error("Each dashboard module warning must be a string of at most 4096 characters.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
139
168
|
if (module.interaction && (module.id !== "next-strategy" || module.status !== "awaiting_decision")) {
|
|
140
169
|
throw new Error("Strategy decisions are available only on an awaiting next-strategy module.");
|
|
141
170
|
}
|
|
@@ -209,6 +238,9 @@ async function normalizeLegacyModuleReceipt(
|
|
|
209
238
|
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("Legacy dashboard build receipt must be a regular file.");
|
|
210
239
|
const old = JSON.parse(await readFile(sourcePath, "utf8")) as { source?: unknown; warnings?: unknown; datasets?: unknown };
|
|
211
240
|
if (!Array.isArray(old.datasets)) throw new Error("Legacy dashboard build receipt datasets are invalid.");
|
|
241
|
+
// The legacy generation receipt carried warnings at the top level too, so
|
|
242
|
+
// they migrate straight across rather than being buried inside `source`.
|
|
243
|
+
const legacyWarnings = Array.isArray(old.warnings) ? old.warnings.filter((entry): entry is string => typeof entry === "string") : [];
|
|
212
244
|
const receipt = {
|
|
213
245
|
kind: "fpa.dashboard.module.build",
|
|
214
246
|
schema_version: 1,
|
|
@@ -216,7 +248,8 @@ async function normalizeLegacyModuleReceipt(
|
|
|
216
248
|
module_revision: legacy.revision,
|
|
217
249
|
published_at: legacy.updatedAt,
|
|
218
250
|
status: legacy.status,
|
|
219
|
-
source: { ...(old.source && typeof old.source === "object" ? old.source : {}),
|
|
251
|
+
source: { ...(old.source && typeof old.source === "object" ? old.source : {}), legacy_generation_id: legacy.revision },
|
|
252
|
+
warnings: legacyWarnings,
|
|
220
253
|
datasets: old.datasets,
|
|
221
254
|
};
|
|
222
255
|
const contents = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
@@ -290,6 +323,7 @@ export async function publishDashboardModule(options: PublishDashboardModuleOpti
|
|
|
290
323
|
published_at: publishedAt,
|
|
291
324
|
status: options.module.status,
|
|
292
325
|
source: options.module.source,
|
|
326
|
+
warnings: options.module.warnings ?? [],
|
|
293
327
|
datasets: datasets.map(({ widget, filename, digest }) => ({ logical: widget.dataset, filename, sha256: digest })),
|
|
294
328
|
};
|
|
295
329
|
const receiptContents = `${JSON.stringify(receipt, null, 2)}\n`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viccydev/pi-fpa",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
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",
|
|
@@ -19,17 +19,20 @@
|
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=22.19"
|
|
21
21
|
},
|
|
22
|
+
"fpaDashboard": {
|
|
23
|
+
"contractVersion": 1
|
|
24
|
+
},
|
|
22
25
|
"files": [
|
|
23
26
|
"README.md",
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
"bin",
|
|
28
|
+
"graphs",
|
|
26
29
|
"prompts",
|
|
27
30
|
"skills",
|
|
28
31
|
"extensions"
|
|
29
32
|
],
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
"bin": {
|
|
34
|
+
"fpa-dashboard-worker": "./bin/fpa-dashboard-worker.mjs"
|
|
35
|
+
},
|
|
33
36
|
"scripts": {
|
|
34
37
|
"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
38
|
"test:structure": "node tests/package-structure.test.mjs",
|
|
@@ -59,7 +62,7 @@
|
|
|
59
62
|
"./skills"
|
|
60
63
|
],
|
|
61
64
|
"extensions": [
|
|
62
|
-
|
|
65
|
+
"./extensions/fpa-routing-guard/index.ts",
|
|
63
66
|
"./extensions/fpa-data/index.ts",
|
|
64
67
|
"./extensions/fpa-artifacts/index.ts",
|
|
65
68
|
"./extensions/fpa-dashboard/index.ts"
|