@viccydev/pi-fpa 0.9.1 → 0.9.2
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.2
|
|
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.2` 对应 `v0.9.2`。工作流会检出该标签,执行 `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
|
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { stableJson } from "../fpa-artifacts/store.ts";
|
|
4
4
|
import type { DashboardModuleBuild } from "./module-publisher.ts";
|
|
5
|
-
import type { TableCell } from "./projector.ts";
|
|
5
|
+
import type { TableCell, TableDataset } from "./projector.ts";
|
|
6
6
|
import type { ApprovedCycleForecast } from "../fpa-artifacts/contracts.ts";
|
|
7
7
|
import type { ArtifactRefV2 } from "../fpa-artifacts/store.ts";
|
|
8
8
|
|
|
@@ -56,10 +56,80 @@ function strategyAction(value: unknown, label: string): StrategyAction {
|
|
|
56
56
|
throw new Error(`${label} must be one of grow, hold, cut, stop.`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function signedPercent(value: unknown): TableCell {
|
|
60
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return cell(value);
|
|
61
|
+
return `${value > 0 ? "+" : ""}${(value * 100).toFixed(1)}%`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `metric` repeats across scopes, so the scope is what makes a headline row identifiable. */
|
|
65
|
+
function headlineLabel(entry: Record<string, unknown>, index: number): string {
|
|
66
|
+
const metric = requiredString(entry.metric ?? `Metric ${index + 1}`, `driver_analysis.headline_results[${index}].metric`);
|
|
67
|
+
return typeof entry.scope === "string" && entry.scope.trim() !== "" ? `${metric} · ${entry.scope}` : metric;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The `driver_analysis` contract publishes `headline_results` as a list of metric rows
|
|
72
|
+
* carrying actual/baseline/variance. Earlier artifacts used a flat `{ metric: value }`
|
|
73
|
+
* map, so both shapes are projected here rather than rejecting the contract shape.
|
|
74
|
+
*/
|
|
75
|
+
function headlineTable(value: unknown): TableDataset {
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return {
|
|
78
|
+
label: "核心结果",
|
|
79
|
+
columns: [
|
|
80
|
+
{ key: "metric", label: "指标" },
|
|
81
|
+
{ key: "actual", label: "实际", align: "right" },
|
|
82
|
+
{ key: "baseline", label: "基准", align: "right" },
|
|
83
|
+
{ key: "variance", label: "变化", align: "right" },
|
|
84
|
+
{ key: "variance_pct", label: "变化率", align: "right" },
|
|
85
|
+
],
|
|
86
|
+
rows: value.map((item, index) => {
|
|
87
|
+
const entry = record(item, `driver_analysis.headline_results[${index}]`);
|
|
88
|
+
return {
|
|
89
|
+
metric: headlineLabel(entry, index),
|
|
90
|
+
actual: cell(entry.actual),
|
|
91
|
+
baseline: cell(entry.baseline),
|
|
92
|
+
variance: cell(entry.variance),
|
|
93
|
+
variance_pct: signedPercent(entry.variance_pct),
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const headline = record(value ?? {}, "driver_analysis.headline_results");
|
|
99
|
+
return {
|
|
100
|
+
label: "核心结果",
|
|
101
|
+
columns: [{ key: "metric", label: "指标" }, { key: "value", label: "结果", align: "right" }],
|
|
102
|
+
rows: Object.entries(headline).map(([metric, entry]) => ({ metric, value: cell(entry) })),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Evidence is a list in the contract; a bare JSON dump of it is unreadable in a table cell. */
|
|
107
|
+
function listCell(value: unknown): TableCell {
|
|
108
|
+
return Array.isArray(value) ? lines(value) : cell(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* `data_limits` is the contract field and `limitations` is what older artifacts wrote.
|
|
113
|
+
* They are merged instead of ranked so a published module never silently drops one set.
|
|
114
|
+
*/
|
|
115
|
+
function limitationRows(analysis: Record<string, unknown>): Array<Record<string, TableCell>> {
|
|
116
|
+
const seen = new Set<string>();
|
|
117
|
+
const rows: Array<Record<string, TableCell>> = [];
|
|
118
|
+
for (const key of ["data_limits", "limitations", "risks"] as const) {
|
|
119
|
+
if (analysis[key] === undefined) continue;
|
|
120
|
+
for (const item of array(analysis[key], `driver_analysis.${key}`)) {
|
|
121
|
+
const dedupe = stableJson(item);
|
|
122
|
+
if (seen.has(dedupe)) continue;
|
|
123
|
+
seen.add(dedupe);
|
|
124
|
+
rows.push({ item: cell(item) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return rows;
|
|
128
|
+
}
|
|
129
|
+
|
|
59
130
|
export function projectReviewModule(input: unknown): DashboardModuleBuild {
|
|
60
131
|
const analysis = record(input, "driver_analysis");
|
|
61
132
|
if (analysis.artifact_type !== "driver_analysis") throw new Error("Review publication requires artifact_type=driver_analysis.");
|
|
62
|
-
const headline = record(analysis.headline_results ?? {}, "driver_analysis.headline_results");
|
|
63
133
|
const drivers = array(analysis.drivers ?? [], "driver_analysis.drivers");
|
|
64
134
|
return {
|
|
65
135
|
id: "period-review",
|
|
@@ -75,25 +145,25 @@ export function projectReviewModule(input: unknown): DashboardModuleBuild {
|
|
|
75
145
|
{
|
|
76
146
|
id: "review-headline",
|
|
77
147
|
type: "table",
|
|
78
|
-
span: "
|
|
148
|
+
span: "full",
|
|
79
149
|
dataset: "review-headline.json",
|
|
80
|
-
data:
|
|
81
|
-
label: "核心结果",
|
|
82
|
-
columns: [{ key: "metric", label: "指标" }, { key: "value", label: "结果", align: "right" }],
|
|
83
|
-
rows: Object.entries(headline).map(([metric, value]) => ({ metric, value: cell(value) })),
|
|
84
|
-
},
|
|
150
|
+
data: headlineTable(analysis.headline_results),
|
|
85
151
|
},
|
|
86
152
|
{
|
|
87
153
|
id: "review-drivers",
|
|
88
154
|
type: "table",
|
|
89
|
-
span: "
|
|
155
|
+
span: "full",
|
|
90
156
|
dataset: "review-drivers.json",
|
|
91
157
|
data: {
|
|
92
158
|
label: "关键驱动",
|
|
93
|
-
columns: [{ key: "driver", label: "驱动" }, { key: "impact", label: "影响" }, { key: "evidence", label: "证据" }],
|
|
159
|
+
columns: [{ key: "driver", label: "驱动" }, { key: "impact", label: "影响", align: "right" }, { key: "evidence", label: "证据" }],
|
|
94
160
|
rows: drivers.map((item, index) => {
|
|
95
161
|
const driver = record(item, `driver_analysis.drivers[${index}]`);
|
|
96
|
-
return {
|
|
162
|
+
return {
|
|
163
|
+
driver: cell(driver.driver ?? driver.name ?? `Driver ${index + 1}`),
|
|
164
|
+
impact: cell(driver.impact ?? driver.effect ?? driver.quantified_contribution),
|
|
165
|
+
evidence: listCell(driver.evidence ?? driver.evidence_ids),
|
|
166
|
+
};
|
|
97
167
|
}),
|
|
98
168
|
},
|
|
99
169
|
},
|
|
@@ -105,7 +175,7 @@ export function projectReviewModule(input: unknown): DashboardModuleBuild {
|
|
|
105
175
|
data: {
|
|
106
176
|
label: "限制与风险",
|
|
107
177
|
columns: [{ key: "item", label: "说明" }],
|
|
108
|
-
rows:
|
|
178
|
+
rows: limitationRows(analysis),
|
|
109
179
|
},
|
|
110
180
|
},
|
|
111
181
|
],
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ actuals_snapshot_id: string
|
|
|
8
8
|
comparison_basis: string
|
|
9
9
|
headline_results:
|
|
10
10
|
- metric: string
|
|
11
|
+
scope: string | absent # required when the same metric is reported for more than one scope
|
|
11
12
|
actual: number | null
|
|
12
13
|
baseline: number | null
|
|
13
14
|
variance: number | null
|