@lmzhen/dsh-skill-usage 0.1.0-rc.9 → 0.2.0-rc.1

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
@@ -2,6 +2,14 @@
2
2
 
3
3
  Skill usage telemetry service
4
4
 
5
+ ## Telemetry sources
6
+
7
+ `record(name, kind)` is the write API (`use` / `view` / `patch`). Reads are
8
+ observed automatically: the service listens for `session/event` `tool/call`
9
+ records of the read tools (`skill`, `skill_load`) and bumps `view` on
10
+ EXISTING records only — an arbitrary read never mints a usage record
11
+ (records are authored by skill creation, patching, or curator seeding).
12
+
5
13
  ## Model Experience
6
14
 
7
15
  ### Indirect model surface
package/lib/index.js CHANGED
@@ -1,62 +1,186 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import z from "@deepseek-ai/schemastery";
3
- import { bumpPatch, bumpUse, bumpView, evolutionIoAdapter, loadUsage, saveUsage, skillsRoot } from "@lmzhen/dsh-evolution-core";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { appendEvolutionEvent, bumpPatch, bumpUse, bumpView, eventsFile, evolutionIoAdapter, getRecord, loadUsage, markAgentCreated, mutateUsage, skillsRoot } from "@lmzhen/dsh-evolution-core";
4
6
  //#region lib/types/index.js
5
7
  /**
6
8
  * Skill usage telemetry service for the evolution family.
7
9
  * @module @lmzhen/dsh-skill-usage
8
10
  */
11
+ /**
12
+ * Read tool names -> usage kind. The single declarative classification table
13
+ * for read-side observation (A2): any tool listed here records a `view` when
14
+ * its call arguments name a skill.
15
+ */
16
+ const READ_TOOL_KIND = {
17
+ skill: "view",
18
+ skill_load: "view"
19
+ };
20
+ /**
21
+ * Skill name from a tool/call arguments payload (A2, mirrors
22
+ * evolution-review's collectReadSkillNames): JSON strings are re-parsed and
23
+ * `name` wins over `skill`. Empty when unparseable or anonymous.
24
+ */
25
+ function skillNameFromToolCall(raw) {
26
+ let parsed = {};
27
+ if (typeof raw === "string") try {
28
+ parsed = JSON.parse(raw);
29
+ } catch {
30
+ return "";
31
+ }
32
+ else if (raw && typeof raw === "object") parsed = raw;
33
+ return typeof parsed.name === "string" ? parsed.name : typeof parsed.skill === "string" ? parsed.skill : "";
34
+ }
35
+ /** Cumulative library-wide usage totals (C observation-window event counts). */
36
+ function usageTotals(map) {
37
+ let views = 0;
38
+ let use = 0;
39
+ let patches = 0;
40
+ for (const record of map.values()) {
41
+ views += record.view_count;
42
+ use += record.use_count;
43
+ patches += record.patch_count;
44
+ }
45
+ return {
46
+ skills: map.size,
47
+ views,
48
+ use,
49
+ patches
50
+ };
51
+ }
9
52
  var SkillUsageRegistry = class extends Service {
10
53
  static inject = ["evolutionIo"];
11
- static Config = z.object({ root: z.string().default("") });
54
+ static Config = z.object({
55
+ root: z.string().default(""),
56
+ eventsHome: z.string().default("")
57
+ });
12
58
  root;
59
+ eventsHome;
13
60
  io;
14
- usage = null;
15
61
  chain = Promise.resolve();
16
62
  constructor(ctx, config = {}) {
17
63
  super(ctx, "skillUsage");
18
- this.root = config.root ?? skillsRoot();
64
+ this.root = config.root || skillsRoot();
65
+ this.eventsHome = config.eventsHome || process.env.DSH_HOME || join(homedir(), ".dsh");
19
66
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
67
+ ctx.on("session/event", (_session, event) => {
68
+ if (event.type !== "tool/call") return;
69
+ if (!READ_TOOL_KIND[event.data.name]) return;
70
+ const name = skillNameFromToolCall(event.data.arguments);
71
+ if (!name) return;
72
+ this.observeRead(name).catch(() => {});
73
+ });
20
74
  }
21
- async map() {
22
- if (!this.usage) this.usage = await loadUsage(this.root, this.io);
23
- return this.usage;
75
+ /**
76
+ * Read-side telemetry (A2): bump the view counter for an EXISTING record
77
+ * only. Creation-free by design — otherwise an arbitrary read mints a
78
+ * record, and "patched many times, reads zero" (the write-ghost signal)
79
+ * stays computable only while records exist from authorship paths.
80
+ *
81
+ * Observation window (C): the FIRST observed read library-wide opens the
82
+ * window — one `type:'usage'` event is appended to the evolution timeline
83
+ * (best-effort; the usage sidecar stays the truth and never fails the read
84
+ * path). Post-A2 deployments treat `view_count` as trustworthy only after
85
+ * that anchor exists (curator gates churn on `usageObserved()`).
86
+ */
87
+ observeRead(name) {
88
+ return this.mutate(async (map) => {
89
+ if (!map.has(name)) return;
90
+ const viewsBefore = usageTotals(map).views;
91
+ bumpView(map, name, /* @__PURE__ */ new Date());
92
+ if (viewsBefore === 0) await this.appendUsageWindowEvent(map);
93
+ });
24
94
  }
25
- async flush() {
26
- if (this.usage) await saveUsage(this.root, this.usage, this.io);
95
+ /** Append the observation-window anchor event; best-effort, never fails the observation. */
96
+ async appendUsageWindowEvent(map) {
97
+ try {
98
+ const at = (/* @__PURE__ */ new Date()).toISOString();
99
+ await appendEvolutionEvent(this.io, eventsFile(this.eventsHome), {
100
+ type: "usage",
101
+ kind: "skill",
102
+ source: "observation",
103
+ note: "observation window opened",
104
+ counts: usageTotals(map),
105
+ window: { opened: at }
106
+ });
107
+ } catch {}
27
108
  }
28
- /** Serialize read-modify-write cycles so concurrent record calls never lose updates. */
109
+ /**
110
+ * Serialize read-modify-write cycles so concurrent record calls never lose
111
+ * updates, then run each cycle as ONE atomic transact (rc.50 P2-2): the map
112
+ * is read from disk inside the lock, so a second process that shares
113
+ * DSH_HOME cannot interleave its own RMW between our read and write.
114
+ */
29
115
  mutate(task) {
30
- const run = this.chain.then(async () => task(await this.map()));
116
+ const run = this.chain.then(async () => {
117
+ let outcome = void 0;
118
+ await mutateUsage(this.root, this.io, async (map) => {
119
+ outcome = await task(map);
120
+ });
121
+ return outcome;
122
+ });
31
123
  this.chain = run.then(() => void 0, () => void 0);
32
124
  return run;
33
125
  }
34
126
  async record(name, kind, at = /* @__PURE__ */ new Date()) {
35
- await this.mutate(async (map) => {
127
+ await this.mutate((map) => {
36
128
  if (kind === "use") bumpUse(map, name, at);
37
129
  else if (kind === "view") bumpView(map, name, at);
38
130
  else bumpPatch(map, name, at);
39
- await this.flush();
40
131
  });
41
132
  }
42
- report() {
43
- return this.mutate((map) => Promise.resolve(new Map(map)));
133
+ /** Read-only snapshot of the sidecar (no disk write — reading never mutates). */
134
+ async report() {
135
+ const run = this.chain.then(async () => new Map(await loadUsage(this.root, this.io)));
136
+ this.chain = run.then(() => void 0, () => void 0);
137
+ return run;
44
138
  }
45
139
  async markAgentCreated(name) {
46
- await this.mutate(async (map) => {
47
- const { markAgentCreated } = await import("@lmzhen/dsh-evolution-core");
140
+ await this.mutate((map) => {
48
141
  markAgentCreated(map, name);
49
- await this.flush();
50
142
  });
51
143
  }
144
+ /**
145
+ * Ensure a usage record exists for `name` without bumping any counter
146
+ * (rc.46 M3-3.3 companion): skill creation is authorship — the record must
147
+ * exist from birth (created_at anchors now) but `patch_count` stays 0 so
148
+ * mutation maturity is not inflated by mere creation.
149
+ */
150
+ async ensureRecord(name) {
151
+ await this.mutate((map) => {
152
+ getRecord(map, name);
153
+ });
154
+ }
155
+ /**
156
+ * Mark a skill's usage record as archived (delete / curator archive paths).
157
+ * Unlike `record('patch')` this never bumps the patch counter — archiving is
158
+ * a state transition, not a content mutation.
159
+ */
160
+ async markArchived(name, at = /* @__PURE__ */ new Date()) {
161
+ await this.mutate((map) => {
162
+ const record = map.get(name);
163
+ if (record) {
164
+ record.state = "archived";
165
+ record.archived_at = at.toISOString();
166
+ }
167
+ });
168
+ }
169
+ /**
170
+ * Barrier for external writers: every mutate reads the sidecar fresh from
171
+ * disk (rc.50 P2-2 transact), so a curator direct-write is visible on the
172
+ * next call without a cache flush; this waits for queued work to drain.
173
+ */
174
+ async invalidate() {
175
+ await this.chain;
176
+ }
52
177
  /** Write feedback-derived quality onto the usage sidecar; curator reads it. */
53
178
  async setQuality(name, score, warn) {
54
- await this.mutate(async (map) => {
179
+ await this.mutate((map) => {
55
180
  const record = map.get(name);
56
181
  if (!record) return;
57
182
  record.quality_score = score;
58
183
  record.quality_warn = warn;
59
- await this.flush();
60
184
  });
61
185
  }
62
186
  };
@@ -12,22 +12,62 @@ declare module '@deepseek-ai/cordis' {
12
12
  }
13
13
  export interface Config {
14
14
  root?: string;
15
+ /** Home for the evolution event timeline (`<eventsHome>/evolution/events.json`); defaults to DSH_HOME or ~/.dsh. */
16
+ eventsHome?: string;
15
17
  }
16
18
  export declare class SkillUsageRegistry extends Service {
17
19
  static inject: string[];
18
20
  static Config: Schema<Config>;
19
21
  readonly root: string;
22
+ private readonly eventsHome;
20
23
  private readonly io;
21
- private usage;
22
24
  private chain;
23
25
  constructor(ctx: Context, config?: Config);
24
- private map;
25
- flush(): Promise<void>;
26
- /** Serialize read-modify-write cycles so concurrent record calls never lose updates. */
26
+ /**
27
+ * Read-side telemetry (A2): bump the view counter for an EXISTING record
28
+ * only. Creation-free by design otherwise an arbitrary read mints a
29
+ * record, and "patched many times, reads zero" (the write-ghost signal)
30
+ * stays computable only while records exist from authorship paths.
31
+ *
32
+ * Observation window (C): the FIRST observed read library-wide opens the
33
+ * window — one `type:'usage'` event is appended to the evolution timeline
34
+ * (best-effort; the usage sidecar stays the truth and never fails the read
35
+ * path). Post-A2 deployments treat `view_count` as trustworthy only after
36
+ * that anchor exists (curator gates churn on `usageObserved()`).
37
+ */
38
+ private observeRead;
39
+ /** Append the observation-window anchor event; best-effort, never fails the observation. */
40
+ private appendUsageWindowEvent;
41
+ /**
42
+ * Serialize read-modify-write cycles so concurrent record calls never lose
43
+ * updates, then run each cycle as ONE atomic transact (rc.50 P2-2): the map
44
+ * is read from disk inside the lock, so a second process that shares
45
+ * DSH_HOME cannot interleave its own RMW between our read and write.
46
+ */
27
47
  private mutate;
28
48
  record(name: string, kind: 'use' | 'view' | 'patch', at?: Date): Promise<void>;
49
+ /** Read-only snapshot of the sidecar (no disk write — reading never mutates). */
29
50
  report(): Promise<UsageMap>;
30
51
  markAgentCreated(name: string): Promise<void>;
52
+ /**
53
+ * Ensure a usage record exists for `name` without bumping any counter
54
+ * (rc.46 M3-3.3 companion): skill creation is authorship — the record must
55
+ * exist from birth (created_at anchors now) but `patch_count` stays 0 so
56
+ * mutation maturity is not inflated by mere creation.
57
+ */
58
+ ensureRecord(name: string): Promise<void>;
59
+ /**
60
+ * Mark a skill's usage record as archived (delete / curator archive paths).
61
+ * Unlike `record('patch')` this never bumps the patch counter — archiving is
62
+ * a state transition, not a content mutation.
63
+ */
64
+ markArchived(name: string, at?: Date): Promise<void>;
65
+ /**
66
+ * Barrier for external writers: every mutate reads the sidecar fresh from
67
+ * disk (rc.50 P2-2 transact), so a curator direct-write is visible on the
68
+ * next call without a cache flush; this waits for queued work to drain.
69
+ */
70
+ invalidate(): Promise<void>;
31
71
  /** Write feedback-derived quality onto the usage sidecar; curator reads it. */
32
72
  setQuality(name: string, score: number, warn: boolean): Promise<void>;
33
73
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-skill-usage",
3
3
  "description": "Skill usage telemetry service (community build)",
4
- "version": "0.1.0-rc.9",
4
+ "version": "0.2.0-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,16 +33,18 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@deepseek-ai/schemastery": "^3.18.1",
36
- "@lmzhen/dsh-evolution-core": "^0.1.0-rc.9"
36
+ "@lmzhen/dsh-evolution-core": "^0.2.0-rc.1"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
40
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
41
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.9"
40
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
41
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
42
+ "@lmzhen/dsh-evolution-io": "^0.2.0-rc.1"
42
43
  },
43
44
  "devDependencies": {
44
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
45
- "@lmzhen/dsh-evolution-core": "^0.1.0-rc.9",
46
- "@lmzhen/dsh-evolution-io": "^0.1.0-rc.9"
45
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
47
+ "@lmzhen/dsh-evolution-core": "^0.2.0-rc.1",
48
+ "@lmzhen/dsh-evolution-io": "^0.2.0-rc.1"
47
49
  }
48
50
  }