@stateforge-labs/herdr 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,19 @@
1
+ import type { SchemaDocument, SchemaRead, SchemaSource, InventoryOf } from './types.js';
2
+ export type SchemaNode = Readonly<Record<string, unknown>>;
3
+ export declare function isNode(value: unknown): value is SchemaNode;
4
+ export declare function isString(value: unknown): value is string;
5
+ export declare function isNumber(value: unknown): value is number;
6
+ export declare function namespaceOf(schema: SchemaDocument, name: string): SchemaNode;
7
+ export declare function definitionsOf(namespace: SchemaNode): SchemaNode;
8
+ export declare function definitionNamed(namespace: SchemaNode, name: string): SchemaNode;
9
+ export declare function branchesOf(node: SchemaNode): readonly SchemaNode[];
10
+ export declare function constOf(node: SchemaNode, property: string): string;
11
+ export declare function stableText(value: unknown): string;
12
+ export declare function fieldShapesOf(schema: SchemaDocument): Record<string, string>;
13
+ export type ReadText = (path: string) => string;
14
+ export type RunHerdr = (args: readonly string[]) => string;
15
+ export declare function committedSource(dir: string, readText: ReadText): SchemaSource;
16
+ export declare function installedSource(runHerdr: RunHerdr): SchemaSource;
17
+ export declare function parseSchemaText(text: string, herdrVersion: string): SchemaRead;
18
+ export declare const inventoryOf: InventoryOf;
19
+ export declare function unmappedMethods(methods: readonly string[]): string[];
@@ -0,0 +1,190 @@
1
+ // Role: reading a schema for drift: the committed and installed sources, the walk, the inventory and its stable text.
2
+ // Informs HERDR-ADR-0009, HERDR-ADR-0008, plan 6, plan 4.2; merged by HERDR-ADR-0021 from schema-walk.ts, stable-text.ts, field-shapes.ts, schema-source.ts, inventory.ts, unmapped-methods.ts.
3
+ import { JsType, SchemaField, SchemaKey, SchemaNamespace, HerdrCommand, ReasonText, SchemaFileName, SystemErrorCode, VERSION_PATTERN, VersionFileField, SchemaDefinition, TagProperty } from './constants.js';
4
+ import { MalformedSchemaError } from '../herdr/errors.js';
5
+ import { join } from 'node:path';
6
+ import { SchemaReadKind } from '../constants.js';
7
+ import { METHOD_RESULT } from '../herdr/generated/index.js';
8
+ export function isNode(value) {
9
+ return typeof value === JsType.Object && value !== null;
10
+ }
11
+ // TypeScript narrows typeof only against a literal, so the scalar checks go through these guards.
12
+ export function isString(value) {
13
+ return typeof value === JsType.String;
14
+ }
15
+ export function isNumber(value) {
16
+ return typeof value === JsType.Number;
17
+ }
18
+ export function namespaceOf(schema, name) {
19
+ const schemas = schema[SchemaField.Schemas];
20
+ const found = isNode(schemas) ? schemas[name] : undefined;
21
+ if (!isNode(found)) {
22
+ throw new MalformedSchemaError(`the schema carries no '${name}' namespace`);
23
+ }
24
+ return found;
25
+ }
26
+ export function definitionsOf(namespace) {
27
+ const defs = namespace[SchemaKey.Defs];
28
+ return isNode(defs) ? defs : {};
29
+ }
30
+ export function definitionNamed(namespace, name) {
31
+ const found = definitionsOf(namespace)[name];
32
+ if (!isNode(found)) {
33
+ throw new MalformedSchemaError(`the schema carries no '${name}' definition`);
34
+ }
35
+ return found;
36
+ }
37
+ export function branchesOf(node) {
38
+ const branches = node[SchemaKey.OneOf];
39
+ if (!Array.isArray(branches)) {
40
+ throw new MalformedSchemaError('a tagged union carries no oneOf branches');
41
+ }
42
+ return branches.filter(isNode);
43
+ }
44
+ export function constOf(node, property) {
45
+ const properties = node[SchemaKey.Properties];
46
+ const field = isNode(properties) ? properties[property] : undefined;
47
+ const value = isNode(field) ? field[SchemaKey.Const] : undefined;
48
+ if (!isString(value)) {
49
+ throw new MalformedSchemaError(`a branch carries no string const at '${property}'`);
50
+ }
51
+ return value;
52
+ }
53
+ // Keywords whose value maps names to schemas: their keys are names, so a name such as `description` is kept.
54
+ const NAME_MAP_KEYWORDS = [SchemaKey.Properties, SchemaKey.Defs];
55
+ export function stableText(value) {
56
+ return JSON.stringify(withSortedKeys(value));
57
+ }
58
+ function withSortedKeys(value) {
59
+ if (Array.isArray(value)) {
60
+ return value.map(withSortedKeys);
61
+ }
62
+ if (!isNode(value)) {
63
+ return value;
64
+ }
65
+ const keys = Object.keys(value).filter((key) => key !== SchemaKey.Description).toSorted();
66
+ return Object.fromEntries(keys.map((key) => [key, valueUnder(key, value[key])]));
67
+ }
68
+ // A child of a name map is a schema whose keys are names too; sorting keeps the same text for the same shape.
69
+ function valueUnder(key, child) {
70
+ return NAME_MAP_KEYWORDS.includes(key) && isNode(child) ? namesOf(child) : withSortedKeys(child);
71
+ }
72
+ function namesOf(map) {
73
+ return Object.fromEntries(Object.keys(map).toSorted().map((name) => [name, withSortedKeys(map[name])]));
74
+ }
75
+ export function fieldShapesOf(schema) {
76
+ const shapes = {};
77
+ for (const bundle of [SchemaNamespace.Request, SchemaNamespace.SuccessResponse]) {
78
+ for (const [name, node] of Object.entries(definitionsOf(namespaceOf(schema, bundle)))) {
79
+ shapes[`${bundle}.${name}`] = stableText(node);
80
+ }
81
+ }
82
+ return shapes;
83
+ }
84
+ export function committedSource(dir, readText) {
85
+ return async () => {
86
+ let schemaText;
87
+ let versionText;
88
+ try {
89
+ schemaText = readText(join(dir, SchemaFileName.Schema));
90
+ }
91
+ catch (error) {
92
+ return readFailure(error, ReasonText.CommittedMissing, ReasonText.CommittedUnreadable);
93
+ }
94
+ try {
95
+ versionText = readText(join(dir, SchemaFileName.Version));
96
+ }
97
+ catch (error) {
98
+ return readFailure(error, ReasonText.VersionMissing, ReasonText.VersionFileUnreadable);
99
+ }
100
+ const version = committedVersionOf(versionText);
101
+ return version === null ? unreadable(ReasonText.VersionUnreadable) : parseSchemaText(schemaText, version);
102
+ };
103
+ }
104
+ export function installedSource(runHerdr) {
105
+ return async () => {
106
+ let schemaText;
107
+ let versionText;
108
+ try {
109
+ schemaText = runHerdr(HerdrCommand.SchemaJson);
110
+ }
111
+ catch (error) {
112
+ return readFailure(error, ReasonText.HerdrMissing, ReasonText.HerdrRefused);
113
+ }
114
+ try {
115
+ versionText = runHerdr(HerdrCommand.Version);
116
+ }
117
+ catch (error) {
118
+ return readFailure(error, ReasonText.HerdrMissing, ReasonText.HerdrVersionMissing);
119
+ }
120
+ const version = herdrVersionOf(versionText);
121
+ return version === null ? unreadable(ReasonText.HerdrVersionMissing) : parseSchemaText(schemaText, version);
122
+ };
123
+ }
124
+ // Reads schema JSON text; the herdr version comes from the caller. A schema without a protocol number is Unreadable.
125
+ export function parseSchemaText(text, herdrVersion) {
126
+ const parsed = tryParse(text);
127
+ if (parsed === undefined) {
128
+ return unreadable(ReasonText.NotJson);
129
+ }
130
+ if (!isNode(parsed) || !isNumber(parsed[SchemaField.Protocol])) {
131
+ return unreadable(ReasonText.NoProtocol);
132
+ }
133
+ return { kind: SchemaReadKind.Found, schema: parsed, herdrVersion };
134
+ }
135
+ // A missing file or executable (code ENOENT) is NotFound; any other failure to read is Unreadable.
136
+ function readFailure(error, missingReason, failedReason) {
137
+ const code = isNode(error) ? error.code : undefined;
138
+ return code === SystemErrorCode.NotFound ? notFound(missingReason) : unreadable(failedReason);
139
+ }
140
+ function committedVersionOf(text) {
141
+ const parsed = tryParse(text);
142
+ const version = isNode(parsed) ? parsed[VersionFileField.Version] : undefined;
143
+ return isString(version) ? version : null;
144
+ }
145
+ function herdrVersionOf(text) {
146
+ const match = VERSION_PATTERN.exec(text);
147
+ return match?.[0] ?? null;
148
+ }
149
+ // JSON.parse never yields undefined, so undefined marks a text that is not JSON.
150
+ function tryParse(text) {
151
+ try {
152
+ return JSON.parse(text);
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
158
+ function notFound(reason) {
159
+ return { kind: SchemaReadKind.NotFound, reason };
160
+ }
161
+ function unreadable(reason) {
162
+ return { kind: SchemaReadKind.Unreadable, reason };
163
+ }
164
+ export const inventoryOf = (schema, herdrVersion) => {
165
+ const request = namespaceOf(schema, SchemaNamespace.Request);
166
+ const success = namespaceOf(schema, SchemaNamespace.SuccessResponse);
167
+ const event = namespaceOf(schema, SchemaNamespace.Event);
168
+ return {
169
+ protocol: protocolOf(schema),
170
+ herdrVersion,
171
+ methods: tagsOf(branchesOf(request), TagProperty.Method),
172
+ resultTags: tagsOf(branchesOf(definitionNamed(success, SchemaDefinition.ResponseResult)), TagProperty.Type),
173
+ eventKinds: tagsOf(branchesOf(definitionNamed(event, SchemaDefinition.EventData)), TagProperty.Type),
174
+ subscriptionTypes: tagsOf(branchesOf(definitionNamed(request, SchemaDefinition.Subscription)), TagProperty.Type),
175
+ fieldShapes: fieldShapesOf(schema),
176
+ };
177
+ };
178
+ function protocolOf(schema) {
179
+ const protocol = schema[SchemaField.Protocol];
180
+ if (!isNumber(protocol)) {
181
+ throw new MalformedSchemaError('the schema carries no protocol number');
182
+ }
183
+ return protocol;
184
+ }
185
+ function tagsOf(branches, property) {
186
+ return branches.map((branch) => constOf(branch, property)).toSorted();
187
+ }
188
+ export function unmappedMethods(methods) {
189
+ return methods.filter((method) => !Object.hasOwn(METHOD_RESULT, method)).toSorted();
190
+ }
@@ -0,0 +1,76 @@
1
+ import type { DriftExitCode, DriftOutcomeKind, SchemaReadKind } from '../constants.js';
2
+ /** A schema as JSON (the committed schema/herdr-api.schema.json or `herdr api schema --json`). Never read from the socket (W4). */
3
+ export type SchemaDocument = Readonly<Record<string, unknown>>;
4
+ /** Everything drift compares, extracted from one schema plus its herdr version. */
5
+ export interface SchemaInventory {
6
+ readonly protocol: number;
7
+ readonly herdrVersion: string;
8
+ readonly methods: readonly string[];
9
+ readonly resultTags: readonly string[];
10
+ readonly eventKinds: readonly string[];
11
+ readonly subscriptionTypes: readonly string[];
12
+ /** Field shape per params/result type, as a stable string (for "changed" detection). */
13
+ readonly fieldShapes: Readonly<Record<string, string>>;
14
+ }
15
+ /** Added and removed are listed separately (HERDR-ADR-0009). */
16
+ export interface NameDiff {
17
+ readonly added: readonly string[];
18
+ readonly removed: readonly string[];
19
+ }
20
+ export interface DriftReport {
21
+ readonly protocol: {
22
+ readonly committed: number;
23
+ readonly installed: number;
24
+ };
25
+ readonly herdrVersion: {
26
+ readonly committed: string;
27
+ readonly installed: string;
28
+ };
29
+ readonly methods: NameDiff;
30
+ readonly resultTags: NameDiff;
31
+ readonly eventKinds: NameDiff;
32
+ readonly subscriptionTypes: NameDiff;
33
+ /** Type names whose field shape differs. */
34
+ readonly changedShapes: readonly string[];
35
+ /** Installed methods without a METHOD_RESULT row (plan 4.2). */
36
+ readonly unmappedMethods: readonly string[];
37
+ /** True when anything above differs. */
38
+ readonly drifted: boolean;
39
+ }
40
+ /** Builds the inventory of one schema. Implemented by drift/inventory.ts (P5c). */
41
+ export type InventoryOf = (schema: SchemaDocument, herdrVersion: string) => SchemaInventory;
42
+ /** Compares two schemas. Pure. Implemented by drift/drift-between.ts (P5c). */
43
+ export type DriftBetween = (committed: {
44
+ readonly schema: SchemaDocument;
45
+ readonly herdrVersion: string;
46
+ }, installed: {
47
+ readonly schema: SchemaDocument;
48
+ readonly herdrVersion: string;
49
+ }) => DriftReport;
50
+ /** What a schema source found. NotFound / Unreadable lead to exit 3 (could not look). */
51
+ export type SchemaRead = {
52
+ readonly kind: typeof SchemaReadKind.Found;
53
+ readonly schema: SchemaDocument;
54
+ readonly herdrVersion: string;
55
+ } | {
56
+ readonly kind: typeof SchemaReadKind.NotFound;
57
+ readonly reason: string;
58
+ } | {
59
+ readonly kind: typeof SchemaReadKind.Unreadable;
60
+ readonly reason: string;
61
+ };
62
+ /** Reads one schema. bin/herdr-drift.ts injects the file reader and the `herdr` CLI runner (P5c); src/drift never does I/O. */
63
+ export type SchemaSource = () => Promise<SchemaRead>;
64
+ /** The CLI outcome before exit codes: a usage error, a source that could not look, or a report. */
65
+ export type DriftOutcome = {
66
+ readonly kind: typeof DriftOutcomeKind.Usage;
67
+ readonly message: string;
68
+ } | {
69
+ readonly kind: typeof DriftOutcomeKind.NotCovered;
70
+ readonly reason: string;
71
+ } | {
72
+ readonly kind: typeof DriftOutcomeKind.Report;
73
+ readonly report: DriftReport;
74
+ };
75
+ /** 0 clean, 1 drift, 2 usage, 3 could not look (never 0). Implemented by drift/exit-code.ts (P5c). */
76
+ export type ExitCodeFor = (outcome: DriftOutcome) => DriftExitCode;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export { driftBetween } from './drift/diff.js';
2
+ export { exitCodeFor } from './drift/diff.js';
3
+ export { inventoryOf } from './drift/schema.js';
4
+ export { DriftExitCode, DriftOutcomeKind } from './constants.js';
5
+ export type * from './drift/types.js';
package/dist/drift.js ADDED
@@ -0,0 +1,6 @@
1
+ // Role: the /drift entry (the bin runs drift/cli.ts): pure inventory, drift report and exit-code rule.
2
+ // Informs HERDR-ADR-0003 (one file per subpath export), HERDR-ADR-0009 and HERDR-ADR-0017; edited only by the integration step.
3
+ export { driftBetween } from './drift/diff.js';
4
+ export { exitCodeFor } from './drift/diff.js';
5
+ export { inventoryOf } from './drift/schema.js';
6
+ export { DriftExitCode, DriftOutcomeKind } from './constants.js';
@@ -0,0 +1,3 @@
1
+ export { toTaggedError } from './herdr/effect/to-tagged-error.js';
2
+ export { EffectErrorTag } from './constants.js';
3
+ export type * from './herdr/effect/types.js';
package/dist/effect.js ADDED
@@ -0,0 +1,4 @@
1
+ // Role: the /effect entry. Until amendment A1 (effect pinned, 2026-11-16) it exposes only the effect-free contract: layer options, error shapes, toTaggedError.
2
+ // Informs HERDR-ADR-0003, HERDR-ADR-0004 and HERDR-ADR-0017; edited only by the integration step.
3
+ export { toTaggedError } from './herdr/effect/to-tagged-error.js';
4
+ export { EffectErrorTag } from './constants.js';
@@ -0,0 +1,5 @@
1
+ import type { Method } from '../generated/index.js';
2
+ import type { ClientCapabilities, ConnectCore, Connect } from './types.js';
3
+ export declare function assertCapability(method: Method, capabilities: ClientCapabilities): void;
4
+ export declare const connectCore: ConnectCore;
5
+ export declare const connect: Connect;
@@ -0,0 +1,81 @@
1
+ // Role: connect(): ping, protocol policy, capabilities, the capability gate and the full client.
2
+ // Informs plan 3.1, HERDR-ADR-0005, plan 3.3, HERDR-ADR-0004, HERDR-ADR-0016; merged by HERDR-ADR-0021 from capability-gate.ts, connect-core.ts, connect.ts.
3
+ import { CAPABILITY_GATE, PROTOCOL } from '../generated/index.js';
4
+ import { UnsupportedByServerError, HerdrTransportError, ProtocolMismatchError } from '../errors.js';
5
+ import { ErrorText, ProtocolPolicy, Timeout, EntryKey } from '../../constants.js';
6
+ import { MethodPing } from '../generated/names/method.js';
7
+ import { isSupportedProtocol } from '../wire/codec.js';
8
+ import { rpc } from '../wire/rpc.js';
9
+ import { assertSocketExists, resolveSocketPath } from '../wire/socket-path.js';
10
+ import { CLIENT_METHODS } from '../generated/entries/client.js';
11
+ import { allowedMethods, composeOrClose, ENTRY_MEMBERS, lazyStreams } from './scoped.js';
12
+ const GATES = CAPABILITY_GATE;
13
+ export function assertCapability(method, capabilities) {
14
+ const capability = GATES[method];
15
+ if (capability !== undefined && capabilities[capability] !== true) {
16
+ throw new UnsupportedByServerError(method, capability);
17
+ }
18
+ }
19
+ export const connectCore = async function connectCore(options = {}) {
20
+ const socketPath = resolveSocketPath({ socketPath: options.socketPath, env: options.env });
21
+ assertSocketExists(socketPath);
22
+ const rpcOptions = requestOptions(options, socketPath);
23
+ const pong = await rpc(rpcOptions, MethodPing, {});
24
+ checkProtocol(pong, options.protocolPolicy ?? ProtocolPolicy.Exact);
25
+ return createClient(rpcOptions, pong, options);
26
+ };
27
+ function requestOptions(options, socketPath) {
28
+ return {
29
+ socketPath,
30
+ timeoutMs: options.timeoutMs ?? Timeout.DefaultMs,
31
+ onCall: options.onCall,
32
+ };
33
+ }
34
+ function checkProtocol(pong, policy) {
35
+ if (!isSupportedProtocol(pong.protocol, policy)) {
36
+ throw new ProtocolMismatchError(PROTOCOL, pong.protocol);
37
+ }
38
+ }
39
+ // A call's own timeoutMs and signal win over the client default; the socket path and onCall always come from the client.
40
+ function callRpcOptions(rpcOptions, callOptions) {
41
+ return {
42
+ ...rpcOptions,
43
+ timeoutMs: callOptions.timeoutMs ?? rpcOptions.timeoutMs,
44
+ signal: callOptions.signal,
45
+ };
46
+ }
47
+ // The capabilities and server facts are fixed at connect time; a closed client refuses every call without contacting herdr
48
+ // and aborts transport.closed so the streams P2b composes end with it (contract ClientBase.close).
49
+ function createClient(rpcOptions, pong, options) {
50
+ const capabilities = Object.freeze({ ...(pong.capabilities ?? {}) });
51
+ const serverInfo = Object.freeze({ protocol: pong.protocol, version: pong.version });
52
+ const closer = new AbortController();
53
+ const transport = Object.freeze({
54
+ socketPath: rpcOptions.socketPath,
55
+ timeoutMs: rpcOptions.timeoutMs,
56
+ onCall: options.onCall,
57
+ onUnknown: options.onUnknown,
58
+ closed: closer.signal,
59
+ });
60
+ let closed = false;
61
+ return {
62
+ capabilities,
63
+ serverInfo,
64
+ transport,
65
+ async call(method, params, callOptions = {}) {
66
+ if (closed) {
67
+ throw new HerdrTransportError(ErrorText.ClientClosed(method));
68
+ }
69
+ assertCapability(method, capabilities);
70
+ return rpc(callRpcOptions(rpcOptions, callOptions), method, params);
71
+ },
72
+ close() {
73
+ closed = true;
74
+ closer.abort();
75
+ },
76
+ };
77
+ }
78
+ export const connect = async function connect(options = {}) {
79
+ const core = await connectCore(options);
80
+ return composeOrClose(core, allowedMethods(CLIENT_METHODS), ENTRY_MEMBERS[EntryKey.client], lazyStreams(core));
81
+ };
@@ -0,0 +1,54 @@
1
+ import type { HeldNamespaces } from '../generated/index.js';
2
+ import type { AllowedMethods, BuildNamespaceTree, ClientCapabilities, NamespaceTree, Invoke } from './types.js';
3
+ import type { OnCall, OpenDedicated, DedicatedConnection } from '../wire/types.js';
4
+ /**
5
+ * One dedicated connection carrying the class C methods as namespaces (`held.client.window_title.set`,
6
+ * `held.client_shell.surface.set`, `held.server.ssh_agent.register`); capability gates apply (UnsupportedByServerError
7
+ * before sending). Closing it ends the registration, lease or title. If the server closes it, `ended` resolves and
8
+ * every later call rejects with HerdrTransportError.
9
+ */
10
+ export interface HeldConnection extends HeldNamespaces, AsyncDisposable {
11
+ readonly ended: Promise<void>;
12
+ /** Destroys the connection. Idempotent. */
13
+ close(): Promise<void>;
14
+ }
15
+ export interface HoldOptions {
16
+ readonly signal?: AbortSignal | undefined;
17
+ }
18
+ /** `herdr.hold(options)`. */
19
+ export type Hold = (options?: HoldOptions) => Promise<HeldConnection>;
20
+ /**
21
+ * What the client hands the hold factory. The held tree is built with the same builder and allow-list as every
22
+ * client (P2b's buildNamespaceTree and allowedMethods over HELD_METHODS); they are injected so P4b's unit tests can pass a tiny
23
+ * builder, and P2b passes the real ones. Each leaf is an Invoke that applies the capability gate, then sends on the
24
+ * dedicated connection.
25
+ */
26
+ export interface HoldDeps {
27
+ readonly socketPath: string;
28
+ /** The client's resolved timeout (ClientTransport.timeoutMs). */
29
+ readonly timeoutMs: number;
30
+ readonly capabilities: ClientCapabilities;
31
+ readonly onCall?: OnCall | undefined;
32
+ readonly open: OpenDedicated;
33
+ readonly buildTree: BuildNamespaceTree;
34
+ readonly allowedMethods: AllowedMethods;
35
+ /** The client close signal (ClientTransport.closed): when it aborts, every held connection of this client closes. */
36
+ readonly signal: AbortSignal;
37
+ }
38
+ /** Builds hold() of one client. Implemented by client/held/hold.ts (P4b). */
39
+ export type CreateHold = (deps: HoldDeps) => Hold;
40
+ export declare function untilAborted<T>(work: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
41
+ export interface EndState {
42
+ /** Resolves when the dedicated connection closed for any reason (server, destroy or abort). */
43
+ readonly ended: Promise<void>;
44
+ /** True once the connection closed; read synchronously before each call. */
45
+ isEnded(): boolean;
46
+ }
47
+ export declare function trackEnd(conn: DedicatedConnection): EndState;
48
+ export declare function toHeldConnection(tree: NamespaceTree, conn: DedicatedConnection, end: EndState, signal: AbortSignal): HeldConnection;
49
+ export interface HeldInvokeDeps {
50
+ readonly capabilities: ClientCapabilities;
51
+ readonly timeoutMs: number;
52
+ }
53
+ export declare function createHeldInvoke(conn: DedicatedConnection, deps: HeldInvokeDeps, isEnded: () => boolean): Invoke;
54
+ export declare const createHold: CreateHold;
@@ -0,0 +1,100 @@
1
+ import { HeldText } from '../../constants.js';
2
+ import { HerdrTransportError } from '../errors.js';
3
+ import { assertCapability } from './connect.js';
4
+ import { HELD_METHODS } from '../generated/entries/held.js';
5
+ // Resolves or rejects with the work, unless the signal aborts first: then it rejects with the signal's reason at once.
6
+ export function untilAborted(work, signal) {
7
+ if (signal === undefined) {
8
+ return work;
9
+ }
10
+ return new Promise((resolve, reject) => {
11
+ const onAbort = () => reject(signal.reason);
12
+ signal.addEventListener('abort', onAbort, { once: true });
13
+ if (signal.aborted) {
14
+ onAbort();
15
+ }
16
+ work.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
17
+ });
18
+ }
19
+ // Records the end in a flag as well as in the promise, so a call made after the end can be refused without waiting.
20
+ export function trackEnd(conn) {
21
+ let closed = false;
22
+ const ended = conn.ended.then(() => {
23
+ closed = true;
24
+ });
25
+ return {
26
+ ended,
27
+ isEnded: () => closed,
28
+ };
29
+ }
30
+ // The single NamespaceTree -> HeldNamespaces assertion. The tree is built by the same builder and allow-list as every client
31
+ // (buildNamespaceTree with the held list, HELD_METHODS), so its leaves have exactly the shape HeldNamespaces declares.
32
+ export function toHeldConnection(tree, conn, end, signal) {
33
+ // Closing detaches the abort listener, so a long-lived client does not keep one per hold.
34
+ const close = closeOnce(conn, end.ended, () => signal.removeEventListener('abort', onAbort));
35
+ const onAbort = () => {
36
+ void close();
37
+ };
38
+ if (signal.aborted) {
39
+ void close();
40
+ }
41
+ else {
42
+ signal.addEventListener('abort', onAbort, { once: true });
43
+ }
44
+ const namespaces = tree;
45
+ return {
46
+ ...namespaces,
47
+ ended: end.ended,
48
+ close,
49
+ [Symbol.asyncDispose]: close,
50
+ };
51
+ }
52
+ // The first call destroys the connection and waits for it to end; later calls return the same promise.
53
+ function closeOnce(conn, ended, detach) {
54
+ let closing;
55
+ return () => {
56
+ closing ??= destroyAndWait(conn, ended, detach);
57
+ return closing;
58
+ };
59
+ }
60
+ async function destroyAndWait(conn, ended, detach) {
61
+ detach();
62
+ conn.destroy();
63
+ await ended;
64
+ }
65
+ export function createHeldInvoke(conn, deps, isEnded) {
66
+ // Every call waits for the previous one, so a second request never reaches the connection while one is pending.
67
+ let previous = Promise.resolve();
68
+ return async function invokeHeld(method, params, options) {
69
+ assertCapability(method, deps.capabilities);
70
+ const signal = options?.signal;
71
+ const send = async () => {
72
+ // A call aborted while it was queued is never sent.
73
+ if (signal?.aborted === true) {
74
+ throw signal.reason;
75
+ }
76
+ if (isEnded()) {
77
+ throw new HerdrTransportError(HeldText.Ended(method));
78
+ }
79
+ // Invoke erases the params type (client/types.ts); the generated namespaces type them at the leaf that calls this.
80
+ return conn.request(method, params, {
81
+ timeoutMs: options?.timeoutMs ?? deps.timeoutMs,
82
+ signal,
83
+ });
84
+ };
85
+ const run = previous.then(send);
86
+ previous = run.catch(() => undefined);
87
+ // The caller hears its own abort at once, even while the call waits for an earlier one.
88
+ return untilAborted(run, signal);
89
+ };
90
+ }
91
+ // One connection per hold(), open until close(), the client's close, the caller's signal, or the server's close. Every held call goes over it.
92
+ export const createHold = (deps) => async function hold(options = {}) {
93
+ // The held connection closes when either the client closes or the caller's signal aborts.
94
+ const signal = options.signal === undefined ? deps.signal : AbortSignal.any([deps.signal, options.signal]);
95
+ const conn = await deps.open({ socketPath: deps.socketPath, signal, onCall: deps.onCall });
96
+ const end = trackEnd(conn);
97
+ const invoke = createHeldInvoke(conn, deps, end.isEnded);
98
+ const tree = deps.buildTree(deps.allowedMethods(HELD_METHODS), invoke);
99
+ return toHeldConnection(tree, conn, end, signal);
100
+ };
@@ -0,0 +1,33 @@
1
+ import { LONG_POLL } from '../generated/index.js';
2
+ import type { Method } from '../generated/index.js';
3
+ /** Methods whose rpc timeout follows a timeout_ms field in their params. */
4
+ export type LongPollMethod = keyof typeof LONG_POLL;
5
+ /**
6
+ * The rpc timeout of one call: for a LongPollMethod whose timeout path is present, timeout_ms + Timeout.LongPollMarginMs;
7
+ * otherwise `fallbackMs` (the client's timeoutMs). Pure. Implemented by client/long-poll/timeout.ts (P4a).
8
+ * The namespace tree (P2b) calls it only for methods that are LONG_POLL keys.
9
+ */
10
+ export type RequestTimeoutMs = (method: Method, params: unknown, fallbackMs: number | undefined) => number | undefined;
11
+ /**
12
+ * Throws HerdrTransportError before sending when a bounded timeout is out of range (agent.start: > 3000 and <= 300000).
13
+ * Pure. Implemented by client/long-poll/bounds.ts (P4a).
14
+ */
15
+ export type AssertLongPollBounds = (method: Method, params: unknown) => void;
16
+ export declare const LongPollText: {
17
+ readonly OutOfBounds: (method: string, value: number, exclusiveMin: number, max: number) => string;
18
+ };
19
+ export declare function readPath(value: unknown, path: readonly string[]): unknown;
20
+ export declare function isNumber(value: unknown): value is number;
21
+ export declare function isPresent(value: unknown): boolean;
22
+ export interface LongPollBounds {
23
+ readonly exclusiveMin: number;
24
+ readonly max: number;
25
+ }
26
+ export interface LongPollRule {
27
+ readonly timeoutPath: readonly string[];
28
+ readonly onlyWhenPresent?: string | undefined;
29
+ readonly bounds?: LongPollBounds | undefined;
30
+ }
31
+ export declare function ruleOf(method: Method): LongPollRule | undefined;
32
+ export declare const assertLongPollBounds: AssertLongPollBounds;
33
+ export declare const requestTimeoutMs: RequestTimeoutMs;
@@ -0,0 +1,66 @@
1
+ // Role: long-poll methods: their bounds, the check of a requested timeout and the request timeout they need.
2
+ // Informs HERDR-ADR-0017, HERDR-ADR-0007, plan 3.6; merged by HERDR-ADR-0021 from types.ts, constants.ts, read-path.ts, rule.ts, bounds.ts, timeout.ts.
3
+ import { LONG_POLL } from '../generated/index.js';
4
+ import { TypeofTag, Timeout } from '../../constants.js';
5
+ import { HerdrTransportError } from '../errors.js';
6
+ export const LongPollText = {
7
+ OutOfBounds: (method, value, exclusiveMin, max) => `${method}: timeout_ms ${value} must be greater than ${exclusiveMin} and at most ${max}`,
8
+ };
9
+ const ValueType = {
10
+ Number: 'number',
11
+ };
12
+ export function readPath(value, path) {
13
+ let current = value;
14
+ for (const key of path) {
15
+ if (!isRecord(current)) {
16
+ return undefined;
17
+ }
18
+ current = current[key];
19
+ }
20
+ return current;
21
+ }
22
+ export function isNumber(value) {
23
+ return typeof value === ValueType.Number;
24
+ }
25
+ // A value the caller actually set: null and undefined both mean "not sent".
26
+ export function isPresent(value) {
27
+ return value !== undefined && value !== null;
28
+ }
29
+ function isRecord(value) {
30
+ return typeof value === TypeofTag.Object && value !== null && !Array.isArray(value);
31
+ }
32
+ export function ruleOf(method) {
33
+ return isLongPollMethod(method) ? LONG_POLL[method] : undefined;
34
+ }
35
+ function isLongPollMethod(method) {
36
+ return Object.hasOwn(LONG_POLL, method);
37
+ }
38
+ export const assertLongPollBounds = function assertLongPollBounds(method, params) {
39
+ const rule = ruleOf(method);
40
+ const bounds = rule?.bounds;
41
+ if (rule === undefined || bounds === undefined) {
42
+ return;
43
+ }
44
+ const timeout = readPath(params, rule.timeoutPath);
45
+ if (!isNumber(timeout) || isInside(timeout, bounds)) {
46
+ return;
47
+ }
48
+ throw new HerdrTransportError(LongPollText.OutOfBounds(method, timeout, bounds.exclusiveMin, bounds.max));
49
+ };
50
+ // Strictly above exclusiveMin, at most max.
51
+ function isInside(value, bounds) {
52
+ return value > bounds.exclusiveMin && value <= bounds.max;
53
+ }
54
+ export const requestTimeoutMs = function requestTimeoutMs(method, params, fallbackMs) {
55
+ const rule = ruleOf(method);
56
+ if (rule === undefined || !isGateOpen(rule, params)) {
57
+ return fallbackMs;
58
+ }
59
+ const timeout = readPath(params, rule.timeoutPath);
60
+ return isNumber(timeout) ? timeout + Timeout.LongPollMarginMs : fallbackMs;
61
+ };
62
+ // agent.prompt only waits when `wait` is sent; without it the call is an ordinary request.
63
+ function isGateOpen(rule, params) {
64
+ const gate = rule.onlyWhenPresent;
65
+ return gate === undefined || isPresent(readPath(params, [gate]));
66
+ }