@kindgi/types 0.0.0-bootstrap.0 → 0.1.1
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/LICENSE +201 -0
- package/README.md +39 -2
- package/SPEC.md +162 -0
- package/dist/filter.d.ts +61 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +4 -0
- package/dist/filter.js.map +1 -0
- package/dist/hash.d.ts +32 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +4 -0
- package/dist/hash.js.map +1 -0
- package/dist/ids.d.ts +171 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +15 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/refs.d.ts +58 -0
- package/dist/refs.d.ts.map +1 -0
- package/dist/refs.js +4 -0
- package/dist/refs.js.map +1 -0
- package/dist/result.d.ts +48 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +4 -0
- package/dist/result.js.map +1 -0
- package/dist/temporal.d.ts +30 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +4 -0
- package/dist/temporal.js.map +1 -0
- package/dist/version.d.ts +36 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/package.json +46 -4
- package/src/filter.ts +66 -0
- package/src/hash.ts +37 -0
- package/src/ids.test-d.ts +54 -0
- package/src/ids.ts +225 -0
- package/src/index.ts +15 -0
- package/src/refs.test-d.ts +93 -0
- package/src/refs.ts +62 -0
- package/src/result.test-d.ts +59 -0
- package/src/result.ts +42 -0
- package/src/temporal.ts +35 -0
- package/src/version.ts +41 -0
- 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;
|
package/src/temporal.ts
ADDED
|
@@ -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
|
+
}
|