@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/assembly.d.ts +40 -0
- package/dist/src/assembly.js +43 -0
- package/dist/src/builtins.d.ts +110 -4
- package/dist/src/builtins.js +64 -3
- package/dist/src/claude-code.d.ts +2 -2
- package/dist/src/claude-code.js +1 -1
- package/dist/src/contract.d.ts +96 -3
- package/dist/src/contract.js +89 -2
- package/dist/src/declarations.js +104 -17
- package/dist/src/generated/ClauseRow.d.ts +21 -6
- package/dist/src/generated/Declarations.d.ts +8 -0
- package/dist/src/generated/InputRow.d.ts +21 -0
- package/dist/src/generated/InputRow.js +2 -0
- package/dist/src/generated/KindFactRow.d.ts +12 -0
- package/dist/src/generated/index.d.ts +1 -0
- package/dist/src/index.d.ts +6 -5
- package/dist/src/index.js +8 -4
- package/dist/src/kind.d.ts +102 -7
- package/dist/src/kind.js +108 -3
- package/dist/src/prose.d.ts +22 -5
- package/dist/src/prose.js +39 -7
- package/package.json +3 -3
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
|
-
|
|
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:
|
|
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
|
+
}
|
package/dist/src/prose.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prose —
|
|
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, `
|
|
5
|
-
* prose spans with embedded-member values in
|
|
6
|
-
* the constructor, the words land byte-identical to their
|
|
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
|
|
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 —
|
|
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, `
|
|
5
|
-
* prose spans with embedded-member values in
|
|
6
|
-
* the constructor, the words land byte-identical to their
|
|
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
|
-
|
|
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.
|
|
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.
|
|
56
|
-
"@dtmd/temper-win32-x64": "0.0.
|
|
55
|
+
"@dtmd/temper-linux-x64": "0.0.19",
|
|
56
|
+
"@dtmd/temper-win32-x64": "0.0.19"
|
|
57
57
|
}
|
|
58
58
|
}
|