functionalscript 0.42.0 → 0.43.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.
@@ -11,7 +11,7 @@ export type TerminalRange = number;
11
11
  */
12
12
  export type Sequence = readonly string[];
13
13
  /** A variant of rule names. */
14
- export type Variant = StringMap<string, string>;
14
+ export type Variant = StringMap<string>;
15
15
  /**
16
16
  * Grammar rule definition.
17
17
  *
@@ -29,7 +29,7 @@ export type RuleSet = Readonly<Record<string, Rule>>;
29
29
  * variant branch.
30
30
  */
31
31
  export type EmptyTag = string | true | undefined;
32
- type EmptyTagMap = StringMap<string, EmptyTag>;
32
+ type EmptyTagMap = StringMap<EmptyTag>;
33
33
  /**
34
34
  * Computes, for every rule in the set, whether it can match empty input, by
35
35
  * the standard nullable-set fixpoint: a sequence is nullable iff all of its
@@ -26,7 +26,7 @@ type DispatchRuleCollection = {
26
26
  readonly tag: string | undefined;
27
27
  readonly rules: DispatchRuleOrName[];
28
28
  };
29
- type DispatchMap = StringMap<string, DispatchRule>;
29
+ type DispatchMap = StringMap<DispatchRule>;
30
30
  /**
31
31
  * Represents a parsed AST sequence.
32
32
  */
@@ -89,7 +89,7 @@ export declare const range: (ab: string) => TerminalRange;
89
89
  /**
90
90
  * A set of terminal ranges compatible with the `Variant` rule.
91
91
  */
92
- export type RangeVariant = StringMap<string, TerminalRange>;
92
+ export type RangeVariant = StringMap<TerminalRange>;
93
93
  export declare const remove: (range: TerminalRange, v: RangeVariant) => RangeVariant;
94
94
  /**
95
95
  * Returns the complement set of the provided ranges over {@link fullRange}.
@@ -45,7 +45,7 @@
45
45
  import { type Effect, type Operation } from '../../effects/module.f.ts';
46
46
  import { type Key, type MemOp } from '../../effects/memory/module.f.ts';
47
47
  import { type Cas } from '../module.f.ts';
48
- import { type Revision } from '../../media/revision/module.f.ts';
48
+ import { type LockMap, type Revision } from '../../media/revision/module.f.ts';
49
49
  import { type Result } from '../../types/result/module.f.ts';
50
50
  import { type StringMap } from '../../types/object/module.f.ts';
51
51
  import type { Vec } from '../../types/bit_vec/module.f.ts';
@@ -98,6 +98,7 @@ export type RevisionData = {
98
98
  readonly subject?: Subject | undefined;
99
99
  readonly archived?: true | undefined;
100
100
  readonly generation?: number | undefined;
101
+ readonly lock?: LockMap | undefined;
101
102
  };
102
103
  /**
103
104
  * Per-subject bookkeeping: every revision hash seen for the subject, every
@@ -119,7 +120,7 @@ export type SubjectState = {
119
120
  };
120
121
  /** In-memory index: subject → its {@link SubjectState}. */
121
122
  export type Cache = {
122
- readonly bySubject: StringMap<string, SubjectState>;
123
+ readonly bySubject: StringMap<SubjectState>;
123
124
  };
124
125
  /** A cache with no known subjects yet — the starting point for {@link buildCache}. */
125
126
  export declare const emptyCache: Cache;
@@ -49,9 +49,7 @@ import { collectRead } from '../module.f.js';
49
49
  import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.js';
50
50
  import { fromVec } from '../../text/utf8/module.f.js';
51
51
  import { tryUtf8 } from '../../text/module.f.js';
52
- import { decodeText, dialect, checkReferences, isHash } from '../../media/revision/module.f.js';
53
- import { stringify } from '../../media/json/module.f.js';
54
- import { identity } from '../../types/function/module.f.js';
52
+ import { decodeText, encodeText, dialect, checkReferences, isHash } from '../../media/revision/module.f.js';
55
53
  import { ok, error } from '../../types/result/module.f.js';
56
54
  import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.js';
57
55
  import { at, definedEntries } from '../../types/object/module.f.js';
@@ -59,8 +57,6 @@ import { unwrap } from '../../types/nullable/module.f.js';
59
57
  import { isNotFound } from '../../effects/node/module.f.js';
60
58
  /** A cache with no known subjects yet — the starting point for {@link buildCache}. */
61
59
  export const emptyCache = { bySubject: {} };
62
- /** Canonical JSON encoder for a `Revision` — key order carries no meaning for detection. */
63
- const toJson = stringify(identity);
64
60
  const emptySubjectState = { hashes: [], parents: [], archived: [] };
65
61
  /** Adds every item of `items` to `set` that isn't already there, preserving `set`'s existing order. */
66
62
  const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc : [...acc, h], set);
@@ -77,6 +73,8 @@ const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc
77
73
  * (`fjs/media/revision` `checkReferences`), so decoding here cannot fail.
78
74
  */
79
75
  const canonicalHash = (h) => vecToCBase32(unwrap(cBase32ToVec(h)));
76
+ /** Canonicalizes every direct hash in a structurally validated flat lock map. */
77
+ const canonicalLock = (lock) => Object.fromEntries(definedEntries(lock).map(([subject, hash]) => [subject, canonicalHash(hash)]));
80
78
  /** A subject's current heads: revision hashes seen that no other revision of the same subject names as a parent. */
81
79
  const headsOf = (state) => state.hashes.filter(h => !state.parents.includes(h));
82
80
  /**
@@ -325,6 +323,7 @@ export const addRevision = (cas) => (cacheKey) => (input) => eff(resolveParents(
325
323
  snapshot: snapshotResult[1],
326
324
  generation: computeGeneration(parentsResult[1]),
327
325
  archived: input.archived,
326
+ lock: input.lock,
328
327
  };
329
328
  const referencesResult = checkReferences(revision);
330
329
  if (referencesResult[0] === 'error') {
@@ -341,8 +340,9 @@ export const addRevision = (cas) => (cacheKey) => (input) => eff(resolveParents(
341
340
  ...revision,
342
341
  parents: revision.parents.map(canonicalHash),
343
342
  snapshot: canonicalHash(revision.snapshot),
343
+ lock: revision.lock === undefined ? undefined : canonicalLock(revision.lock),
344
344
  };
345
- const bytes = tryUtf8(toJson(canonicalRevision));
345
+ const bytes = tryUtf8(encodeText(canonicalRevision));
346
346
  if (bytes === null) {
347
347
  return pure(error('revision too large to encode'));
348
348
  }
@@ -370,12 +370,13 @@ export const addRevision = (cas) => (cacheKey) => (input) => eff(resolveParents(
370
370
  * inside `canonicalHash` is safe. Field order follows the stored blob's
371
371
  * (minus `dialect`), which is what a JSON encoding of the result shows.
372
372
  */
373
- const toRevisionData = ({ subject, parents, snapshot, generation, archived }) => ({
373
+ const toRevisionData = ({ subject, parents, snapshot, generation, archived, lock }) => ({
374
374
  subject,
375
375
  parents: parents.map(canonicalHash),
376
376
  snapshot: canonicalHash(snapshot),
377
377
  generation,
378
378
  archived,
379
+ lock: lock === undefined ? undefined : canonicalLock(lock),
379
380
  });
380
381
  /**
381
382
  * Second stage of {@link readRevision}: interprets an already-performed read
@@ -35,6 +35,10 @@ export declare const proof: {
35
35
  revisionNonRevisionBlobIsError: () => void;
36
36
  revisionCanonicalizesReferenceSpellings: () => void;
37
37
  revisionRoundTripsThroughAdd: () => void;
38
+ revisionLockRoundTripsAndCanonicalizes: () => void;
39
+ revisionAbsentAndEmptyLocksRemainDistinct: () => void;
40
+ revisionInvalidLockValueIsRejected: () => void;
41
+ equivalentLockOrdersReuseOneCasAddress: () => void;
38
42
  syncRevisionFoldsValidRevisionIntoCache: () => void;
39
43
  syncRevisionIgnoresNonRevisionContent: () => void;
40
44
  evoHeadUnknownSubjectIsEmpty: () => void;
@@ -596,6 +596,60 @@ export const proof = {
596
596
  assert(readded[0] === 'ok', ['expected re-add ok', readded]);
597
597
  assertEq(readded[1], child[1]);
598
598
  },
599
+ revisionLockRoundTripsAndCanonicalizes: () => {
600
+ const c = fileCas(sha256)(home);
601
+ const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
602
+ const e = evo(c)(cacheKey);
603
+ const canonical = vecToCBase32(vec8(0xffn));
604
+ const alias = canonical.toUpperCase();
605
+ const [state1, added] = virtual(state0)(e.add({
606
+ parents: [], subject: 'doc', snapshot: canonical,
607
+ lock: { dependency: alias },
608
+ }));
609
+ assert(added[0] === 'ok', ['expected add ok', added]);
610
+ const [, result] = virtual(state1)(e.revision(added[1]));
611
+ assert(result[0] === 'ok', ['expected revision ok', result]);
612
+ assertEq(result[1].lock?.dependency, canonical);
613
+ },
614
+ revisionAbsentAndEmptyLocksRemainDistinct: () => {
615
+ const c = fileCas(sha256)(home);
616
+ const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
617
+ const e = evo(c)(cacheKey);
618
+ const snapshot = vecToCBase32(vec8(0x42n));
619
+ const [state1, absent] = virtual(state0)(e.add({ parents: [], subject: 'absent', snapshot }));
620
+ assert(absent[0] === 'ok', ['expected add ok', absent]);
621
+ const [state2, empty] = virtual(state1)(e.add({ parents: [], subject: 'empty', snapshot, lock: {} }));
622
+ assert(empty[0] === 'ok', ['expected add ok', empty]);
623
+ const [state3, absentRead] = virtual(state2)(e.revision(absent[1]));
624
+ assert(absentRead[0] === 'ok', ['expected revision ok', absentRead]);
625
+ const [, emptyRead] = virtual(state3)(e.revision(empty[1]));
626
+ assert(emptyRead[0] === 'ok', ['expected revision ok', emptyRead]);
627
+ assertEq(absentRead[1].lock, undefined);
628
+ assertEq(Object.keys(emptyRead[1].lock ?? {}).length, 0);
629
+ },
630
+ revisionInvalidLockValueIsRejected: () => {
631
+ const c = fileCas(sha256)(home);
632
+ const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
633
+ const e = evo(c)(cacheKey);
634
+ const [, result] = virtual(state0)(e.add({
635
+ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)),
636
+ lock: { dependency: 'not a hash!' },
637
+ }));
638
+ assertEq(result[0], 'error');
639
+ },
640
+ equivalentLockOrdersReuseOneCasAddress: () => {
641
+ const c = fileCas(sha256)(home);
642
+ const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
643
+ const e = evo(c)(cacheKey);
644
+ const a = vecToCBase32(vec8(0x1an));
645
+ const b = vecToCBase32(vec8(0x2bn));
646
+ const snapshot = vecToCBase32(vec8(0x3cn));
647
+ const [state1, first] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot, lock: { '2': b, '10': a } }));
648
+ assert(first[0] === 'ok', ['expected add ok', first]);
649
+ const [, second] = virtual(state1)(e.add({ lock: { '10': a, '2': b }, snapshot, subject: 'doc', parents: [] }));
650
+ assert(second[0] === 'ok', ['expected add ok', second]);
651
+ assertEq(second[1], first[1]);
652
+ },
599
653
  // A raw CAS write (e.g. `cas_add`) of valid revision content is folded
600
654
  // into the cache exactly as `addRevision` would, without going through
601
655
  // `evo.add` — this is what keeps `cas_add` and `evo_add` writes to the
@@ -10,7 +10,7 @@ export type Module = {
10
10
  readonly proof?: unknown;
11
11
  readonly [k: string]: unknown;
12
12
  };
13
- export type ModuleMap = StringMap<string, Module>;
13
+ export type ModuleMap = StringMap<Module>;
14
14
  /**
15
15
  * Returns `true` if the file should be loaded for proof discovery.
16
16
  *
@@ -109,7 +109,7 @@ export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
109
109
  export declare const stat: Func<Stat>;
110
110
  export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
111
111
  export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
112
- export type Headers = StringMap<string, string>;
112
+ export type Headers = StringMap<string>;
113
113
  export type IncomingMessage = {
114
114
  readonly method: string;
115
115
  readonly url: string;
@@ -129,7 +129,7 @@ export declare const listen: Func<Listen>;
129
129
  export type Http = CreateServer | Listen;
130
130
  export type Forever = ['forever', () => never];
131
131
  export declare const forever: Func<Forever>;
132
- export type Module = StringMap<string, unknown>;
132
+ export type Module = StringMap<unknown>;
133
133
  export type Import = ['import', (path: string) => IoResult<Module>];
134
134
  export declare const import_: Func<Import>;
135
135
  /** Named output streams accepted by the `Write` effect. */
@@ -9,7 +9,7 @@ import { type ByteSet } from '../types/byte_set/module.f.ts';
9
9
  import { type RangeMapArray } from '../types/range_map/module.f.ts';
10
10
  type Rule = readonly [string, ByteSet, string];
11
11
  export type Grammar = List<Rule>;
12
- type Dfa = StringMap<string, RangeMapArray<string>>;
12
+ type Dfa = StringMap<RangeMapArray<string>>;
13
13
  export declare const toRange: (s: string) => ByteSet;
14
14
  export declare const toUnion: (s: string) => ByteSet;
15
15
  export declare const dfa: (grammar: Grammar) => Dfa;
@@ -17,7 +17,7 @@ type Element2 = readonly [Tag, Attributes, ...Node[]];
17
17
  * - `[tag, attributes, ...children]` for elements with attributes.
18
18
  */
19
19
  export type Element = Element1 | Element2;
20
- type Attributes = StringMap<string, string>;
20
+ type Attributes = StringMap<string>;
21
21
  export type Node = Element | string;
22
22
  /**
23
23
  * Converts a FunctionalScript element into a list of HTML string chunks.
@@ -20,6 +20,10 @@ export declare const mediaType: "application/vnd.fjs.revision+json";
20
20
  * not by this schema on its own.
21
21
  */
22
22
  export declare const hash: import("../../types/rtti/module.f.ts").String;
23
+ /** Structural schema for a Stage 1 flat lock map. */
24
+ declare const lock: import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>;
25
+ /** A flat set of subject-to-snapshot bindings supplied to dependency resolvers. */
26
+ export type LockMap = Ts<typeof lock>;
23
27
  /**
24
28
  * rtti schema for a `revision` BLOB. See the README for the full semantics of
25
29
  * each field; `dialect` is the type discriminant, matched here as an exact
@@ -32,9 +36,12 @@ export declare const revisionSchema: {
32
36
  readonly snapshot: import("../../types/rtti/module.f.ts").String;
33
37
  readonly generation: import("../../types/rtti/module.f.ts").Number;
34
38
  readonly archived: import("../../types/rtti/module.f.ts").Or<readonly [true, undefined]>;
39
+ readonly lock: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>, undefined]>;
35
40
  };
36
41
  /** The TypeScript type derived from {@link revisionSchema} — the single source of truth. */
37
42
  export type Revision = Ts<typeof revisionSchema>;
43
+ /** Serializes a revision canonically, recursively sorting every object's property names. */
44
+ export declare const encodeText: (revision: Revision) => string;
38
45
  /** True when `s` decodes as a cbase32 CAS hash (rejects `https://` and any other non-cbase32 string). */
39
46
  export declare const isHash: (s: string) => boolean;
40
47
  /** Either a structural validation error or a semantic (hash / generation) error message. */
@@ -84,3 +91,4 @@ export declare const decodeText: (text: string) => Result<Revision, RevisionErro
84
91
  * `snapshot` is not a cbase32 hash is not one.
85
92
  */
86
93
  export declare const revisionDialect: DialectEntry;
94
+ export {};
@@ -14,12 +14,14 @@
14
14
  *
15
15
  * @module
16
16
  */
17
- import { array, number, option, string } from '../../types/rtti/module.f.js';
17
+ import { array, number, option, record, string } from '../../types/rtti/module.f.js';
18
18
  import { validate as rttiValidate } from '../../types/rtti/validate/module.f.js';
19
19
  import { parse as parseJson } from '../json/module.f.js';
20
20
  import { cBase32ToVec } from '../../basen/cbase32/module.f.js';
21
21
  import { error, ok } from '../../types/result/module.f.js';
22
22
  import { dialectEntry } from '../module.f.js';
23
+ import { definedEntries, sort } from '../../types/object/module.f.js';
24
+ import { stringify } from '../json/module.f.js';
23
25
  /**
24
26
  * Format tag: names the dialect of this BLOB. The media type it is served
25
27
  * with is derived mechanically: `application/` + `dialect` + `+json`.
@@ -37,6 +39,8 @@ export const mediaType = `application/${dialect}+json`;
37
39
  * not by this schema on its own.
38
40
  */
39
41
  export const hash = string;
42
+ /** Structural schema for a Stage 1 flat lock map. */
43
+ const lock = record(string);
40
44
  /**
41
45
  * rtti schema for a `revision` BLOB. See the README for the full semantics of
42
46
  * each field; `dialect` is the type discriminant, matched here as an exact
@@ -49,7 +53,10 @@ export const revisionSchema = {
49
53
  snapshot: hash,
50
54
  generation: number,
51
55
  archived: option(true),
56
+ lock: option(lock),
52
57
  };
58
+ /** Serializes a revision canonically, recursively sorting every object's property names. */
59
+ export const encodeText = stringify(sort);
53
60
  /** Structural-only validator: checks the shape, not the hash / generation semantics. */
54
61
  const validateShape = rttiValidate(revisionSchema);
55
62
  /** True when `s` decodes as a cbase32 CAS hash (rejects `https://` and any other non-cbase32 string). */
@@ -89,6 +96,11 @@ export const checkReferences = (r) => {
89
96
  if (!isHash(r.snapshot)) {
90
97
  return error(`snapshot is not a valid hash: ${r.snapshot}`);
91
98
  }
99
+ for (const [subject, snapshot] of definedEntries(r.lock ?? {})) {
100
+ if (!isHash(snapshot)) {
101
+ return error(`lock value for ${subject} is not a valid hash: ${snapshot}`);
102
+ }
103
+ }
92
104
  if (!Number.isSafeInteger(r.generation) || r.generation < 0) {
93
105
  return error(`generation must be a non-negative safe integer: ${r.generation}`);
94
106
  }
@@ -18,6 +18,12 @@ export declare const proof: {
18
18
  positiveGenerationAccepted: () => void;
19
19
  httpsRejectedInParents: () => void;
20
20
  archivedAccepted: () => void;
21
+ lockAbsentAccepted: () => void;
22
+ emptyLockAccepted: () => void;
23
+ validLockAccepted: () => void;
24
+ malformedLockRejected: () => void;
25
+ invalidHashLockRejected: () => void;
26
+ aliasHashLockAccepted: () => void;
21
27
  wrongDialectRejected: () => void;
22
28
  missingSubjectRejected: () => void;
23
29
  extraFieldsAccepted: () => void;
@@ -28,4 +34,9 @@ export declare const proof: {
28
34
  malformedJsonRejected: () => void;
29
35
  ordinaryJsonRejected: () => void;
30
36
  };
37
+ encodeText: {
38
+ recursivelySortsObjectsLexicographically: () => void;
39
+ preservesArrayOrder: () => void;
40
+ parsedEquivalentSourcesConverge: () => void;
41
+ };
31
42
  };
@@ -1,9 +1,12 @@
1
1
  import { assert, assertEq } from '../../asserts/module.f.js';
2
- import { dialect, mediaType, isHash, validate, decodeText } from './module.f.js';
2
+ import { dialect, mediaType, isHash, validate, decodeText, encodeText } from './module.f.js';
3
3
  // Valid cbase32 hashes (round-tripped in fjs/basen/cbase32/proof.f.ts): single
4
4
  // cbase32 symbols, cheap to write inline here.
5
5
  const h1 = '8';
6
6
  const h2 = 'r';
7
+ // `I` is accepted as an alias spelling of canonical cBase32 `1`.
8
+ const alias = 'I';
9
+ const _lockMapAllowsMissingSubjects = {};
7
10
  // A shape-valid revision: every required field present (`snapshot` and
8
11
  // `generation` included), with `extra` overriding or adding fields per test.
9
12
  const revisionOf = (extra) => ({
@@ -95,6 +98,32 @@ export const proof = {
95
98
  const [t] = validate(revisionOf({ archived: true }));
96
99
  assertEq(t, 'ok');
97
100
  },
101
+ lockAbsentAccepted: () => {
102
+ const r = validate(revisionOf({}));
103
+ assert(r[0] === 'ok', ['expected ok', r]);
104
+ assertEq(r[1].lock, undefined);
105
+ },
106
+ emptyLockAccepted: () => {
107
+ const r = validate(revisionOf({ lock: {} }));
108
+ assert(r[0] === 'ok', ['expected ok', r]);
109
+ assertEq(Object.keys(r[1].lock ?? {}).length, 0);
110
+ },
111
+ validLockAccepted: () => {
112
+ const [t] = validate(revisionOf({ lock: { dependency: h1 } }));
113
+ assertEq(t, 'ok');
114
+ },
115
+ malformedLockRejected: () => {
116
+ const [t] = validate(revisionOf({ lock: { dependency: 1 } }));
117
+ assertEq(t, 'error');
118
+ },
119
+ invalidHashLockRejected: () => {
120
+ const [t] = validate(revisionOf({ lock: { dependency: 'https://example.com/x' } }));
121
+ assertEq(t, 'error');
122
+ },
123
+ aliasHashLockAccepted: () => {
124
+ const [t] = validate(revisionOf({ lock: { dependency: alias } }));
125
+ assertEq(t, 'ok');
126
+ },
98
127
  // Wrong dialect tag: structural validation rejects it outright.
99
128
  wrongDialectRejected: () => {
100
129
  const [t] = validate({ dialect: 'vnd.fjs.other', subject: h1, parents: [], snapshot: h2, generation: 0 });
@@ -137,4 +166,25 @@ export const proof = {
137
166
  assertEq(t, 'error');
138
167
  },
139
168
  },
169
+ encodeText: {
170
+ recursivelySortsObjectsLexicographically: () => {
171
+ const revision = revisionOf({ lock: { '2': h2, '10': h1 } });
172
+ const decoded = validate(revision);
173
+ assert(decoded[0] === 'ok', ['expected ok', decoded]);
174
+ assertEq(encodeText(decoded[1]), `{"dialect":"${dialect}","generation":0,"lock":{"10":"${h1}","2":"${h2}"},"parents":[],"snapshot":"${h2}","subject":"${h1}"}`);
175
+ },
176
+ preservesArrayOrder: () => {
177
+ const decoded = validate(revisionOf({ parents: [h2, h1] }));
178
+ assert(decoded[0] === 'ok', ['expected ok', decoded]);
179
+ assert(encodeText(decoded[1]).includes(`"parents":["${h2}","${h1}"]`));
180
+ },
181
+ parsedEquivalentSourcesConverge: () => {
182
+ const a = decodeText(` { "subject":"${h1}", "snapshot":"${h2}", "parents":[], "generation":0, "dialect":"${dialect}" } `);
183
+ const b = decodeText(`{"dialect":"${dialect}","generation":0,"parents":[],"snapshot":"${h2}","subject":"${h1}"}`);
184
+ assert(a[0] === 'ok', ['expected ok', a]);
185
+ assert(b[0] === 'ok', ['expected ok', b]);
186
+ assertEq(encodeText(a[1]), encodeText(b[1]));
187
+ },
188
+ },
140
189
  };
190
+ void _lockMapAllowsMissingSubjects;
@@ -1,12 +1,35 @@
1
1
  import { type List } from '../list/module.f.ts';
2
2
  import { type Nullable } from '../nullable/module.f.ts';
3
3
  import { type OrderedMap } from '../ordered_map/module.f.ts';
4
- export type Map<T> = StringMap<string, T>;
4
+ /** A record over the keys of `K`, each value possibly missing at runtime. */
5
+ export type OptionalMap<K extends string, T> = {
6
+ readonly [k in K]?: T;
7
+ };
8
+ /**
9
+ * A record over the keys of `K`, each value required.
10
+ *
11
+ * `K` has to be a finite union of string literals: `RequiredMap<string, T>` is
12
+ * `never`, because no object can carry every string as a key. Use `StringMap<T>`
13
+ * for an open key set — its values are optional, which is what such a key set
14
+ * means at runtime.
15
+ *
16
+ * There is no known way to ask TypeScript whether `K` is finite, so the guard
17
+ * approximates it with `string extends K`, which holds exactly when `K` is
18
+ * `string`. Other infinite key sets are not caught: a template literal such as
19
+ * `x-${string}` yields a template index signature instead of `never`, and its
20
+ * reads are typed `T` while the runtime value is `undefined`. Keep `K` a union
21
+ * of string literals.
22
+ */
23
+ export type RequiredMap<K extends string, T> = string extends K ? never : {
24
+ readonly [k in K]: T;
25
+ };
26
+ /** A record with an open key set. Every value can be missing at runtime. */
27
+ export type StringMap<T> = OptionalMap<string, T>;
5
28
  export type Entry<T> = readonly [string, T];
6
- export declare const at: (name: string) => <T>(object: Map<T>) => Nullable<Exclude<T, undefined>>;
29
+ export declare const at: (name: string) => <T>(object: StringMap<T>) => Nullable<Exclude<T, undefined>>;
7
30
  export declare const sort: <T>(e: List<Entry<T>>) => List<Entry<T>>;
8
- export declare const fromEntries: <T>(e: List<Entry<T>>) => Map<T>;
9
- export declare const fromMap: <T>(m: OrderedMap<T>) => Map<T>;
31
+ export declare const fromEntries: <T>(e: List<Entry<T>>) => StringMap<T>;
32
+ export declare const fromMap: <T>(m: OrderedMap<T>) => StringMap<T>;
10
33
  /**
11
34
  * A set of objects with a single key.
12
35
  *
@@ -14,7 +37,7 @@ export declare const fromMap: <T>(m: OrderedMap<T>) => Map<T>;
14
37
  * https://stackoverflow.com/questions/57571664/typescript-type-for-an-object-with-only-one-key-no-union-type-allowed-as-a-key
15
38
  */
16
39
  export type OneKey<K extends string, V> = {
17
- [P in K]: (StringMap<P, V> & Partial<StringMap<Exclude<K, P>, never>>) extends infer O ? {
40
+ [P in K]: (RequiredMap<P, V> & OptionalMap<Exclude<K, P>, never>) extends infer O ? {
18
41
  [Q in keyof O]: O[Q];
19
42
  } : never;
20
43
  }[K];
@@ -24,13 +47,8 @@ export type OneKey<K extends string, V> = {
24
47
  export type NotUnion<T, U = T> = T extends unknown ? [
25
48
  U
26
49
  ] extends [T] ? T : never : never;
27
- export type SingleProperty<T extends StringMap<string, never>> = keyof T extends NotUnion<keyof T> ? T : never;
50
+ export type SingleProperty<T extends StringMap<never>> = keyof T extends NotUnion<keyof T> ? T : never;
28
51
  export declare const isObject: (value: unknown) => value is { readonly [k in string]: unknown; };
29
52
  /** Returns only the defined (non-undefined) values of a partial record. */
30
- export declare const definedValues: <T>(map: StringMap<string, Exclude<T, undefined>>) => readonly Exclude<T, undefined>[];
31
- export type StringMap<K extends string, T> = string extends K ? {
32
- readonly [k in string]?: T;
33
- } : {
34
- readonly [k in K]: T;
35
- };
36
- export declare const definedEntries: <T>(cmd: StringMap<string, Exclude<T, undefined>>) => readonly (readonly [string, Exclude<T, undefined>])[];
53
+ export declare const definedValues: <T>(map: StringMap<Exclude<T, undefined>>) => readonly Exclude<T, undefined>[];
54
+ export declare const definedEntries: <T>(cmd: StringMap<Exclude<T, undefined>>) => readonly (readonly [string, Exclude<T, undefined>])[];
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Plain-object helpers and types: `Map<T>`/`Entry<T>` shapes, safe property
3
- * lookup via `at`, conversions between entries and `OrderedMap`, and the
4
- * `OneKey`/`SingleProperty`/`NotUnion` utility types.
2
+ * Plain-object helpers and types: the `OptionalMap`/`RequiredMap`/`StringMap`
3
+ * record shapes and `Entry<T>`, safe property lookup via `at`, conversions
4
+ * between entries and `OrderedMap`, and the `OneKey`/`SingleProperty`/`NotUnion`
5
+ * utility types.
5
6
  *
6
7
  * @module
7
8
  */
@@ -1,18 +1,4 @@
1
- import type { StringMap } from './module.f.ts';
2
- type E<A, B> = A extends B ? B extends A ? true : false : false;
3
- type _InfiniteIsOptional = E<StringMap<string, bigint>, {
4
- readonly [k in string]?: bigint;
5
- }>;
6
- type _FiniteIsRequired = E<StringMap<'a' | 'b', bigint>, {
7
- readonly a: bigint;
8
- readonly b: bigint;
9
- }>;
10
1
  export declare const proof: {
11
- stringMap: {
12
- infiniteIsOptional: _InfiniteIsOptional;
13
- finiteIsRequired: _FiniteIsRequired;
14
- };
15
2
  ctor: () => void;
16
3
  property: () => void;
17
4
  };
18
- export {};
@@ -1,10 +1,6 @@
1
1
  import { at } from './module.f.js';
2
2
  import { assertEq } from '../../asserts/module.f.js';
3
3
  export const proof = {
4
- stringMap: {
5
- infiniteIsOptional: true,
6
- finiteIsRequired: true,
7
- },
8
4
  ctor: () => {
9
5
  const a = {};
10
6
  const value = at('constructor')(a);
@@ -74,11 +74,11 @@ export type Visitor<R> = {
74
74
  /** Type guard narrowing `Unknown` to a specific container type `C`. */
75
75
  export type IsContainer<C extends Unknown> = (value: Unknown) => value is C;
76
76
  /** Maps a `Tag1` to its runtime container type. */
77
- export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : StringMap<string, Unknown>;
77
+ export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : StringMap<Unknown>;
78
78
  /** `IsContainer` guard for arrays, shared by `validate` and `parse`. */
79
79
  export declare const isArray: IsContainer<ReadonlyArray<Unknown>>;
80
80
  /** `IsContainer` guard for records/structs, shared by `validate` and `parse`. */
81
- export declare const isObject: IsContainer<StringMap<string, Unknown>>;
81
+ export declare const isObject: IsContainer<StringMap<Unknown>>;
82
82
  /**
83
83
  * Runs `item` over each `[key, value]` entry, bailing out with the first
84
84
  * error, path-prefixed with that entry's key. On success, folds each item's
@@ -6,7 +6,7 @@ export type Const = null | boolean | number | string | undefined | bigint | {
6
6
  } | readonly Type[];
7
7
  export type ConstObject = Struct | Tuple;
8
8
  /** A struct schema: plain object whose values are nested `Type`s. */
9
- export type Struct = StringMap<string, Type>;
9
+ export type Struct = StringMap<Type>;
10
10
  /** A tuple schema: readonly array whose elements are nested `Type`s. */
11
11
  export type Tuple = readonly Type[];
12
12
  declare const primitive0List: readonly ['bigint', 'boolean', 'number', 'string'];
@@ -31,7 +31,7 @@ export type Object = {
31
31
  /** Maps a `Tag0` to its TypeScript type. */
32
32
  export type Info0Ts<T extends Tag0> = T extends 'boolean' ? boolean : T extends 'number' ? number : T extends 'string' ? string : T extends 'bigint' ? bigint : T extends 'unknown' ? Unknown : never;
33
33
  /** Maps a `Const` schema to its TypeScript type. */
34
- export type ConstTs<T> = T extends readonly Type[] ? TupleTs<T> : T extends StringMap<string, Type> ? StructTs<T> : T;
34
+ export type ConstTs<T> = T extends readonly Type[] ? TupleTs<T> : T extends StringMap<Type> ? StructTs<T> : T;
35
35
  /** Maps a `Tag1` and inner type to its TypeScript type. */
36
36
  export type Info1Ts<K extends Tag1, T extends Type> = K extends 'array' ? ArrayTs<T> : K extends 'record' ? RecordTs<T> : never;
37
37
  /** Maps an array schema `T` to `readonly Ts<T>[]`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "functionalscript",
3
- "version": "0.42.0",
3
+ "version": "0.43.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "**/*.js",