@dtmd/temper 0.0.17 → 0.0.19

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The member-address grammar — one home for every spelling a member's identity takes and
3
+ * for the reader those writers round-trip against. Three spellings, each the one before
4
+ * it plus a segment:
5
+ *
6
+ * - `<kind>:<name>` — a **host address**, a top-level member's own identity.
7
+ * - `<host-address>/<kind>/<key>` — a **nested-member address**, the identity a member
8
+ * embedded in a host carries.
9
+ * - `<member>/<kind>/<key>/<leaf>` — a **leaf address**, one authored string beneath a
10
+ * nested member. A grain of its own, and no member address at all.
11
+ *
12
+ * Because the third is the second plus a `/<leaf>` tail, both are read off one
13
+ * segmentation ({@link segment}) rather than two independently-shaped parses that could
14
+ * come to disagree about where the member ends. The engine's `src/member_address.rs` is
15
+ * the other end of the same grammar: a spelling moves on both sides at once or the seam
16
+ * breaks.
17
+ */
18
+ /** The two halves a host address names — the reader's answer, and never re-split by hand. */
19
+ export interface HostAddress {
20
+ /** The member's kind. */
21
+ readonly kind: string;
22
+ /** The member's name among its kind. */
23
+ readonly name: string;
24
+ }
25
+ /** One parsed nested-member address. */
26
+ export interface NestedAddress {
27
+ /** The host member's own `<kind>:<name>` address — the segment before the first `/`. */
28
+ readonly host: string;
29
+ /** The nested member's kind. */
30
+ readonly kind: string;
31
+ /** The nested member's key among its host's members of that kind. */
32
+ readonly key: string;
33
+ }
34
+ /** One parsed leaf address — a nested-member address plus its `/<leaf>` tail. */
35
+ export interface LeafAddress {
36
+ /**
37
+ * The member the leaf lives under, verbatim as its author spelled it: the canonical
38
+ * `<kind>:<name>` host address, or the bare member id this SDK's own leaf writer and
39
+ * the committed lock mention targets spell. Which of the two a head is, is resolution's
40
+ * question, not the grammar's.
41
+ */
42
+ readonly member: string;
43
+ /** The nested member's kind. */
44
+ readonly kind: string;
45
+ /** The nested member's key among its host's members of that kind. */
46
+ readonly key: string;
47
+ /** The leaf's path within the nested member — the whole remainder after the third slash. */
48
+ readonly childPath: string;
49
+ }
50
+ /** Spell a top-level member's own `<kind>:<name>` address. */
51
+ export declare function hostAddress(kind: string, name: string): string;
52
+ /** Spell a nested member's address from its host's address, its own kind and its key. */
53
+ export declare function nestedAddress(host: string, kind: string, key: string): string;
54
+ /**
55
+ * Spell one leaf's address beneath a nested member. `member` is carried verbatim, so a
56
+ * writer holding the bare member id spells the short form the lock already commits
57
+ * ({@link LeafAddress.member}).
58
+ */
59
+ export declare function leafAddress(member: string, kind: string, key: string, childPath: string): string;
60
+ /**
61
+ * The member-table **lookup key** a bare `<kind>:<key>` reference spells — shaped like a
62
+ * host address and never one: a nested member's address composes through its host, and a
63
+ * corpus-unique key was rejected as the grammar because uniqueness is the resolver's bar,
64
+ * not the grammar's. The engine strips this prefix back off before matching, so the two
65
+ * ends agree and the apparent mismatch is not a defect to fix.
66
+ */
67
+ export declare function bareLookupKey(kind: string, key: string): string;
68
+ /**
69
+ * The key an authored edge address is looked up under: a one-element `to` set resolves a
70
+ * bare address within its one admissible kind, so an unqualified address is lifted into a
71
+ * {@link bareLookupKey}; an address already carrying a colon is the kind-qualified form
72
+ * the author wrote and stands as it is.
73
+ */
74
+ export declare function edgeLookupKey(address: string, to: readonly string[]): string;
75
+ /**
76
+ * The `(kind, name)` a host address spells, or `undefined` when it spells none — the
77
+ * reader half of {@link hostAddress}. Both halves are non-empty: an address names exactly
78
+ * one thing or it names nothing. Splits at the **first** colon, so a name carrying one
79
+ * stays whole.
80
+ *
81
+ * A `/` anywhere is this grammar's own segment separator ({@link segment} splits on it),
82
+ * so an address carrying one is a segmented spelling with its own reader
83
+ * ({@link parseNestedAddress}, {@link parseLeafAddress}) and no host address at all. The
84
+ * refusal lives here rather than at each caller, so a reader that asks "is this a host
85
+ * address?" never has to re-spell the separator to get the answer right.
86
+ */
87
+ export declare function parseHostAddress(address: string): HostAddress | undefined;
88
+ /**
89
+ * Parse a nested-member address, or `undefined` when `address` is not one: exactly three
90
+ * non-empty segments, the first of them a host address.
91
+ *
92
+ * A `/<leaf>` tail is ruled out here explicitly — a leaf is its own grain, and truncating
93
+ * one to the member that happens to contain it would answer a leaf reference with a member
94
+ * the author never named.
95
+ */
96
+ export declare function parseNestedAddress(address: string): NestedAddress | undefined;
97
+ /**
98
+ * Parse a leaf address, or `undefined` when `target` carries no tail or a segment-shaped
99
+ * hole. The head is carried on verbatim rather than split, because the bare member id is
100
+ * a live short form ({@link LeafAddress.member}).
101
+ */
102
+ export declare function parseLeafAddress(target: string): LeafAddress | undefined;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The member-address grammar — one home for every spelling a member's identity takes and
3
+ * for the reader those writers round-trip against. Three spellings, each the one before
4
+ * it plus a segment:
5
+ *
6
+ * - `<kind>:<name>` — a **host address**, a top-level member's own identity.
7
+ * - `<host-address>/<kind>/<key>` — a **nested-member address**, the identity a member
8
+ * embedded in a host carries.
9
+ * - `<member>/<kind>/<key>/<leaf>` — a **leaf address**, one authored string beneath a
10
+ * nested member. A grain of its own, and no member address at all.
11
+ *
12
+ * Because the third is the second plus a `/<leaf>` tail, both are read off one
13
+ * segmentation ({@link segment}) rather than two independently-shaped parses that could
14
+ * come to disagree about where the member ends. The engine's `src/member_address.rs` is
15
+ * the other end of the same grammar: a spelling moves on both sides at once or the seam
16
+ * breaks.
17
+ */
18
+ /** Spell a top-level member's own `<kind>:<name>` address. */
19
+ export function hostAddress(kind, name) {
20
+ return `${kind}:${name}`;
21
+ }
22
+ /** Spell a nested member's address from its host's address, its own kind and its key. */
23
+ export function nestedAddress(host, kind, key) {
24
+ return `${host}/${kind}/${key}`;
25
+ }
26
+ /**
27
+ * Spell one leaf's address beneath a nested member. `member` is carried verbatim, so a
28
+ * writer holding the bare member id spells the short form the lock already commits
29
+ * ({@link LeafAddress.member}).
30
+ */
31
+ export function leafAddress(member, kind, key, childPath) {
32
+ return `${nestedAddress(member, kind, key)}/${childPath}`;
33
+ }
34
+ /**
35
+ * The member-table **lookup key** a bare `<kind>:<key>` reference spells — shaped like a
36
+ * host address and never one: a nested member's address composes through its host, and a
37
+ * corpus-unique key was rejected as the grammar because uniqueness is the resolver's bar,
38
+ * not the grammar's. The engine strips this prefix back off before matching, so the two
39
+ * ends agree and the apparent mismatch is not a defect to fix.
40
+ */
41
+ export function bareLookupKey(kind, key) {
42
+ return `${kind}:${key}`;
43
+ }
44
+ /**
45
+ * The key an authored edge address is looked up under: a one-element `to` set resolves a
46
+ * bare address within its one admissible kind, so an unqualified address is lifted into a
47
+ * {@link bareLookupKey}; an address already carrying a colon is the kind-qualified form
48
+ * the author wrote and stands as it is.
49
+ */
50
+ export function edgeLookupKey(address, to) {
51
+ return to.length === 1 && !address.includes(":") ? bareLookupKey(to[0], address) : address;
52
+ }
53
+ /**
54
+ * The `(kind, name)` a host address spells, or `undefined` when it spells none — the
55
+ * reader half of {@link hostAddress}. Both halves are non-empty: an address names exactly
56
+ * one thing or it names nothing. Splits at the **first** colon, so a name carrying one
57
+ * stays whole.
58
+ *
59
+ * A `/` anywhere is this grammar's own segment separator ({@link segment} splits on it),
60
+ * so an address carrying one is a segmented spelling with its own reader
61
+ * ({@link parseNestedAddress}, {@link parseLeafAddress}) and no host address at all. The
62
+ * refusal lives here rather than at each caller, so a reader that asks "is this a host
63
+ * address?" never has to re-spell the separator to get the answer right.
64
+ */
65
+ export function parseHostAddress(address) {
66
+ if (address.includes("/"))
67
+ return undefined;
68
+ const colon = address.indexOf(":");
69
+ if (colon <= 0 || colon === address.length - 1)
70
+ return undefined;
71
+ return { kind: address.slice(0, colon), name: address.slice(colon + 1) };
72
+ }
73
+ /**
74
+ * Cut an address into its segments, or `undefined` when it carries fewer than three or a
75
+ * segment-shaped hole. The tail keeps its own dots and slashes, so it is the whole
76
+ * remainder after the third slash rather than a fourth segment among more.
77
+ */
78
+ function segment(address) {
79
+ const parts = address.split("/");
80
+ if (parts.length < 3)
81
+ return undefined;
82
+ const [host, kind, key] = parts;
83
+ const tail = parts.length > 3 ? parts.slice(3).join("/") : undefined;
84
+ if (host === "" || kind === "" || key === "" || tail === "")
85
+ return undefined;
86
+ return { host, kind, key, tail };
87
+ }
88
+ /**
89
+ * Parse a nested-member address, or `undefined` when `address` is not one: exactly three
90
+ * non-empty segments, the first of them a host address.
91
+ *
92
+ * A `/<leaf>` tail is ruled out here explicitly — a leaf is its own grain, and truncating
93
+ * one to the member that happens to contain it would answer a leaf reference with a member
94
+ * the author never named.
95
+ */
96
+ export function parseNestedAddress(address) {
97
+ const segments = segment(address);
98
+ if (segments === undefined || segments.tail !== undefined)
99
+ return undefined;
100
+ if (parseHostAddress(segments.host) === undefined)
101
+ return undefined;
102
+ return { host: segments.host, kind: segments.kind, key: segments.key };
103
+ }
104
+ /**
105
+ * Parse a leaf address, or `undefined` when `target` carries no tail or a segment-shaped
106
+ * hole. The head is carried on verbatim rather than split, because the bare member id is
107
+ * a live short form ({@link LeafAddress.member}).
108
+ */
109
+ export function parseLeafAddress(target) {
110
+ const segments = segment(target);
111
+ if (segments?.tail === undefined)
112
+ return undefined;
113
+ return { member: segments.host, kind: segments.kind, key: segments.key, childPath: segments.tail };
114
+ }
@@ -1,9 +1,13 @@
1
1
  /**
2
- * Prose — three constructors, one field type. A member's words are data the
2
+ * Prose — four constructors, one field type. A member's words are data the
3
3
  * member declares: `file()` for a document that keeps its medium, `` text`…` ``
4
- * for short inline prose, `blocks()` for a composed body that interleaves verbatim
5
- * prose spans with embedded-member values in authored order. Whatever
6
- * the constructor, the words land byte-identical to their authored text.
4
+ * for short inline prose, `span()` for prose the program computes, `blocks()` for a
5
+ * composed body that interleaves verbatim prose spans with embedded-member values in
6
+ * authored order. Whatever the constructor, the words land byte-identical to their
7
+ * authored text. The two inline constructors build the same {@link Text} and differ
8
+ * only in where the words come from: reach for `` text`…` `` when they are literal, for
9
+ * `span()` when they are a string the program derived — the tag's interpolations are
10
+ * references, never words, so a computed string has no slot to ride in.
7
11
  * Interpolations in `` text`…` `` are references, two intents apart: a **mention**
8
12
  * (a {@link Mentionable}) is a declared one-way edge that moves no content, and an
9
13
  * **include** (an {@link Include}) pulls the target file's bytes into the host's emitted
@@ -19,8 +23,8 @@ export interface Mentionable {
19
23
  }
20
24
  /**
21
25
  * Spell a top-level member as the {@link Mentionable} a mention carries: its
22
- * `kind:name` address, its bare name the display text — the convention every
23
- * corpus repeats to cite a member from prose, captured once here.
26
+ * {@link hostAddress}, its bare name the display text — the convention every corpus
27
+ * repeats to cite a member from prose.
24
28
  */
25
29
  export declare function mentionOf(member: Member): Mentionable;
26
30
  /** One authored interpolation: position in the template plus its target. */
@@ -57,11 +61,11 @@ export interface MentionScope {
57
61
  }
58
62
  /**
59
63
  * Whether a mention's unresolved address **defers to the gate** rather than refusing at
60
- * emit: a top-level `kind:name` address whose kind is one the program declares at a
61
- * discovery locus (an `at`-locus kind) may name a member discovered on disk, so `check`
62
- * owns the verdict. An embedded leaf address (a `<host>/<kind>/<key>` form, carrying a
63
- * `/`), a bare requirement name (no `:`), or a kind the program does not declare has no
64
- * discovery locus and stays a dangling refusal.
64
+ * emit: a host address ({@link parseHostAddress}) whose kind is one the program declares
65
+ * at a discovery locus (an `at`-locus kind) may name a member discovered on disk, so
66
+ * `check` owns the verdict. Anything the grammar's reader answers `undefined` for a
67
+ * segmented embedded address, a bare requirement name, a name-less `kind:` names no
68
+ * discoverable member, and stays a dangling refusal however deferrable its head reads.
65
69
  */
66
70
  export declare function defersToGate(address: string, deferrableKinds: ReadonlySet<string>): boolean;
67
71
  /**
@@ -116,7 +120,7 @@ export interface Blocks {
116
120
  readonly kind: "blocks";
117
121
  readonly values: readonly (Text | EmbeddedMemberValue)[];
118
122
  }
119
- /** A member's prose — one of the three constructors, one field type. */
123
+ /** A member's prose — one of the four constructors, one field type. */
120
124
  export type Prose = File | Text | Blocks;
121
125
  /**
122
126
  * Declare a document whose medium is preserved — read in whole at emit.
@@ -136,6 +140,19 @@ export declare function file(moduleUrl: string, path: string): File;
136
140
  * are the tool's alone, so a stray one is a loud authoring error, not a silent mis-split.
137
141
  */
138
142
  export declare function text(strings: TemplateStringsArray, ...targets: Reference[]): Text;
143
+ /**
144
+ * The computed-prose constructor: the same inline prose as `` text`…` ``, from a string
145
+ * the program built rather than a template the author typed — a rolled-up participants
146
+ * line, a table derived from the members. An author reaches for the tag whenever the
147
+ * words are literal, since only the tag can carry a {@link Reference}; a computed string
148
+ * cannot, because the tag's interpolations *are* the references. So a span is prose with
149
+ * zero references by construction, dedented by the same rule, and composes wherever a
150
+ * `` text`…` `` span does — a member's whole body or a {@link blocks} child.
151
+ *
152
+ * # Throws
153
+ * If `words` carries {@link MENTION_SLOT} or {@link INCLUDE_SLOT} ({@link refuseReservedMarkers}).
154
+ */
155
+ export declare function span(words: string): Text;
139
156
  /**
140
157
  * Declare an include target — its bytes are pulled into the host's projection at emit,
141
158
  * a dependency the lock fingerprints. `moduleUrl` is the declaring module's own
package/dist/src/prose.js CHANGED
@@ -1,35 +1,38 @@
1
1
  /**
2
- * Prose — three constructors, one field type. A member's words are data the
2
+ * Prose — four constructors, one field type. A member's words are data the
3
3
  * member declares: `file()` for a document that keeps its medium, `` text`…` ``
4
- * for short inline prose, `blocks()` for a composed body that interleaves verbatim
5
- * prose spans with embedded-member values in authored order. Whatever
6
- * the constructor, the words land byte-identical to their authored text.
4
+ * for short inline prose, `span()` for prose the program computes, `blocks()` for a
5
+ * composed body that interleaves verbatim prose spans with embedded-member values in
6
+ * authored order. Whatever the constructor, the words land byte-identical to their
7
+ * authored text. The two inline constructors build the same {@link Text} and differ
8
+ * only in where the words come from: reach for `` text`…` `` when they are literal, for
9
+ * `span()` when they are a string the program derived — the tag's interpolations are
10
+ * references, never words, so a computed string has no slot to ride in.
7
11
  * Interpolations in `` text`…` `` are references, two intents apart: a **mention**
8
12
  * (a {@link Mentionable}) is a declared one-way edge that moves no content, and an
9
13
  * **include** (an {@link Include}) pulls the target file's bytes into the host's emitted
10
14
  * projection. Both are authored per word and resolution-checked at emit, never mined.
11
15
  */
16
+ import { hostAddress, parseHostAddress } from "./member-address.js";
12
17
  /**
13
18
  * Spell a top-level member as the {@link Mentionable} a mention carries: its
14
- * `kind:name` address, its bare name the display text — the convention every
15
- * corpus repeats to cite a member from prose, captured once here.
19
+ * {@link hostAddress}, its bare name the display text — the convention every corpus
20
+ * repeats to cite a member from prose.
16
21
  */
17
22
  export function mentionOf(member) {
18
- return { address: `${member.kind}:${member.name}`, display: member.name };
23
+ return { address: hostAddress(member.kind, member.name), display: member.name };
19
24
  }
20
25
  /**
21
26
  * Whether a mention's unresolved address **defers to the gate** rather than refusing at
22
- * emit: a top-level `kind:name` address whose kind is one the program declares at a
23
- * discovery locus (an `at`-locus kind) may name a member discovered on disk, so `check`
24
- * owns the verdict. An embedded leaf address (a `<host>/<kind>/<key>` form, carrying a
25
- * `/`), a bare requirement name (no `:`), or a kind the program does not declare has no
26
- * discovery locus and stays a dangling refusal.
27
+ * emit: a host address ({@link parseHostAddress}) whose kind is one the program declares
28
+ * at a discovery locus (an `at`-locus kind) may name a member discovered on disk, so
29
+ * `check` owns the verdict. Anything the grammar's reader answers `undefined` for a
30
+ * segmented embedded address, a bare requirement name, a name-less `kind:` names no
31
+ * discoverable member, and stays a dangling refusal however deferrable its head reads.
27
32
  */
28
33
  export function defersToGate(address, deferrableKinds) {
29
- if (address.includes("/"))
30
- return false;
31
- const colon = address.indexOf(":");
32
- return colon > 0 && deferrableKinds.has(address.slice(0, colon));
34
+ const host = parseHostAddress(address);
35
+ return host !== undefined && deferrableKinds.has(host.kind);
33
36
  }
34
37
  /**
35
38
  * Refuse a mention whose address neither resolves against the scope's `mentionable`
@@ -66,6 +69,20 @@ const MENTION_SLOT = "\u0000";
66
69
  * carries one, so the split is unambiguous.
67
70
  */
68
71
  const INCLUDE_SLOT = "\u0001";
72
+ /**
73
+ * Refuse authored words carrying {@link MENTION_SLOT} or {@link INCLUDE_SLOT} — the
74
+ * markers are the tool's alone. A stray one would split into a chunk no reference
75
+ * claims, and {@link renderText} drops every chunk past the last mention, so the loud
76
+ * error is the only alternative to silently swallowing the words after it.
77
+ *
78
+ * # Throws
79
+ * If `words` carries either reserved marker.
80
+ */
81
+ function refuseReservedMarkers(words) {
82
+ if (words.includes(MENTION_SLOT) || words.includes(INCLUDE_SLOT)) {
83
+ throw new Error("authored prose contains a reserved reference marker (U+0000/U+0001); remove it.");
84
+ }
85
+ }
69
86
  /** Strip the common leading indentation a template literal picks up from its module. */
70
87
  function dedent(text) {
71
88
  const lines = text.split("\n");
@@ -103,9 +120,7 @@ export function text(strings, ...targets) {
103
120
  const mentions = [];
104
121
  const includes = [];
105
122
  strings.forEach((chunk, i) => {
106
- if (chunk.includes(MENTION_SLOT) || chunk.includes(INCLUDE_SLOT)) {
107
- throw new Error("authored prose contains a reserved reference marker (U+0000/U+0001); remove it.");
108
- }
123
+ refuseReservedMarkers(chunk);
109
124
  if (i === 0)
110
125
  return;
111
126
  const target = targets[i - 1];
@@ -120,6 +135,22 @@ export function text(strings, ...targets) {
120
135
  });
121
136
  return { kind: "text", template: dedent(template), mentions, includes };
122
137
  }
138
+ /**
139
+ * The computed-prose constructor: the same inline prose as `` text`…` ``, from a string
140
+ * the program built rather than a template the author typed — a rolled-up participants
141
+ * line, a table derived from the members. An author reaches for the tag whenever the
142
+ * words are literal, since only the tag can carry a {@link Reference}; a computed string
143
+ * cannot, because the tag's interpolations *are* the references. So a span is prose with
144
+ * zero references by construction, dedented by the same rule, and composes wherever a
145
+ * `` text`…` `` span does — a member's whole body or a {@link blocks} child.
146
+ *
147
+ * # Throws
148
+ * If `words` carries {@link MENTION_SLOT} or {@link INCLUDE_SLOT} ({@link refuseReservedMarkers}).
149
+ */
150
+ export function span(words) {
151
+ refuseReservedMarkers(words);
152
+ return { kind: "text", template: dedent(words), mentions: [], includes: [] };
153
+ }
123
154
  /**
124
155
  * Declare an include target — its bytes are pulled into the host's projection at emit,
125
156
  * a dependency the lock fingerprints. `moduleUrl` is the declaring module's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtmd/temper",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "description": "The temper authoring face — the six-noun model as typed modules: harness(), kind<T>(), clause values, needs, and file()/text/blocks(). Emit compiles to the declaration rows the engine reads, a byte-faithful projection, and the lock.",
5
5
  "license": "(MIT OR Apache-2.0)",
6
6
  "repository": {
@@ -52,7 +52,7 @@
52
52
  "temper": "bin/temper.js"
53
53
  },
54
54
  "optionalDependencies": {
55
- "@dtmd/temper-linux-x64": "0.0.17",
56
- "@dtmd/temper-win32-x64": "0.0.17"
55
+ "@dtmd/temper-linux-x64": "0.0.19",
56
+ "@dtmd/temper-win32-x64": "0.0.19"
57
57
  }
58
58
  }