Skip to content

Commit 485170e

Browse files
committed
mods: the telemetry contract exports its types at top level, its hooks and tests importing them from the folder, so the engine's repository reads the same file by path
1 parent b95b015 commit 485170e

8 files changed

Lines changed: 116 additions & 115 deletions

File tree

‎mods/README.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,21 @@ rendered, as a click in the terminal does.
8181
## Composing mods: noun contracts
8282

8383
A mod that adds a noun to `$` in the `engine.create` fold owns that noun's
84-
types, and keeps them in one place: its `types/index.d.ts`, an ambient file
85-
with no imports that merges into `claude-code`, declaring the noun on
86-
`EngineInterface` and exporting the types it is made of, each named for the
87-
noun (`telemetry/types/index.d.ts` declares `$.telemetry` and exports
88-
`Telemetry`, `TelemetryLogEntry`, `TelemetryMarkEntry` and the rest).
84+
types, and keeps them in one place: its `types/index.d.ts`, a declaration
85+
file with no imports that exports the types the noun is made of, each named
86+
for the noun, and declares the noun on `EngineInterface` in `claude-code`
87+
(`telemetry/types/index.d.ts` exports `Telemetry`, `TelemetryLogEntry`,
88+
`TelemetryMarkEntry` and the rest, and declares `$.telemetry`).
8989

9090
- The contract is the only declaration of the noun. The mod's own hooks
91-
import its types from `claude-code` (`import type { Telemetry } from
92-
'claude-code'`), and the value its `engine.create` hook returns is checked
91+
import its types from the folder (`import type { Telemetry } from
92+
'../types'`), and the value its `engine.create` hook returns is checked
9393
against `EngineInterface['telemetry']`, so the implementation cannot drift
9494
from what callers read.
9595
- A mod that calls another's noun reads the same file and never copies it:
9696
`mods/tsconfig.json` includes `*/types/**/*.d.ts`, so `$.telemetry.log(…)`
97-
in `diff` types against `telemetry`'s contract as it stands.
97+
in `diff` types against `telemetry`'s contract as it stands, and a helper
98+
that must name one of its types imports it from that folder by path.
9899
- A test of a mod that calls another's noun seats a provider for it, an inline
99100
plugin whose `engine.create` hook adds the noun, and answers the calls the
100101
way it answers the engine's: `on('telemetry.log', ($, e) => ({ value:

‎mods/telemetry/hooks/entries/is-mark-kind/is-mark-kind.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { TelemetryMarkKind } from 'claude-code'
2-
1+
import type { TelemetryMarkKind } from '../../../types'
32
import { MARK_KINDS } from '../mark-kinds'
43

54
/**

‎mods/telemetry/hooks/entries/mark-kinds/mark-kinds.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TelemetryMarkKind } from 'claude-code'
1+
import type { TelemetryMarkKind } from '../../../types'
22

33
/**
44
* The three kinds a mark may be, in the order the feature events name them.

‎mods/telemetry/hooks/entries/mark/mark.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { TelemetryMarkKind } from 'claude-code'
2-
1+
import type { TelemetryMarkKind } from '../../../types'
32
import type { Fields } from '../fields'
43

54
/**

‎mods/telemetry/hooks/telemetry-of/telemetry-of.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { Telemetry } from 'claude-code'
2-
1+
import type { Telemetry } from '../../types'
32
import Entries from '../entries'
43
import { isAnalyticsOff } from '../is-analytics-off'
54
import type { TelemetryDeps } from '../telemetry-deps'

‎mods/telemetry/tests/fixtures/record.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { CommandRunInput, TelemetryLogEntry } from 'claude-code'
1+
import type { CommandRunInput } from 'claude-code'
2+
3+
import type { TelemetryLogEntry } from '../../types'
24

35
/**
46
* The command that has the recording plugin log an entry, typed as the

‎mods/telemetry/tests/fixtures/survey-answer.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TelemetryLogEntry } from 'claude-code'
1+
import type { TelemetryLogEntry } from '../../types'
22

33
/**
44
* A survey answered, as a plugin logs it.

‎mods/telemetry/types/index.d.ts‎

Lines changed: 99 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,116 @@
11
/**
22
* The `$.telemetry` noun as every caller sees it: the one contract for the
3-
* noun, merged into `claude-code` beside the engine's own declarations.
3+
* noun, its types exported here and the noun declared on `EngineInterface`.
44
*
55
* The telemetry mod adds the noun in the `engine.create` fold and checks its
6-
* return against `EngineInterface['telemetry']`; a mod that calls it, and a
7-
* test that answers it with `on('telemetry.log', …)`, read the same types by
8-
* including this folder in their tsconfig. Nothing here is imported: the file
9-
* is ambient, so it merges wherever it is included.
6+
* return against `EngineInterface['telemetry']`; its hooks import these types
7+
* from this folder, a mod that calls the noun and a test that answers it read
8+
* them by including it in their tsconfig, and the engine's repository imports
9+
* the folder by path. Nothing here is imported, so it stands on its own.
1010
*/
11-
declare module 'claude-code' {
12-
interface EngineInterface {
13-
/**
14-
* A plugin's analytics, one first-party row per call; present only where
15-
* the telemetry mod is seated (internal builds), absent everywhere else.
16-
*/
17-
telemetry: Telemetry
18-
}
1911

12+
/**
13+
* A plugin's analytics, sent one event at a time through `$.telemetry`.
14+
*
15+
* Internal builds alone: the telemetry mod adds the noun in the
16+
* `engine.create` fold, so a plugin on an external build, or one where the
17+
* mod is off, finds no `$.telemetry` and its call throws.
18+
*/
19+
export type Telemetry = {
2020
/**
21-
* A plugin's analytics, sent one event at a time through `$.telemetry`.
21+
* Sends one event, `tengu_plugin_<event>`, as one first-party row;
22+
* resolves once the ingest accepted it.
23+
*
24+
* The calling mod names itself in `event`; one already named `tengu_…` is
25+
* sent as named. A value is a finite number, a boolean or a
26+
* TelemetryChoice; free text is refused. One input, as every op on `$`
27+
* takes.
2228
*
23-
* Internal builds alone: the telemetry mod adds the noun in the
24-
* `engine.create` fold, so a plugin on an external build, or one where the
25-
* mod is off, finds no `$.telemetry` and its call throws.
29+
* @param entry the event's name, a snake_case token, and its properties by
30+
* snake_case key
31+
* @example
32+
* await $.telemetry.log({
33+
* event: "suggest_learning_survey_answered",
34+
* props: {
35+
* answer: 2,
36+
* page: { value: "ready", of: ["ready", "later"] },
37+
* },
38+
* })
2639
*/
27-
export type Telemetry = {
28-
/**
29-
* Sends one event, `tengu_plugin_<event>`, as one first-party row;
30-
* resolves once the ingest accepted it.
31-
*
32-
* The calling mod names itself in `event`; one already named `tengu_…` is
33-
* sent as named. A value is a finite number, a boolean or a
34-
* TelemetryChoice; free text is refused. One input, as every op on `$`
35-
* takes.
36-
*
37-
* @param entry the event's name, a snake_case token, and its properties by
38-
* snake_case key
39-
* @example
40-
* await $.telemetry.log({
41-
* event: "suggest_learning_survey_answered",
42-
* props: {
43-
* answer: 2,
44-
* page: { value: "ready", of: ["ready", "later"] },
45-
* },
46-
* })
47-
*/
48-
log: (entry: TelemetryLogEntry) => Promise<void>
49-
50-
/**
51-
* Marks one use of a feature as the CLI's own feature events do, one
52-
* `tengu_feature_<kind>` row; resolves once the ingest accepted it.
53-
*
54-
* The row carries `feature_name`, `error_code` on sad or bad (`reason`,
55-
* required there and refused on ok) and the entry's `props`, checked as
56-
* `log`'s are; it joins the product-wide feature surface, so no prefix.
57-
*
58-
* @param entry the feature, how it went, why when not ok, and the row's
59-
* properties by snake_case key
60-
* @example
61-
* await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
62-
* await $.telemetry.mark({
63-
* feature: "learn_page",
64-
* kind: "sad",
65-
* reason: "blocked",
66-
* })
67-
*/
68-
mark: (entry: TelemetryMarkEntry) => Promise<void>
69-
}
40+
log: (entry: TelemetryLogEntry) => Promise<void>
7041

7142
/**
72-
* What `$.telemetry.log` takes: the event's name after the prefix, and its
73-
* properties by snake_case key.
43+
* Marks one use of a feature as the CLI's own feature events do, one
44+
* `tengu_feature_<kind>` row; resolves once the ingest accepted it.
45+
*
46+
* The row carries `feature_name`, `error_code` on sad or bad (`reason`,
47+
* required there and refused on ok) and the entry's `props`, checked as
48+
* `log`'s are; it joins the product-wide feature surface, so no prefix.
49+
*
50+
* @param entry the feature, how it went, why when not ok, and the row's
51+
* properties by snake_case key
52+
* @example
53+
* await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
54+
* await $.telemetry.mark({
55+
* feature: "learn_page",
56+
* kind: "sad",
57+
* reason: "blocked",
58+
* })
7459
*/
75-
export type TelemetryLogEntry = {
76-
event: string
77-
props?: Readonly<Record<string, TelemetryProp>>
78-
}
60+
mark: (entry: TelemetryMarkEntry) => Promise<void>
61+
}
7962

80-
/**
81-
* What `$.telemetry.mark` takes: the feature, how it went, why when not
82-
* ok, and the properties the row carries beside them by snake_case key.
83-
*/
84-
export type TelemetryMarkEntry = {
85-
feature: string
86-
kind: TelemetryMarkKind
87-
reason?: string
88-
props?: Readonly<Record<string, TelemetryProp>>
89-
}
63+
/**
64+
* What `$.telemetry.log` takes: the event's name after the prefix, and its
65+
* properties by snake_case key.
66+
*/
67+
export type TelemetryLogEntry = {
68+
event: string
69+
props?: Readonly<Record<string, TelemetryProp>>
70+
}
9071

91-
/**
92-
* How a feature went, as the CLI's own feature events count it.
93-
*
94-
* `ok`: used, the person got what they asked. `sad`: degraded, a fallback
95-
* or a partial, the person still got something. `bad`: failed, the person
96-
* got nothing.
97-
*/
98-
export type TelemetryMarkKind = 'ok' | 'sad' | 'bad'
72+
/**
73+
* What `$.telemetry.mark` takes: the feature, how it went, why when not
74+
* ok, and the properties the row carries beside them by snake_case key.
75+
*/
76+
export type TelemetryMarkEntry = {
77+
feature: string
78+
kind: TelemetryMarkKind
79+
reason?: string
80+
props?: Readonly<Record<string, TelemetryProp>>
81+
}
9982

100-
/**
101-
* A property's value: a finite number, a boolean, or a TelemetryChoice;
102-
* never free text.
103-
*/
104-
export type TelemetryProp = number | boolean | TelemetryChoice
83+
/**
84+
* How a feature went, as the CLI's own feature events count it.
85+
*
86+
* `ok`: used, the person got what they asked. `sad`: degraded, a fallback
87+
* or a partial, the person still got something. `bad`: failed, the person
88+
* got nothing.
89+
*/
90+
export type TelemetryMarkKind = 'ok' | 'sad' | 'bad'
10591

106-
/**
107-
* A string property: the value and the list it is chosen from, declared
108-
* beside it, so no free text reaches the row.
109-
*
110-
* Every member of `of` is a lowercase token of letters, digits, `_` and
111-
* `-`, which may start with a digit, at most 32 of them; `value` is one of
112-
* them.
113-
*/
114-
export type TelemetryChoice = { value: string; of: readonly string[] }
92+
/**
93+
* A property's value: a finite number, a boolean, or a TelemetryChoice;
94+
* never free text.
95+
*/
96+
export type TelemetryProp = number | boolean | TelemetryChoice
97+
98+
/**
99+
* A string property: the value and the list it is chosen from, declared
100+
* beside it, so no free text reaches the row.
101+
*
102+
* Every member of `of` is a lowercase token of letters, digits, `_` and
103+
* `-`, which may start with a digit, at most 32 of them; `value` is one of
104+
* them.
105+
*/
106+
export type TelemetryChoice = { value: string; of: readonly string[] }
107+
108+
declare module 'claude-code' {
109+
interface EngineInterface {
110+
/**
111+
* A plugin's analytics, one first-party row per call; present only where
112+
* the telemetry mod is seated (internal builds), absent everywhere else.
113+
*/
114+
telemetry: Telemetry
115+
}
115116
}

0 commit comments

Comments
 (0)