@stateforge-labs/herdr-sdk 0.0.0-stage → 0.3.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.
Files changed (171) hide show
  1. package/LICENSE +18 -0
  2. package/NOTICE.md +51 -0
  3. package/README.md +211 -2
  4. package/bin/herdr-drift.js +14 -0
  5. package/bin/herdr-skill.js +38 -0
  6. package/dist/admin.d.ts +2 -0
  7. package/dist/admin.js +3 -0
  8. package/dist/constants.d.ts +236 -0
  9. package/dist/constants.js +294 -0
  10. package/dist/core/backoff.d.ts +20 -0
  11. package/dist/core/backoff.js +53 -0
  12. package/dist/core/constants.d.ts +143 -0
  13. package/dist/core/constants.js +98 -0
  14. package/dist/core/errors.d.ts +35 -0
  15. package/dist/core/errors.js +16 -0
  16. package/dist/core/events.d.ts +42 -0
  17. package/dist/core/events.js +128 -0
  18. package/dist/core/kernel.d.ts +65 -0
  19. package/dist/core/kernel.js +138 -0
  20. package/dist/core/link.d.ts +47 -0
  21. package/dist/core/link.js +187 -0
  22. package/dist/core/test-kit/constants.d.ts +51 -0
  23. package/dist/core/test-kit/constants.js +43 -0
  24. package/dist/core/test-kit/fake-core.d.ts +61 -0
  25. package/dist/core/test-kit/fake-core.js +228 -0
  26. package/dist/core/test-kit/frames.d.ts +1 -0
  27. package/dist/core/test-kit/frames.js +480 -0
  28. package/dist/core/test-kit/setup-test.d.ts +18 -0
  29. package/dist/core/test-kit/setup-test.js +35 -0
  30. package/dist/core/wire.d.ts +85 -0
  31. package/dist/core/wire.js +199 -0
  32. package/dist/core-test-kit.d.ts +5 -0
  33. package/dist/core-test-kit.js +5 -0
  34. package/dist/core.d.ts +15 -0
  35. package/dist/core.js +12 -0
  36. package/dist/drift/cli.d.ts +22 -0
  37. package/dist/drift/cli.js +146 -0
  38. package/dist/drift/constants.d.ts +83 -0
  39. package/dist/drift/constants.js +73 -0
  40. package/dist/drift/diff.d.ts +7 -0
  41. package/dist/drift/diff.js +91 -0
  42. package/dist/drift/schema.d.ts +19 -0
  43. package/dist/drift/schema.js +190 -0
  44. package/dist/drift/types.d.ts +76 -0
  45. package/dist/drift/types.js +1 -0
  46. package/dist/drift.d.ts +5 -0
  47. package/dist/drift.js +6 -0
  48. package/dist/effect.d.ts +3 -0
  49. package/dist/effect.js +4 -0
  50. package/dist/herdr/client/connect.d.ts +5 -0
  51. package/dist/herdr/client/connect.js +81 -0
  52. package/dist/herdr/client/held.d.ts +54 -0
  53. package/dist/herdr/client/held.js +100 -0
  54. package/dist/herdr/client/long-poll.d.ts +33 -0
  55. package/dist/herdr/client/long-poll.js +66 -0
  56. package/dist/herdr/client/scoped.d.ts +45 -0
  57. package/dist/herdr/client/scoped.js +241 -0
  58. package/dist/herdr/client/streams.d.ts +9 -0
  59. package/dist/herdr/client/streams.js +40 -0
  60. package/dist/herdr/client/subscribe.d.ts +75 -0
  61. package/dist/herdr/client/subscribe.js +208 -0
  62. package/dist/herdr/client/types.d.ts +150 -0
  63. package/dist/herdr/client/types.js +1 -0
  64. package/dist/herdr/client/watch-pane-set.d.ts +34 -0
  65. package/dist/herdr/client/watch-pane-set.js +271 -0
  66. package/dist/herdr/client/watch.d.ts +117 -0
  67. package/dist/herdr/client/watch.js +138 -0
  68. package/dist/herdr/effect/to-tagged-error.d.ts +2 -0
  69. package/dist/herdr/effect/to-tagged-error.js +54 -0
  70. package/dist/herdr/effect/types.d.ts +51 -0
  71. package/dist/herdr/effect/types.js +26 -0
  72. package/dist/herdr/errors.d.ts +53 -0
  73. package/dist/herdr/errors.js +87 -0
  74. package/dist/herdr/generated/entries/admin.d.ts +2 -0
  75. package/dist/herdr/generated/entries/admin.js +5 -0
  76. package/dist/herdr/generated/entries/client.d.ts +2 -0
  77. package/dist/herdr/generated/entries/client.js +5 -0
  78. package/dist/herdr/generated/entries/held.d.ts +2 -0
  79. package/dist/herdr/generated/entries/held.js +5 -0
  80. package/dist/herdr/generated/entries/prompt.d.ts +2 -0
  81. package/dist/herdr/generated/entries/prompt.js +5 -0
  82. package/dist/herdr/generated/entries/read-only-content.d.ts +2 -0
  83. package/dist/herdr/generated/entries/read-only-content.js +5 -0
  84. package/dist/herdr/generated/entries/read-only.d.ts +2 -0
  85. package/dist/herdr/generated/entries/read-only.js +5 -0
  86. package/dist/herdr/generated/entry-methods.d.ts +9 -0
  87. package/dist/herdr/generated/entry-methods.js +17 -0
  88. package/dist/herdr/generated/errors.d.ts +4 -0
  89. package/dist/herdr/generated/errors.js +3 -0
  90. package/dist/herdr/generated/events.d.ts +274 -0
  91. package/dist/herdr/generated/events.js +3 -0
  92. package/dist/herdr/generated/index.d.ts +20 -0
  93. package/dist/herdr/generated/index.js +19 -0
  94. package/dist/herdr/generated/method-table.d.ts +615 -0
  95. package/dist/herdr/generated/method-table.js +106 -0
  96. package/dist/herdr/generated/names/agent-status.d.ts +10 -0
  97. package/dist/herdr/generated/names/agent-status.js +10 -0
  98. package/dist/herdr/generated/names/capability-class.d.ts +14 -0
  99. package/dist/herdr/generated/names/capability-class.js +15 -0
  100. package/dist/herdr/generated/names/event-kind.d.ts +31 -0
  101. package/dist/herdr/generated/names/event-kind.js +31 -0
  102. package/dist/herdr/generated/names/event-match-kind.d.ts +23 -0
  103. package/dist/herdr/generated/names/event-match-kind.js +24 -0
  104. package/dist/herdr/generated/names/index.d.ts +10 -0
  105. package/dist/herdr/generated/names/index.js +12 -0
  106. package/dist/herdr/generated/names/method.d.ts +209 -0
  107. package/dist/herdr/generated/names/method.js +209 -0
  108. package/dist/herdr/generated/names/result-tag.d.ts +68 -0
  109. package/dist/herdr/generated/names/result-tag.js +68 -0
  110. package/dist/herdr/generated/names/server-capability.d.ts +10 -0
  111. package/dist/herdr/generated/names/server-capability.js +11 -0
  112. package/dist/herdr/generated/names/subscription-event-kind.d.ts +8 -0
  113. package/dist/herdr/generated/names/subscription-event-kind.js +8 -0
  114. package/dist/herdr/generated/names/subscription-type.d.ts +31 -0
  115. package/dist/herdr/generated/names/subscription-type.js +32 -0
  116. package/dist/herdr/generated/names/token-name-pattern.d.ts +2 -0
  117. package/dist/herdr/generated/names/token-name-pattern.js +4 -0
  118. package/dist/herdr/generated/namespaces.d.ts +785 -0
  119. package/dist/herdr/generated/namespaces.js +3 -0
  120. package/dist/herdr/generated/overrides.d.ts +33 -0
  121. package/dist/herdr/generated/overrides.js +20 -0
  122. package/dist/herdr/generated/params.d.ts +799 -0
  123. package/dist/herdr/generated/params.js +3 -0
  124. package/dist/herdr/generated/preset-methods.d.ts +4 -0
  125. package/dist/herdr/generated/preset-methods.js +7 -0
  126. package/dist/herdr/generated/presets/sessions-reader.d.ts +2 -0
  127. package/dist/herdr/generated/presets/sessions-reader.js +5 -0
  128. package/dist/herdr/generated/results.d.ts +978 -0
  129. package/dist/herdr/generated/results.js +52 -0
  130. package/dist/herdr/generated/test-data/defaults.d.ts +739 -0
  131. package/dist/herdr/generated/test-data/defaults.js +741 -0
  132. package/dist/herdr/generated/test-data/shapes.d.ts +1325 -0
  133. package/dist/herdr/generated/test-data/shapes.js +2245 -0
  134. package/dist/herdr/test-kit/fake-herdr.d.ts +13 -0
  135. package/dist/herdr/test-kit/fake-herdr.js +216 -0
  136. package/dist/herdr/test-kit/fake-protocol.d.ts +107 -0
  137. package/dist/herdr/test-kit/fake-protocol.js +436 -0
  138. package/dist/herdr/test-kit/types.d.ts +88 -0
  139. package/dist/herdr/test-kit/types.js +1 -0
  140. package/dist/herdr/wire/codec.d.ts +36 -0
  141. package/dist/herdr/wire/codec.js +62 -0
  142. package/dist/herdr/wire/connection.d.ts +13 -0
  143. package/dist/herdr/wire/connection.js +65 -0
  144. package/dist/herdr/wire/dedicated.d.ts +2 -0
  145. package/dist/herdr/wire/dedicated.js +238 -0
  146. package/dist/herdr/wire/push-decoder.d.ts +2 -0
  147. package/dist/herdr/wire/push-decoder.js +40 -0
  148. package/dist/herdr/wire/rpc.d.ts +3 -0
  149. package/dist/herdr/wire/rpc.js +141 -0
  150. package/dist/herdr/wire/socket-path.d.ts +8 -0
  151. package/dist/herdr/wire/socket-path.js +23 -0
  152. package/dist/herdr/wire/types.d.ts +78 -0
  153. package/dist/herdr/wire/types.js +1 -0
  154. package/dist/index.d.ts +11 -0
  155. package/dist/index.js +7 -0
  156. package/dist/read-only.d.ts +5 -0
  157. package/dist/read-only.js +6 -0
  158. package/dist/schema.d.ts +5 -0
  159. package/dist/schema.js +6 -0
  160. package/dist/test-kit.d.ts +2 -0
  161. package/dist/test-kit.js +3 -0
  162. package/docs/api/events.md +43 -0
  163. package/docs/api/methods.md +232 -0
  164. package/package.json +112 -4
  165. package/schema/herdr-api.schema.json +10759 -0
  166. package/schema/herdr-version.json +5 -0
  167. package/skills/herdr-sdk/SKILL.md +75 -0
  168. package/skills/herdr-sdk/references/api-map.md +28 -0
  169. package/skills/herdr-sdk/references/errors.md +19 -0
  170. package/skills/herdr-sdk/references/recipes.md +171 -0
  171. package/skills/herdr-sdk/references/versions.md +7 -0
@@ -0,0 +1,199 @@
1
+ // Role: the herdr-core wire: NDJSON JSON-RPC 2.0 codec, message parsers, the socket path rule and the backoff step.
2
+ // Informs HERDR-ADR-0020 (the herdr-core client); ported from the desktop work in herdr-plugins.
3
+ import { StringDecoder } from 'node:string_decoder';
4
+ import { join } from 'node:path';
5
+ import { err, isErr, isOk, must, ok } from './kernel.js';
6
+ import { brand } from './kernel.js';
7
+ import { parseCount } from './kernel.js';
8
+ import { duration } from './kernel.js';
9
+ import { parseNonEmptyText, parseText } from './kernel.js';
10
+ import { isFields, isNumber, isString } from './kernel.js';
11
+ import { EMPTY_TEXT, HC_BACKOFF_MAX_DOUBLINGS, HC_JSONRPC_VERSION, HC_LINE_SEPARATOR, ParseFailureKind, HcField, HcKind, HcNotificationMethod, UTF8 } from './constants.js';
12
+ import { HC_LINE_CAP, HC_DEFAULT_SESSION, HC_RUNTIME_ENV, HC_RUN_ROOT, HC_SESSION_ENV, HC_SOCKET_ENV, HC_SOCKET_FOLDER, HC_SOCKET_SUFFIX } from './constants.js';
13
+ const NOT_A_ROW = { kind: ParseFailureKind.NotARow };
14
+ const NO_DATA = must(parseHcPayload({}));
15
+ export function parseHcPayload(input) {
16
+ return isFields(input) ? ok(brand(input)) : err(NOT_A_ROW);
17
+ }
18
+ export function parseHcParams(input) {
19
+ return isFields(input) ? ok(brand(input)) : err(NOT_A_ROW);
20
+ }
21
+ export function parseHcTransport(input) {
22
+ const text = parseNonEmptyText(input);
23
+ return isErr(text) ? text : ok(brand(text.value.trim().toLowerCase()));
24
+ }
25
+ export function parseWireChunk(input) {
26
+ return Buffer.isBuffer(input) ? ok(brand(input)) : err(NOT_A_ROW);
27
+ }
28
+ export function parseWireLine(input) {
29
+ const text = parseText(input);
30
+ return isErr(text) ? text : ok(brand(text.value));
31
+ }
32
+ export function parseSocketPath(input) {
33
+ const text = parseNonEmptyText(input);
34
+ return isErr(text) ? text : ok(brand(text.value));
35
+ }
36
+ export function parseHcCallId(input) {
37
+ const count = parseCount(input);
38
+ return isErr(count) ? count : ok(brand(count.value));
39
+ }
40
+ export function parseRpcCode(input) {
41
+ if (!isNumber(input)) {
42
+ return err({ kind: ParseFailureKind.NotANumber });
43
+ }
44
+ return Number.isInteger(input) ? ok(brand(input)) : err({ kind: ParseFailureKind.NotAnInteger });
45
+ }
46
+ export function encodeHcCoreRequest(id, method, params) {
47
+ return must(parseWireLine(JSON.stringify({ [HcField.JsonRpc]: HC_JSONRPC_VERSION, [HcField.Id]: id, [HcField.Method]: method, [HcField.Params]: params }) + HC_LINE_SEPARATOR));
48
+ }
49
+ export class HcCoreLines {
50
+ decoder = new StringDecoder(UTF8);
51
+ overrun;
52
+ cap;
53
+ rest = EMPTY_TEXT;
54
+ constructor(overrun = () => undefined, cap = HC_LINE_CAP) {
55
+ this.overrun = overrun;
56
+ this.cap = cap;
57
+ }
58
+ feed(chunk) {
59
+ const fresh = this.decoder.write(chunk);
60
+ const cut = fresh.lastIndexOf(HC_LINE_SEPARATOR);
61
+ const text = this.rest + (cut < 0 ? fresh : fresh.slice(0, cut));
62
+ if (text.length > this.cap) {
63
+ this.rest = EMPTY_TEXT;
64
+ this.overrun();
65
+ return [];
66
+ }
67
+ if (cut < 0) {
68
+ this.rest = must(parseText(text));
69
+ return [];
70
+ }
71
+ this.rest = must(parseText(fresh.slice(cut + 1)));
72
+ const found = [];
73
+ for (const line of text.split(HC_LINE_SEPARATOR)) {
74
+ try {
75
+ const parsed = parseHcPayload(JSON.parse(line));
76
+ if (isOk(parsed)) {
77
+ found.push(parsed.value);
78
+ }
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ }
84
+ return found;
85
+ }
86
+ }
87
+ export function hcCoreSocketPath() {
88
+ const direct = parseSocketPath(process.env[HC_SOCKET_ENV]);
89
+ if (isOk(direct)) {
90
+ return direct.value;
91
+ }
92
+ const runtime = parseNonEmptyText(process.env[HC_RUNTIME_ENV]);
93
+ const root = isOk(runtime) ? runtime.value : join(HC_RUN_ROOT, String(process.getuid?.() ?? 0));
94
+ const session = parseNonEmptyText(process.env[HC_SESSION_ENV]);
95
+ const name = isOk(session) ? session.value : HC_DEFAULT_SESSION;
96
+ return must(parseSocketPath(join(root, HC_SOCKET_FOLDER, name + HC_SOCKET_SUFFIX)));
97
+ }
98
+ export function nextBackoff(failures, base, cap) {
99
+ return duration(Math.min(cap, base * 2 ** Math.min(failures, HC_BACKOFF_MAX_DOUBLINGS)));
100
+ }
101
+ export function parseHcSnapshot(input) {
102
+ if (!isFields(input)) {
103
+ return err(NOT_A_ROW);
104
+ }
105
+ const seq = parseCount(input[HcField.Seq]);
106
+ const rows = input[HcField.Terminals];
107
+ if (isErr(seq) || !Array.isArray(rows)) {
108
+ return err(NOT_A_ROW);
109
+ }
110
+ const terminals = rows.flatMap((row) => {
111
+ const payload = parseHcPayload(row);
112
+ return isOk(payload) ? [payload.value] : [];
113
+ });
114
+ return ok({ seq: seq.value, terminals });
115
+ }
116
+ export function parseHcCoreHello(input) {
117
+ if (!isFields(input)) {
118
+ return err(NOT_A_ROW);
119
+ }
120
+ const generation = parseCount(input[HcField.Generation]);
121
+ const version = parseText(input[HcField.Version]);
122
+ const names = input[HcField.Methods];
123
+ if (isErr(generation) || isErr(version) || !Array.isArray(names)) {
124
+ return err(NOT_A_ROW);
125
+ }
126
+ const methods = names.flatMap((name) => {
127
+ const method = parseNonEmptyText(name);
128
+ return isOk(method) ? [method.value] : [];
129
+ });
130
+ return ok({ version: version.value, methods, generation: generation.value });
131
+ }
132
+ export function parseHcFault(id, error) {
133
+ if (!isFields(error)) {
134
+ return err(NOT_A_ROW);
135
+ }
136
+ const code = parseRpcCode(error[HcField.Code]);
137
+ const message = parseText(error[HcField.Message]);
138
+ const data = error[HcField.Data];
139
+ const reason = parseText(isFields(data) ? data[HcField.Kind] : EMPTY_TEXT);
140
+ if (isErr(code) || isErr(message) || isErr(reason)) {
141
+ return err(NOT_A_ROW);
142
+ }
143
+ return ok({ kind: HcKind.Fault, id, code: code.value, reason: reason.value, message: message.value });
144
+ }
145
+ export function parseHcReply(input) {
146
+ if (!isFields(input)) {
147
+ return err(NOT_A_ROW);
148
+ }
149
+ const id = parseHcCallId(input[HcField.Id]);
150
+ if (isErr(id)) {
151
+ return id;
152
+ }
153
+ if (Object.hasOwn(input, HcField.Error)) {
154
+ return parseHcFault(id.value, input[HcField.Error]);
155
+ }
156
+ if (!Object.hasOwn(input, HcField.Result)) {
157
+ return err(NOT_A_ROW);
158
+ }
159
+ const result = parseHcPayload(input[HcField.Result]);
160
+ return ok(isOk(result) ? { kind: HcKind.Reply, id: id.value, result: result.value } : { kind: HcKind.Odd, id: id.value });
161
+ }
162
+ export function parseHcEvent(input) {
163
+ if (!isFields(input)) {
164
+ return err(NOT_A_ROW);
165
+ }
166
+ const seq = parseCount(input[HcField.Seq]);
167
+ const type = parseNonEmptyText(input[HcField.Type]);
168
+ const terminal = parseNonEmptyText(input[HcField.Terminal]);
169
+ const data = parseHcPayload(input[HcField.Data]);
170
+ if (isErr(seq) || isErr(type)) {
171
+ return err(NOT_A_ROW);
172
+ }
173
+ return ok({ kind: HcKind.Event, seq: seq.value, terminal: isOk(terminal) ? terminal.value : null, type: type.value, data: isOk(data) ? data.value : NO_DATA });
174
+ }
175
+ export function parseHcHeartbeat(input) {
176
+ const seq = parseCount(isFields(input) ? input[HcField.Seq] : undefined);
177
+ return isErr(seq) ? seq : ok({ kind: HcKind.Heartbeat, seq: seq.value });
178
+ }
179
+ export function parseHcMissed(input) {
180
+ const snapshot = parseHcSnapshot(input);
181
+ return isErr(snapshot) ? snapshot : ok({ kind: HcKind.Missed, snapshot: snapshot.value });
182
+ }
183
+ const NOTIFICATIONS = {
184
+ [HcNotificationMethod.Event]: parseHcEvent,
185
+ [HcNotificationMethod.Heartbeat]: parseHcHeartbeat,
186
+ [HcNotificationMethod.Missed]: parseHcMissed,
187
+ };
188
+ function isNotificationMethod(method) {
189
+ return isString(method) && Object.hasOwn(NOTIFICATIONS, method);
190
+ }
191
+ export function parseHcNotification(method, params) {
192
+ return isNotificationMethod(method) ? NOTIFICATIONS[method](params) : err(NOT_A_ROW);
193
+ }
194
+ export function parseHcCoreMessage(input) {
195
+ if (!isFields(input)) {
196
+ return err(NOT_A_ROW);
197
+ }
198
+ return Object.hasOwn(input, HcField.Method) ? parseHcNotification(input[HcField.Method], input[HcField.Params]) : parseHcReply(input);
199
+ }
@@ -0,0 +1,5 @@
1
+ export { FakeFault, FakeHcCore, agentTerminal, frame, later, nextWithin, shellTerminal } from './core/test-kit/fake-core.js';
2
+ export { DemoTerminal, FakeFaultKind, FakeRpcCode, FrameName } from './core/test-kit/constants.js';
3
+ export type { Handler, Seen } from './core/test-kit/fake-core.js';
4
+ export { FAST_EVENTS, FAST_LINK, openWorld, setupTest } from './core/test-kit/setup-test.js';
5
+ export type { DesktopWorld as CoreWorld } from './core/test-kit/setup-test.js';
@@ -0,0 +1,5 @@
1
+ // Role: the core test-kit entry ("./core/test-kit"): the fake herdr-core and the setup for tests of code built on the core client (imported by tests only).
2
+ // Informs HERDR-ADR-0020 (the herdr-core client); ported from the desktop work in herdr-plugins.
3
+ export { FakeFault, FakeHcCore, agentTerminal, frame, later, nextWithin, shellTerminal } from './core/test-kit/fake-core.js';
4
+ export { DemoTerminal, FakeFaultKind, FakeRpcCode, FrameName } from './core/test-kit/constants.js';
5
+ export { FAST_EVENTS, FAST_LINK, openWorld, setupTest } from './core/test-kit/setup-test.js';
package/dist/core.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './core/backoff.js';
2
+ export * from './core/constants.js';
3
+ export * from './core/events.js';
4
+ export * from './core/link.js';
5
+ export * from './core/wire.js';
6
+ export { duration } from './core/kernel.js';
7
+ export type { Duration, Instant } from './core/kernel.js';
8
+ export { parseCount } from './core/kernel.js';
9
+ export type { Count } from './core/kernel.js';
10
+ export { parseNonEmptyText, parseText } from './core/kernel.js';
11
+ export type { NonEmptyText, Text } from './core/kernel.js';
12
+ export { byKind, isErr, isOk, must } from './core/kernel.js';
13
+ export type { Err, KindMap, KindOf, Ok, Result } from './core/kernel.js';
14
+ export { CoreInvariantError, isUnknown, unknown } from './core/errors.js';
15
+ export type { ParseFailure, Unknowable, Unknown } from './core/errors.js';
package/dist/core.js ADDED
@@ -0,0 +1,12 @@
1
+ // Role: the core entry ("./core"): a typed client of the herdr-core socket (herdr generation 1): CoreLink for calls, CoreEvents for the event feed, the wire parsers and the socket path rule.
2
+ // Informs HERDR-ADR-0020 (the herdr-core client); ported from the desktop work in herdr-plugins.
3
+ export * from './core/backoff.js';
4
+ export * from './core/constants.js';
5
+ export * from './core/events.js';
6
+ export * from './core/link.js';
7
+ export * from './core/wire.js';
8
+ export { duration } from './core/kernel.js';
9
+ export { parseCount } from './core/kernel.js';
10
+ export { parseNonEmptyText, parseText } from './core/kernel.js';
11
+ export { byKind, isErr, isOk, must } from './core/kernel.js';
12
+ export { CoreInvariantError, isUnknown, unknown } from './core/errors.js';
@@ -0,0 +1,22 @@
1
+ import { DriftExitCode } from '../constants.js';
2
+ import { ArgsKind } from './constants.js';
3
+ import type { DriftOutcome } from './types.js';
4
+ import type { RunHerdr } from './schema.js';
5
+ export declare const HELP_TEXT: string;
6
+ export interface RunArgs {
7
+ readonly kind: typeof ArgsKind.Run;
8
+ readonly committed: string | null;
9
+ readonly herdr: string | null;
10
+ readonly json: boolean;
11
+ }
12
+ export type ParsedArgs = RunArgs | {
13
+ readonly kind: typeof ArgsKind.Help;
14
+ } | {
15
+ readonly kind: typeof ArgsKind.Usage;
16
+ readonly message: string;
17
+ };
18
+ export declare function parseArgs(argv: readonly string[]): ParsedArgs;
19
+ export declare function renderOutcome(outcome: DriftOutcome): string[];
20
+ export declare function renderJson(outcome: DriftOutcome): string;
21
+ export type SpawnHerdr = (binary: string) => RunHerdr;
22
+ export declare function runDriftCli(argv: readonly string[], spawnHerdr: SpawnHerdr): Promise<DriftExitCode>;
@@ -0,0 +1,146 @@
1
+ // Role: the herdr-drift command: arguments, help, rendering and the run.
2
+ // Informs HERDR-ADR-0009, HERDR-ADR-0008; merged by HERDR-ADR-0021 from help-text.ts, args.ts, render.ts, cli.ts.
3
+ import { DriftExitCode, DriftOutcomeKind } from '../constants.js';
4
+ import { DriftName, ArgFlag, ArgsKind, UsageText, DriftFacet, DEFAULT_HERDR_BIN } from './constants.js';
5
+ import { hasMovement, exitCodeFor, outcomeOf, outcomeOrNotCovered } from './diff.js';
6
+ import { readFileSync } from 'node:fs';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { committedSource, installedSource } from './schema.js';
9
+ export const HELP_TEXT = `${DriftName} [--committed <dir>] [--herdr <bin>] [--json] [--help]
10
+
11
+ Answers ONE question: does the schema committed in schema/ still match the
12
+ schema the installed herdr publishes?
13
+
14
+ It compares the protocol number, the herdr version and four facets by name:
15
+ methods, result tags, event kinds and subscribable types. It also reports
16
+ changed field shapes and methods without a METHOD_RESULT row. A facet that
17
+ moved is named with what was added and what was removed.
18
+
19
+ --committed <dir> directory holding herdr-api.schema.json and herdr-version.json
20
+ --herdr <bin> the herdr binary to ask (default: herdr on PATH)
21
+ --json print the report as JSON
22
+
23
+ Exits: ${DriftExitCode.Clean} the copy matches · ${DriftExitCode.Drift} it drifted · ${DriftExitCode.Usage} misuse · ${DriftExitCode.NotCovered} NOT COVERED`;
24
+ export function parseArgs(argv) {
25
+ const run = { committed: null, herdr: null, json: false };
26
+ let index = 0;
27
+ while (index < argv.length) {
28
+ const flag = argv[index] ?? '';
29
+ if (flag === ArgFlag.Help) {
30
+ return { kind: ArgsKind.Help };
31
+ }
32
+ if (flag === ArgFlag.Json) {
33
+ run.json = true;
34
+ index += 1;
35
+ continue;
36
+ }
37
+ if (flag === ArgFlag.Committed || flag === ArgFlag.Herdr) {
38
+ const value = argv[index + 1];
39
+ if (value === undefined) {
40
+ return usage(UsageText.MissingValue(flag));
41
+ }
42
+ if (flag === ArgFlag.Committed) {
43
+ run.committed = value;
44
+ }
45
+ else {
46
+ run.herdr = value;
47
+ }
48
+ index += 2;
49
+ continue;
50
+ }
51
+ return usage(UsageText.Unexpected(flag));
52
+ }
53
+ return { kind: ArgsKind.Run, ...run };
54
+ }
55
+ function usage(message) {
56
+ return { kind: ArgsKind.Usage, message };
57
+ }
58
+ export function renderOutcome(outcome) {
59
+ switch (outcome.kind) {
60
+ case DriftOutcomeKind.Usage:
61
+ return [statusLine(DriftExitCode.Usage, `USAGE: ${outcome.message}`)];
62
+ case DriftOutcomeKind.NotCovered:
63
+ return [statusLine(DriftExitCode.NotCovered, `NOT COVERED: ${outcome.reason}`)];
64
+ case DriftOutcomeKind.Report:
65
+ return reportLines(outcome.report);
66
+ }
67
+ }
68
+ export function renderJson(outcome) {
69
+ return JSON.stringify(outcome.kind === DriftOutcomeKind.Report ? outcome.report : outcome, null, 2);
70
+ }
71
+ function reportLines(report) {
72
+ const findings = findingsOf(report);
73
+ if (findings.length === 0) {
74
+ return [statusLine(DriftExitCode.Clean, `clean (protocol ${report.protocol.installed}, herdr ${report.herdrVersion.installed})`)];
75
+ }
76
+ return [...findings, statusLine(DriftExitCode.Drift, `${findings.length} finding(s)`)];
77
+ }
78
+ function findingsOf(report) {
79
+ const findings = [];
80
+ if (report.protocol.committed !== report.protocol.installed) {
81
+ findings.push(`protocol moved ${report.protocol.committed} -> ${report.protocol.installed}`);
82
+ }
83
+ if (report.herdrVersion.committed !== report.herdrVersion.installed) {
84
+ findings.push(`herdr version moved ${report.herdrVersion.committed} -> ${report.herdrVersion.installed}`);
85
+ }
86
+ const facets = [
87
+ [DriftFacet.Methods, report.methods],
88
+ [DriftFacet.ResultTags, report.resultTags],
89
+ [DriftFacet.EventKinds, report.eventKinds],
90
+ [DriftFacet.SubscriptionTypes, report.subscriptionTypes],
91
+ ];
92
+ for (const [label, diff] of facets) {
93
+ if (hasMovement(diff)) {
94
+ findings.push(facetLine(label, diff));
95
+ }
96
+ }
97
+ if (report.changedShapes.length > 0) {
98
+ findings.push(`field shapes changed: ${report.changedShapes.join(', ')}`);
99
+ }
100
+ if (report.unmappedMethods.length > 0) {
101
+ findings.push(`methods without a METHOD_RESULT row: ${report.unmappedMethods.join(', ')}`);
102
+ }
103
+ return findings;
104
+ }
105
+ function facetLine(label, diff) {
106
+ const parts = [];
107
+ if (diff.added.length > 0) {
108
+ parts.push(`herdr ADDED ${diff.added.length} the copy lacks: ${diff.added.join(', ')}`);
109
+ }
110
+ if (diff.removed.length > 0) {
111
+ parts.push(`herdr REMOVED ${diff.removed.length} the copy still has: ${diff.removed.join(', ')}`);
112
+ }
113
+ return `${label}: ${parts.join(' · ')}`;
114
+ }
115
+ function statusLine(code, text) {
116
+ return `${DriftName}: ${code} — ${text}`;
117
+ }
118
+ // The committed schema directory of the package (src/drift and dist/drift are both two levels below the package root).
119
+ const DEFAULT_SCHEMA_DIR = fileURLToPath(new URL('../../schema', import.meta.url));
120
+ function emit(outcome, json) {
121
+ const code = exitCodeFor(outcome);
122
+ const text = json ? renderJson(outcome) : renderOutcome(outcome).join('\n');
123
+ const stream = code >= DriftExitCode.Usage ? process.stderr : process.stdout;
124
+ stream.write(`${text}\n`);
125
+ return code;
126
+ }
127
+ // Reads both schemas and compares them. Any error thrown here is caught by runDriftCli and reported as NOT COVERED.
128
+ async function lookAt(parsed, spawnHerdr) {
129
+ const readText = (path) => readFileSync(path, 'utf8');
130
+ const committed = await committedSource(parsed.committed ?? DEFAULT_SCHEMA_DIR, readText)();
131
+ const installed = await installedSource(spawnHerdr(parsed.herdr ?? DEFAULT_HERDR_BIN))();
132
+ return outcomeOf(committed, installed);
133
+ }
134
+ // Runs the CLI for the given arguments (without the node or bun and script entries) and returns the exit code the process should use.
135
+ // `spawnHerdr` runs the herdr CLI; bin/herdr-drift.js passes the real one.
136
+ export async function runDriftCli(argv, spawnHerdr) {
137
+ const parsed = parseArgs(argv);
138
+ if (parsed.kind === ArgsKind.Help) {
139
+ process.stdout.write(`${HELP_TEXT}\n`);
140
+ return DriftExitCode.Clean;
141
+ }
142
+ if (parsed.kind === ArgsKind.Usage) {
143
+ return emit({ kind: DriftOutcomeKind.Usage, message: parsed.message }, false);
144
+ }
145
+ return emit(await outcomeOrNotCovered(() => lookAt(parsed, spawnHerdr)), parsed.json);
146
+ }
@@ -0,0 +1,83 @@
1
+ export declare const DriftName = "herdr-drift";
2
+ export declare const SchemaField: {
3
+ readonly Protocol: 'protocol';
4
+ readonly Schemas: 'schemas';
5
+ };
6
+ export declare const SchemaNamespace: {
7
+ readonly Request: 'request';
8
+ readonly SuccessResponse: 'success_response';
9
+ readonly Event: 'event';
10
+ };
11
+ export declare const SchemaDefinition: {
12
+ readonly ResponseResult: 'ResponseResult';
13
+ readonly EventData: 'EventData';
14
+ readonly Subscription: 'Subscription';
15
+ };
16
+ export declare const SchemaKey: {
17
+ readonly Defs: '$defs';
18
+ readonly OneOf: 'oneOf';
19
+ readonly Properties: 'properties';
20
+ readonly Const: 'const';
21
+ readonly Description: 'description';
22
+ };
23
+ export declare const TagProperty: {
24
+ readonly Method: 'method';
25
+ readonly Type: 'type';
26
+ };
27
+ export declare const DriftFacet: {
28
+ readonly Methods: 'methods';
29
+ readonly ResultTags: 'result tags';
30
+ readonly EventKinds: 'event kinds';
31
+ readonly SubscriptionTypes: 'subscribable types';
32
+ };
33
+ export declare const JsType: {
34
+ readonly Object: 'object';
35
+ readonly String: 'string';
36
+ readonly Number: 'number';
37
+ };
38
+ export declare const SystemErrorCode: {
39
+ readonly NotFound: 'ENOENT';
40
+ };
41
+ export declare const SchemaFileName: {
42
+ readonly Schema: 'herdr-api.schema.json';
43
+ readonly Version: 'herdr-version.json';
44
+ };
45
+ export declare const VersionFileField: {
46
+ readonly Version: 'version';
47
+ };
48
+ export declare const HerdrCommand: {
49
+ readonly Version: readonly ['--version'];
50
+ readonly SchemaJson: readonly ['api', 'schema', '--json'];
51
+ };
52
+ export declare const HerdrCall: {
53
+ readonly TimeoutMs: 60000;
54
+ };
55
+ export declare const VERSION_PATTERN: RegExp;
56
+ export declare const DEFAULT_HERDR_BIN = "herdr";
57
+ export declare const ArgFlag: {
58
+ readonly Committed: '--committed';
59
+ readonly Herdr: '--herdr';
60
+ readonly Json: '--json';
61
+ readonly Help: '--help';
62
+ };
63
+ export declare const ArgsKind: {
64
+ readonly Run: 'run';
65
+ readonly Help: 'help';
66
+ readonly Usage: 'usage';
67
+ };
68
+ export declare const ReasonText: {
69
+ readonly CommittedMissing: 'the committed schema is not there';
70
+ readonly CommittedUnreadable: 'the committed schema could not be read';
71
+ readonly VersionMissing: 'the committed herdr-version.json is not there';
72
+ readonly VersionFileUnreadable: 'the committed herdr-version.json could not be read';
73
+ readonly VersionUnreadable: 'the committed herdr-version.json carries no version';
74
+ readonly NotJson: 'the schema is not JSON';
75
+ readonly NoProtocol: 'the schema carries no protocol number';
76
+ readonly HerdrMissing: 'herdr is not on PATH, so the live schema cannot be read';
77
+ readonly HerdrRefused: 'herdr did not return a schema';
78
+ readonly HerdrVersionMissing: 'herdr did not report a version';
79
+ };
80
+ export declare const UsageText: {
81
+ readonly Unexpected: (argument: string) => string;
82
+ readonly MissingValue: (flag: string) => string;
83
+ };
@@ -0,0 +1,73 @@
1
+ // Role: lane-local constants of schema drift: schema bundle and definition names, facet labels, reason texts, herdr calls and CLI flags.
2
+ // Informs HERDR-ADR-0009 (drift) and HERDR-ADR-0017 (lane P5c owns src/drift/**). As const objects, so no string literal is compared in code. The exit codes and outcome kinds stay in the root constants.ts.
3
+ export const DriftName = 'herdr-drift';
4
+ export const SchemaField = { Protocol: 'protocol', Schemas: 'schemas' };
5
+ // The three bundles of the published schema that drift reads (schemas.<bundle>).
6
+ export const SchemaNamespace = {
7
+ Request: 'request',
8
+ SuccessResponse: 'success_response',
9
+ Event: 'event',
10
+ };
11
+ // Named definitions drift looks up inside a bundle.
12
+ export const SchemaDefinition = {
13
+ ResponseResult: 'ResponseResult',
14
+ EventData: 'EventData',
15
+ Subscription: 'Subscription',
16
+ };
17
+ // Keywords and property names read inside schema nodes.
18
+ export const SchemaKey = {
19
+ Defs: '$defs',
20
+ OneOf: 'oneOf',
21
+ Properties: 'properties',
22
+ Const: 'const',
23
+ Description: 'description',
24
+ };
25
+ // The property that names a branch of a tagged union: a request method or a result, event or subscription type.
26
+ export const TagProperty = { Method: 'method', Type: 'type' };
27
+ // Labels of the facets in the human report (HERDR-ADR-0009).
28
+ export const DriftFacet = {
29
+ Methods: 'methods',
30
+ ResultTags: 'result tags',
31
+ EventKinds: 'event kinds',
32
+ SubscriptionTypes: 'subscribable types',
33
+ };
34
+ // JavaScript typeof results, so no string literal is compared in code.
35
+ export const JsType = { Object: 'object', String: 'string', Number: 'number' };
36
+ // The one system error code drift reads: a missing executable (`herdr` not on PATH).
37
+ export const SystemErrorCode = { NotFound: 'ENOENT' };
38
+ // Files of the committed schema directory (`schema/`, written by `bun run capture`).
39
+ export const SchemaFileName = { Schema: 'herdr-api.schema.json', Version: 'herdr-version.json' };
40
+ export const VersionFileField = { Version: 'version' };
41
+ // The herdr CLI calls drift makes. They run only in bin/herdr-drift.ts, never against the socket (HERDR-ADR-0008).
42
+ export const HerdrCommand = {
43
+ Version: ['--version'],
44
+ SchemaJson: ['api', 'schema', '--json'],
45
+ };
46
+ export const HerdrCall = { TimeoutMs: 60_000 };
47
+ // A semantic version inside the text `herdr --version` prints.
48
+ export const VERSION_PATTERN = /\d+\.\d+\.\d+/;
49
+ export const DEFAULT_HERDR_BIN = 'herdr';
50
+ export const ArgFlag = {
51
+ Committed: '--committed',
52
+ Herdr: '--herdr',
53
+ Json: '--json',
54
+ Help: '--help',
55
+ };
56
+ export const ArgsKind = { Run: 'run', Help: 'help', Usage: 'usage' };
57
+ // Why a source could not look (exit 3, HERDR-ADR-0009).
58
+ export const ReasonText = {
59
+ CommittedMissing: 'the committed schema is not there',
60
+ CommittedUnreadable: 'the committed schema could not be read',
61
+ VersionMissing: 'the committed herdr-version.json is not there',
62
+ VersionFileUnreadable: 'the committed herdr-version.json could not be read',
63
+ VersionUnreadable: 'the committed herdr-version.json carries no version',
64
+ NotJson: 'the schema is not JSON',
65
+ NoProtocol: 'the schema carries no protocol number',
66
+ HerdrMissing: 'herdr is not on PATH, so the live schema cannot be read',
67
+ HerdrRefused: 'herdr did not return a schema',
68
+ HerdrVersionMissing: 'herdr did not report a version',
69
+ };
70
+ export const UsageText = {
71
+ Unexpected: (argument) => `unexpected argument '${argument}'`,
72
+ MissingValue: (flag) => `${flag} needs a value`,
73
+ };
@@ -0,0 +1,7 @@
1
+ import type { NameDiff, DriftBetween, DriftOutcome, SchemaRead, ExitCodeFor } from './types.js';
2
+ export declare function nameDiff(committed: readonly string[], installed: readonly string[]): NameDiff;
3
+ export declare function hasMovement(diff: NameDiff): boolean;
4
+ export declare const driftBetween: DriftBetween;
5
+ export declare function outcomeOf(committed: SchemaRead, installed: SchemaRead): DriftOutcome;
6
+ export declare function outcomeOrNotCovered(look: () => Promise<DriftOutcome>): Promise<DriftOutcome>;
7
+ export declare const exitCodeFor: ExitCodeFor;
@@ -0,0 +1,91 @@
1
+ import { inventoryOf, unmappedMethods } from './schema.js';
2
+ import { DriftOutcomeKind, SchemaReadKind, DriftExitCode } from '../constants.js';
3
+ import { MalformedSchemaError } from '../herdr/errors.js';
4
+ export function nameDiff(committed, installed) {
5
+ return {
6
+ added: missingFrom(installed, committed),
7
+ removed: missingFrom(committed, installed),
8
+ };
9
+ }
10
+ export function hasMovement(diff) {
11
+ return diff.added.length > 0 || diff.removed.length > 0;
12
+ }
13
+ function missingFrom(names, others) {
14
+ const known = new Set(others);
15
+ return names.filter((name) => !known.has(name)).toSorted();
16
+ }
17
+ export const driftBetween = (committed, installed) => {
18
+ const copy = inventoryOf(committed.schema, committed.herdrVersion);
19
+ const live = inventoryOf(installed.schema, installed.herdrVersion);
20
+ const methods = nameDiff(copy.methods, live.methods);
21
+ const resultTags = nameDiff(copy.resultTags, live.resultTags);
22
+ const eventKinds = nameDiff(copy.eventKinds, live.eventKinds);
23
+ const subscriptionTypes = nameDiff(copy.subscriptionTypes, live.subscriptionTypes);
24
+ const changedShapes = changedShapesOf(copy, live);
25
+ const unmapped = unmappedMethods(live.methods);
26
+ const drifted = copy.protocol !== live.protocol
27
+ || copy.herdrVersion !== live.herdrVersion
28
+ || [methods, resultTags, eventKinds, subscriptionTypes].some(hasMovement)
29
+ || changedShapes.length > 0
30
+ || unmapped.length > 0;
31
+ return {
32
+ protocol: { committed: copy.protocol, installed: live.protocol },
33
+ herdrVersion: { committed: copy.herdrVersion, installed: live.herdrVersion },
34
+ methods,
35
+ resultTags,
36
+ eventKinds,
37
+ subscriptionTypes,
38
+ changedShapes,
39
+ unmappedMethods: unmapped,
40
+ drifted,
41
+ };
42
+ };
43
+ // Types present on both sides whose stable field shape differs (added and removed types are reported by the facets).
44
+ function changedShapesOf(copy, live) {
45
+ return Object.keys(copy.fieldShapes)
46
+ .filter((name) => Object.hasOwn(live.fieldShapes, name) && copy.fieldShapes[name] !== live.fieldShapes[name])
47
+ .toSorted();
48
+ }
49
+ export function outcomeOf(committed, installed) {
50
+ if (committed.kind !== SchemaReadKind.Found) {
51
+ return notCovered(committed.reason);
52
+ }
53
+ if (installed.kind !== SchemaReadKind.Found) {
54
+ return notCovered(installed.reason);
55
+ }
56
+ return reportOf(committed, installed);
57
+ }
58
+ // Runs the look; any error it throws, expected or not, is NOT COVERED with its message.
59
+ export async function outcomeOrNotCovered(look) {
60
+ try {
61
+ return await look();
62
+ }
63
+ catch (error) {
64
+ return notCovered(error instanceof Error ? error.message : String(error));
65
+ }
66
+ }
67
+ function reportOf(committed, installed) {
68
+ try {
69
+ return { kind: DriftOutcomeKind.Report, report: driftBetween(committed, installed) };
70
+ }
71
+ catch (error) {
72
+ // A schema without a required definition or tag cannot be compared, so it counts as not looked at.
73
+ if (error instanceof MalformedSchemaError) {
74
+ return notCovered(error.message);
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ function notCovered(reason) {
80
+ return { kind: DriftOutcomeKind.NotCovered, reason };
81
+ }
82
+ export const exitCodeFor = (outcome) => {
83
+ switch (outcome.kind) {
84
+ case DriftOutcomeKind.Usage:
85
+ return DriftExitCode.Usage;
86
+ case DriftOutcomeKind.NotCovered:
87
+ return DriftExitCode.NotCovered;
88
+ case DriftOutcomeKind.Report:
89
+ return outcome.report.drifted ? DriftExitCode.Drift : DriftExitCode.Clean;
90
+ }
91
+ };