@dtmd/temper 0.0.18 → 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.
package/dist/src/kind.js CHANGED
@@ -8,8 +8,43 @@
8
8
  * rows. Identity travels by import, never by string — a `kind` reference is the imported
9
9
  * value.
10
10
  */
11
+ /**
12
+ * Declare a file a member's claims rest on — the lock fingerprints it and no byte
13
+ * reaches the projection. The path resolves against the stating module, so the call is
14
+ * `input(import.meta.url, "./schema.json")`.
15
+ */
16
+ export function input(moduleUrl, path) {
17
+ return { kind: "input", path, moduleUrl };
18
+ }
11
19
  /** The framework keys of a member init — everything else is a typed field (flat). */
12
- const FRAMEWORK_KEYS = new Set(["name", "host", "prose", "satisfies", "requires", "needs"]);
20
+ const FRAMEWORK_KEYS = new Set(["name", "host", "prose", "satisfies", "requires", "needs", "inputs", "residue"]);
21
+ /**
22
+ * The **reserved leaf key** a nested member's own span lands under (0051) —
23
+ * `<host-address>/<kind>/<key>/prose`. On the read half a layout collection member's
24
+ * own paragraph, the text under its heading before the first child heading, lands here
25
+ * (`src/layout.rs`'s `OWN_SPAN_LEAF`); leaves stay one family, so leaf predicates, leaf
26
+ * addresses and `explain`'s narration range over one source.
27
+ *
28
+ * `prose` is already this word at member grain ({@link MemberInit.prose}), so it is a
29
+ * framework key for an init and a reserved name for a leaf — the same thing spelled once.
30
+ * The composed half authors no own span (its leaves are all author-named), so the
31
+ * reservation binds here as a refusal: a composed leaf of this name would address
32
+ * identically to a read member's own span and mean something else.
33
+ */
34
+ const RESERVED_LEAF = "prose";
35
+ /**
36
+ * The **residue key** — the channel a partially-governed external format's undocumented
37
+ * keys ride under. A kind whose schema is large and version-evolving types what it
38
+ * governs and leaves the remainder opaque and *named* rather than indicted; `residue` is
39
+ * that name, spliced flat into the projected fields by {@link orderedFields}.
40
+ *
41
+ * Like {@link RESERVED_LEAF}, this is a name the framework owns — a kind whose external
42
+ * format documents a top-level key of it cannot spell that key as an ordinary field. The
43
+ * reservation is kindless, but the channel is opt-in by **type**: only a surface `T`
44
+ * declaring `residue?` can spell the bag, so a closed-frontmatter kind (a skill, a rule,
45
+ * an agent) keeps refusing it by excess-property check like any other unknown key.
46
+ */
47
+ const RESIDUE_KEY = "residue";
13
48
  /**
14
49
  * Build the ordered projected fields for a member: nothing for a frontmatterless
15
50
  * body-bearing kind (memory declares no `format` and is not fields-only), else the
@@ -17,6 +52,9 @@ const FRAMEWORK_KEYS = new Set(["name", "host", "prose", "satisfies", "requires"
17
52
  * typed fields in the author's declared order. A fields-only registration kind (a
18
53
  * hook, an MCP server) carries its typed fields though it declares no `format` —
19
54
  * the fields are the whole member, folded into a manifest entry, never a header.
55
+ *
56
+ * A {@link RESIDUE_KEY} bag splices in last: what the program types, it orders; what it
57
+ * merely carries, it sorts.
20
58
  */
21
59
  function orderedFields(facts, init) {
22
60
  if (facts.format === undefined && facts.shape !== "fields")
@@ -27,7 +65,41 @@ function orderedFields(facts, init) {
27
65
  typed.push([key, value]);
28
66
  }
29
67
  const head = facts.identityField !== undefined ? [[facts.identityField, init.name]] : [];
30
- return [...head, ...typed];
68
+ const projected = [...head, ...typed];
69
+ return [...projected, ...residueFields(facts, init, projected)];
70
+ }
71
+ /**
72
+ * The init's residue bag as projected fields, key-sorted and disjoint from what the
73
+ * member already projects. The bag is a record, so it carries no authored order to
74
+ * preserve — sorting is what makes the projection a function of the keys alone, the same
75
+ * stability the harness-level residue rows already take (`declarations.ts`'s
76
+ * `settingsRows`).
77
+ *
78
+ * # Throws
79
+ * If a residue key is one `projected` already carries. The bag holds what the surface
80
+ * does not type, so a key in both is a mis-spelling, not an override — and a silent one
81
+ * downstream, since the engine's writer collects the field list into a map and the
82
+ * residue pair, spliced last, would take the typed value's place.
83
+ */
84
+ function residueFields(facts, init, projected) {
85
+ const residue = init[RESIDUE_KEY];
86
+ if (residue === undefined)
87
+ return [];
88
+ const already = new Set(projected.map(([key]) => key));
89
+ const fields = [];
90
+ // Default `sort()` is UTF-16 code-unit order — the same total order `compareStrings`
91
+ // gives every declaration family, reached without importing `declarations.ts` (which
92
+ // imports `builtins.ts`, which imports this module).
93
+ for (const key of Object.keys(residue).sort()) {
94
+ if (already.has(key)) {
95
+ throw new Error(`member \`${init.name}\` of kind \`${facts.name}\`: \`${RESIDUE_KEY}\` key \`${key}\` is ` +
96
+ `already a field this member projects — the bag carries the keys the kind's surface ` +
97
+ `does not type, so spell this one as the field it is ` +
98
+ `(specs/builtins.md, "The shipped kinds").`);
99
+ }
100
+ fields.push([key, residue[key]]);
101
+ }
102
+ return fields;
31
103
  }
32
104
  /**
33
105
  * The host a member init names, checked against its kind's locus: a nested-file child's
@@ -56,6 +128,11 @@ function hostOf(facts, init) {
56
128
  * into emit is the seven facts plus flat field data. `options.render`, when given,
57
129
  * rides alongside `facts`/`key` on the returned constructor — never on the member
58
130
  * it builds, since it is erased before a member reaches emit.
131
+ *
132
+ * The facts are typed over the same `T` the constructor is, which is what binds a
133
+ * declared {@link LeafSet} witness to `keyof T`: the leaf set is the kind's own surface,
134
+ * checked here at the keystroke, and its lowering to the row is the one place it is
135
+ * spelled again (`declarations.ts`).
59
136
  */
60
137
  export function kind(facts, options = {}) {
61
138
  const construct = (init) => ({
@@ -68,6 +145,7 @@ export function kind(facts, options = {}) {
68
145
  satisfies: init.satisfies ?? [],
69
146
  requires: init.requires ?? {},
70
147
  needs: init.needs ?? [],
148
+ inputs: init.inputs ?? [],
71
149
  });
72
150
  return Object.assign(construct, { facts, key: facts.name, render: options.render });
73
151
  }
@@ -147,14 +225,26 @@ export function relocate(base, delta) {
147
225
  // Two spellings, not one with an optional `locus`: `KindFacts` is a union discriminated
148
226
  // on the locus, so the moved case must carry the `at` locus as its own literal branch.
149
227
  const facts = locus === undefined ? relocated : { ...relocated, locus };
228
+ // The base's leaf-set witness rides through as data, and the assertion is that
229
+ // pass-through spelled: a relocation appends *edge fields* over the kind's own surface,
230
+ // never a leaf, so the set the base's own constructor already bound still names what a
231
+ // member carries. `T` here is the widened surface (the base's fields plus the added
232
+ // edges), so re-binding the witness to `keyof T` would demand the edge fields be leaves.
150
233
  return kind(facts, { render: base.render });
151
234
  }
152
235
  export function embeddedMemberValue(init) {
153
236
  const definition = typeof init.kind === "string" ? undefined : init.kind;
237
+ const kindKey = definition?.key ?? init.kind;
238
+ refuseReservedLeaf(kindKey, init.key, init.leaves);
239
+ for (const [collection, entries] of Object.entries(init.collections ?? {})) {
240
+ for (const entry of entries) {
241
+ refuseReservedLeaf(kindKey, `${init.key}.${collection}.${entry.key}`, entry.leaves);
242
+ }
243
+ }
154
244
  const render = definition?.render;
155
245
  const edgeFields = definition?.facts.edgeFields;
156
246
  return {
157
- kind: definition?.key ?? init.kind,
247
+ kind: kindKey,
158
248
  key: init.key,
159
249
  leaves: init.leaves,
160
250
  collections: init.collections ?? {},
@@ -162,3 +252,18 @@ export function embeddedMemberValue(init) {
162
252
  ...(edgeFields !== undefined ? { edgeFields } : {}),
163
253
  };
164
254
  }
255
+ /**
256
+ * Refuse a composed leaf named {@link RESERVED_LEAF} — the key a read member's own span
257
+ * owns, so a second meaning under it is a coincident leaf address, refused at compose
258
+ * rather than resolved by precedence (0051). Loud at the authoring seam, where the author
259
+ * can rename the field, not at emit over bytes already written.
260
+ *
261
+ * # Throws
262
+ * If `leaves` carries the reserved key.
263
+ */
264
+ function refuseReservedLeaf(kind, key, leaves) {
265
+ if (!Object.hasOwn(leaves, RESERVED_LEAF))
266
+ return;
267
+ throw new Error(`embedded member \`${kind}\` \`${key}\`: leaf \`${RESERVED_LEAF}\` is reserved for a ` +
268
+ `member's own span — rename the field, or author the words as the member's prose`);
269
+ }
@@ -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
@@ -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,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
@@ -65,6 +69,20 @@ const MENTION_SLOT = "\u0000";
65
69
  * carries one, so the split is unambiguous.
66
70
  */
67
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
+ }
68
86
  /** Strip the common leading indentation a template literal picks up from its module. */
69
87
  function dedent(text) {
70
88
  const lines = text.split("\n");
@@ -102,9 +120,7 @@ export function text(strings, ...targets) {
102
120
  const mentions = [];
103
121
  const includes = [];
104
122
  strings.forEach((chunk, i) => {
105
- if (chunk.includes(MENTION_SLOT) || chunk.includes(INCLUDE_SLOT)) {
106
- throw new Error("authored prose contains a reserved reference marker (U+0000/U+0001); remove it.");
107
- }
123
+ refuseReservedMarkers(chunk);
108
124
  if (i === 0)
109
125
  return;
110
126
  const target = targets[i - 1];
@@ -119,6 +135,22 @@ export function text(strings, ...targets) {
119
135
  });
120
136
  return { kind: "text", template: dedent(template), mentions, includes };
121
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
+ }
122
154
  /**
123
155
  * Declare an include target — its bytes are pulled into the host's projection at emit,
124
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.18",
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.18",
56
- "@dtmd/temper-win32-x64": "0.0.18"
55
+ "@dtmd/temper-linux-x64": "0.0.19",
56
+ "@dtmd/temper-win32-x64": "0.0.19"
57
57
  }
58
58
  }