@comms-id/asx 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/browser-profile.d.ts +21 -0
- package/dist/browser-profile.js +115 -0
- package/dist/client-replies.d.ts +161 -0
- package/dist/client-replies.js +54 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +179 -0
- package/dist/collection.d.ts +555 -0
- package/dist/collection.js +194 -0
- package/dist/company-events.d.ts +2 -0
- package/dist/company-events.js +48 -0
- package/dist/company-record.d.ts +4 -0
- package/dist/company-record.js +58 -0
- package/dist/company-stream-client.d.ts +4 -0
- package/dist/company-stream-client.js +139 -0
- package/dist/company-stream.d.ts +46 -0
- package/dist/company-stream.js +117 -0
- package/dist/descriptor.d.ts +2 -2
- package/dist/descriptor.js +2 -2
- package/dist/directory-search.d.ts +2 -0
- package/dist/directory-search.js +33 -0
- package/dist/enrichment.d.ts +75 -0
- package/dist/enrichment.js +114 -0
- package/dist/generated/fixtures.js +2 -2
- package/dist/generated/operations.js +23 -4
- package/dist/generated/types.d.ts +2 -1
- package/dist/http.d.ts +1 -0
- package/dist/http.js +49 -0
- package/dist/live.d.ts +56 -0
- package/dist/live.js +129 -0
- package/dist/model.d.ts +112 -0
- package/dist/model.js +86 -0
- package/dist/parse.d.ts +4 -0
- package/dist/parse.js +130 -0
- package/dist/publication.d.ts +25 -0
- package/dist/publication.js +89 -0
- package/dist/refresh-coordinator.d.ts +96 -0
- package/dist/refresh-coordinator.js +194 -0
- package/dist/refresh.d.ts +126 -0
- package/dist/refresh.js +153 -0
- package/dist/runtime/http.js +8 -1
- package/dist/search-service.d.ts +16 -0
- package/dist/search-service.js +64 -0
- package/dist/service.d.ts +35 -0
- package/dist/service.js +99 -0
- package/dist/snapshot.d.ts +295 -0
- package/dist/snapshot.js +131 -0
- package/dist/source-record.d.ts +63 -0
- package/dist/source-record.js +91 -0
- package/package.json +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { DirectoryEntry } from "./model.js";
|
|
3
|
+
import { ConfirmedEnrichment } from "./enrichment.js";
|
|
4
|
+
export declare const CompanyRecord: Schema.Struct<{
|
|
5
|
+
readonly directory: Schema.Struct<{
|
|
6
|
+
readonly ticker: Schema.String;
|
|
7
|
+
readonly companyName: Schema.String;
|
|
8
|
+
readonly industryGroup: Schema.NullOr<Schema.String>;
|
|
9
|
+
readonly listingDate: Schema.NullOr<Schema.String>;
|
|
10
|
+
readonly marketCapitalisation: Schema.NullOr<Schema.Number>;
|
|
11
|
+
}>;
|
|
12
|
+
readonly observation: Schema.NullOr<Schema.Struct<{
|
|
13
|
+
readonly company: Schema.Struct<{
|
|
14
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
15
|
+
readonly ticker: Schema.String;
|
|
16
|
+
readonly sourceId: Schema.String;
|
|
17
|
+
readonly companyName: Schema.String;
|
|
18
|
+
readonly address: Schema.NullOr<Schema.String>;
|
|
19
|
+
readonly tel: Schema.NullOr<Schema.String>;
|
|
20
|
+
readonly fax: Schema.NullOr<Schema.String>;
|
|
21
|
+
readonly website: Schema.NullOr<Schema.String>;
|
|
22
|
+
readonly description: Schema.NullOr<Schema.String>;
|
|
23
|
+
readonly issueType: Schema.NullOr<Schema.String>;
|
|
24
|
+
readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
|
|
25
|
+
readonly people: Schema.$Array<Schema.Struct<{
|
|
26
|
+
readonly sourceName: Schema.String;
|
|
27
|
+
readonly displayName: Schema.String;
|
|
28
|
+
readonly title: Schema.String;
|
|
29
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
30
|
+
}>>;
|
|
31
|
+
}>;
|
|
32
|
+
readonly startedAt: Schema.String;
|
|
33
|
+
readonly observedAt: Schema.String;
|
|
34
|
+
readonly lastChangedAt: Schema.String;
|
|
35
|
+
}>>;
|
|
36
|
+
readonly sources: Schema.optionalKey<Schema.Struct<{
|
|
37
|
+
readonly asx: Schema.optionalKey<Schema.Struct<{
|
|
38
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
39
|
+
readonly ticker: Schema.String;
|
|
40
|
+
readonly sourceUrl: Schema.String;
|
|
41
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
42
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
43
|
+
readonly startedAt: Schema.String;
|
|
44
|
+
readonly observedAt: Schema.String;
|
|
45
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
46
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
47
|
+
}>>;
|
|
48
|
+
readonly morningstar: Schema.optionalKey<Schema.Struct<{
|
|
49
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
50
|
+
readonly ticker: Schema.String;
|
|
51
|
+
readonly sourceUrl: Schema.String;
|
|
52
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
53
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
54
|
+
readonly startedAt: Schema.String;
|
|
55
|
+
readonly observedAt: Schema.String;
|
|
56
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
57
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
58
|
+
}>>;
|
|
59
|
+
readonly "google-finance": Schema.optionalKey<Schema.Struct<{
|
|
60
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
61
|
+
readonly ticker: Schema.String;
|
|
62
|
+
readonly sourceUrl: Schema.String;
|
|
63
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
64
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
65
|
+
readonly startedAt: Schema.String;
|
|
66
|
+
readonly observedAt: Schema.String;
|
|
67
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
68
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
69
|
+
}>>;
|
|
70
|
+
}>>;
|
|
71
|
+
readonly enrichment: Schema.optionalKey<Schema.Struct<{
|
|
72
|
+
readonly ticker: Schema.String;
|
|
73
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
74
|
+
readonly abn: Schema.String;
|
|
75
|
+
readonly acn: Schema.NullOr<Schema.String>;
|
|
76
|
+
readonly legalName: Schema.NonEmptyString;
|
|
77
|
+
readonly identityEvidence: Schema.Struct<{
|
|
78
|
+
readonly url: Schema.String;
|
|
79
|
+
readonly sha256: Schema.String;
|
|
80
|
+
readonly observedAt: Schema.String;
|
|
81
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
82
|
+
}>;
|
|
83
|
+
readonly abrEvidence: Schema.Struct<{
|
|
84
|
+
readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
|
|
85
|
+
readonly observedAt: Schema.String;
|
|
86
|
+
}>;
|
|
87
|
+
readonly website: Schema.NullOr<Schema.Struct<{
|
|
88
|
+
readonly url: Schema.String;
|
|
89
|
+
readonly evidence: Schema.Struct<{
|
|
90
|
+
readonly url: Schema.String;
|
|
91
|
+
readonly sha256: Schema.String;
|
|
92
|
+
readonly observedAt: Schema.String;
|
|
93
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
94
|
+
}>;
|
|
95
|
+
}>>;
|
|
96
|
+
}>>;
|
|
97
|
+
readonly missingAt: Schema.optionalKey<Schema.Struct<{
|
|
98
|
+
readonly startedAt: Schema.String;
|
|
99
|
+
readonly observedAt: Schema.String;
|
|
100
|
+
}>>;
|
|
101
|
+
}>;
|
|
102
|
+
export interface CompanyRecord extends Schema.Schema.Type<typeof CompanyRecord> {
|
|
103
|
+
}
|
|
104
|
+
declare const Dataset: Schema.Struct<{
|
|
105
|
+
readonly formatVersion: Schema.Literal<1>;
|
|
106
|
+
readonly directoryObservedAt: Schema.String;
|
|
107
|
+
readonly records: Schema.$Array<Schema.Struct<{
|
|
108
|
+
readonly directory: Schema.Struct<{
|
|
109
|
+
readonly ticker: Schema.String;
|
|
110
|
+
readonly companyName: Schema.String;
|
|
111
|
+
readonly industryGroup: Schema.NullOr<Schema.String>;
|
|
112
|
+
readonly listingDate: Schema.NullOr<Schema.String>;
|
|
113
|
+
readonly marketCapitalisation: Schema.NullOr<Schema.Number>;
|
|
114
|
+
}>;
|
|
115
|
+
readonly observation: Schema.NullOr<Schema.Struct<{
|
|
116
|
+
readonly company: Schema.Struct<{
|
|
117
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
118
|
+
readonly ticker: Schema.String;
|
|
119
|
+
readonly sourceId: Schema.String;
|
|
120
|
+
readonly companyName: Schema.String;
|
|
121
|
+
readonly address: Schema.NullOr<Schema.String>;
|
|
122
|
+
readonly tel: Schema.NullOr<Schema.String>;
|
|
123
|
+
readonly fax: Schema.NullOr<Schema.String>;
|
|
124
|
+
readonly website: Schema.NullOr<Schema.String>;
|
|
125
|
+
readonly description: Schema.NullOr<Schema.String>;
|
|
126
|
+
readonly issueType: Schema.NullOr<Schema.String>;
|
|
127
|
+
readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
|
|
128
|
+
readonly people: Schema.$Array<Schema.Struct<{
|
|
129
|
+
readonly sourceName: Schema.String;
|
|
130
|
+
readonly displayName: Schema.String;
|
|
131
|
+
readonly title: Schema.String;
|
|
132
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
133
|
+
}>>;
|
|
134
|
+
}>;
|
|
135
|
+
readonly startedAt: Schema.String;
|
|
136
|
+
readonly observedAt: Schema.String;
|
|
137
|
+
readonly lastChangedAt: Schema.String;
|
|
138
|
+
}>>;
|
|
139
|
+
readonly sources: Schema.optionalKey<Schema.Struct<{
|
|
140
|
+
readonly asx: Schema.optionalKey<Schema.Struct<{
|
|
141
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
142
|
+
readonly ticker: Schema.String;
|
|
143
|
+
readonly sourceUrl: Schema.String;
|
|
144
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
145
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
146
|
+
readonly startedAt: Schema.String;
|
|
147
|
+
readonly observedAt: Schema.String;
|
|
148
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
149
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
150
|
+
}>>;
|
|
151
|
+
readonly morningstar: Schema.optionalKey<Schema.Struct<{
|
|
152
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
153
|
+
readonly ticker: Schema.String;
|
|
154
|
+
readonly sourceUrl: Schema.String;
|
|
155
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
156
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
157
|
+
readonly startedAt: Schema.String;
|
|
158
|
+
readonly observedAt: Schema.String;
|
|
159
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
160
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
161
|
+
}>>;
|
|
162
|
+
readonly "google-finance": Schema.optionalKey<Schema.Struct<{
|
|
163
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
164
|
+
readonly ticker: Schema.String;
|
|
165
|
+
readonly sourceUrl: Schema.String;
|
|
166
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
167
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
168
|
+
readonly startedAt: Schema.String;
|
|
169
|
+
readonly observedAt: Schema.String;
|
|
170
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
171
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
172
|
+
}>>;
|
|
173
|
+
}>>;
|
|
174
|
+
readonly enrichment: Schema.optionalKey<Schema.Struct<{
|
|
175
|
+
readonly ticker: Schema.String;
|
|
176
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
177
|
+
readonly abn: Schema.String;
|
|
178
|
+
readonly acn: Schema.NullOr<Schema.String>;
|
|
179
|
+
readonly legalName: Schema.NonEmptyString;
|
|
180
|
+
readonly identityEvidence: Schema.Struct<{
|
|
181
|
+
readonly url: Schema.String;
|
|
182
|
+
readonly sha256: Schema.String;
|
|
183
|
+
readonly observedAt: Schema.String;
|
|
184
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
185
|
+
}>;
|
|
186
|
+
readonly abrEvidence: Schema.Struct<{
|
|
187
|
+
readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
|
|
188
|
+
readonly observedAt: Schema.String;
|
|
189
|
+
}>;
|
|
190
|
+
readonly website: Schema.NullOr<Schema.Struct<{
|
|
191
|
+
readonly url: Schema.String;
|
|
192
|
+
readonly evidence: Schema.Struct<{
|
|
193
|
+
readonly url: Schema.String;
|
|
194
|
+
readonly sha256: Schema.String;
|
|
195
|
+
readonly observedAt: Schema.String;
|
|
196
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
197
|
+
}>;
|
|
198
|
+
}>>;
|
|
199
|
+
}>>;
|
|
200
|
+
readonly missingAt: Schema.optionalKey<Schema.Struct<{
|
|
201
|
+
readonly startedAt: Schema.String;
|
|
202
|
+
readonly observedAt: Schema.String;
|
|
203
|
+
}>>;
|
|
204
|
+
}>>;
|
|
205
|
+
}>;
|
|
206
|
+
export type AsxDataset = typeof Dataset.Type;
|
|
207
|
+
export declare const SnapshotOutcome: Schema.Union<readonly [Schema.Struct<{
|
|
208
|
+
readonly status: Schema.Literal<"found">;
|
|
209
|
+
readonly observation: Schema.Struct<{
|
|
210
|
+
readonly company: Schema.Struct<{
|
|
211
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
212
|
+
readonly ticker: Schema.String;
|
|
213
|
+
readonly sourceId: Schema.String;
|
|
214
|
+
readonly companyName: Schema.String;
|
|
215
|
+
readonly address: Schema.NullOr<Schema.String>;
|
|
216
|
+
readonly tel: Schema.NullOr<Schema.String>;
|
|
217
|
+
readonly fax: Schema.NullOr<Schema.String>;
|
|
218
|
+
readonly website: Schema.NullOr<Schema.String>;
|
|
219
|
+
readonly description: Schema.NullOr<Schema.String>;
|
|
220
|
+
readonly issueType: Schema.NullOr<Schema.String>;
|
|
221
|
+
readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
|
|
222
|
+
readonly people: Schema.$Array<Schema.Struct<{
|
|
223
|
+
readonly sourceName: Schema.String;
|
|
224
|
+
readonly displayName: Schema.String;
|
|
225
|
+
readonly title: Schema.String;
|
|
226
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
227
|
+
}>>;
|
|
228
|
+
}>;
|
|
229
|
+
readonly startedAt: Schema.String;
|
|
230
|
+
readonly observedAt: Schema.String;
|
|
231
|
+
readonly lastChangedAt: Schema.String;
|
|
232
|
+
}>;
|
|
233
|
+
readonly enrichment: Schema.optionalKey<Schema.Struct<{
|
|
234
|
+
readonly ticker: Schema.String;
|
|
235
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
236
|
+
readonly abn: Schema.String;
|
|
237
|
+
readonly acn: Schema.NullOr<Schema.String>;
|
|
238
|
+
readonly legalName: Schema.NonEmptyString;
|
|
239
|
+
readonly identityEvidence: Schema.Struct<{
|
|
240
|
+
readonly url: Schema.String;
|
|
241
|
+
readonly sha256: Schema.String;
|
|
242
|
+
readonly observedAt: Schema.String;
|
|
243
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
244
|
+
}>;
|
|
245
|
+
readonly abrEvidence: Schema.Struct<{
|
|
246
|
+
readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
|
|
247
|
+
readonly observedAt: Schema.String;
|
|
248
|
+
}>;
|
|
249
|
+
readonly website: Schema.NullOr<Schema.Struct<{
|
|
250
|
+
readonly url: Schema.String;
|
|
251
|
+
readonly evidence: Schema.Struct<{
|
|
252
|
+
readonly url: Schema.String;
|
|
253
|
+
readonly sha256: Schema.String;
|
|
254
|
+
readonly observedAt: Schema.String;
|
|
255
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
256
|
+
}>;
|
|
257
|
+
}>>;
|
|
258
|
+
}>>;
|
|
259
|
+
readonly generation: Schema.String;
|
|
260
|
+
}>, Schema.Struct<{
|
|
261
|
+
readonly status: Schema.Literal<"missing">;
|
|
262
|
+
readonly generation: Schema.String;
|
|
263
|
+
}>, Schema.Struct<{
|
|
264
|
+
readonly status: Schema.Literal<"unavailable">;
|
|
265
|
+
readonly generation: Schema.String;
|
|
266
|
+
}>]>;
|
|
267
|
+
export type SnapshotOutcome = typeof SnapshotOutcome.Type;
|
|
268
|
+
export interface AsxSnapshot {
|
|
269
|
+
readonly generation: string;
|
|
270
|
+
/** Entire company row, including independent source payloads even when ASX details are absent. */
|
|
271
|
+
readonly record: (ticker: string) => CompanyRecord | null;
|
|
272
|
+
readonly lookup: (ticker: string, signal?: AbortSignal) => Promise<SnapshotOutcome>;
|
|
273
|
+
readonly suggest: (query: string, limit?: number) => readonly DirectoryEntry[];
|
|
274
|
+
readonly findByAbn: (abn: string) => readonly {
|
|
275
|
+
directory: DirectoryEntry;
|
|
276
|
+
enrichment: ConfirmedEnrichment;
|
|
277
|
+
}[];
|
|
278
|
+
readonly metadata: () => {
|
|
279
|
+
directoryObservedAt: string;
|
|
280
|
+
records: number;
|
|
281
|
+
availableProfiles: number;
|
|
282
|
+
confirmedAbns: number;
|
|
283
|
+
unresolvedAbns: number;
|
|
284
|
+
confirmedWebsites: number;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
export declare function decodeDataset(input: unknown): AsxDataset;
|
|
288
|
+
/** Build content-addressed bytes; the host must verify its write before selection. */
|
|
289
|
+
export declare function encodeSnapshot(input: unknown): Promise<{
|
|
290
|
+
generation: string;
|
|
291
|
+
text: string;
|
|
292
|
+
}>;
|
|
293
|
+
/** The trusted host pins the expected generation; body content cannot choose it. */
|
|
294
|
+
export declare function openSnapshot(text: string, expectedGeneration: string): Promise<AsxSnapshot>;
|
|
295
|
+
export {};
|
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { suggestDirectory } from "./directory-search.js";
|
|
2
|
+
import { Result, Schema } from "effect";
|
|
3
|
+
import { DirectoryEntry, failure, isTimestamp, Observation, validObservation } from "./model.js";
|
|
4
|
+
import { normalizeTicker } from "./parse.js";
|
|
5
|
+
import { ConfirmedEnrichment, validAbn, validEnrichment } from "./enrichment.js";
|
|
6
|
+
import { CompanySources, decodeCompanySources } from "./source-record.js";
|
|
7
|
+
export const CompanyRecord = Schema.Struct({
|
|
8
|
+
directory: DirectoryEntry,
|
|
9
|
+
observation: Schema.NullOr(Observation),
|
|
10
|
+
sources: Schema.optionalKey(CompanySources),
|
|
11
|
+
enrichment: Schema.optionalKey(ConfirmedEnrichment),
|
|
12
|
+
missingAt: Schema.optionalKey(Schema.Struct({ startedAt: Schema.String, observedAt: Schema.String })),
|
|
13
|
+
});
|
|
14
|
+
const Dataset = Schema.Struct({
|
|
15
|
+
formatVersion: Schema.Literal(1),
|
|
16
|
+
directoryObservedAt: Schema.String,
|
|
17
|
+
records: Schema.Array(CompanyRecord),
|
|
18
|
+
});
|
|
19
|
+
const Generation = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/));
|
|
20
|
+
export const SnapshotOutcome = Schema.Union([
|
|
21
|
+
Schema.Struct({
|
|
22
|
+
status: Schema.Literal("found"),
|
|
23
|
+
observation: Observation,
|
|
24
|
+
enrichment: Schema.optionalKey(ConfirmedEnrichment),
|
|
25
|
+
generation: Generation,
|
|
26
|
+
}),
|
|
27
|
+
Schema.Struct({ status: Schema.Literal("missing"), generation: Generation }),
|
|
28
|
+
Schema.Struct({ status: Schema.Literal("unavailable"), generation: Generation }),
|
|
29
|
+
]);
|
|
30
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
31
|
+
export function decodeDataset(input) {
|
|
32
|
+
const decoded = Schema.decodeUnknownResult(Dataset)(input);
|
|
33
|
+
if (Result.isFailure(decoded))
|
|
34
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
35
|
+
const dataset = decoded.success;
|
|
36
|
+
if (!isTimestamp(dataset.directoryObservedAt) ||
|
|
37
|
+
dataset.records.length === 0 ||
|
|
38
|
+
dataset.records.length > 20_000)
|
|
39
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
40
|
+
const tickers = new Set();
|
|
41
|
+
for (const record of dataset.records) {
|
|
42
|
+
if (tickers.has(record.directory.ticker) || !record.directory.companyName.trim())
|
|
43
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
44
|
+
tickers.add(record.directory.ticker);
|
|
45
|
+
if (record.sources)
|
|
46
|
+
decodeCompanySources(record.sources, record.directory.ticker);
|
|
47
|
+
if (record.enrichment &&
|
|
48
|
+
(!record.observation || !validEnrichment(record.enrichment, record.observation.company)))
|
|
49
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
50
|
+
if (record.missingAt &&
|
|
51
|
+
(record.observation !== null ||
|
|
52
|
+
!isTimestamp(record.missingAt.startedAt) ||
|
|
53
|
+
!isTimestamp(record.missingAt.observedAt) ||
|
|
54
|
+
record.missingAt.startedAt > record.missingAt.observedAt))
|
|
55
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
56
|
+
if (record.observation &&
|
|
57
|
+
(!validObservation(record.observation) ||
|
|
58
|
+
record.observation.company.ticker !== record.directory.ticker))
|
|
59
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
60
|
+
}
|
|
61
|
+
return dataset;
|
|
62
|
+
}
|
|
63
|
+
async function digest(text) {
|
|
64
|
+
const bytes = new TextEncoder().encode(text);
|
|
65
|
+
if (bytes.byteLength > MAX_BYTES)
|
|
66
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
67
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
68
|
+
return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
69
|
+
}
|
|
70
|
+
/** Build content-addressed bytes; the host must verify its write before selection. */
|
|
71
|
+
export async function encodeSnapshot(input) {
|
|
72
|
+
const dataset = decodeDataset(input);
|
|
73
|
+
const text = JSON.stringify(dataset);
|
|
74
|
+
return { generation: await digest(text), text };
|
|
75
|
+
}
|
|
76
|
+
/** The trusted host pins the expected generation; body content cannot choose it. */
|
|
77
|
+
export async function openSnapshot(text, expectedGeneration) {
|
|
78
|
+
if (!/^[a-f0-9]{64}$/.test(expectedGeneration) ||
|
|
79
|
+
typeof text !== "string" ||
|
|
80
|
+
text.length > MAX_BYTES)
|
|
81
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
82
|
+
if ((await digest(text)) !== expectedGeneration)
|
|
83
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
84
|
+
let input;
|
|
85
|
+
try {
|
|
86
|
+
input = JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
90
|
+
}
|
|
91
|
+
const dataset = decodeDataset(input);
|
|
92
|
+
const records = new Map(dataset.records.map((record) => [record.directory.ticker, record]));
|
|
93
|
+
return {
|
|
94
|
+
generation: expectedGeneration,
|
|
95
|
+
record: (ticker) => structuredClone(records.get(normalizeTicker(ticker)) ?? null),
|
|
96
|
+
lookup: async (inputTicker, signal) => {
|
|
97
|
+
if (signal?.aborted)
|
|
98
|
+
throw failure("CANCELLED");
|
|
99
|
+
const record = records.get(normalizeTicker(inputTicker));
|
|
100
|
+
if (!record)
|
|
101
|
+
return { status: "missing", generation: expectedGeneration };
|
|
102
|
+
if (record.missingAt)
|
|
103
|
+
return { status: "missing", generation: expectedGeneration };
|
|
104
|
+
if (!record.observation)
|
|
105
|
+
return { status: "unavailable", generation: expectedGeneration };
|
|
106
|
+
return {
|
|
107
|
+
status: "found",
|
|
108
|
+
observation: structuredClone(record.observation),
|
|
109
|
+
...(record.enrichment ? { enrichment: structuredClone(record.enrichment) } : {}),
|
|
110
|
+
generation: expectedGeneration,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
suggest: (query, limit = 8) => suggestDirectory(dataset.records.map((record) => record.directory), query, limit),
|
|
114
|
+
findByAbn: (abn) => {
|
|
115
|
+
if (typeof abn !== "string" || !validAbn(abn))
|
|
116
|
+
throw failure("INVALID_INPUT");
|
|
117
|
+
return dataset.records.flatMap((record) => record.enrichment?.abn === abn
|
|
118
|
+
? [{ directory: { ...record.directory }, enrichment: structuredClone(record.enrichment) }]
|
|
119
|
+
: []);
|
|
120
|
+
},
|
|
121
|
+
metadata: () => ({
|
|
122
|
+
directoryObservedAt: dataset.directoryObservedAt,
|
|
123
|
+
records: records.size,
|
|
124
|
+
availableProfiles: dataset.records.filter((record) => record.observation !== null).length,
|
|
125
|
+
confirmedAbns: dataset.records.filter((record) => record.enrichment !== undefined).length,
|
|
126
|
+
unresolvedAbns: dataset.records.filter((record) => record.enrichment === undefined).length,
|
|
127
|
+
confirmedWebsites: dataset.records.filter((record) => record.enrichment?.website != null)
|
|
128
|
+
.length,
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type LiveOutcome } from "./model.js";
|
|
3
|
+
export declare const ASX_COMPANY_BASE = "https://asx.api.markitdigital.com/asx-research/1.0/companies/";
|
|
4
|
+
export declare function asxSourceObservation(ticker: string, result: LiveOutcome): SourceObservation;
|
|
5
|
+
export declare const SourceName: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
6
|
+
export type SourceName = typeof SourceName.Type;
|
|
7
|
+
export declare const SOURCE_NAMES: readonly SourceName[];
|
|
8
|
+
/** Provider data remains separate. These fields do not select a preferred website. */
|
|
9
|
+
export declare const SourceObservation: Schema.Struct<{
|
|
10
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
11
|
+
readonly ticker: Schema.String;
|
|
12
|
+
readonly sourceUrl: Schema.String;
|
|
13
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
14
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
15
|
+
readonly startedAt: Schema.String;
|
|
16
|
+
readonly observedAt: Schema.String;
|
|
17
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
18
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
19
|
+
}>;
|
|
20
|
+
export interface SourceObservation extends Schema.Schema.Type<typeof SourceObservation> {
|
|
21
|
+
}
|
|
22
|
+
export declare const CompanySources: Schema.Struct<{
|
|
23
|
+
readonly asx: Schema.optionalKey<Schema.Struct<{
|
|
24
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
25
|
+
readonly ticker: Schema.String;
|
|
26
|
+
readonly sourceUrl: Schema.String;
|
|
27
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
28
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
29
|
+
readonly startedAt: Schema.String;
|
|
30
|
+
readonly observedAt: Schema.String;
|
|
31
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
32
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
33
|
+
}>>;
|
|
34
|
+
readonly morningstar: Schema.optionalKey<Schema.Struct<{
|
|
35
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
36
|
+
readonly ticker: Schema.String;
|
|
37
|
+
readonly sourceUrl: Schema.String;
|
|
38
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
39
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
40
|
+
readonly startedAt: Schema.String;
|
|
41
|
+
readonly observedAt: Schema.String;
|
|
42
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
43
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
44
|
+
}>>;
|
|
45
|
+
readonly "google-finance": Schema.optionalKey<Schema.Struct<{
|
|
46
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
47
|
+
readonly ticker: Schema.String;
|
|
48
|
+
readonly sourceUrl: Schema.String;
|
|
49
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
50
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
51
|
+
readonly startedAt: Schema.String;
|
|
52
|
+
readonly observedAt: Schema.String;
|
|
53
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
54
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
55
|
+
}>>;
|
|
56
|
+
}>;
|
|
57
|
+
export interface CompanySources extends Schema.Schema.Type<typeof CompanySources> {
|
|
58
|
+
}
|
|
59
|
+
/** Decode at every collector, storage and consumer boundary; never discard unknown payload fields. */
|
|
60
|
+
export declare function decodeSourceObservation(input: unknown, ticker: string, source: SourceName): SourceObservation;
|
|
61
|
+
export declare function decodeCompanySources(input: unknown, ticker: string): CompanySources;
|
|
62
|
+
/** Only a newer observation of this source can replace it. Failure is not an observation. */
|
|
63
|
+
export declare function mergeCompanySources(ticker: string, previous: CompanySources, incoming: CompanySources): CompanySources;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Result, Schema } from "effect";
|
|
2
|
+
import { failure, isTimestamp, Ticker, Timestamp } from "./model.js";
|
|
3
|
+
export const ASX_COMPANY_BASE = "https://asx.api.markitdigital.com/asx-research/1.0/companies/";
|
|
4
|
+
export function asxSourceObservation(ticker, result) {
|
|
5
|
+
const found = result.status === "found" ? result.observation : null;
|
|
6
|
+
return decodeSourceObservation({
|
|
7
|
+
ticker,
|
|
8
|
+
source: "asx",
|
|
9
|
+
sourceId: found?.company.sourceId ?? ticker,
|
|
10
|
+
sourceUrl: `${ASX_COMPANY_BASE}${ticker}/about`,
|
|
11
|
+
status: result.status,
|
|
12
|
+
startedAt: found?.startedAt ?? (result.status === "missing" ? result.startedAt : ""),
|
|
13
|
+
observedAt: found?.observedAt ?? (result.status === "missing" ? result.observedAt : ""),
|
|
14
|
+
payload: found?.company.raw ?? (result.status === "missing" ? result.raw : undefined),
|
|
15
|
+
websites: found?.company.website ? [found.company.website] : [],
|
|
16
|
+
}, ticker, "asx");
|
|
17
|
+
}
|
|
18
|
+
export const SourceName = Schema.Literals(["asx", "morningstar", "google-finance"]);
|
|
19
|
+
export const SOURCE_NAMES = ["asx", "morningstar", "google-finance"];
|
|
20
|
+
/** Provider data remains separate. These fields do not select a preferred website. */
|
|
21
|
+
export const SourceObservation = Schema.Struct({
|
|
22
|
+
source: SourceName,
|
|
23
|
+
ticker: Ticker,
|
|
24
|
+
sourceUrl: Schema.String,
|
|
25
|
+
sourceId: Schema.NonEmptyString,
|
|
26
|
+
status: Schema.Literals(["found", "missing"]),
|
|
27
|
+
startedAt: Timestamp,
|
|
28
|
+
observedAt: Timestamp,
|
|
29
|
+
payload: Schema.JsonObject,
|
|
30
|
+
websites: Schema.Array(Schema.String).check(Schema.isMaxLength(100)),
|
|
31
|
+
});
|
|
32
|
+
export const CompanySources = Schema.Struct({
|
|
33
|
+
asx: Schema.optionalKey(SourceObservation),
|
|
34
|
+
morningstar: Schema.optionalKey(SourceObservation),
|
|
35
|
+
"google-finance": Schema.optionalKey(SourceObservation),
|
|
36
|
+
});
|
|
37
|
+
function publicUrl(value) {
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(value);
|
|
40
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Decode at every collector, storage and consumer boundary; never discard unknown payload fields. */
|
|
47
|
+
export function decodeSourceObservation(input, ticker, source) {
|
|
48
|
+
const result = Schema.decodeUnknownResult(SourceObservation)(input);
|
|
49
|
+
if (Result.isFailure(result))
|
|
50
|
+
throw failure("PARSE_FAILED");
|
|
51
|
+
const value = result.success;
|
|
52
|
+
if (value.ticker !== ticker || value.source !== source)
|
|
53
|
+
throw failure("SOURCE_MISMATCH");
|
|
54
|
+
if (!isTimestamp(value.startedAt) ||
|
|
55
|
+
!isTimestamp(value.observedAt) ||
|
|
56
|
+
value.startedAt > value.observedAt ||
|
|
57
|
+
!publicUrl(value.sourceUrl) ||
|
|
58
|
+
value.websites.some((url) => !publicUrl(url)) ||
|
|
59
|
+
(value.status === "missing" && value.websites.length > 0) ||
|
|
60
|
+
new TextEncoder().encode(JSON.stringify(value)).byteLength > 1024 * 1024)
|
|
61
|
+
throw failure("PARSE_FAILED");
|
|
62
|
+
return structuredClone(value);
|
|
63
|
+
}
|
|
64
|
+
export function decodeCompanySources(input, ticker) {
|
|
65
|
+
const decoded = Schema.decodeUnknownResult(CompanySources)(input);
|
|
66
|
+
if (Result.isFailure(decoded))
|
|
67
|
+
throw failure("PARSE_FAILED");
|
|
68
|
+
const sources = {};
|
|
69
|
+
for (const source of SOURCE_NAMES) {
|
|
70
|
+
const value = decoded.success[source];
|
|
71
|
+
if (value)
|
|
72
|
+
sources[source] = decodeSourceObservation(value, ticker, source);
|
|
73
|
+
}
|
|
74
|
+
return sources;
|
|
75
|
+
}
|
|
76
|
+
/** Only a newer observation of this source can replace it. Failure is not an observation. */
|
|
77
|
+
export function mergeCompanySources(ticker, previous, incoming) {
|
|
78
|
+
const merged = Object.assign({}, decodeCompanySources(previous, ticker));
|
|
79
|
+
const next = decodeCompanySources(incoming, ticker);
|
|
80
|
+
for (const source of SOURCE_NAMES) {
|
|
81
|
+
const value = next[source];
|
|
82
|
+
const prior = merged[source];
|
|
83
|
+
if (value &&
|
|
84
|
+
(!prior ||
|
|
85
|
+
value.startedAt > prior.startedAt ||
|
|
86
|
+
(value.startedAt === prior.startedAt && value.observedAt > prior.observedAt))) {
|
|
87
|
+
merged[source] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return merged;
|
|
91
|
+
}
|