@lmzhen/dsh-evolution-skill-health 0.0.0-stage → 0.25.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
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @lmzhen/dsh-evolution-skill-health
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 技能健康面板:把体检引擎(`evolution-core` 的 skill-lint/skill-health)的结论**按规则聚合**呈现,
4
+ 让"哪条规则在哪些技能上出现、本次改动新引入了什么"一眼可见。**只读**:不写任何技能、不改任何配置。
5
+
6
+ ## 座位与注册(照 `evolution-skill-history` 的成例)
7
+
8
+ - 侧栏行:`ctx.slots.inject('sidebar.panellist', …)` + `slots.register({ name: 'sidebar.panellist', id, order, label: () => … })`;
9
+ `label` 必须是 **thunk**(跟语言切换走 ✓),`order` 取 40 一带(与技能历史相邻 ✓);`sidebar.panellist` 是**列表座位** ⇒ 多个面板可并存 ✓。
10
+ - 主区:`slots.register({ name: 'main', key: <同一个 id>, inject })` —— **同一个 id 注册两次**,这是本家族面板的既定契约 ✓。
11
+
12
+ ## 数据来源与边界
13
+
14
+ - **数据**:只经只读通道取引擎结论(`healthView()` 一类的**全量行**:`{ name, verdict, dims, findings }` ✓;读不到的技能以 `unreadable-skill` 呈现,**不沉默** ✓)。
15
+ - **呈现**:总览四态(ok/warn/error/不可读)+ **按规则聚合**的清单 + 与技能历史/技能目录的交叉跳转;
16
+ `line: …`/`line: UNKNOWN` 两态要写清(未知就是未知 ✓)。
17
+ - **不上屏**:**未标定**的阈值(如 `SUPPORT_SPRAWL_FILES`)**只报规则名、不报数字** ✓ —— 口径冻结之前不许把数字给读者 ✓。
18
+ - 客户端半**不得** import 其它插件的值/类型 ⇒ 只声明自己消费的座位形状(见 `evolution-settings-ui/src/client/seam.ts` 的头注释 ✓)。
19
+
20
+ ## 当前状态
21
+
22
+ **占名已发布**(npm `@lmzhen/dsh-evolution-skill-health`,占位版本 `0.0.0-stage` ✓);本包的 `src/` 内容随后补齐,
23
+ **首次真实内容必须发更高版本** ✓("已发布只能抬版本")。
package/lib/index.js ADDED
@@ -0,0 +1,83 @@
1
+ //#region lib/types/routes.js
2
+ /**
3
+ * The panel's ONE read-only route: the health rows the browser half renders.
4
+ *
5
+ * Read-only by construction: it never writes a skill, a marker or a config. It also carries the
6
+ * house rule down to the client — a rule whose threshold is NOT calibrated yet is reported with
7
+ * `countIsMeaningful: false`, so the panel can name it without printing a number we do not have.
8
+ * @module @lmzhen/dsh-evolution-skill-health
9
+ */
10
+ /** Where the panel asks for its rows. */
11
+ const ROUTES = { rows: "/api/dsh-evolution/skill-health/rows" };
12
+ /** Rules whose threshold is not calibrated yet: named on screen, never counted. */
13
+ const UNCALIBRATED_RULES = ["references-sprawl"];
14
+ /**
15
+ * Project the curator's health view into what the panel renders.
16
+ * @param rows - `{ name, verdict, dims, findings }` per skill, as `healthView()` returns them.
17
+ * @returns the same rows, with unreadable skills kept apart and uncalibrated rules uncounted.
18
+ */
19
+ function toRouteRows(rows) {
20
+ return rows.map((row) => ({
21
+ name: row.name,
22
+ verdict: row.dims?.readable === false ? "unreadable" : row.verdict,
23
+ findings: row.findings.map((finding) => ({
24
+ rule: finding.rule,
25
+ count: finding.count,
26
+ line: finding.line ?? null,
27
+ countIsMeaningful: !UNCALIBRATED_RULES.includes(finding.rule)
28
+ }))
29
+ }));
30
+ }
31
+ /**
32
+ * Build the panel's routes, in the platform shape: `{ path, handler(req, res) }` — the same objects
33
+ * `ctx.webServer.register` takes for every other row in this family.
34
+ * @param deps - a getter for the curator service (absent until the profile mounts it).
35
+ * @returns one GET route; with no curator it answers an EMPTY row list plus `truncated: true`, which
36
+ * the panel shows as "cannot decide right now" rather than as a pass.
37
+ */
38
+ function makeSkillHealthRoutes(deps) {
39
+ return [{
40
+ kind: "exact",
41
+ path: ROUTES.rows,
42
+ handler: async (_req, res) => {
43
+ const curator = deps.getCurator();
44
+ const view = curator?.healthView === void 0 ? void 0 : await curator.healthView();
45
+ const rows = Array.isArray(view) ? toRouteRows(view) : [];
46
+ res.writeHead(200, { "content-type": "application/json" });
47
+ res.end(JSON.stringify({
48
+ rows,
49
+ truncated: view === void 0
50
+ }));
51
+ }
52
+ }];
53
+ }
54
+ //#endregion
55
+ //#region lib/types/index.js
56
+ /**
57
+ * Host half: ONE read-only route, mounted when the profile gives this row a web server.
58
+ *
59
+ * **Why `ctx.inject` and not `ctx.get`**: profile rows apply in file order, so at this row's apply
60
+ * time the web server may not exist yet — a `ctx.get('webServer')` of undefined is a TIMING fact,
61
+ * not a verdict, and acting on it leaves the route unregistered while the panel renders and every
62
+ * call answers 404. Waiting on the service mounts the route whenever it arrives, and a profile that
63
+ * never gets one simply never runs this callback — inert, never pending.
64
+ * @module @lmzhen/dsh-evolution-skill-health
65
+ */
66
+ /** No required services: the web server is WAITED ON below, not demanded here. */
67
+ const inject = [];
68
+ /**
69
+ * Mount the route as ONE effect (disposers run on unload and on HMR).
70
+ * @param ctx - the host context; the web server and curator arrive through it.
71
+ */
72
+ function apply(ctx) {
73
+ ctx.inject(["webServer"], (scope) => {
74
+ scope.effect(() => {
75
+ const disposers = makeSkillHealthRoutes({ getCurator: () => scope.get("evolutionCurator") }).map((route) => scope.webServer.register(route));
76
+ return () => {
77
+ for (const dispose of disposers) dispose();
78
+ };
79
+ }, "evolution-skill-health: routes");
80
+ });
81
+ }
82
+ //#endregion
83
+ export { apply, inject };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The panel body: a four-state summary plus the list aggregated BY RULE.
3
+ *
4
+ * It renders plain data and callbacks handed in by the sidebar (`inject`), never a service, and it
5
+ * holds no facts of its own. Two rules of the house are visible here: a rule whose threshold is not
6
+ * calibrated yet is NAMED WITHOUT A NUMBER (the number is the thing we do not have), and a skill
7
+ * that could not be read is listed as unreadable — silence would read as a pass.
8
+ * @module @lmzhen/dsh-evolution-skill-health/client
9
+ */
10
+ import { type ReactNode } from 'react';
11
+ import type { LocaleSeat } from './seam.ts';
12
+ /** One finding, as the host reports it: `line` is null when the position cannot be named. */
13
+ export interface HealthFinding {
14
+ rule: string;
15
+ count: number;
16
+ line: string | null;
17
+ /** False for a rule whose threshold is not calibrated: the rule is named, the count is not shown. */
18
+ countIsMeaningful: boolean;
19
+ }
20
+ /** One skill row. `readable: false` is its own verdict — never folded into `ok`. */
21
+ export interface HealthRow {
22
+ name: string;
23
+ verdict: 'ok' | 'warn' | 'error' | 'unreadable';
24
+ findings: HealthFinding[];
25
+ }
26
+ /** What the sidebar hands the component. */
27
+ export interface HealthFace {
28
+ rows: HealthRow[];
29
+ /** True when the host could not finish the scan: the counts below are then a lower bound. */
30
+ truncated: boolean;
31
+ /** Opens the skill-history panel for one skill, when that panel is present. */
32
+ openHistory?: (name: string) => void;
33
+ }
34
+ /**
35
+ * Render the panel.
36
+ * @param props - the face (`{ rows, truncated, openHistory? }`) and the locale seat's translator.
37
+ * @returns the panel body.
38
+ */
39
+ export declare function SkillHealthPanel(props: {
40
+ face: HealthFace;
41
+ locale: LocaleSeat;
42
+ }): ReactNode;
43
+ //# sourceMappingURL=Panel.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Browser half: one left-sidebar row (`技能健康`) and the main-column body it opens.
3
+ *
4
+ * Two places, one id: `sidebar.panellist` owns the row and the layout's keyed `main` slot owns the
5
+ * body, dispatched by that same id. Facts arrive through a loader the HOST half supplies, so this
6
+ * bundle keeps no state of its own and touches no service.
7
+ * @module @lmzhen/dsh-evolution-skill-health/client
8
+ */
9
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
10
+ import { type HealthFace } from './Panel.ts';
11
+ /** Required client services: the slot registry and the locale seat. */
12
+ export declare const inject: string[];
13
+ /** The host half hands the panel a way to read its rows (a read-only route on the host side). */
14
+ export type HealthFaceLoader = () => Promise<HealthFace>;
15
+ /**
16
+ * Register the row and the body.
17
+ * @param ctx - the client context; it must carry the two seats above.
18
+ */
19
+ export declare function apply(ctx: ClientContext): void;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Panel copy, in a zh/en dictionary registered through the platform's locale seat.
3
+ *
4
+ * Client copy is locale-owned: the entry label, the group headings, the verdict words, the empty
5
+ * and failure lines all resolve here, and the panel carries no literal string of its own. Facts come
6
+ * from the host; only the WORDS live here. Two lines exist to keep silence honest: a rule whose
7
+ * threshold is NOT calibrated is named without a number, and an unreadable skill is never a pass.
8
+ * @module @lmzhen/dsh-evolution-skill-health/client
9
+ */
10
+ /** The locale namespace this bundle registers. */
11
+ export declare const NS = "evolution-skill-health";
12
+ /** Chinese dictionary. */
13
+ export declare const zh: Record<string, string>;
14
+ /** English dictionary. */
15
+ export declare const en: Record<string, string>;
16
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Structural view of the client seams this bundle consumes: the sidebar's global panel row, the
3
+ * main-column body it addresses, the slot registry and the locale seat.
4
+ *
5
+ * The package ships outside the platform repository, so it DECLARES the seams it uses instead of
6
+ * importing another plugin's values (forbidden by the client bundle purity rule) or its types
7
+ * (which would drag platform client sources in). The shapes below are the shell's contract:
8
+ * `sidebar.panellist` takes `id`/`order`/`label` and owns the button and the layout, while the body
9
+ * is the root layout's keyed `main` slot dispatched by that SAME id.
10
+ * @module @lmzhen/dsh-evolution-skill-health/client
11
+ */
12
+ import type { ReactNode } from 'react';
13
+ /** The panel id. The row and the `main` body MUST share it: the sidebar resolves the body by it. */
14
+ export declare const PANEL_ID = "skill-health";
15
+ /** Row order inside the global panel list: beside the skill-history row (35), after it. */
16
+ export declare const PANEL_ORDER = 40;
17
+ /** Registration options for the global panel row (the sidebar's own shape). */
18
+ export interface PanelRowOptions {
19
+ name: 'sidebar.panellist';
20
+ id: string;
21
+ order: number;
22
+ /** Lazy label: the sidebar re-reads it on every projection, so the locale can change underneath. */
23
+ label: () => string;
24
+ }
25
+ /** Registration options for the main-column body: a keyed slot dispatched by sidebar entry id. */
26
+ export interface PanelBodyOptions {
27
+ name: 'main';
28
+ key: string;
29
+ /** The face handed to the component: plain data and callbacks, never a service. */
30
+ inject: () => unknown;
31
+ }
32
+ /** The locale seat: register this bundle's namespace, bind a translator. */
33
+ export interface LocaleSeat {
34
+ register(namespace: string, dictionaries: {
35
+ zh: Record<string, string>;
36
+ en: Record<string, string>;
37
+ }): () => void;
38
+ bind(namespace: string): (key: string) => string;
39
+ }
40
+ /** The slot registry: a generator body may register (and is disposed when it unmounts). */
41
+ export interface SlotsSeat {
42
+ inject(name: string, body: () => Generator<() => void, void, void>): void;
43
+ register(options: PanelRowOptions | PanelBodyOptions, component: (props: unknown) => ReactNode): () => void;
44
+ }
45
+ /** Everything this bundle asks the shell for. */
46
+ export interface ClientSeam {
47
+ slots: SlotsSeat;
48
+ locale: LocaleSeat;
49
+ }
50
+ //# sourceMappingURL=seam.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Host half: ONE read-only route, mounted when the profile gives this row a web server.
3
+ *
4
+ * **Why `ctx.inject` and not `ctx.get`**: profile rows apply in file order, so at this row's apply
5
+ * time the web server may not exist yet — a `ctx.get('webServer')` of undefined is a TIMING fact,
6
+ * not a verdict, and acting on it leaves the route unregistered while the panel renders and every
7
+ * call answers 404. Waiting on the service mounts the route whenever it arrives, and a profile that
8
+ * never gets one simply never runs this callback — inert, never pending.
9
+ * @module @lmzhen/dsh-evolution-skill-health
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ /** No required services: the web server is WAITED ON below, not demanded here. */
13
+ export declare const inject: string[];
14
+ /**
15
+ * Mount the route as ONE effect (disposers run on unload and on HMR).
16
+ * @param ctx - the host context; the web server and curator arrive through it.
17
+ */
18
+ export declare function apply(ctx: Context): void;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,58 @@
1
+ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
2
+ /**
3
+ * The panel's ONE read-only route: the health rows the browser half renders.
4
+ *
5
+ * Read-only by construction: it never writes a skill, a marker or a config. It also carries the
6
+ * house rule down to the client — a rule whose threshold is NOT calibrated yet is reported with
7
+ * `countIsMeaningful: false`, so the panel can name it without printing a number we do not have.
8
+ * @module @lmzhen/dsh-evolution-skill-health
9
+ */
10
+ /** Where the panel asks for its rows. */
11
+ export declare const ROUTES: {
12
+ readonly rows: "/api/dsh-evolution/skill-health/rows";
13
+ };
14
+ /** Rules whose threshold is not calibrated yet: named on screen, never counted. */
15
+ export declare const UNCALIBRATED_RULES: readonly string[];
16
+ /** One finding as the client sees it. `line` is null when the position cannot be named. */
17
+ export interface RouteFinding {
18
+ rule: string;
19
+ count: number;
20
+ line: string | null;
21
+ countIsMeaningful: boolean;
22
+ }
23
+ /** One row as the client sees it. `readable: false` becomes its own verdict, never `ok`. */
24
+ export interface RouteRow {
25
+ name: string;
26
+ verdict: string;
27
+ findings: RouteFinding[];
28
+ }
29
+ /**
30
+ * Project the curator's health view into what the panel renders.
31
+ * @param rows - `{ name, verdict, dims, findings }` per skill, as `healthView()` returns them.
32
+ * @returns the same rows, with unreadable skills kept apart and uncalibrated rules uncounted.
33
+ */
34
+ export declare function toRouteRows(rows: ReadonlyArray<{
35
+ name: string;
36
+ verdict: string;
37
+ findings: ReadonlyArray<{
38
+ rule: string;
39
+ count: number;
40
+ line?: string | null;
41
+ }>;
42
+ dims?: {
43
+ readable?: boolean;
44
+ } | null;
45
+ }>): RouteRow[];
46
+ /**
47
+ * Build the panel's routes, in the platform shape: `{ path, handler(req, res) }` — the same objects
48
+ * `ctx.webServer.register` takes for every other row in this family.
49
+ * @param deps - a getter for the curator service (absent until the profile mounts it).
50
+ * @returns one GET route; with no curator it answers an EMPTY row list plus `truncated: true`, which
51
+ * the panel shows as "cannot decide right now" rather than as a pass.
52
+ */
53
+ export declare function makeSkillHealthRoutes(deps: {
54
+ getCurator: () => {
55
+ healthView?: () => Promise<unknown>;
56
+ } | undefined;
57
+ }): WebRoute[];
58
+ //# sourceMappingURL=routes.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-skill-health",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "description": "Web surface for skill health: the sidebar panel that shows what the linter found (community build)",
4
+ "version": "0.25.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lmzhen/dsh-evolution.git",
11
+ "directory": "packages/evolution-skill-health"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "lib/*.js",
25
+ "lib/types/**/*.d.ts"
26
+ ],
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "@deepseek-ai/cordis": "^4.0.1",
30
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
31
+ "react": "^18.2.0",
32
+ "@lmzhen/dsh-evolution-curator": "^0.25.0"
33
+ },
34
+ "devDependencies": {
35
+ "@deepseek-ai/cordis": "^4.0.1",
36
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
37
+ "@types/react": "~18.3.1",
38
+ "react": "^18.2.0",
39
+ "@lmzhen/dsh-evolution-curator": "^0.25.0"
40
+ }
41
+ }