@lmzhen/dsh-evolution-skill-health 0.26.26 → 0.27.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/lib/client.js CHANGED
@@ -5,34 +5,141 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- //#region src/client/api.ts
9
- /** Route paths, mirrored from the host half. */
10
- const HOST_ROUTES = { rows: "/api/dsh-evolution/skill-health/rows" };
8
+ //#region src/client/routes.generated.ts
11
9
  /**
12
- * Read the scan result.
13
- * @returns the payload, or the host's own refusal sentence.
10
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
11
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
12
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
13
+ * client copies, and the generator's --check makes a stale copy red.
14
14
  */
15
- async function rows() {
15
+ const ROUTES = {
16
+ approval: {
17
+ pending: "/api/dsh-evolution/approval/pending",
18
+ preview: "/api/dsh-evolution/approval/preview",
19
+ approve: "/api/dsh-evolution/approval/approve",
20
+ reject: "/api/dsh-evolution/approval/reject"
21
+ },
22
+ "skill-history": {
23
+ skills: "/api/dsh-evolution/skill-history/skills",
24
+ versions: "/api/dsh-evolution/skill-history/versions",
25
+ diff: "/api/dsh-evolution/skill-history/versions/diff",
26
+ body: "/api/dsh-evolution/skill-history/versions/body",
27
+ undo: "/api/dsh-evolution/skill-history/undo",
28
+ health: "/api/dsh-evolution/skill-history/health"
29
+ },
30
+ "skill-health": { rows: "/api/dsh-evolution/skill-health/rows" },
31
+ diagnostics: { view: "/api/dsh-evolution/diagnostics" }
32
+ };
33
+ //#endregion
34
+ //#region src/client/route-client.generated.ts
35
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
36
+ function classify(status) {
37
+ if (status === 401) return {
38
+ kind: "unauthenticated",
39
+ status: 401
40
+ };
41
+ if (status === 403) return {
42
+ kind: "forbidden",
43
+ status: 403
44
+ };
45
+ if (status === 404) return {
46
+ kind: "unmounted",
47
+ status: 404
48
+ };
49
+ return {
50
+ kind: "server",
51
+ status
52
+ };
53
+ }
54
+ /**
55
+ * One request, no retries and no body parsing for the platform's admission answers.
56
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
57
+ * @param path - the exact route path from the generated ROUTES table.
58
+ * @param init - standard RequestInit.
59
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
60
+ */
61
+ async function requestJson(doFetch, path, init) {
62
+ let response;
16
63
  try {
17
- const response = await fetch(HOST_ROUTES.rows, { headers: { accept: "application/json" } });
18
- const body = await response.json();
19
- if (!response.ok) return {
64
+ response = await doFetch(path, init);
65
+ } catch (error) {
66
+ return {
20
67
  ok: false,
21
- message: body.message ?? "skill-health: the route refused the request"
68
+ failure: {
69
+ kind: "server",
70
+ status: 0,
71
+ detail: error instanceof Error ? error.message : String(error)
72
+ }
22
73
  };
74
+ }
75
+ if (!response.ok) return {
76
+ ok: false,
77
+ failure: classify(response.status)
78
+ };
79
+ let body;
80
+ try {
81
+ body = await response.json();
82
+ } catch {
23
83
  return {
24
- ok: true,
25
- payload: {
26
- rows: body.rows ?? [],
27
- truncated: body.truncated === true
84
+ ok: false,
85
+ failure: {
86
+ kind: "server",
87
+ status: response.status
28
88
  }
29
89
  };
30
- } catch (error) {
31
- return {
90
+ }
91
+ if (typeof body === "object" && body !== null) {
92
+ const record = body;
93
+ if (record.ok === true) return {
94
+ ok: true,
95
+ data: record.data
96
+ };
97
+ if (record.ok === false && typeof record.message === "string" && record.message !== "") return {
32
98
  ok: false,
33
- message: "skill-health: " + String(error)
99
+ failure: {
100
+ kind: "refused",
101
+ code: typeof record.code === "string" && record.code !== "" ? record.code : "refused",
102
+ message: record.message
103
+ }
34
104
  };
35
105
  }
106
+ return {
107
+ ok: false,
108
+ failure: {
109
+ kind: "server",
110
+ status: response.status
111
+ }
112
+ };
113
+ }
114
+ //#endregion
115
+ //#region src/client/api.ts
116
+ /**
117
+ * The host route this bundle reads, and the ONE place a request is made.
118
+ *
119
+ * G5/W7: the path comes from the generated client route table (rendered from evolution-core
120
+ * HOST_ROUTES) and the transport is the shared generated route-client. A refusal arrives classified
121
+ * with the host's own sentence, so the panel shows what the route actually said.
122
+ * @module @lmzhen/dsh-evolution-skill-health/client
123
+ */
124
+ /** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
125
+ const HOST_ROUTES = ROUTES["skill-health"];
126
+ /**
127
+ * Read the scan result.
128
+ * @returns the payload, or the host's own refusal sentence.
129
+ */
130
+ async function rows() {
131
+ const answer = await requestJson(fetch, HOST_ROUTES.rows, { headers: { accept: "application/json" } });
132
+ if (!answer.ok) return {
133
+ ok: false,
134
+ failure: answer.failure
135
+ };
136
+ return {
137
+ ok: true,
138
+ payload: {
139
+ rows: answer.data.rows,
140
+ truncated: answer.data.truncated
141
+ }
142
+ };
36
143
  }
37
144
  //#endregion
38
145
  //#region src/client/seam.ts
@@ -75,7 +182,12 @@ window.__ModuleLoader__.load({
75
182
  "line.unknown": "位置:未知",
76
183
  "unreadable.note": "读不到内容,因此无法判断(不是「通过」)。",
77
184
  "loading.failed": "读取失败——现在判断不了;点「刷新」重试。",
78
- "read.failed": "这次没读到数据(下面一行是原始信息):"
185
+ "read.failed": "这次没读到数据(下面一行是原始信息):",
186
+ "failure.unauthorized": "需要登录:重新打开这个页面完成登录后再试。",
187
+ "failure.forbidden": "平台拒绝了这个请求(身份不是 owner)。",
188
+ "failure.unmounted": "该功能没有挂载。",
189
+ "failure.server": "读取失败,可以重试。",
190
+ "fallback.hint": "可以改用命令:{command}"
79
191
  };
80
192
  /** English dictionary. */
81
193
  const en = {
@@ -101,7 +213,12 @@ window.__ModuleLoader__.load({
101
213
  "line": "line: {path}",
102
214
  "line.unknown": "line: UNKNOWN",
103
215
  "unreadable.note": "The content could not be read, so this cannot be judged (it is not a pass).",
104
- "loading.failed": "Reading failed — this cannot be judged right now; press Refresh to retry."
216
+ "loading.failed": "Reading failed — this cannot be judged right now; press Refresh to retry.",
217
+ "failure.unauthorized": "Sign-in is required: reopen this page and sign in, then retry.",
218
+ "failure.forbidden": "The platform refused this request (not the owner principal).",
219
+ "failure.unmounted": "This feature is not mounted.",
220
+ "failure.server": "The read failed; you can retry.",
221
+ "fallback.hint": "Command fallback: {command}"
105
222
  };
106
223
  //#endregion
107
224
  //#region src/client/styles.ts
@@ -162,6 +279,51 @@ window.__ModuleLoader__.load({
162
279
  ":where(.dsh-evolution-skill-health [data-role=\"note\"]) { margin: 0; color: var(--dsw-alias-label-secondary); }"
163
280
  ].join(String.fromCharCode(10));
164
281
  //#endregion
282
+ //#region src/client/states.generated.ts
283
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
284
+ function Block(props) {
285
+ const tone = props.tone ?? "neutral";
286
+ return (0, react.createElement)("div", {
287
+ "data-tone": tone,
288
+ ...tone === "fail" ? { role: "alert" } : {}
289
+ }, props.title === void 0 ? null : (0, react.createElement)("div", { "data-tone-title": tone }, props.title), props.children);
290
+ }
291
+ /** One classified failure as a reader-facing sentence. */
292
+ function failureText(failure, t) {
293
+ switch (failure.kind) {
294
+ case "unauthenticated": return t("failureUnauthorized");
295
+ case "forbidden": return t("failureForbidden");
296
+ case "unmounted": return t("failureUnmounted");
297
+ case "server": return failure.status === 0 ? t("failureServer") : t("failureServer") + " (HTTP " + String(failure.status) + ")";
298
+ case "refused": return failure.message;
299
+ }
300
+ }
301
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
302
+ function FailureBlock(props) {
303
+ const hint = props.fallback === void 0 ? null : (0, react.createElement)("div", { "data-tone-hint": "true" }, props.t("fallbackHint", { command: props.fallback.command }));
304
+ return Block({
305
+ tone: "fail",
306
+ title: failureText(props.failure, props.t),
307
+ children: hint
308
+ });
309
+ }
310
+ //#endregion
311
+ //#region src/client/fallbacks.generated.ts
312
+ /**
313
+ * The command-face fallback for every Host surface, generated from evolution-commands'
314
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
315
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
316
+ */
317
+ const FALLBACKS = {
318
+ approval: { command: "/evolution pending … /evolution approve <id> … /evolution reject <id> … /evolution release <id>" },
319
+ "skill-history": { command: "/evolution restore <name> [v](查看:/evolution skills history <name>)" },
320
+ "skill-health": { command: "/evolution skills refresh(健康行:/evolution doctor)" },
321
+ diagnostics: {
322
+ command: "/evolution doctor",
323
+ platformCli: "dsh --json /evolution doctor"
324
+ }
325
+ };
326
+ //#endregion
165
327
  //#region src/client/Panel.ts
166
328
  /**
167
329
  * The panel body: a four-state summary plus the list aggregated BY RULE.
@@ -234,6 +396,25 @@ window.__ModuleLoader__.load({
234
396
  * @param props - the face (`{ rows, truncated, openHistory? }`) and the locale seat's translator.
235
397
  * @returns the panel body.
236
398
  */
399
+ /** Fill the generated block's `{slot}` copies with this bundle's dictionary. */
400
+ function fillCopy(template, values) {
401
+ return template.replace(/\{(\w+)\}/g, (match, slot) => {
402
+ const value = values[slot];
403
+ return value === void 0 ? match : String(value);
404
+ });
405
+ }
406
+ /** The shared four-state failure block, spoken through this bundle's keys. */
407
+ function failureBlock(t, failure) {
408
+ const translate = (key, values) => {
409
+ const text = t(key);
410
+ return values === void 0 ? text : fillCopy(text, values);
411
+ };
412
+ return FailureBlock({
413
+ failure,
414
+ t: translate,
415
+ fallback: FALLBACKS["skill-health"]
416
+ });
417
+ }
237
418
  function SkillHealthPanel(face) {
238
419
  const t = face.t;
239
420
  const [view, setView] = (0, react.useState)(face);
@@ -249,9 +430,12 @@ window.__ModuleLoader__.load({
249
430
  rows: outcome.payload.rows,
250
431
  truncated: outcome.payload.truncated
251
432
  });
252
- else setFailed(outcome.message);
253
- }).catch((error) => {
254
- if (live) setFailed(String(error));
433
+ else setFailed(outcome.failure);
434
+ }).catch(() => {
435
+ if (live) setFailed({
436
+ kind: "server",
437
+ status: 0
438
+ });
255
439
  });
256
440
  return () => {
257
441
  live = false;
@@ -263,7 +447,7 @@ window.__ModuleLoader__.load({
263
447
  "data-role": "stat",
264
448
  "data-tone": kind
265
449
  }, (0, react.createElement)("div", { "data-role": "stat-value" }, String(counts.get(kind) ?? 0)), (0, react.createElement)("div", { "data-role": "stat-label" }, t("verdict." + kind)))));
266
- return (0, react.createElement)("section", { className: CSS_TAG_ID }, (0, react.createElement)("p", { "data-role": "hint" }, t("hint")), stats, view.truncated ? (0, react.createElement)("p", { "data-role": "note" }, t("loading.failed")) : null, failed === void 0 ? null : (0, react.createElement)("p", { "data-role": "note" }, t("read.failed") + " " + failed), view.rows.length === 0 ? (0, react.createElement)("p", { "data-role": "note" }, t("empty.noneReported")) : null, (0, react.createElement)("h3", { "data-role": "group-title" }, t("group.byRule")), ruleTable(view, t), view.rows.filter((row) => row.verdict === "unreadable").map((row) => (0, react.createElement)("p", {
450
+ return (0, react.createElement)("section", { className: CSS_TAG_ID }, (0, react.createElement)("p", { "data-role": "hint" }, t("hint")), stats, view.truncated ? (0, react.createElement)("p", { "data-role": "note" }, t("loading.failed")) : null, failed === void 0 ? null : (0, react.createElement)("div", { "data-role": "note" }, failureBlock(t, failed)), view.rows.length === 0 ? (0, react.createElement)("p", { "data-role": "note" }, t("empty.noneReported")) : null, (0, react.createElement)("h3", { "data-role": "group-title" }, t("group.byRule")), ruleTable(view, t), view.rows.filter((row) => row.verdict === "unreadable").map((row) => (0, react.createElement)("p", {
267
451
  key: row.name + ":unreadable",
268
452
  "data-role": "note"
269
453
  }, row.name + " — " + t("unreadable.note"))));
package/lib/index.js CHANGED
@@ -1,14 +1,17 @@
1
+ import { HOST_ROUTES, json, methodNotAllowed, registerHostRoutes } from "@lmzhen/dsh-evolution-core/http-routes";
1
2
  //#region lib/types/routes.js
2
3
  /**
3
4
  * The panel's ONE read-only route: the health rows the browser half renders.
4
5
  *
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.
6
+ * W6/G4: exact FetchRoute on the platform Host connection data plane — trust/auth/owner admission are
7
+ * the platform's, so there is no fence here. Read-only by construction: it never writes a skill, a
8
+ * marker or a config. It also carries the house rule down to the client — a rule whose threshold is
9
+ * NOT calibrated yet is reported with `countIsMeaningful: false`, so the panel can name it without
10
+ * printing a number we do not have.
8
11
  * @module @lmzhen/dsh-evolution-skill-health
9
12
  */
10
- /** Where the panel asks for its rows. */
11
- const ROUTES = { rows: "/api/dsh-evolution/skill-health/rows" };
13
+ /** Where the panel asks for its rows. The ONE declaration lives in core (W1). */
14
+ const ROUTES = HOST_ROUTES.skillHealth;
12
15
  /** Rules whose threshold is not calibrated yet: named on screen, never counted. */
13
16
  const UNCALIBRATED_RULES = ["references-sprawl"];
14
17
  /**
@@ -29,55 +32,41 @@ function toRouteRows(rows) {
29
32
  }));
30
33
  }
31
34
  /**
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.
35
+ * Build the panel's route in the platform's exact FetchRoute shape.
34
36
  * @param deps - a getter for the curator service (absent until the profile mounts it).
37
+ * @param _audit - the registration audit (read-only surface: nothing to record).
35
38
  * @returns one GET route; with no curator it answers an EMPTY row list plus `truncated: true`, which
36
39
  * the panel shows as "cannot decide right now" rather than as a pass.
37
40
  */
38
- function makeSkillHealthRoutes(deps) {
41
+ function makeSkillHealthRoutes(deps, _audit) {
39
42
  return [{
40
- kind: "exact",
41
43
  path: ROUTES.rows,
42
- handler: async (_req, res) => {
44
+ methods: ["GET"],
45
+ requestBody: "buffered",
46
+ fetch: async (request) => {
47
+ if (request.method !== "GET") return methodNotAllowed(["GET"]);
43
48
  const curator = deps.getCurator();
44
49
  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
- }));
50
+ return json(200, {
51
+ ok: true,
52
+ data: {
53
+ rows: Array.isArray(view) ? toRouteRows(view) : [],
54
+ truncated: view === void 0
55
+ }
56
+ });
51
57
  }
52
58
  }];
53
59
  }
54
60
  //#endregion
55
61
  //#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. */
62
+ /** No required services: `connection` is waited on inside registerHostRoutes, not demanded here. */
67
63
  const inject = [];
68
64
  /**
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.
65
+ * Mount the route as ONE owner effect (disposers run on unload and on HMR).
66
+ * @param ctx - the host context; the connection and curator arrive through it.
71
67
  */
72
68
  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
- });
69
+ registerHostRoutes(ctx, (audit) => makeSkillHealthRoutes({ getCurator: () => ctx.get("evolutionCurator") }, audit));
81
70
  }
82
71
  //#endregion
83
72
  export { apply, inject };
@@ -35,10 +35,5 @@ export interface HealthFace {
35
35
  /** The seat's translator for this bundle's dictionary — the panel never reaches for a service. */
36
36
  t: (key: string) => string;
37
37
  }
38
- /**
39
- * Render the panel.
40
- * @param props - the face (`{ rows, truncated, openHistory? }`) and the locale seat's translator.
41
- * @returns the panel body.
42
- */
43
38
  export declare function SkillHealthPanel(face: HealthFace): ReactNode;
44
39
  //# sourceMappingURL=Panel.d.ts.map
@@ -1,13 +1,6 @@
1
- /**
2
- * The host route this bundle reads, and the ONE place a request is made.
3
- *
4
- * The path mirrors the host half's `ROUTES.rows` in `src/routes.ts` (a spec asserts the two sides
5
- * agree). A refusal arrives as `ok:false` with the host's own sentence, so the panel shows what the
6
- * route actually said instead of inventing a second vocabulary.
7
- * @module @lmzhen/dsh-evolution-skill-health/client
8
- */
1
+ import type { RouteFailure } from './route-client.generated.ts';
9
2
  import type { HealthRow } from './Panel.ts';
10
- /** Route paths, mirrored from the host half. */
3
+ /** Route paths: the generated mirror of the host's HOST_ROUTES (one declaration in core). */
11
4
  export declare const HOST_ROUTES: {
12
5
  readonly rows: "/api/dsh-evolution/skill-health/rows";
13
6
  };
@@ -17,13 +10,13 @@ export interface HealthPayload {
17
10
  /** True when the host could not finish the scan: the counts below are then a lower bound. */
18
11
  truncated: boolean;
19
12
  }
20
- /** Either the payload or the host's refusal sentence — never a thrown error the panel must guess at. */
13
+ /** Either the payload or the classified four-state failure — never a thrown error the panel must guess at. */
21
14
  export type HealthOutcome = {
22
15
  ok: true;
23
16
  payload: HealthPayload;
24
17
  } | {
25
18
  ok: false;
26
- message: string;
19
+ failure: RouteFailure;
27
20
  };
28
21
  /**
29
22
  * Read the scan result.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Literal mirror of the frozen Host diagnostics contract (W1 §2.8 / core diagnostics.ts). The browser
3
+ * build keeps every @deepseek-ai/* specifier external, so the client CANNOT import the host type — the
4
+ * shape is mirrored mechanically here instead. The key unions are rendered from core diagnostics.ts by
5
+ * the generator, so a key change regenerates all three copies (a stale copy fails --check).
6
+ */
7
+ export type DiagnosticsSchemaVersion = 2;
8
+ export type DiagnosticsFactKey = 'platform-line' | 'family-version' | 'install-form' | 'preset-ids' | 'readonly-contract' | 'upgrade-order' | 'fallback-map';
9
+ export type DiagnosticsRuntimeKey = 'subagent-channel' | 'restricted-preset' | 'host-surface' | 'session-fold' | 'platform-services' | 'runs' | 'platform-pairing' | 'install-form-detected';
10
+ export type DiagnosticStatus = 'ok' | 'warn' | 'fail' | 'unknown';
11
+ export interface DocFactRow {
12
+ readonly key: DiagnosticsFactKey;
13
+ readonly label: string;
14
+ readonly value: string;
15
+ readonly home: string;
16
+ }
17
+ export interface DiagnosticRow {
18
+ readonly key: DiagnosticsRuntimeKey;
19
+ readonly label: string;
20
+ readonly status: DiagnosticStatus;
21
+ readonly detail?: string;
22
+ readonly remedy?: string;
23
+ }
24
+ export interface DiagnosticsView {
25
+ readonly schemaVersion: DiagnosticsSchemaVersion;
26
+ readonly generatedAt: string;
27
+ readonly facts: readonly DocFactRow[];
28
+ readonly runtime: readonly DiagnosticRow[];
29
+ }
30
+ /** The installation form the doctor reports (W8 install-form-detected row). */
31
+ export type InstallFormId = 'M1' | 'M2' | 'M3' | 'M4';
32
+ /** One surface's command-face path(s) — the client `fallbacks.generated.ts` table's row type. */
33
+ export interface FallbackHint {
34
+ readonly command: string;
35
+ readonly platformCli?: string;
36
+ }
37
+ //# sourceMappingURL=contracts.generated.d.ts.map
@@ -0,0 +1,8 @@
1
+ import type { FallbackHint } from './contracts.generated.ts';
2
+ /**
3
+ * The command-face fallback for every Host surface, generated from evolution-commands'
4
+ * COMMAND_FALLBACKS (the ONE home). When the platform's connection carrier is absent (E-325) the
5
+ * panels render this hint instead of a bare HTTP failure — same use case, command face.
6
+ */
7
+ export declare const FALLBACKS: Readonly<Record<'approval' | 'skill-history' | 'skill-health' | 'diagnostics', FallbackHint>>;
8
+ //# sourceMappingURL=fallbacks.generated.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The four-state transport classifier every family client shares (W7 §2). The platform owns
3
+ * admission (401/403/404 are its answers, not ours), so the client reads the STATUS only for those
4
+ * three and never tries to interpret a platform body. Everything the family route itself answers
5
+ * keeps its business envelope: 2xx + {ok:true,data} or 2xx + {ok:false,code,message}.
6
+ */
7
+ export type RouteFailure = {
8
+ readonly kind: 'unauthenticated';
9
+ readonly status: 401;
10
+ } | {
11
+ readonly kind: 'forbidden';
12
+ readonly status: 403;
13
+ } | {
14
+ readonly kind: 'unmounted';
15
+ readonly status: 404;
16
+ } | {
17
+ readonly kind: 'server';
18
+ readonly status: number;
19
+ readonly detail?: string;
20
+ } | {
21
+ readonly kind: 'refused';
22
+ readonly code: string;
23
+ readonly message: string;
24
+ };
25
+ /** One route answer: the data, or the classified failure. */
26
+ export type RouteAnswer<T> = {
27
+ readonly ok: true;
28
+ readonly data: T;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly failure: RouteFailure;
32
+ };
33
+ /** Classify a non-2xx status: 401/403/404 keep their own kind, everything else is a server answer. */
34
+ export declare function classify(status: number): RouteFailure;
35
+ /**
36
+ * One request, no retries and no body parsing for the platform's admission answers.
37
+ * @param doFetch - the fetch in use (the browser's, or a spec's stub).
38
+ * @param path - the exact route path from the generated ROUTES table.
39
+ * @param init - standard RequestInit.
40
+ * @returns the data, or the classified failure (network/parse failures read as 'server').
41
+ */
42
+ export declare function requestJson<T>(doFetch: typeof fetch, path: string, init?: RequestInit): Promise<RouteAnswer<T>>;
43
+ /** The one-sentence rendering legacy callers attach to a classified failure. */
44
+ export declare function failureMessage(failure: RouteFailure): string;
45
+ //# sourceMappingURL=route-client.generated.d.ts.map
@@ -0,0 +1,31 @@
1
+ /** One host data-plane surface the browser half can call. */
2
+ export type SurfaceId = 'approval' | 'skill-history' | 'skill-health' | 'diagnostics';
3
+ /**
4
+ * Every family host path, generated from evolution-core's HOST_ROUTES (the ONE declaration).
5
+ * A browser half cannot import the host package at runtime (client bundles keep @deepseek-ai/*
6
+ * external), so the table is mirrored here mechanically; a rename in core regenerates all three
7
+ * client copies, and the generator's --check makes a stale copy red.
8
+ */
9
+ export declare const ROUTES: {
10
+ readonly approval: {
11
+ readonly pending: "/api/dsh-evolution/approval/pending";
12
+ readonly preview: "/api/dsh-evolution/approval/preview";
13
+ readonly approve: "/api/dsh-evolution/approval/approve";
14
+ readonly reject: "/api/dsh-evolution/approval/reject";
15
+ };
16
+ readonly 'skill-history': {
17
+ readonly skills: "/api/dsh-evolution/skill-history/skills";
18
+ readonly versions: "/api/dsh-evolution/skill-history/versions";
19
+ readonly diff: "/api/dsh-evolution/skill-history/versions/diff";
20
+ readonly body: "/api/dsh-evolution/skill-history/versions/body";
21
+ readonly undo: "/api/dsh-evolution/skill-history/undo";
22
+ readonly health: "/api/dsh-evolution/skill-history/health";
23
+ };
24
+ readonly 'skill-health': {
25
+ readonly rows: "/api/dsh-evolution/skill-health/rows";
26
+ };
27
+ readonly diagnostics: {
28
+ readonly view: "/api/dsh-evolution/diagnostics";
29
+ };
30
+ };
31
+ //# sourceMappingURL=routes.generated.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { RouteAnswer, RouteFailure } from './route-client.generated.ts';
3
+ import type { FallbackHint } from './contracts.generated.ts';
4
+ /**
5
+ * The four-state base of every family card (W7 §2). Load states are read from the generated
6
+ * route-client's answer — a card owns no HTTP vocabulary and no second state machine.
7
+ */
8
+ export type LoadState<T> = {
9
+ readonly phase: 'loading';
10
+ } | {
11
+ readonly phase: 'ready';
12
+ readonly data: T;
13
+ } | {
14
+ readonly phase: 'empty';
15
+ } | {
16
+ readonly phase: 'failed';
17
+ readonly failure: RouteFailure;
18
+ };
19
+ /** Map one classified answer onto the card's four states. */
20
+ export declare function loadStateOf<T>(answer: RouteAnswer<T>, isEmpty: (data: T) => boolean): LoadState<T>;
21
+ /** The locale lookup every block calls (the platform's own `t`). */
22
+ export interface Translate {
23
+ (key: string, values?: Readonly<Record<string, string | number>>): string;
24
+ }
25
+ export type BlockTone = 'neutral' | 'warn' | 'fail';
26
+ export interface BlockProps {
27
+ readonly tone?: BlockTone;
28
+ readonly title?: string;
29
+ readonly children?: ReactNode;
30
+ }
31
+ /** A neutral/ warn/ fail block; a fail block is role=alert so assistive tech announces it. */
32
+ export declare function Block(props: BlockProps): ReactNode;
33
+ export interface FailureBlockProps {
34
+ readonly failure: RouteFailure;
35
+ readonly t: Translate;
36
+ /** The command-face fallback for this surface (rendered on unmounted/failed answers). */
37
+ readonly fallback?: FallbackHint | undefined;
38
+ }
39
+ /** One classified failure as a reader-facing sentence. */
40
+ export declare function failureText(failure: RouteFailure, t: Translate): string;
41
+ /** The failed-state block: sentence + (when the surface exists but is unreachable) the command fallback. */
42
+ export declare function FailureBlock(props: FailureBlockProps): ReactNode;
43
+ //# sourceMappingURL=states.generated.d.ts.map
@@ -1,19 +1,17 @@
1
1
  /**
2
- * Host half: ONE read-only route, mounted when the profile gives this row a web server.
2
+ * Host half: ONE read-only route, mounted on the platform's Host connection data plane.
3
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.
4
+ * W6/G4: `registerHostRoutes` waits on the `connection` service (it may apply after this row),
5
+ * registers via one owner effect and captures the DecisionAudit there. The old webServer + loopback
6
+ * fence path is gone: trust/auth/owner admission are the platform's.
9
7
  * @module @lmzhen/dsh-evolution-skill-health
10
8
  */
11
9
  import type { Context } from '@deepseek-ai/cordis';
12
- /** No required services: the web server is WAITED ON below, not demanded here. */
10
+ /** No required services: `connection` is waited on inside registerHostRoutes, not demanded here. */
13
11
  export declare const inject: string[];
14
12
  /**
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.
13
+ * Mount the route as ONE owner effect (disposers run on unload and on HMR).
14
+ * @param ctx - the host context; the connection and curator arrive through it.
17
15
  */
18
16
  export declare function apply(ctx: Context): void;
19
17
  //# sourceMappingURL=index.d.ts.map
@@ -1,13 +1,5 @@
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. */
1
+ import type { ConnectionFetchRouteLike, DecisionAudit } from '@lmzhen/dsh-evolution-core/http-routes';
2
+ /** Where the panel asks for its rows. The ONE declaration lives in core (W1). */
11
3
  export declare const ROUTES: {
12
4
  readonly rows: "/api/dsh-evolution/skill-health/rows";
13
5
  };
@@ -44,9 +36,9 @@ export declare function toRouteRows(rows: ReadonlyArray<{
44
36
  } | null;
45
37
  }>): RouteRow[];
46
38
  /**
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.
39
+ * Build the panel's route in the platform's exact FetchRoute shape.
49
40
  * @param deps - a getter for the curator service (absent until the profile mounts it).
41
+ * @param _audit - the registration audit (read-only surface: nothing to record).
50
42
  * @returns one GET route; with no curator it answers an EMPTY row list plus `truncated: true`, which
51
43
  * the panel shows as "cannot decide right now" rather than as a pass.
52
44
  */
@@ -54,5 +46,5 @@ export declare function makeSkillHealthRoutes(deps: {
54
46
  getCurator: () => {
55
47
  healthView?: () => Promise<unknown>;
56
48
  } | undefined;
57
- }): WebRoute[];
49
+ }, _audit: DecisionAudit): ConnectionFetchRouteLike[];
58
50
  //# sourceMappingURL=routes.d.ts.map
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  ]
12
12
  }
13
13
  },
14
- "version": "0.26.26",
14
+ "version": "0.27.0",
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
@@ -40,16 +40,19 @@
40
40
  ],
41
41
  "license": "MIT",
42
42
  "peerDependencies": {
43
- "@deepseek-ai/cordis": "^4.0.1",
44
- "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
43
+ "@deepseek-ai/cordis": "^4.0.5",
45
44
  "react": "^18.2.0",
46
- "@lmzhen/dsh-evolution-curator": "^0.26.26"
45
+ "@lmzhen/dsh-evolution-core": "^0.27.0",
46
+ "@lmzhen/dsh-evolution-curator": "^0.27.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@deepseek-ai/cordis": "^4.0.1",
50
- "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
49
+ "@deepseek-ai/cordis": "^4.0.5",
51
50
  "@types/react": "~18.3.1",
52
51
  "react": "^18.2.0",
53
- "@lmzhen/dsh-evolution-curator": "^0.26.26"
52
+ "@lmzhen/dsh-evolution-core": "^0.27.0",
53
+ "@lmzhen/dsh-evolution-curator": "^0.27.0"
54
+ },
55
+ "engines": {
56
+ "dsh": ">=0.2.1-rc.1 <0.2.2-0"
54
57
  }
55
58
  }