@kindgi/types 0.0.0-bootstrap.0 → 0.1.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 (48) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -2
  3. package/SPEC.md +162 -0
  4. package/dist/filter.d.ts +61 -0
  5. package/dist/filter.d.ts.map +1 -0
  6. package/dist/filter.js +4 -0
  7. package/dist/filter.js.map +1 -0
  8. package/dist/hash.d.ts +32 -0
  9. package/dist/hash.d.ts.map +1 -0
  10. package/dist/hash.js +4 -0
  11. package/dist/hash.js.map +1 -0
  12. package/dist/ids.d.ts +171 -0
  13. package/dist/ids.d.ts.map +1 -0
  14. package/dist/ids.js +15 -0
  15. package/dist/ids.js.map +1 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +7 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/refs.d.ts +58 -0
  21. package/dist/refs.d.ts.map +1 -0
  22. package/dist/refs.js +4 -0
  23. package/dist/refs.js.map +1 -0
  24. package/dist/result.d.ts +48 -0
  25. package/dist/result.d.ts.map +1 -0
  26. package/dist/result.js +4 -0
  27. package/dist/result.js.map +1 -0
  28. package/dist/temporal.d.ts +30 -0
  29. package/dist/temporal.d.ts.map +1 -0
  30. package/dist/temporal.js +4 -0
  31. package/dist/temporal.js.map +1 -0
  32. package/dist/version.d.ts +36 -0
  33. package/dist/version.d.ts.map +1 -0
  34. package/dist/version.js +4 -0
  35. package/dist/version.js.map +1 -0
  36. package/package.json +46 -4
  37. package/src/filter.ts +66 -0
  38. package/src/hash.ts +37 -0
  39. package/src/ids.test-d.ts +54 -0
  40. package/src/ids.ts +225 -0
  41. package/src/index.ts +15 -0
  42. package/src/refs.test-d.ts +93 -0
  43. package/src/refs.ts +62 -0
  44. package/src/result.test-d.ts +59 -0
  45. package/src/result.ts +42 -0
  46. package/src/temporal.ts +35 -0
  47. package/src/version.ts +41 -0
  48. package/type-manifest.json +99 -0
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { assertType, describe, expectTypeOf, test } from 'vitest';
5
+ import type { AgentId, Brand, TenantId, ToolId, UserId } from './ids.js';
6
+
7
+ describe('branded IDs', () => {
8
+ test('a branded id is assignable from a cast, not from a bare string', () => {
9
+ const raw = 'abc';
10
+ // A cast is the sanctioned way to construct a branded id.
11
+ const tenant: TenantId = raw as TenantId;
12
+ expectTypeOf(tenant).toEqualTypeOf<TenantId>();
13
+ // A bare string is NOT assignable to a branded id (compile error).
14
+ // @ts-expect-error — bare string cannot be assigned to a branded id.
15
+ const _bad: TenantId = raw;
16
+ });
17
+
18
+ test('two different branded ids are not interchangeable', () => {
19
+ const tenant = 'abc' as TenantId;
20
+ // TenantId is not assignable to UserId, and vice versa.
21
+ // @ts-expect-error — TenantId is not a UserId.
22
+ const _asUser: UserId = tenant;
23
+ // @ts-expect-error — a UserId literal isn't automatically a TenantId either.
24
+ const _asTenant: TenantId = 'u1' as UserId;
25
+ });
26
+
27
+ test('branded ids still behave structurally as strings', () => {
28
+ const tool = 'acme.verify-citation' as ToolId;
29
+ // String operations preserve the underlying primitive.
30
+ expectTypeOf(tool.length).toEqualTypeOf<number>();
31
+ expectTypeOf(tool.toString()).toEqualTypeOf<string>();
32
+ // But the operation result is NOT branded (any string producer drops the brand).
33
+ expectTypeOf(`${tool}`).toEqualTypeOf<string>();
34
+ });
35
+
36
+ test('Brand utility composes cleanly for pack-scoped ids', () => {
37
+ type MatterId = Brand<string, 'MatterId'>;
38
+ const matter = 'm-42' as MatterId;
39
+ expectTypeOf(matter).toEqualTypeOf<MatterId>();
40
+ // Pack-scoped ids don't collide with framework ids.
41
+ // @ts-expect-error — MatterId is not an AgentId.
42
+ const _agent: AgentId = matter;
43
+ });
44
+
45
+ test('assertType negative: passing wrong id shape must fail at call site', () => {
46
+ function useTenant(t: TenantId): void {
47
+ assertType<TenantId>(t);
48
+ }
49
+ const tenant = 'abc' as TenantId;
50
+ useTenant(tenant);
51
+ // @ts-expect-error — a UserId cannot be passed where TenantId is expected.
52
+ useTenant('u1' as UserId);
53
+ });
54
+ });
package/src/ids.ts ADDED
@@ -0,0 +1,225 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * Branded ID types for every entity kind in Kindgi.
6
+ *
7
+ * Every ID is a nominal string type — structurally a string, but statically distinct.
8
+ * A `TenantId` cannot be passed where a `UserId` is expected, catching a whole class
9
+ * of bugs at compile time.
10
+ *
11
+ * These are pure compile-time constructs; there is no runtime cost. Construction
12
+ * is by cast (`as`). The one exception is `EnvName`, which has the validated
13
+ * factory `makeEnvName` (the only runtime export of this package); validation of
14
+ * wire payloads lives in `@kindgi/schema`.
15
+ *
16
+ * Naming convention:
17
+ * - `<Entity>Id` — a single identifier for that entity kind.
18
+ * - Most entity kinds in the `@kindgi/specs/*.schema.json` files have an ID
19
+ * here (eval suites, for one, use plain strings); some IDs (e.g.
20
+ * `ConversationId`, `TriggerId`) name entities that have no schema.
21
+ *
22
+ * If you add a new entity kind to Kindgi, add its ID here. Consumers rely on
23
+ * this being the canonical registry of branded IDs.
24
+ */
25
+
26
+ /**
27
+ * Generic brand utility. Prefer using the pre-defined branded types below;
28
+ * this is exported for downstream packs that need to declare pack-scoped IDs
29
+ * (e.g. `Brand<string, 'MatterId'>` in a pack that tracks legal matters).
30
+ */
31
+ export type Brand<T, B extends string> = T & { readonly __brand: B };
32
+
33
+ /**
34
+ * `T` with every branded string in it (`FlowId`, `NodeId`, `EdgeId`, …)
35
+ * loosened to plain `string`, all the way down: through arrays, objects
36
+ * and unions. For authoring inputs, so an author writes `'acme.review'`
37
+ * rather than `'acme.review' as FlowId`. A branded value still fits, since
38
+ * it is a string. The validated result is branded again.
39
+ */
40
+ export type Unbranded<T> = T extends string & { readonly __brand: string }
41
+ ? string
42
+ : T extends readonly (infer E)[]
43
+ ? readonly Unbranded<E>[]
44
+ : T extends object
45
+ ? { readonly [K in keyof T]: Unbranded<T[K]> }
46
+ : T;
47
+
48
+ /** Tenant — the sovereignty boundary. Cross-tenant access is denied by policy. */
49
+ export type TenantId = Brand<string, 'TenantId'>;
50
+
51
+ /** User within a tenant. */
52
+ export type UserId = Brand<string, 'UserId'>;
53
+
54
+ /** Organization scope within a tenant — optional structural subdivision (a firm's litigation department, a hospital's cardiology). Small tenants ignore this; large tenants use it. */
55
+ export type OrgId = Brand<string, 'OrgId'>;
56
+
57
+ /** Team scope — a people-group. A named collection of users who work together; users can belong to many teams. Same team works on many projects; same project can have many teams collaborating. Distinct from Org (structural) — Team is about working groups. */
58
+ export type TeamId = Brand<string, 'TeamId'>;
59
+
60
+ /** Project scope — vertical-agnostic ('matter' in legal, 'case' in HR, 'engagement' in accounting, 'episode' in healthcare). This is the primary content-scope: facts, artifacts, runs, blobs belong to a project. */
61
+ export type ProjectId = Brand<string, 'ProjectId'>;
62
+
63
+ /** Conversation thread within a session. */
64
+ export type ThreadId = Brand<string, 'ThreadId'>;
65
+
66
+ /**
67
+ * Agent conversation. A conversation is a first-class primitive (see the
68
+ * `agent_conversations` migration in `@kindgi/agents`): it groups many
69
+ * agent invocations into one durable thread the user can reopen, hand
70
+ * off, or reference in a later run. Distinct from `ThreadId` (which is a
71
+ * lower-level session-thread identifier used by the session store).
72
+ */
73
+ export type ConversationId = Brand<string, 'ConversationId'>;
74
+
75
+ /** User session. */
76
+ export type SessionId = Brand<string, 'SessionId'>;
77
+
78
+ /** A run — one execution of a flow. */
79
+ export type RunId = Brand<string, 'RunId'>;
80
+
81
+ /**
82
+ * Task flow definition id. Convention: `<pack-id>.<flow-name>`
83
+ * (kebab-case, dot-namespaced) — e.g. `acme.brief-review-flow`,
84
+ * `demo.echo-flow`. The framework's built-in agent-turn flow is
85
+ * `agent.turn` (`AGENT_TURN_FLOW_ID` in `@kindgi/agents`).
86
+ */
87
+ export type FlowId = Brand<string, 'FlowId'>;
88
+
89
+ /** A node within a flow. Convention: kebab-case, unique within the flow — e.g. `dispatch-tool`, `wait-for-approval`. */
90
+ export type NodeId = Brand<string, 'NodeId'>;
91
+
92
+ /** An edge within a flow. Declared by the flow author (`FlowEdge.id` in `@kindgi/flow`); must be unique across the flow, including its loop bodies. */
93
+ export type EdgeId = Brand<string, 'EdgeId'>;
94
+
95
+ /**
96
+ * Agent definition id. Convention: `<pack-id>.<agent-name>`
97
+ * (kebab-case, dot-namespaced) — e.g. `acme.brief-writer`,
98
+ * `demo.echo-agent`. The `<pack-id>` prefix scopes the agent to
99
+ * its pack; `<agent-name>` describes the agent's role. Enforced by
100
+ * `defineAgent` at author time.
101
+ */
102
+ export type AgentId = Brand<string, 'AgentId'>;
103
+
104
+ /**
105
+ * Tool definition id. Convention: `<pack-id>.<tool-name>`
106
+ * (kebab-case, dot-namespaced) — e.g. `acme.verify-citation`,
107
+ * `demo.echo`. The `<pack-id>` prefix scopes the tool to its pack;
108
+ * `<tool-name>` names the operation. Enforced by `defineTool` at
109
+ * author time.
110
+ */
111
+ export type ToolId = Brand<string, 'ToolId'>;
112
+
113
+ /**
114
+ * Guardrail declaration id. Convention: `<pack-id>.<guardrail-name>`
115
+ * (kebab-case, dot-namespaced) — e.g. `acme.no-fabricated-quotes`,
116
+ * `demo.response-not-empty`. `<guardrail-name>` describes the
117
+ * property being asserted (usually a positive rule the agent turn
118
+ * must satisfy). Enforced by `defineGuardrail` at author time.
119
+ */
120
+ export type GuardrailId = Brand<string, 'GuardrailId'>;
121
+
122
+ /** A tenant policy document. */
123
+ export type PolicyId = Brand<string, 'PolicyId'>;
124
+
125
+ /** A pack manifest (installable vertical application). */
126
+ export type PackId = Brand<string, 'PackId'>;
127
+
128
+ /** An event (runtime-emitted or user-emitted). */
129
+ export type EventId = Brand<string, 'EventId'>;
130
+
131
+ /** A memory Fact — typed, versioned, retrievable. */
132
+ export type FactId = Brand<string, 'FactId'>;
133
+
134
+ /** A memory LogEntry — one line in the append-only event stream. */
135
+ export type LogEntryId = Brand<string, 'LogEntryId'>;
136
+
137
+ /** A generated artifact (draft, memo, report). */
138
+ export type ArtifactId = Brand<string, 'ArtifactId'>;
139
+
140
+ /** A provenance DAG record. */
141
+ export type ProvenanceId = Brand<string, 'ProvenanceId'>;
142
+
143
+ /** A subscription registration for the events system. */
144
+ export type SubscriptionId = Brand<string, 'SubscriptionId'>;
145
+
146
+ /** An approval request in the HITL queue. */
147
+ export type ApprovalId = Brand<string, 'ApprovalId'>;
148
+
149
+ /** A reviewer registered with HITL — carries a role class (standard/senior/admin). */
150
+ export type ReviewerId = Brand<string, 'ReviewerId'>;
151
+
152
+ /** A supervisor definition — the actor that observes runs and proposes fixes. */
153
+ export type SupervisorId = Brand<string, 'SupervisorId'>;
154
+
155
+ /** A recorded supervisor observation of a single agent run. */
156
+ export type ObservationId = Brand<string, 'ObservationId'>;
157
+
158
+ /** A supervisor's proposed fix to an agent artifact (prompt / retrieval / tool-config). */
159
+ export type FixProposalId = Brand<string, 'FixProposalId'>;
160
+
161
+ /** A signed audit bundle exported from HITL. Wraps approvals + evidence + provenance refs. */
162
+ export type AuditBundleId = Brand<string, 'AuditBundleId'>;
163
+
164
+ /** A compliance evidence bundle. */
165
+ export type ComplianceEvidenceId = Brand<string, 'ComplianceEvidenceId'>;
166
+
167
+ /** A wait token issued for pausing a run (HITL, external callback). */
168
+ export type WaitTokenId = Brand<string, 'WaitTokenId'>;
169
+
170
+ /** A training dataset registered in memory. */
171
+ export type DatasetId = Brand<string, 'DatasetId'>;
172
+
173
+ /** A recurring schedule that triggers agent / flow runs on a cadence. */
174
+ export type ScheduleId = Brand<string, 'ScheduleId'>;
175
+
176
+ /** A registered capability provider (model provider, embedding provider, etc.). */
177
+ export type ProviderId = Brand<string, 'ProviderId'>;
178
+
179
+ /** An API token issued to a caller for authenticating platform requests. */
180
+ export type ApiTokenId = Brand<string, 'ApiTokenId'>;
181
+
182
+ /** The routable public identifier of an inbound webhook trigger: the id in its receiver URL (see `TriggerId`). */
183
+ export type WebhookId = Brand<string, 'WebhookId'>;
184
+
185
+ /** An outbound webhook endpoint: a URL the platform sends signed events to. */
186
+ export type WebhookEndpointId = Brand<string, 'WebhookEndpointId'>;
187
+
188
+ /** One outbound webhook event. Sent as the `webhook-id` header and the same on every retry, so receivers deduplicate on it. */
189
+ export type WebhookEventId = Brand<string, 'WebhookEventId'>;
190
+
191
+ /** One event's delivery to one outbound endpoint, across all its attempts. */
192
+ export type WebhookDeliveryId = Brand<string, 'WebhookDeliveryId'>;
193
+
194
+ /** An installed pack instance for a tenant. Reserved: no package in this repository issues one. */
195
+ export type InstallationId = Brand<string, 'InstallationId'>;
196
+
197
+ /** A signing key — identifies a key pair in a caller-provided key store (see `SigningKeyBinding` in `@kindgi/crypto`). Keys themselves are opaque `Uint8Array` values; this ID is how consumers reference them at runtime without owning the store. */
198
+ export type SigningKeyId = Brand<string, 'SigningKeyId'>;
199
+
200
+ /** A registered trigger — its storage primary key. Distinct from `WebhookId` (routable public identifier used on the receiver URL); a webhook trigger has both a `TriggerId` (private, primary key) and a `WebhookId` (public, routable). */
201
+ export type TriggerId = Brand<string, 'TriggerId'>;
202
+
203
+ /**
204
+ * A per-environment slug carried as a required dimension by `EnvBinding`
205
+ * and `SecretBinding`.
206
+ *
207
+ * Grammar: `[a-z][a-z0-9-]{0,62}` — RFC-1035-like label (lowercase alnum
208
+ * + hyphen, 1–63 chars, starts with a letter). Cross-provider portable
209
+ * (Vault path chars, GCP secret names, AWS ARN paths, k8s namespaces).
210
+ * Use `makeEnvName` for validated construction; unchecked casts are
211
+ * a policy violation for any code path that receives external input.
212
+ */
213
+ export type EnvName = Brand<string, 'EnvName'>;
214
+
215
+ const ENV_NAME_GRAMMAR = /^[a-z][a-z0-9-]{0,62}$/;
216
+
217
+ /**
218
+ * Validated `EnvName` factory. Returns `null` on any grammar violation
219
+ * (uppercase, underscore, dot, slash, non-ASCII, empty, >63 chars).
220
+ */
221
+ export function makeEnvName(raw: string): EnvName | null {
222
+ if (typeof raw !== 'string') return null;
223
+ if (!ENV_NAME_GRAMMAR.test(raw)) return null;
224
+ return raw as EnvName;
225
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ export type * from './filter.js';
5
+ export type * from './hash.js';
6
+ export type * from './ids.js';
7
+ export type * from './refs.js';
8
+ export type * from './result.js';
9
+ export type * from './temporal.js';
10
+ export type * from './version.js';
11
+
12
+ // Runtime factories on branded IDs (validated construction). Kept as a
13
+ // separate value re-export so the `export type *` barrels above stay
14
+ // pure-type — value exports live only where explicitly listed.
15
+ export { makeEnvName } from './ids.js';
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { describe, expectTypeOf, test } from 'vitest';
5
+ import type { ContentHash, KeyId } from './hash.js';
6
+ import type { TenantId } from './ids.js';
7
+ import type { BlobRef, DatasetRef } from './refs.js';
8
+ import type { Timestamp } from './temporal.js';
9
+
10
+ describe('BlobRef', () => {
11
+ test('all required fields present and typed correctly', () => {
12
+ const ref: BlobRef = {
13
+ provider: 'minio',
14
+ bucket: 'kindgi-blobs',
15
+ key: 'tenants/abc/uploads/doc-42',
16
+ size: 1024,
17
+ sha256: 'sha256:abc' as ContentHash,
18
+ contentType: 'application/pdf',
19
+ tenantId: 'abc' as TenantId,
20
+ createdAt: '2026-09-16T14:00:00Z' as Timestamp,
21
+ };
22
+ expectTypeOf(ref.provider).toEqualTypeOf<string>();
23
+ expectTypeOf(ref.sha256).toEqualTypeOf<ContentHash>();
24
+ expectTypeOf(ref.tenantId).toEqualTypeOf<TenantId>();
25
+ });
26
+
27
+ test('retentionUntil is optional', () => {
28
+ const withoutRetention: BlobRef = {
29
+ provider: 'minio',
30
+ bucket: 'b',
31
+ key: 'k',
32
+ size: 0,
33
+ sha256: 'sha256:0' as ContentHash,
34
+ contentType: 'application/octet-stream',
35
+ tenantId: 'abc' as TenantId,
36
+ createdAt: '2026-09-16T14:00:00Z' as Timestamp,
37
+ };
38
+ expectTypeOf(withoutRetention.retentionUntil).toEqualTypeOf<Timestamp | undefined>();
39
+ });
40
+
41
+ test('missing required fields fail at compile time', () => {
42
+ // @ts-expect-error — 'bucket' is required.
43
+ const _bad: BlobRef = {
44
+ provider: 'minio',
45
+ key: 'k',
46
+ size: 0,
47
+ sha256: 'sha256:0' as ContentHash,
48
+ contentType: 'application/octet-stream',
49
+ tenantId: 'abc' as TenantId,
50
+ createdAt: '2026-09-16T14:00:00Z' as Timestamp,
51
+ };
52
+ });
53
+
54
+ test('fields are readonly', () => {
55
+ const ref: BlobRef = {
56
+ provider: 'minio',
57
+ bucket: 'b',
58
+ key: 'k',
59
+ size: 0,
60
+ sha256: 'sha256:0' as ContentHash,
61
+ contentType: 'application/octet-stream',
62
+ tenantId: 'abc' as TenantId,
63
+ createdAt: '2026-09-16T14:00:00Z' as Timestamp,
64
+ };
65
+ // @ts-expect-error — bucket is readonly.
66
+ ref.bucket = 'other';
67
+ });
68
+ });
69
+
70
+ describe('DatasetRef', () => {
71
+ test('has the expected shape', () => {
72
+ const ref: DatasetRef = {
73
+ datasetId: 'acme.contract-eval',
74
+ tenantId: 'abc' as TenantId,
75
+ version: '1.0.0',
76
+ factType: 'acme.contract',
77
+ count: 128,
78
+ createdAt: '2026-09-16T14:00:00Z' as Timestamp,
79
+ };
80
+ expectTypeOf(ref.count).toEqualTypeOf<number>();
81
+ expectTypeOf(ref.factType).toEqualTypeOf<string>();
82
+ });
83
+ });
84
+
85
+ describe('KeyId is opaque', () => {
86
+ test('constructed via cast', () => {
87
+ const k = 'ed25519-local:tenant-abc-v1' as KeyId;
88
+ expectTypeOf(k).toEqualTypeOf<KeyId>();
89
+ // KeyId is not string-assignable without cast.
90
+ // @ts-expect-error — bare string is not a KeyId.
91
+ const _bad: KeyId = 'foo';
92
+ });
93
+ });
package/src/refs.ts ADDED
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { ContentHash } from './hash.js';
5
+ import type { TenantId } from './ids.js';
6
+ import type { Timestamp } from './temporal.js';
7
+
8
+ /**
9
+ * Reference to a blob in object storage.
10
+ *
11
+ * BlobRef is a handle to bytes held by an object-storage provider (e.g.
12
+ * MinIO, S3, R2, GCS).
13
+ *
14
+ * The required fields are populated by the storage layer that wrote the
15
+ * bytes. Consumers must NEVER construct a BlobRef by hand; a real one always
16
+ * comes from a provider that has already written the bytes.
17
+ */
18
+ export interface BlobRef {
19
+ /** Which provider holds this blob (e.g. 'minio', 's3', 'r2', 'gcs'). */
20
+ readonly provider: string;
21
+ /** Bucket name within the provider. */
22
+ readonly bucket: string;
23
+ /** Object key within the bucket. Never contains a leading slash. */
24
+ readonly key: string;
25
+ /** Size in bytes. */
26
+ readonly size: number;
27
+ /** Content hash of the blob bytes — enables dedup and tamper detection. */
28
+ readonly sha256: ContentHash;
29
+ /** MIME type. */
30
+ readonly contentType: string;
31
+ /** Owning tenant. Cross-tenant access is denied by policy. */
32
+ readonly tenantId: TenantId;
33
+ /** When the blob was written. */
34
+ readonly createdAt: Timestamp;
35
+ /**
36
+ * Optional retention lock — after this timestamp, provider MAY delete;
37
+ * before, provider MUST refuse deletion. Enables legal-hold semantics.
38
+ */
39
+ readonly retentionUntil?: Timestamp;
40
+ }
41
+
42
+ /**
43
+ * Reference to a versioned dataset: a collection of memory Facts (see
44
+ * `@kindgi/memory`) of one declared type — e.g. eval corpora, held-out sets,
45
+ * or examples a pack ships with.
46
+ *
47
+ * The DatasetRef captures which collection and which version.
48
+ */
49
+ export interface DatasetRef {
50
+ /** Dataset identifier. */
51
+ readonly datasetId: string;
52
+ /** Owning tenant. */
53
+ readonly tenantId: TenantId;
54
+ /** Version of the dataset (semver). */
55
+ readonly version: string;
56
+ /** Fact type stored in this dataset (matches memory.schema.json Fact.type). */
57
+ readonly factType: string;
58
+ /** Number of items in this version. */
59
+ readonly count: number;
60
+ /** When this version was created. */
61
+ readonly createdAt: Timestamp;
62
+ }
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import { describe, expectTypeOf, test } from 'vitest';
5
+ import type { ErrOf, OkOf, Result } from './result.js';
6
+
7
+ describe('Result<T, E>', () => {
8
+ test('kind narrows to the corresponding member', () => {
9
+ // Wrapped in a function so TS keeps the full Result<number, string> type
10
+ // rather than narrowing to the assigned object literal at a `const` binding.
11
+ function inspect(r: Result<number, string>): void {
12
+ if (r.kind === 'ok') {
13
+ expectTypeOf(r.value).toEqualTypeOf<number>();
14
+ // The err member is not accessible in the ok branch.
15
+ // @ts-expect-error — 'error' is only on the err member.
16
+ const _e = r.error;
17
+ } else {
18
+ expectTypeOf(r.error).toEqualTypeOf<string>();
19
+ // Symmetric: 'value' is not accessible in the err branch.
20
+ // @ts-expect-error — 'value' is only on the ok member.
21
+ const _v = r.value;
22
+ }
23
+ }
24
+ inspect({ kind: 'ok', value: 42 });
25
+ inspect({ kind: 'err', error: 'bad' });
26
+ });
27
+
28
+ test('Result cannot be constructed without a kind tag', () => {
29
+ // @ts-expect-error — missing 'kind' discriminator.
30
+ const _bad: Result<number, string> = { value: 1 };
31
+ // @ts-expect-error — 'kind' must be 'ok' or 'err'.
32
+ const _bad2: Result<number, string> = { kind: 'maybe', value: 1 };
33
+ });
34
+
35
+ test('OkOf extracts the success type', () => {
36
+ type R = Result<{ id: string }, Error>;
37
+ expectTypeOf<OkOf<R>>().toEqualTypeOf<{ id: string }>();
38
+ });
39
+
40
+ test('ErrOf extracts the error type', () => {
41
+ type R = Result<number, { code: 'timeout' } | { code: 'refused' }>;
42
+ expectTypeOf<ErrOf<R>>().toEqualTypeOf<{ code: 'timeout' } | { code: 'refused' }>();
43
+ });
44
+
45
+ test('OkOf / ErrOf return never for non-Result inputs', () => {
46
+ expectTypeOf<OkOf<string>>().toEqualTypeOf<never>();
47
+ expectTypeOf<ErrOf<{ foo: 'bar' }>>().toEqualTypeOf<never>();
48
+ });
49
+
50
+ test('values are readonly by declaration', () => {
51
+ function mutate(r: Result<number, string>): void {
52
+ if (r.kind === 'ok') {
53
+ // @ts-expect-error — value is readonly.
54
+ r.value = 2;
55
+ }
56
+ }
57
+ mutate({ kind: 'ok', value: 1 });
58
+ });
59
+ });
package/src/result.ts ADDED
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ /**
5
+ * A discriminated-union result type. Preferred over throwing for boundary
6
+ * operations where the caller must handle both success and failure explicitly.
7
+ *
8
+ * Every member carries a runtime `kind` tag so consumers can narrow via
9
+ * `if (r.kind === 'ok') { ... }`. This is the codebase's standard shape;
10
+ * do not invent parallel Ok/Err representations.
11
+ *
12
+ * @example
13
+ * function loadSchema(path: string): Result<Schema, SchemaLoadError> {
14
+ * try {
15
+ * return { kind: 'ok', value: parse(readFileSync(path)) };
16
+ * } catch (cause) {
17
+ * return { kind: 'err', error: { code: 'load-failed', path, cause } };
18
+ * }
19
+ * }
20
+ *
21
+ * const r = loadSchema('./flow.schema.json');
22
+ * if (r.kind === 'ok') { use(r.value); } else { handle(r.error); }
23
+ */
24
+ export type Result<T, E> =
25
+ | { readonly kind: 'ok'; readonly value: T }
26
+ | { readonly kind: 'err'; readonly error: E };
27
+
28
+ /**
29
+ * Extract the success type from a Result.
30
+ *
31
+ * @example
32
+ * type Value = OkOf<Result<number, string>>; // number
33
+ */
34
+ export type OkOf<R> = R extends { readonly kind: 'ok'; readonly value: infer T } ? T : never;
35
+
36
+ /**
37
+ * Extract the error type from a Result.
38
+ *
39
+ * @example
40
+ * type Failure = ErrOf<Result<number, string>>; // string
41
+ */
42
+ export type ErrOf<R> = R extends { readonly kind: 'err'; readonly error: infer E } ? E : never;
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Brand } from './ids.js';
5
+
6
+ /**
7
+ * An ISO 8601 timestamp string, always UTC (trailing 'Z').
8
+ *
9
+ * Example: '2026-09-16T14:23:45.123Z'
10
+ *
11
+ * Kept as a string rather than a Date object for two reasons:
12
+ * 1. JSON-serializable by construction — no `.toISOString()` traps at boundaries.
13
+ * 2. Deterministic ordering by lexicographic sort, which matches temporal order
14
+ * for well-formed ISO 8601 UTC strings. Anything that orders records by
15
+ * timestamp (e.g. run replay) relies on this.
16
+ */
17
+ export type Timestamp = Brand<string, 'Timestamp'>;
18
+
19
+ /**
20
+ * A duration expressed in milliseconds.
21
+ *
22
+ * Branded to distinguish from arbitrary numbers at call sites like
23
+ * `wait(500)` vs `wait(500 as DurationMs)`.
24
+ */
25
+ export type DurationMs = Brand<number, 'DurationMs'>;
26
+
27
+ /**
28
+ * A duration expressed as an ISO 8601 duration string.
29
+ *
30
+ * Example: 'PT30S' (30 seconds), 'PT2H' (2 hours), 'P1D' (1 day).
31
+ *
32
+ * Preferred over `DurationMs` in declarative artifacts (task flows, retention
33
+ * policies) because it's human-readable and unambiguous across serialization.
34
+ */
35
+ export type IsoDuration = Brand<string, 'IsoDuration'>;
package/src/version.ts ADDED
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Brand } from './ids.js';
5
+
6
+ /**
7
+ * A semver version string (major.minor.patch, with optional pre-release).
8
+ *
9
+ * Regex convention: `^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$`.
10
+ *
11
+ * Used for:
12
+ * - Artifact versions (flow.version, policy.version, provenance.version).
13
+ * - Pack manifest versions.
14
+ * - Schema versions in `$comment: "schema-version: X.Y.Z"`.
15
+ *
16
+ * The brand prevents mixing free-form strings with validated semver values.
17
+ */
18
+ export type Semver = Brand<string, 'Semver'>;
19
+
20
+ /**
21
+ * Major version number extracted from a semver, used for schema $id path segments.
22
+ *
23
+ * Convention: `1`, `2`, `3` (positive integer). No `v` prefix — the `v` is added
24
+ * by the URL scheme (`/v1/flow.schema.json`), not part of the value.
25
+ */
26
+ export type SchemaMajor = Brand<number, 'SchemaMajor'>;
27
+
28
+ /**
29
+ * Version-pinned artifact reference. Combines an artifact id with an exact
30
+ * semver, so replays and audit trails always know which version was in force.
31
+ *
32
+ * @example
33
+ * const flowRef: VersionedRef<FlowId> = {
34
+ * id: 'acme.contract-review' as FlowId,
35
+ * version: '1.2.3' as Semver,
36
+ * };
37
+ */
38
+ export interface VersionedRef<Id extends string> {
39
+ readonly id: Id;
40
+ readonly version: Semver;
41
+ }