@cavelang/core 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.
- package/README.md +76 -0
- package/dist/src/claim.d.ts +108 -0
- package/dist/src/claim.d.ts.map +1 -0
- package/dist/src/claim.js +65 -0
- package/dist/src/claim.js.map +1 -0
- package/dist/src/confidence.d.ts +26 -0
- package/dist/src/confidence.d.ts.map +1 -0
- package/dist/src/confidence.js +34 -0
- package/dist/src/confidence.js.map +1 -0
- package/dist/src/context.d.ts +26 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +31 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/entity.d.ts +26 -0
- package/dist/src/entity.d.ts.map +1 -0
- package/dist/src/entity.js +41 -0
- package/dist/src/entity.js.map +1 -0
- package/dist/src/index.d.ts +23 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +23 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/key.d.ts +28 -0
- package/dist/src/key.d.ts.map +1 -0
- package/dist/src/key.js +49 -0
- package/dist/src/key.js.map +1 -0
- package/dist/src/multiplier.d.ts +17 -0
- package/dist/src/multiplier.d.ts.map +1 -0
- package/dist/src/multiplier.js +19 -0
- package/dist/src/multiplier.js.map +1 -0
- package/dist/src/tag.d.ts +29 -0
- package/dist/src/tag.d.ts.map +1 -0
- package/dist/src/tag.js +28 -0
- package/dist/src/tag.js.map +1 -0
- package/dist/src/uncertainty.d.ts +21 -0
- package/dist/src/uncertainty.d.ts.map +1 -0
- package/dist/src/uncertainty.js +26 -0
- package/dist/src/uncertainty.js.map +1 -0
- package/dist/src/uuidv7.d.ts +28 -0
- package/dist/src/uuidv7.d.ts.map +1 -0
- package/dist/src/uuidv7.js +69 -0
- package/dist/src/uuidv7.js.map +1 -0
- package/dist/src/value.d.ts +57 -0
- package/dist/src/value.d.ts.map +1 -0
- package/dist/src/value.js +104 -0
- package/dist/src/value.js.map +1 -0
- package/dist/src/verb.d.ts +47 -0
- package/dist/src/verb.d.ts.map +1 -0
- package/dist/src/verb.js +55 -0
- package/dist/src/verb.js.map +1 -0
- package/package.json +39 -0
- package/src/claim.ts +148 -0
- package/src/confidence.ts +42 -0
- package/src/context.ts +43 -0
- package/src/entity.ts +49 -0
- package/src/index.ts +23 -0
- package/src/key.ts +55 -0
- package/src/multiplier.ts +28 -0
- package/src/tag.ts +42 -0
- package/src/uncertainty.ts +29 -0
- package/src/uuidv7.ts +76 -0
- package/src/value.ts +138 -0
- package/src/verb.ts +78 -0
package/src/claim.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims — the CAVE model (spec §2.1).
|
|
3
|
+
*
|
|
4
|
+
* Every CAVE line denotes a claim c = ⟨s, v, o, n, m⟩: subject, verb,
|
|
5
|
+
* object-or-attribute/value, negated, metadata (confidence, contexts, tags,
|
|
6
|
+
* value uncertainty, importance, comment, transaction identity). Claims are
|
|
7
|
+
* immutable — belief changes by appending (spec §9).
|
|
8
|
+
*
|
|
9
|
+
* This module defines the *canonical* claim shape: subject/verb/object are in
|
|
10
|
+
* primary direction (inverse forms are normalized before keying, spec §5.5),
|
|
11
|
+
* with the author's original text preserved in `raw`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as Value from './value.ts'
|
|
15
|
+
import * as Tag from './tag.ts'
|
|
16
|
+
import * as Context from './context.ts'
|
|
17
|
+
import * as Confidence from './confidence.ts'
|
|
18
|
+
|
|
19
|
+
/** Subject or object term (spec §16: atom, literal or code literal). */
|
|
20
|
+
export type TermKind = 'entity' | 'text' | 'code'
|
|
21
|
+
|
|
22
|
+
export type Term = {
|
|
23
|
+
readonly kind: TermKind
|
|
24
|
+
readonly text: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns entity term. */
|
|
28
|
+
export const entity = (text: string): Term =>
|
|
29
|
+
({ kind: 'entity', text })
|
|
30
|
+
|
|
31
|
+
/** @returns double-quoted natural-language literal term. */
|
|
32
|
+
export const text = (value: string): Term =>
|
|
33
|
+
({ kind: 'text', text: value })
|
|
34
|
+
|
|
35
|
+
/** @returns backticked code literal term. */
|
|
36
|
+
export const code = (value: string): Term =>
|
|
37
|
+
({ kind: 'code', text: value })
|
|
38
|
+
|
|
39
|
+
/** @returns canonical text of a term with its delimiters. */
|
|
40
|
+
export const formatTerm = (term: Term): string => {
|
|
41
|
+
switch (term.kind) {
|
|
42
|
+
case 'text':
|
|
43
|
+
return `"${term.text}"`
|
|
44
|
+
case 'code':
|
|
45
|
+
return `\`${term.text}\``
|
|
46
|
+
default:
|
|
47
|
+
return term.text
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Claim payload — the canonical line shapes (spec §3.1) plus the object-less
|
|
53
|
+
* form used by bare existence assertions (spec §5.2 `EXISTS`):
|
|
54
|
+
*
|
|
55
|
+
* - `relation`: `subject VERB object`
|
|
56
|
+
* - `attribute`: `subject HAS attribute: value`
|
|
57
|
+
* - `metric`: `metric IS value`
|
|
58
|
+
* - `none`: `memory-leak EXISTS @production`
|
|
59
|
+
*/
|
|
60
|
+
export type Payload =
|
|
61
|
+
| { readonly kind: 'relation', readonly object: Term }
|
|
62
|
+
| { readonly kind: 'attribute', readonly attribute: string, readonly value: Value.t }
|
|
63
|
+
| { readonly kind: 'metric', readonly value: Value.t }
|
|
64
|
+
| { readonly kind: 'none' }
|
|
65
|
+
|
|
66
|
+
/** Canonical (primary-direction) claim. */
|
|
67
|
+
export type Claim = {
|
|
68
|
+
readonly subject: Term
|
|
69
|
+
/** Canonical primary verb, uppercase (spec §13.4 steps 1–2). */
|
|
70
|
+
readonly verb: string
|
|
71
|
+
/** `VERB NOT` logical negation (spec §5.6). */
|
|
72
|
+
readonly negated: boolean
|
|
73
|
+
readonly payload: Payload
|
|
74
|
+
/** Contexts in author order, deduplicated. */
|
|
75
|
+
readonly contexts: readonly Context.t[]
|
|
76
|
+
readonly tags: readonly Tag.t[]
|
|
77
|
+
/** Epistemic confidence in [0, 1]; 1 when omitted (spec §6.3). */
|
|
78
|
+
readonly conf: Confidence.t
|
|
79
|
+
/** `!` marker (spec §6). */
|
|
80
|
+
readonly importance: boolean
|
|
81
|
+
/** `+/-` value uncertainty (spec §7.2). */
|
|
82
|
+
readonly delta?: Value.t
|
|
83
|
+
/** `(Nσ)` σ-level override; semantic default is 2 (spec §7.2). */
|
|
84
|
+
readonly sigmaLevel?: number
|
|
85
|
+
/** `;` persisted prose (spec §6.4). */
|
|
86
|
+
readonly comment?: string
|
|
87
|
+
/** The line exactly as written, including inverse form (spec §5.5). */
|
|
88
|
+
readonly raw: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type t = Claim
|
|
92
|
+
|
|
93
|
+
export type Init = {
|
|
94
|
+
subject: Term
|
|
95
|
+
verb: string
|
|
96
|
+
payload: Payload
|
|
97
|
+
negated?: boolean
|
|
98
|
+
contexts?: readonly Context.t[]
|
|
99
|
+
tags?: readonly Tag.t[]
|
|
100
|
+
conf?: Confidence.t
|
|
101
|
+
importance?: boolean
|
|
102
|
+
delta?: Value.t
|
|
103
|
+
sigmaLevel?: number
|
|
104
|
+
comment?: string
|
|
105
|
+
raw?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @returns claim with defaults applied (spec §6: all suffixes optional). */
|
|
109
|
+
export const of = (init: Init): Claim => ({
|
|
110
|
+
subject: init.subject,
|
|
111
|
+
verb: init.verb,
|
|
112
|
+
negated: init.negated ?? false,
|
|
113
|
+
payload: init.payload,
|
|
114
|
+
contexts: Context.dedupe(init.contexts ?? []),
|
|
115
|
+
tags: init.tags ?? [],
|
|
116
|
+
conf: init.conf ?? Confidence.defaultConfidence,
|
|
117
|
+
importance: init.importance ?? false,
|
|
118
|
+
...init.delta !== undefined ? { delta: init.delta } : {},
|
|
119
|
+
...init.sigmaLevel !== undefined ? { sigmaLevel: init.sigmaLevel } : {},
|
|
120
|
+
...init.comment !== undefined ? { comment: init.comment } : {},
|
|
121
|
+
raw: init.raw ?? ''
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** @returns relational claim payload. */
|
|
125
|
+
export const relation = (object: Term): Payload =>
|
|
126
|
+
({ kind: 'relation', object })
|
|
127
|
+
|
|
128
|
+
/** @returns attribute/value claim payload. */
|
|
129
|
+
export const attribute = (attribute: string, value: Value.t): Payload =>
|
|
130
|
+
({ kind: 'attribute', attribute, value })
|
|
131
|
+
|
|
132
|
+
/** @returns metric claim payload. */
|
|
133
|
+
export const metric = (value: Value.t): Payload =>
|
|
134
|
+
({ kind: 'metric', value })
|
|
135
|
+
|
|
136
|
+
/** Object-less payload — bare existence assertion (spec §5.2 `EXISTS`). */
|
|
137
|
+
export const none: Payload =
|
|
138
|
+
{ kind: 'none' }
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* σ derived from the claim's `+/- Δ (kσ)` metadata: σ = Δ / k with k
|
|
142
|
+
* defaulting to 2 (spec §7.2). `undefined` when the claim carries no numeric
|
|
143
|
+
* uncertainty.
|
|
144
|
+
*/
|
|
145
|
+
export const sigmaOf = (claim: Claim): undefined | number =>
|
|
146
|
+
claim.delta?.num === undefined ?
|
|
147
|
+
undefined :
|
|
148
|
+
claim.delta.num / (claim.sigmaLevel ?? 2)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim confidence — `@ N%` (spec §6.3).
|
|
3
|
+
*
|
|
4
|
+
* Epistemic belief in the assertion, stored as a decimal in [0, 1].
|
|
5
|
+
* Omitted confidence means `@ 100%` — directly observed, certain for
|
|
6
|
+
* practical purposes. `@ 0%` means evidentially false or fully rejected
|
|
7
|
+
* (retraction, spec §9.3).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Confidence = number
|
|
11
|
+
|
|
12
|
+
export type t = Confidence
|
|
13
|
+
|
|
14
|
+
/** Confidence of a claim with no explicit `@ N%` (spec §6.3). */
|
|
15
|
+
export const defaultConfidence = 1
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses a percentage token (`90%`) to a decimal clamped to [0, 1]
|
|
19
|
+
* (spec §13.4 step 6: `@ 90%` → `0.9`). The `%` is required — the grammar
|
|
20
|
+
* (spec §16) defines confidence as a percentage ending in `%`, and demanding
|
|
21
|
+
* it keeps a mistyped context like `@ 2026` from silently becoming
|
|
22
|
+
* certainty.
|
|
23
|
+
* @returns `undefined` when the token is not a percentage.
|
|
24
|
+
*/
|
|
25
|
+
export const parse = (token: string): undefined | Confidence => {
|
|
26
|
+
const match = /^(\d+(?:\.\d+)?)%$/.exec(token.trim())
|
|
27
|
+
if (!match || match[1] === undefined) {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
return clamp(Number(match[1]) / 100)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @returns confidence clamped to [0, 1]. */
|
|
34
|
+
export const clamp = (conf: number): Confidence =>
|
|
35
|
+
Math.min(1, Math.max(0, conf))
|
|
36
|
+
|
|
37
|
+
/** @returns canonical `N%` text: `0.9` → `90%`. */
|
|
38
|
+
export const format = (conf: Confidence): string => {
|
|
39
|
+
const percent = conf * 100
|
|
40
|
+
const rounded = Math.round(percent * 100) / 100
|
|
41
|
+
return `${rounded}%`
|
|
42
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contexts — `@ctx` (spec §6.1).
|
|
3
|
+
*
|
|
4
|
+
* A context scopes a claim by time, place, source or logical scope:
|
|
5
|
+
* `@production`, `@2026-Q1`, `@auth.ts:42`, `@src:filing`. No space after
|
|
6
|
+
* `@` (with a space it is confidence, spec §6.3). Multiple contexts per
|
|
7
|
+
* claim are allowed and are part of the claim key (spec §9.2).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Context = string
|
|
11
|
+
|
|
12
|
+
export type t = Context
|
|
13
|
+
|
|
14
|
+
/** Recommended context prefixes (spec §6.1). */
|
|
15
|
+
export const prefixes = ['src', 'time', 'loc', 'scope'] as const
|
|
16
|
+
|
|
17
|
+
export type Prefix = (typeof prefixes)[number]
|
|
18
|
+
|
|
19
|
+
const prefixSet = new Set<string>(prefixes)
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @returns the recommended prefix of a context, when it has one:
|
|
23
|
+
* `src:filing` → `src`, `production` → `undefined`.
|
|
24
|
+
*/
|
|
25
|
+
export const prefix = (context: Context): undefined | Prefix => {
|
|
26
|
+
const colonAt = context.indexOf(':')
|
|
27
|
+
if (colonAt === -1) {
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
const head = context.slice(0, colonAt)
|
|
31
|
+
return prefixSet.has(head) ? head as Prefix : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @returns canonical `@ctx` text. */
|
|
35
|
+
export const format = (context: Context): string =>
|
|
36
|
+
`@${context}`
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @returns deduplicated contexts in original order. Claim keys use the
|
|
40
|
+
* sorted form (see `Key`); emission preserves author order.
|
|
41
|
+
*/
|
|
42
|
+
export const dedupe = (contexts: readonly Context[]): Context[] =>
|
|
43
|
+
[...new Set(contexts)]
|
package/src/entity.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entity names (spec §4.1).
|
|
3
|
+
*
|
|
4
|
+
* Entities are compact names: `auth/middleware`, `react/hooks/use-memo`,
|
|
5
|
+
* `PostgreSQL`, `Sarah`. `/` separates scope segments (at most 3:
|
|
6
|
+
* `domain/entity/aspect`), segments are kebab-case, proper nouns keep their
|
|
7
|
+
* casing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type Entity = string
|
|
11
|
+
|
|
12
|
+
export type t = Entity
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalizes an entity name (spec §13.4 step 4): trims and collapses runs of
|
|
16
|
+
* internal whitespace to `-`. Casing is preserved — proper nouns keep theirs.
|
|
17
|
+
*
|
|
18
|
+
* `token expiry` → `token-expiry`, `PostgreSQL` → `PostgreSQL`.
|
|
19
|
+
*/
|
|
20
|
+
export const normalize = (name: string): Entity =>
|
|
21
|
+
name.trim().replace(/\s+/g, '-')
|
|
22
|
+
|
|
23
|
+
/** @returns scope segments of an entity name: `a/b/c` → `['a', 'b', 'c']`. */
|
|
24
|
+
export const segments = (entity: Entity): string[] =>
|
|
25
|
+
entity.split('/')
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Advisory well-formedness check (spec §4.1). Returns a list of human-readable
|
|
29
|
+
* problems; an empty list means the name follows every convention. Problems
|
|
30
|
+
* are advisories, not parse errors — CAVE tolerates messy extraction input.
|
|
31
|
+
*/
|
|
32
|
+
export const check = (entity: Entity): string[] => {
|
|
33
|
+
const problems: string[] = []
|
|
34
|
+
if (entity.length === 0) {
|
|
35
|
+
problems.push('empty entity name')
|
|
36
|
+
return problems
|
|
37
|
+
}
|
|
38
|
+
const parts = segments(entity)
|
|
39
|
+
if (parts.length > 3) {
|
|
40
|
+
problems.push(`more than 3 scope segments (${parts.length}): domain/entity/aspect is the maximum`)
|
|
41
|
+
}
|
|
42
|
+
if (parts.some(part => part.length === 0)) {
|
|
43
|
+
problems.push('empty scope segment')
|
|
44
|
+
}
|
|
45
|
+
if (/\s/.test(entity)) {
|
|
46
|
+
problems.push('whitespace in entity name (normalize to -)')
|
|
47
|
+
}
|
|
48
|
+
return problems
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavelang/core` — the CAVE domain model (spec §2, §6, §7, §9).
|
|
3
|
+
*
|
|
4
|
+
* Dependency-free foundation of the CAVE monorepo: canonical claim shapes,
|
|
5
|
+
* values/units/multipliers, uncertainty and confidence semantics, claim keys
|
|
6
|
+
* and UUIDv7 transaction identifiers. Higher packages (parser, canonical,
|
|
7
|
+
* store, query, fusion, loop, cli) build on these types.
|
|
8
|
+
*
|
|
9
|
+
* Modules follow the `@prelude` convention: `import * as Claim from ...`,
|
|
10
|
+
* with each module exporting its principal type as `t`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * as Claim from './claim.ts'
|
|
14
|
+
export * as Confidence from './confidence.ts'
|
|
15
|
+
export * as Context from './context.ts'
|
|
16
|
+
export * as Entity from './entity.ts'
|
|
17
|
+
export * as Key from './key.ts'
|
|
18
|
+
export * as Multiplier from './multiplier.ts'
|
|
19
|
+
export * as Tag from './tag.ts'
|
|
20
|
+
export * as Uncertainty from './uncertainty.ts'
|
|
21
|
+
export * as Uuidv7 from './uuidv7.ts'
|
|
22
|
+
export * as Value from './value.ts'
|
|
23
|
+
export * as Verb from './verb.ts'
|
package/src/key.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim keys (spec §9.2).
|
|
3
|
+
*
|
|
4
|
+
* The claim key identifies the *fact* whose belief evolves over time: the
|
|
5
|
+
* latest transaction with the same key is the current belief (spec §9.1).
|
|
6
|
+
* Keys are computed on the canonical (primary-direction) form, so a forward
|
|
7
|
+
* claim and its inverse reading share one key — one fact, two names
|
|
8
|
+
* (spec §5.5).
|
|
9
|
+
*
|
|
10
|
+
* Key components:
|
|
11
|
+
*
|
|
12
|
+
* - relational claim: subject, verb, object, negated, context set
|
|
13
|
+
* - attribute/value claim: subject, verb, attribute, negated, context set
|
|
14
|
+
* - metric claim: subject, verb, negated, context set
|
|
15
|
+
*
|
|
16
|
+
* The value is *excluded* — it may change over time while the key stays
|
|
17
|
+
* about the same property. Confidence, tags, uncertainty, importance and
|
|
18
|
+
* comments are metadata and never key components. Contexts participate as a
|
|
19
|
+
* sorted, deduplicated set, which is what lets competing hypotheses
|
|
20
|
+
* differentiate by `@hyp:` context (spec §10.3).
|
|
21
|
+
*
|
|
22
|
+
* The key format is a JSON array — deterministic, collision-free and
|
|
23
|
+
* readable in the database.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type * as Claim from './claim.ts'
|
|
27
|
+
|
|
28
|
+
// Every term kind gets its own prefix so the three encodings occupy
|
|
29
|
+
// disjoint namespaces — an entity literally named `code:<=` must not
|
|
30
|
+
// collide with the backticked code literal `<=`.
|
|
31
|
+
const termPart = (term: Claim.Term): string =>
|
|
32
|
+
term.kind === 'entity' ? `e:${term.text}` : `${term.kind}:${term.text}`
|
|
33
|
+
|
|
34
|
+
const payloadPart = (payload: Claim.Payload): string => {
|
|
35
|
+
switch (payload.kind) {
|
|
36
|
+
case 'relation':
|
|
37
|
+
return `r:${termPart(payload.object)}`
|
|
38
|
+
case 'attribute':
|
|
39
|
+
return `a:${payload.attribute}`
|
|
40
|
+
case 'metric':
|
|
41
|
+
return 'm'
|
|
42
|
+
case 'none':
|
|
43
|
+
return 'n'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @returns stable claim key of a canonical claim. */
|
|
48
|
+
export const of = (claim: Claim.t): string =>
|
|
49
|
+
JSON.stringify([
|
|
50
|
+
termPart(claim.subject),
|
|
51
|
+
claim.verb,
|
|
52
|
+
claim.negated ? 1 : 0,
|
|
53
|
+
payloadPart(claim.payload),
|
|
54
|
+
[...new Set(claim.contexts)].sort()
|
|
55
|
+
])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric multipliers (spec §7.1).
|
|
3
|
+
*
|
|
4
|
+
* A multiplier is a single uppercase letter glued to a number: `20B`, `900M`,
|
|
5
|
+
* `1.5T`, `10K`. It scales the numeric value during canonicalization
|
|
6
|
+
* (spec §13.4 step 8: `20B` → `20000000000`); the raw text is preserved.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Multiplier letter. */
|
|
10
|
+
export type Multiplier = 'T' | 'B' | 'M' | 'K'
|
|
11
|
+
|
|
12
|
+
export type t = Multiplier
|
|
13
|
+
|
|
14
|
+
/** Scale factor per multiplier letter. */
|
|
15
|
+
export const factors: Readonly<Record<Multiplier, number>> = {
|
|
16
|
+
T: 1e12,
|
|
17
|
+
B: 1e9,
|
|
18
|
+
M: 1e6,
|
|
19
|
+
K: 1e3
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @returns `true` if `s` is exactly one multiplier letter. */
|
|
23
|
+
export const is = (s: string): s is Multiplier =>
|
|
24
|
+
s === 'T' || s === 'B' || s === 'M' || s === 'K'
|
|
25
|
+
|
|
26
|
+
/** @returns scale factor of multiplier. */
|
|
27
|
+
export const factor = (m: Multiplier): number =>
|
|
28
|
+
factors[m]
|
package/src/tag.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tags — flat `#tag` and scoped `#key:value` (spec §6.2).
|
|
3
|
+
*
|
|
4
|
+
* A flat `#security` is `key=security, value=undefined`; a scoped
|
|
5
|
+
* `#topic:auth-security` splits on the first `:`. Scoped tags subsume flat
|
|
6
|
+
* tags — a flat `#tag` is simply a tag with null value.
|
|
7
|
+
*
|
|
8
|
+
* Tags classify the claim, not the entity, and carry no independent belief
|
|
9
|
+
* history (the two-lane rule, spec §11.1).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type Tag = {
|
|
13
|
+
readonly key: string
|
|
14
|
+
/** `undefined` for flat tags; spec stores NULL (spec §13.2). */
|
|
15
|
+
readonly value?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type t = Tag
|
|
19
|
+
|
|
20
|
+
/** @returns tag from key and optional scoped value. */
|
|
21
|
+
export const of = (key: string, value?: string): Tag =>
|
|
22
|
+
value === undefined ? { key } : { key, value }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Splits a tag body (without the leading `#`) on the first `:`
|
|
26
|
+
* (spec §6.2 disambiguation): `topic:auth-security` → scoped,
|
|
27
|
+
* `security` → flat.
|
|
28
|
+
*/
|
|
29
|
+
export const parse = (body: string): Tag => {
|
|
30
|
+
const colonAt = body.indexOf(':')
|
|
31
|
+
return colonAt === -1 ?
|
|
32
|
+
{ key: body } :
|
|
33
|
+
{ key: body.slice(0, colonAt), value: body.slice(colonAt + 1) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @returns canonical `#key[:value]` text. */
|
|
37
|
+
export const format = (tag: Tag): string =>
|
|
38
|
+
tag.value === undefined ? `#${tag.key}` : `#${tag.key}:${tag.value}`
|
|
39
|
+
|
|
40
|
+
/** @returns `true` when both tags have the same key and value. */
|
|
41
|
+
export const equals = (a: Tag, b: Tag): boolean =>
|
|
42
|
+
a.key === b.key && a.value === b.value
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value uncertainty — `+/- delta [(Nσ)]` (spec §7.2).
|
|
3
|
+
*
|
|
4
|
+
* `+/-` defines a symmetric interval around the value; the default
|
|
5
|
+
* interpretation is 2σ (≈95% interval). If the interval is Δ at kσ then
|
|
6
|
+
* σ = Δ / k:
|
|
7
|
+
*
|
|
8
|
+
* - `20B +/- 2B` → σ = 1B (default 2σ)
|
|
9
|
+
* - `20B +/- 2B (1σ)` → σ = 2B (wider)
|
|
10
|
+
* - `20B +/- 2B (3σ)` → σ ≈ 0.67B (tighter)
|
|
11
|
+
*
|
|
12
|
+
* Value uncertainty is aleatory (the quantity itself is imprecise) and is
|
|
13
|
+
* independent of epistemic claim confidence `@ N%` (spec §7.3).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Default σ level when `(Nσ)` is omitted (spec §7.2). */
|
|
17
|
+
export const defaultSigmaLevel = 2
|
|
18
|
+
|
|
19
|
+
/** @returns σ from an interval `delta` given at `level`σ: σ = Δ / k. */
|
|
20
|
+
export const sigma = (delta: number, level: number = defaultSigmaLevel): number => {
|
|
21
|
+
if (!(level > 0)) {
|
|
22
|
+
throw new Error(`Expected positive sigma level, got ${level}.`)
|
|
23
|
+
}
|
|
24
|
+
return delta / level
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @returns symmetric `[low, high]` interval around `mean`. */
|
|
28
|
+
export const interval = (mean: number, delta: number): [number, number] =>
|
|
29
|
+
[mean - delta, mean + delta]
|
package/src/uuidv7.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUIDv7 transaction identifiers (spec §9.1).
|
|
3
|
+
*
|
|
4
|
+
* CAVE storage is append-only; each appended claim carries a transaction id.
|
|
5
|
+
* UUIDv7 is recommended because it encodes timestamp and ordering — ids sort
|
|
6
|
+
* lexicographically by creation time, so `MAX(tx)` resolves current belief.
|
|
7
|
+
*
|
|
8
|
+
* {@link next} is strictly monotonic within a process: same-millisecond calls
|
|
9
|
+
* increment a 12-bit sequence in the `rand_a` field, and a backwards system
|
|
10
|
+
* clock never produces a smaller id.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomFillSync } from 'node:crypto'
|
|
14
|
+
|
|
15
|
+
const hex = (n: number, digits: number): string =>
|
|
16
|
+
n.toString(16).padStart(digits, '0')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Formats a UUIDv7 from parts — pure, for tests and deterministic imports.
|
|
20
|
+
*
|
|
21
|
+
* @param ms unix timestamp in milliseconds (48 bits)
|
|
22
|
+
* @param seq 12-bit sequence placed in `rand_a`
|
|
23
|
+
* @param rand 62 bits of randomness for `rand_b` as 8 bytes (top 2 bits are
|
|
24
|
+
* overwritten by the variant)
|
|
25
|
+
*/
|
|
26
|
+
export const at = (ms: number, seq: number, rand: Uint8Array): string => {
|
|
27
|
+
if (!Number.isInteger(ms) || ms < 0 || ms > 0xffff_ffff_ffff) {
|
|
28
|
+
throw new Error(`Expected 48-bit millisecond timestamp, got ${ms}.`)
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(seq) || seq < 0 || seq > 0xfff) {
|
|
31
|
+
throw new Error(`Expected 12-bit sequence, got ${seq}.`)
|
|
32
|
+
}
|
|
33
|
+
if (rand.length < 8) {
|
|
34
|
+
throw new Error(`Expected 8 random bytes, got ${rand.length}.`)
|
|
35
|
+
}
|
|
36
|
+
const time = hex(ms, 12)
|
|
37
|
+
const variantByte = (rand[0]! & 0x3f) | 0x80
|
|
38
|
+
return [
|
|
39
|
+
time.slice(0, 8),
|
|
40
|
+
time.slice(8, 12),
|
|
41
|
+
`7${hex(seq, 3)}`,
|
|
42
|
+
hex(variantByte, 2) + hex(rand[1]!, 2),
|
|
43
|
+
[...rand.slice(2, 8)].map(byte => hex(byte, 2)).join('')
|
|
44
|
+
].join('-')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let lastMs = -1
|
|
48
|
+
let lastSeq = 0
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @returns next monotonic UUIDv7. Strictly increasing (lexicographically)
|
|
52
|
+
* within the process even across same-millisecond calls and clock skew.
|
|
53
|
+
*/
|
|
54
|
+
export const next = (now: () => number = Date.now): string => {
|
|
55
|
+
let ms = now()
|
|
56
|
+
if (ms <= lastMs) {
|
|
57
|
+
ms = lastMs
|
|
58
|
+
lastSeq += 1
|
|
59
|
+
if (lastSeq > 0xfff) {
|
|
60
|
+
ms += 1
|
|
61
|
+
lastSeq = 0
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
lastSeq = 0
|
|
65
|
+
}
|
|
66
|
+
lastMs = ms
|
|
67
|
+
const rand = new Uint8Array(8)
|
|
68
|
+
randomFillSync(rand)
|
|
69
|
+
return at(ms, lastSeq, rand)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
|
73
|
+
|
|
74
|
+
/** @returns `true` if `s` is a well-formed UUIDv7 string. */
|
|
75
|
+
export const is = (s: string): boolean =>
|
|
76
|
+
uuidRe.test(s)
|
package/src/value.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Values, units and multipliers (spec §7.1).
|
|
3
|
+
*
|
|
4
|
+
* A value is the payload of an attribute claim (`HAS attr: value`), a metric
|
|
5
|
+
* claim (`metric IS value`), or a `+/-` uncertainty delta. Parsing keeps the
|
|
6
|
+
* raw text verbatim and additionally extracts a normalized numeric value and
|
|
7
|
+
* unit when possible (spec §13.4 steps 7–9):
|
|
8
|
+
*
|
|
9
|
+
* - `30ms` → num 30, unit `ms`
|
|
10
|
+
* - `20B USD/yr` → num 20000000000, unit `USD/yr`
|
|
11
|
+
* - `~20B USD/yr` → the same, `approx` set
|
|
12
|
+
* - `94.5%` → num 94.5, unit `%`
|
|
13
|
+
* - `20 conn` → num 20, unit `conn`
|
|
14
|
+
* - `2026-H2` → date-like, kept textual
|
|
15
|
+
* - `token-expiry` → atom, kept textual
|
|
16
|
+
*
|
|
17
|
+
* Quoted (`"..."`) and backticked (`` `...` ``) values arrive from the parser
|
|
18
|
+
* with their kind already known; use {@link ofText} / {@link ofCode}.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as Multiplier from './multiplier.ts'
|
|
22
|
+
|
|
23
|
+
/** How the value text was classified. */
|
|
24
|
+
export type Kind =
|
|
25
|
+
| 'number' // numeric, possibly with multiplier and unit
|
|
26
|
+
| 'date' // date-like: 2026-H2, 2026-Q1, 2026-04-10
|
|
27
|
+
| 'atom' // bare word(s): token-expiry, critical
|
|
28
|
+
| 'text' // double-quoted natural-language literal
|
|
29
|
+
| 'code' // backticked exact literal
|
|
30
|
+
|
|
31
|
+
export type Value = {
|
|
32
|
+
/** Exactly as written, including `~` and multiplier letter. */
|
|
33
|
+
readonly raw: string
|
|
34
|
+
readonly kind: Kind
|
|
35
|
+
/** `~` prefix (spec §7.1): the value is approximate. */
|
|
36
|
+
readonly approx: boolean
|
|
37
|
+
/** Normalized numeric value with multiplier expanded, when parseable. */
|
|
38
|
+
readonly num?: number
|
|
39
|
+
/** Normalized unit expression (`USD/yr`, `ms`, `%`), when present. */
|
|
40
|
+
readonly unit?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type t = Value
|
|
44
|
+
|
|
45
|
+
const numberRe = /^(-?\d+(?:\.\d+)?)([A-Za-z%]*)$/
|
|
46
|
+
const unitRe = /^[A-Za-z%$][A-Za-z0-9%$]*(?:\/[A-Za-z0-9%$]+)*$/
|
|
47
|
+
const dateRe = /^\d{4}-(?:H[1-2]|Q[1-4]|W\d{1,2}|\d{2})(?:-\d{2})?$/
|
|
48
|
+
|
|
49
|
+
/** @returns `true` if `s` is a valid unit expression (`USD/yr`, `ms`, `%`). */
|
|
50
|
+
export const isUnit = (s: string): boolean =>
|
|
51
|
+
unitRe.test(s)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @returns `true` if `s` is date-like (spec §16 `date_like`): `2026-H2`,
|
|
55
|
+
* `2026-Q1`, `2026-04`, `2026-04-10`. A bare year parses as a number instead.
|
|
56
|
+
*/
|
|
57
|
+
export const isDateLike = (s: string): boolean =>
|
|
58
|
+
dateRe.test(s)
|
|
59
|
+
|
|
60
|
+
type Numeric = { num: number, unit?: string }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parses `body` as number [multiplier] [unit] where the unit is either glued
|
|
64
|
+
* (`30ms`, `94.5%`) or space-separated (`20B USD/yr`, `20 conn`).
|
|
65
|
+
* @returns `undefined` when `body` is not numeric.
|
|
66
|
+
*/
|
|
67
|
+
const parseNumeric = (body: string): undefined | Numeric => {
|
|
68
|
+
const spaceAt = body.indexOf(' ')
|
|
69
|
+
const head = spaceAt === -1 ? body : body.slice(0, spaceAt)
|
|
70
|
+
const tail = spaceAt === -1 ? undefined : body.slice(spaceAt + 1).trim()
|
|
71
|
+
const match = numberRe.exec(head)
|
|
72
|
+
if (!match) {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
const [, digits, glued] = match
|
|
76
|
+
let num = Number(digits)
|
|
77
|
+
let unit: undefined | string
|
|
78
|
+
if (glued !== undefined && glued !== '') {
|
|
79
|
+
if (Multiplier.is(glued)) {
|
|
80
|
+
num *= Multiplier.factor(glued)
|
|
81
|
+
} else if (isUnit(glued)) {
|
|
82
|
+
unit = glued
|
|
83
|
+
} else {
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (tail !== undefined) {
|
|
88
|
+
if (unit !== undefined || !isUnit(tail)) {
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
unit = tail
|
|
92
|
+
}
|
|
93
|
+
return unit === undefined ? { num } : { num, unit }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parses an unquoted value string (spec §16 `value`), classifying it as
|
|
98
|
+
* number, date or atom. The raw text (including `~` and multiplier letters)
|
|
99
|
+
* is preserved verbatim.
|
|
100
|
+
*/
|
|
101
|
+
export const parse = (raw: string): Value => {
|
|
102
|
+
const approx = raw.startsWith('~')
|
|
103
|
+
const body = approx ? raw.slice(1) : raw
|
|
104
|
+
const numeric = parseNumeric(body)
|
|
105
|
+
if (numeric !== undefined) {
|
|
106
|
+
return numeric.unit === undefined ?
|
|
107
|
+
{ raw, kind: 'number', approx, num: numeric.num } :
|
|
108
|
+
{ raw, kind: 'number', approx, num: numeric.num, unit: numeric.unit }
|
|
109
|
+
}
|
|
110
|
+
if (isDateLike(body)) {
|
|
111
|
+
return { raw, kind: 'date', approx }
|
|
112
|
+
}
|
|
113
|
+
return { raw, kind: 'atom', approx }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @returns value for a double-quoted natural-language literal (spec §4.2). */
|
|
117
|
+
export const ofText = (text: string): Value =>
|
|
118
|
+
({ raw: text, kind: 'text', approx: false })
|
|
119
|
+
|
|
120
|
+
/** @returns value for a backticked exact code literal (spec §4.2). */
|
|
121
|
+
export const ofCode = (code: string): Value =>
|
|
122
|
+
({ raw: code, kind: 'code', approx: false })
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @returns canonical text of the value for emission. Raw text is already
|
|
126
|
+
* canonical (spec normalizes multipliers only in storage, never in text);
|
|
127
|
+
* quoted kinds are re-wrapped in their delimiters.
|
|
128
|
+
*/
|
|
129
|
+
export const format = (value: Value): string => {
|
|
130
|
+
switch (value.kind) {
|
|
131
|
+
case 'text':
|
|
132
|
+
return `"${value.raw}"`
|
|
133
|
+
case 'code':
|
|
134
|
+
return `\`${value.raw}\``
|
|
135
|
+
default:
|
|
136
|
+
return value.raw
|
|
137
|
+
}
|
|
138
|
+
}
|