@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.
@@ -137,6 +137,24 @@ export interface Layout {
137
137
  * {@link Layout}.
138
138
  */
139
139
  export type Shape = "fields";
140
+ /**
141
+ * A kind's **leaf-set witness** — the leaf names a member of the kind carries, handed
142
+ * over by the kind's own typed surface `T` (decision 0053). The type is the declaration:
143
+ * nothing here is a second schema to keep in step with `T`, because the record's keys
144
+ * *are* `keyof T`. It exists at all because TypeScript erases `T` at the seam, so the
145
+ * set the compiler knows has to reach emit as a runtime value.
146
+ *
147
+ * The record is **exhaustive** — `-?` strips optionality, so omitting one key of `T` is a
148
+ * compile error and a key that is not `T`'s own is one too. That binding is what keeps
149
+ * the witness from degrading into the free-hand leaf schema the decision rejected: a
150
+ * partial set is unwritable rather than merely discouraged.
151
+ *
152
+ * Key order is the order the lowered row carries (`declarations.ts`), so a kind's leaves
153
+ * read in the order its author declared them.
154
+ */
155
+ export type LeafSet<T extends object> = {
156
+ readonly [K in keyof T]-?: true;
157
+ };
140
158
  /**
141
159
  * A registration member's **collection address** — where inside a host manifest its
142
160
  * registration surfaces: which `manifest` (`settings.json`, `.mcp.json`) and which
@@ -168,16 +186,38 @@ export interface Template {
168
186
  /** Where a file child's unit sits, relative to the parent's unit; absent for an embedded layer. */
169
187
  readonly path?: string;
170
188
  }
171
- /** The seven facts of a kind's runtime residue. */
172
- export interface KindFacts {
189
+ /**
190
+ * The seven facts of a kind's runtime residue, plus the derived {@link LeafSet} witness
191
+ * its typed surface hands over. `T` is that surface — the interface the kind's
192
+ * constructor is generic over. It defaults to an erased `Record<string, unknown>`, so
193
+ * every signature that only ever *reads* a facts value (`declarations.ts`'s lowering,
194
+ * {@link Member}, {@link KindDefinition}) names `KindFacts` bare and no call site
195
+ * re-spells a type argument.
196
+ */
197
+ export type KindFacts<T extends object = Record<string, unknown>> = {
173
198
  /** Fact 1, label — the compiled debug label findings speak; the kind's name. */
174
199
  readonly name: string;
175
200
  /** The declared provider authority, when the kind qualifies by one. */
176
201
  readonly provider?: string;
202
+ /**
203
+ * The built-in kind this facts value **relocates** — set only by {@link relocate},
204
+ * from the base kind's own name, never authored. It is the authoring layer's
205
+ * *provenance* fact: the value was derived from the imported built-in, so a
206
+ * second same-named kind in play is a sanctioned relocation rather than a name
207
+ * collision. It never reaches a kind-fact row — the lock reader holds no base
208
+ * value to compare against and re-decides *structurally* instead
209
+ * (`src/compose.rs`'s `row_relocates_builtin`).
210
+ */
211
+ readonly relocates?: string;
177
212
  /** Fact 2, locus — where members live, and for a file locus whether their documents
178
213
  * are committed: a `local` commitment class declares the kind reviewed and its
179
214
  * members' documents not. */
180
- readonly locus: Locus;
215
+ readonly locus: {
216
+ readonly kind: "at";
217
+ readonly root: string;
218
+ readonly glob: string;
219
+ readonly commitment?: "local";
220
+ };
181
221
  /** Fact 3a, projection — the artifact format; omitted for a kind that declares none. */
182
222
  readonly format?: Format;
183
223
  /** Fact 3b, projection — the on-disk unit shape. */
@@ -186,15 +226,78 @@ export interface KindFacts {
186
226
  * the world reaches a member (never rivals — a member is live if any one is). */
187
227
  readonly registration: readonly Registration[];
188
228
  /**
189
- * The frontmatter key the member's name writes under. For `unitShape:
190
- * "named-field"` this is the id **source** — the declared field a member's
191
- * identity is read from (an agent's `name`), never the filename or directory.
192
- * For `"directory"` it is a projection-order detail only (a skill's `name`
193
- * still writes into frontmatter, but identity is the directory name); absent
194
- * when identity is the file stem and no field carries it (a rule), or the
195
- * starred directory segment (`"starred-segment"`) — both path-derived, never a
196
- * field.
197
- */
229
+ * The frontmatter key the member's name writes under. For `unitShape:
230
+ * "named-field"` this is the id **source** — the declared field a member's
231
+ * identity is read from (an agent's `name`), never the filename or directory.
232
+ * For `"directory"` it is a projection-order detail only (a skill's `name`
233
+ * still writes into frontmatter, but identity is the directory name); absent
234
+ * when identity is the file stem and no field carries it (a rule), or the
235
+ * starred directory segment (`"starred-segment"`) — both path-derived, never a
236
+ * field.
237
+ */
238
+ readonly identityField?: string;
239
+ /** Fact 5, edge fields — the kind's fields that are references to other members. */
240
+ readonly edgeFields?: readonly EdgeField[];
241
+ /** Fact 6, content — a declared {@link Layout} over the body's heading tree; absent
242
+ * leaves the kind `file`-content (one verbatim prose body, the default). */
243
+ readonly content?: Layout;
244
+ /** Fact 6b, content — the fields-only body shape (`"fields"`, no body slot); absent
245
+ * leaves the kind body-bearing (`file` or a {@link Layout}). */
246
+ readonly shape?: Shape;
247
+ /** The registration member's {@link CollectionAddress} — which manifest and key path
248
+ * its registration surfaces at; absent for a kind that owns its own file locus. */
249
+ readonly collectionAddress?: CollectionAddress;
250
+ /** Fact 7, template — one {@link Template} per inner layer of nested members the kind
251
+ * hosts; absent for a kind that nests nothing. */
252
+ readonly templates?: readonly Template[];
253
+ /** The kind's **leaf set**, witnessed exhaustively over its own typed surface and
254
+ * lowered to the row's `leaves` column at emit ({@link LeafSet}, decision 0053) —
255
+ * what a read verb renders where the surface holds no member of the kind yet.
256
+ * Absent for a kind whose constructor declares no witness. */
257
+ readonly leaves?: LeafSet<T>;
258
+ /** Advisory authoring counsel for the kind as a whole — teaching at authoring time via
259
+ * `schema` hover or `explain`, carrying no predicate or severity (decision 0045). */
260
+ readonly guidance?: string;
261
+ /** External-fact source backing the guidance — a doc URL plus retrieved date,
262
+ * carried as data. */
263
+ readonly cite?: string;
264
+ } | {
265
+ /** Fact 1, label — the compiled debug label findings speak; the kind's name. */
266
+ readonly name: string;
267
+ /** The declared provider authority, when the kind qualifies by one. */
268
+ readonly provider?: string;
269
+ /**
270
+ * The built-in kind this facts value **relocates** — set only by {@link relocate},
271
+ * from the base kind's own name, never authored. It is the authoring layer's
272
+ * *provenance* fact: the value was derived from the imported built-in, so a
273
+ * second same-named kind in play is a sanctioned relocation rather than a name
274
+ * collision. It never reaches a kind-fact row — the lock reader holds no base
275
+ * value to compare against and re-decides *structurally* instead
276
+ * (`src/compose.rs`'s `row_relocates_builtin`).
277
+ */
278
+ readonly relocates?: string;
279
+ /** Fact 2, locus — where members live, and for a file locus whether their documents
280
+ * are committed: a `local` commitment class declares the kind reviewed and its
281
+ * members' documents not. */
282
+ readonly locus: {
283
+ readonly kind: "embedded";
284
+ };
285
+ /** Fact 3a, projection — the artifact format; omitted for a kind that declares none. */
286
+ readonly format?: Format;
287
+ /** Fact 3b, projection — the on-disk unit shape. */
288
+ readonly unitShape: UnitShape;
289
+ /** Fact 4, registration — embedded members register nothing. */
290
+ readonly registration: readonly [];
291
+ /**
292
+ * The frontmatter key the member's name writes under. For `unitShape:
293
+ * "named-field"` this is the id **source** — the declared field a member's
294
+ * identity is read from (an agent's `name`), never the filename or directory.
295
+ * For `"directory"` it is a projection-order detail only (a skill's `name`
296
+ * still writes into frontmatter, but identity is the directory name); absent
297
+ * when identity is the file stem and no field carries it (a rule), or the
298
+ * starred directory segment (`"starred-segment"`) — both path-derived, never a
299
+ * field.
300
+ */
198
301
  readonly identityField?: string;
199
302
  /** Fact 5, edge fields — the kind's fields that are references to other members. */
200
303
  readonly edgeFields?: readonly EdgeField[];
@@ -210,13 +313,106 @@ export interface KindFacts {
210
313
  /** Fact 7, template — one {@link Template} per inner layer of nested members the kind
211
314
  * hosts; absent for a kind that nests nothing. */
212
315
  readonly templates?: readonly Template[];
316
+ /** The kind's **leaf set**, witnessed exhaustively over its own typed surface and
317
+ * lowered to the row's `leaves` column at emit ({@link LeafSet}, decision 0053) —
318
+ * what a read verb renders where the surface holds no member of the kind yet.
319
+ * Absent for a kind whose constructor declares no witness. */
320
+ readonly leaves?: LeafSet<T>;
213
321
  /** Advisory authoring counsel for the kind as a whole — teaching at authoring time via
214
322
  * `schema` hover or `explain`, carrying no predicate or severity (decision 0045). */
215
323
  readonly guidance?: string;
216
324
  /** External-fact source backing the guidance — a doc URL plus retrieved date,
217
325
  * carried as data. */
218
326
  readonly cite?: string;
327
+ } | {
328
+ /** Fact 1, label — the compiled debug label findings speak; the kind's name. */
329
+ readonly name: string;
330
+ /** The declared provider authority, when the kind qualifies by one. */
331
+ readonly provider?: string;
332
+ /**
333
+ * The built-in kind this facts value **relocates** — set only by {@link relocate},
334
+ * from the base kind's own name, never authored. It is the authoring layer's
335
+ * *provenance* fact: the value was derived from the imported built-in, so a
336
+ * second same-named kind in play is a sanctioned relocation rather than a name
337
+ * collision. It never reaches a kind-fact row — the lock reader holds no base
338
+ * value to compare against and re-decides *structurally* instead
339
+ * (`src/compose.rs`'s `row_relocates_builtin`).
340
+ */
341
+ readonly relocates?: string;
342
+ /** Fact 2, locus — where members live, and for a file locus whether their documents
343
+ * are committed: a `local` commitment class declares the kind reviewed and its
344
+ * members' documents not. */
345
+ readonly locus: {
346
+ readonly kind: "nested-file";
347
+ };
348
+ /** Fact 3a, projection — the artifact format; omitted for a kind that declares none. */
349
+ readonly format?: Format;
350
+ /** Fact 3b, projection — the on-disk unit shape. */
351
+ readonly unitShape: UnitShape;
352
+ /** Fact 4, registration — nested-file members register nothing. */
353
+ readonly registration: readonly [];
354
+ /**
355
+ * The frontmatter key the member's name writes under. For `unitShape:
356
+ * "named-field"` this is the id **source** — the declared field a member's
357
+ * identity is read from (an agent's `name`), never the filename or directory.
358
+ * For `"directory"` it is a projection-order detail only (a skill's `name`
359
+ * still writes into frontmatter, but identity is the directory name); absent
360
+ * when identity is the file stem and no field carries it (a rule), or the
361
+ * starred directory segment (`"starred-segment"`) — both path-derived, never a
362
+ * field.
363
+ */
364
+ readonly identityField?: string;
365
+ /** Fact 5, edge fields — the kind's fields that are references to other members. */
366
+ readonly edgeFields?: readonly EdgeField[];
367
+ /** Fact 6, content — a declared {@link Layout} over the body's heading tree; absent
368
+ * leaves the kind `file`-content (one verbatim prose body, the default). */
369
+ readonly content?: Layout;
370
+ /** Fact 6b, content — the fields-only body shape (`"fields"`, no body slot); absent
371
+ * leaves the kind body-bearing (`file` or a {@link Layout}). */
372
+ readonly shape?: Shape;
373
+ /** The registration member's {@link CollectionAddress} — which manifest and key path
374
+ * its registration surfaces at; absent for a kind that owns its own file locus. */
375
+ readonly collectionAddress?: CollectionAddress;
376
+ /** Fact 7, template — one {@link Template} per inner layer of nested members the kind
377
+ * hosts; absent for a kind that nests nothing. */
378
+ readonly templates?: readonly Template[];
379
+ /** The kind's **leaf set**, witnessed exhaustively over its own typed surface and
380
+ * lowered to the row's `leaves` column at emit ({@link LeafSet}, decision 0053) —
381
+ * what a read verb renders where the surface holds no member of the kind yet.
382
+ * Absent for a kind whose constructor declares no witness. */
383
+ readonly leaves?: LeafSet<T>;
384
+ /** Advisory authoring counsel for the kind as a whole — teaching at authoring time via
385
+ * `schema` hover or `explain`, carrying no predicate or severity (decision 0045). */
386
+ readonly guidance?: string;
387
+ /** External-fact source backing the guidance — a doc URL plus retrieved date,
388
+ * carried as data. */
389
+ readonly cite?: string;
390
+ };
391
+ /**
392
+ * A **declared input** — a file the member's claims rest on, fingerprinted by the lock
393
+ * and moved nowhere (`specs/model/authoring.md`, "The SDK"). The path resolves against
394
+ * the stating module ({@link moduleUrl}), never the workspace — the same anchor
395
+ * `include()` and a `file()` body take.
396
+ *
397
+ * It lives here rather than beside the prose references it rhymes with: an input rides
398
+ * no text span and pairs with no body slot, so it is a member-grain framework key, not a
399
+ * word in a body. When the input's bytes move, the member's own freshness finding routes
400
+ * the author to re-verify the claims *before* re-emitting — temper never judges whether
401
+ * the claim still holds, only names the place to look.
402
+ */
403
+ export interface Input {
404
+ readonly kind: "input";
405
+ /** Path to the input, resolved against {@link moduleUrl}. */
406
+ readonly path: string;
407
+ /** The declaring module's own `import.meta.url` — what {@link path} resolves against. */
408
+ readonly moduleUrl: string;
219
409
  }
410
+ /**
411
+ * Declare a file a member's claims rest on — the lock fingerprints it and no byte
412
+ * reaches the projection. The path resolves against the stating module, so the call is
413
+ * `input(import.meta.url, "./schema.json")`.
414
+ */
415
+ export declare function input(moduleUrl: string, path: string): Input;
220
416
  /**
221
417
  * One authored member — a typed value in the library. Kind identity travels by
222
418
  * import (`facts`), never by string; the
@@ -242,8 +438,27 @@ export interface Member {
242
438
  readonly requires: Readonly<Record<string, Requirement>>;
243
439
  /** The capabilities the member's behavior uses — the permission union's source. */
244
440
  readonly needs: readonly Capability[];
441
+ /** The files this member's claims rest on — fingerprinted, never moved. */
442
+ readonly inputs: readonly Input[];
245
443
  }
246
- /** The init a kind constructor takes — the framework keys plus the kind's typed fields `T`. */
444
+ /**
445
+ * The bag a surface `T` declares its {@link RESIDUE_KEY} channel as — the keys `T` does
446
+ * *not* type, and only those. The two halves partition the format's key space, so a key
447
+ * in both is a mis-spelling rather than an override: open by key, then narrowed against
448
+ * `keyof T`, which leaves a typed key's slot here holding nothing a value can fill.
449
+ *
450
+ * The narrowing is the front door, never the whole gate — a bag the program computed
451
+ * arrives as a bare `Record<string, unknown>` and types fine, so the constructor refuses
452
+ * the collision again over the keys it actually holds ({@link orderedFields}).
453
+ */
454
+ export type Residue<T> = Readonly<Record<string, unknown>> & {
455
+ readonly [K in keyof T]?: never;
456
+ };
457
+ /**
458
+ * The init a kind constructor takes — the framework keys plus the kind's typed fields `T`.
459
+ * {@link RESIDUE_KEY} is a framework key too, deliberately not spelled below: a kind opts
460
+ * into that channel through its own surface `T`.
461
+ */
247
462
  export type MemberInit<T> = {
248
463
  readonly name: string;
249
464
  /** The host member this member's unit composes under — a nested-file child's, and only its. */
@@ -252,6 +467,12 @@ export type MemberInit<T> = {
252
467
  readonly satisfies?: readonly string[];
253
468
  readonly requires?: Readonly<Record<string, Requirement>>;
254
469
  readonly needs?: readonly Capability[];
470
+ /**
471
+ * The files this member's claims rest on ({@link input}) — fingerprinted by the lock,
472
+ * with no byte reaching the projection. The surface binds a *member*: an embedded
473
+ * value takes no `MemberInit`, and its claims are its host's.
474
+ */
475
+ readonly inputs?: readonly Input[];
255
476
  } & T;
256
477
  /**
257
478
  * A kind — a callable constructor carrying its seven facts. Calling it builds a
@@ -283,8 +504,80 @@ export interface KindOptions {
283
504
  * into emit is the seven facts plus flat field data. `options.render`, when given,
284
505
  * rides alongside `facts`/`key` on the returned constructor — never on the member
285
506
  * it builds, since it is erased before a member reaches emit.
507
+ *
508
+ * The facts are typed over the same `T` the constructor is, which is what binds a
509
+ * declared {@link LeafSet} witness to `keyof T`: the leaf set is the kind's own surface,
510
+ * checked here at the keystroke, and its lowering to the row is the one place it is
511
+ * spelled again (`declarations.ts`).
512
+ */
513
+ export declare function kind<T extends object>(facts: KindFacts<T>, options?: KindOptions): KindDefinition<T>;
514
+ /**
515
+ * A **relocation delta** — the facts a relocated built-in kind diverges from its base
516
+ * on. Two faces, either or both:
517
+ *
518
+ * - `edgeFields`, *added* to whatever the base already declares (never replacing them,
519
+ * which would drop a shipped kind's own edges silently). Each added field names a key
520
+ * of the relocated kind's typed surface `T`, so an edge can never be declared over a
521
+ * field the kind does not carry.
522
+ * - `governs`, *replacing* the base's `at` locus root and glob — moving where the kind's
523
+ * members are found, the one fact the engine's own overlay exists to apply
524
+ * (`src/compose.rs`'s `overlay_builtin_kind`).
525
+ *
526
+ * Every other fact — format, unit shape, registration, content, templates, the declared
527
+ * leaf set — rides through unchanged, which is exactly what makes the emitted row still
528
+ * read as a relocation rather than a name collision on the reading side.
529
+ */
530
+ export interface KindRelocation<T> {
531
+ /** The edge fields this relocation adds, each over a field of the kind's own surface. */
532
+ readonly edgeFields?: readonly {
533
+ readonly field: keyof T & string;
534
+ readonly to: readonly [string, ...string[]];
535
+ }[];
536
+ /**
537
+ * The locus this relocation moves the kind's members to — root and glob, the two
538
+ * columns the lock reader's overlay writes back. A file locus's `commitment` class is
539
+ * deliberately absent: the overlay writes `Governs { root, glob }` and nothing else, so
540
+ * a `commitment` delta would author a fact the reader drops in silence — a built-in's
541
+ * commitment class stays the built-in's.
542
+ */
543
+ readonly governs?: {
544
+ readonly root: string;
545
+ readonly glob: string;
546
+ };
547
+ }
548
+ /**
549
+ * **Relocate** a built-in kind: the sanctioned way an adopting corpus moves a kind it
550
+ * does not own to its own root, adds an edge field to it, or both. Returns a fresh
551
+ * constructor over the widened typed surface `T` (the base's fields plus the added edge
552
+ * fields, spelled by the caller as one interface), carrying the base's facts with
553
+ * `delta`'s edge fields appended, `delta`'s `governs` in place of the base's locus root
554
+ * and glob, and the base's own {@link KindDefinition.render} hook preserved. Ownership,
555
+ * not privilege — a relocated built-in is an ordinary kind value from here on, and its
556
+ * added edge reaches the lock as an assembly `edge` row keyed by `from`, exactly as any
557
+ * kind's does (`declarations.ts`), never as a column on a kind-fact row.
558
+ *
559
+ * A moved locus, by contrast, *is* a kind-fact row column pair: the emitted row carries
560
+ * the delta's `governs_root`/`governs_glob` while `format`, `unit_shape` and
561
+ * `registration` stay the base's — which is precisely the three-fact test the lock
562
+ * reader applies before overlaying the row onto its compiled-in built-in
563
+ * (`src/compose.rs`'s `row_relocates_builtin`), so the engine reads members at the new
564
+ * root rather than treating the row as a colliding kind.
565
+ *
566
+ * The produced facts carry `relocates`, naming the base — the marker that tells a
567
+ * legitimate relocation from a genuine name collision when two same-named kinds are in
568
+ * play. Identity travels by import: the base is the imported built-in value, so the
569
+ * provenance is proven here rather than inferred downstream.
570
+ *
571
+ * # Throws
572
+ * If an added edge field re-declares one the base already carries, or one another
573
+ * entry of the same delta already added — two `edge` rows over one `<from, field>`
574
+ * cross-wire the graph instead of declaring one relationship. If a `governs` delta
575
+ * names a base whose locus is not `at` ({@link relocatedLocus}). And if the delta
576
+ * declares neither face, which would mint a marker-bearing clone of the base — a second
577
+ * same-named kind diverging on nothing, which is a name collision spelled as a
578
+ * relocation.
286
579
  */
287
- export declare function kind<T extends object>(facts: KindFacts, options?: KindOptions): KindDefinition<T>;
580
+ export declare function relocate<T extends object>(base: KindDefinition<any>, delta: KindRelocation<T>): KindDefinition<T>;
288
581
  /**
289
582
  * One entry in a sibling collection: its own key plus its leaf fields
290
583
  * (`rejected."baked-projection"`) — an ordered list element, never a positional
@@ -390,8 +683,14 @@ export interface ResolvedEmbeddedMemberValue {
390
683
  * composed value's shape. A bare string names a kind whose facts are out of reach, so
391
684
  * such a value renders with no target facts.
392
685
  */
686
+ export declare function embeddedMemberValue<T extends object>(init: {
687
+ kind: KindDefinition<T>;
688
+ key: string;
689
+ leaves: Readonly<Record<keyof T, string | Text>>;
690
+ collections?: EmbeddedMemberValue["collections"];
691
+ }): EmbeddedMemberValue;
393
692
  export declare function embeddedMemberValue(init: {
394
- kind: string | KindDefinition<any>;
693
+ kind: string;
395
694
  key: string;
396
695
  leaves: Readonly<Record<string, string | Text>>;
397
696
  collections?: EmbeddedMemberValue["collections"];
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,23 +145,106 @@ 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
  }
74
152
  /**
75
- * Compose an embedded member's value for `blocks()` the shape any project's own
76
- * child kind uses. `kind` names the child kind: a bare string, or the child kind's
77
- * own `KindDefinition` — passing the definition carries its `render` hook and its
78
- * declared edge fields (when declared) through to emit, with no other change to the
79
- * composed value's shape. A bare string names a kind whose facts are out of reach, so
80
- * such a value renders with no target facts.
153
+ * The locus a `governs` delta moves the base to: the base's `at` root and glob replaced,
154
+ * every other locus fact (its commitment class among them) riding through.
155
+ *
156
+ * # Throws
157
+ * If the base's locus is not `at` an embedded or nested-file kind governs no glob at
158
+ * all (its members compose their paths from a host's unit, or own no file), so there is
159
+ * no locus to move and its row carries no `governs` columns to move it to.
160
+ */
161
+ function relocatedLocus(base, governs) {
162
+ if (base.locus.kind !== "at") {
163
+ throw new Error(`relocating kind \`${base.name}\`: a \`governs\` delta moves the ` +
164
+ `path glob a kind's members are found at, and this kind's locus is \`${base.locus.kind}\` ` +
165
+ `— it governs no glob of its own. Drop the \`governs\` face of the delta ` +
166
+ `(specs/model/representation.md, "locus").`);
167
+ }
168
+ return { ...base.locus, root: governs.root, glob: governs.glob };
169
+ }
170
+ /**
171
+ * **Relocate** a built-in kind: the sanctioned way an adopting corpus moves a kind it
172
+ * does not own to its own root, adds an edge field to it, or both. Returns a fresh
173
+ * constructor over the widened typed surface `T` (the base's fields plus the added edge
174
+ * fields, spelled by the caller as one interface), carrying the base's facts with
175
+ * `delta`'s edge fields appended, `delta`'s `governs` in place of the base's locus root
176
+ * and glob, and the base's own {@link KindDefinition.render} hook preserved. Ownership,
177
+ * not privilege — a relocated built-in is an ordinary kind value from here on, and its
178
+ * added edge reaches the lock as an assembly `edge` row keyed by `from`, exactly as any
179
+ * kind's does (`declarations.ts`), never as a column on a kind-fact row.
180
+ *
181
+ * A moved locus, by contrast, *is* a kind-fact row column pair: the emitted row carries
182
+ * the delta's `governs_root`/`governs_glob` while `format`, `unit_shape` and
183
+ * `registration` stay the base's — which is precisely the three-fact test the lock
184
+ * reader applies before overlaying the row onto its compiled-in built-in
185
+ * (`src/compose.rs`'s `row_relocates_builtin`), so the engine reads members at the new
186
+ * root rather than treating the row as a colliding kind.
187
+ *
188
+ * The produced facts carry `relocates`, naming the base — the marker that tells a
189
+ * legitimate relocation from a genuine name collision when two same-named kinds are in
190
+ * play. Identity travels by import: the base is the imported built-in value, so the
191
+ * provenance is proven here rather than inferred downstream.
192
+ *
193
+ * # Throws
194
+ * If an added edge field re-declares one the base already carries, or one another
195
+ * entry of the same delta already added — two `edge` rows over one `<from, field>`
196
+ * cross-wire the graph instead of declaring one relationship. If a `governs` delta
197
+ * names a base whose locus is not `at` ({@link relocatedLocus}). And if the delta
198
+ * declares neither face, which would mint a marker-bearing clone of the base — a second
199
+ * same-named kind diverging on nothing, which is a name collision spelled as a
200
+ * relocation.
81
201
  */
202
+ export function relocate(base, delta) {
203
+ const declaredEdges = delta.edgeFields ?? [];
204
+ if (declaredEdges.length === 0 && delta.governs === undefined) {
205
+ throw new Error(`relocating kind \`${base.facts.name}\`: a relocation declares at least one diverging ` +
206
+ `fact — \`edgeFields\`, \`governs\`, or both. A delta declaring neither mints a ` +
207
+ `second kind of the base's own name that diverges on nothing, which reads as a name ` +
208
+ `collision rather than a relocation (specs/model/representation.md, "kind").`);
209
+ }
210
+ const locus = delta.governs === undefined ? undefined : relocatedLocus(base.facts, delta.governs);
211
+ const inherited = base.facts.edgeFields ?? [];
212
+ const claimed = new Set(inherited.map((edge) => edge.field));
213
+ const added = [];
214
+ for (const edge of declaredEdges) {
215
+ if (claimed.has(edge.field)) {
216
+ throw new Error(`relocating kind \`${base.facts.name}\`: edge field \`${edge.field}\` is already ` +
217
+ `declared, and a second declaration of one field cross-wires the graph rather than ` +
218
+ `adding a relationship (specs/model/representation.md, "kind").`);
219
+ }
220
+ claimed.add(edge.field);
221
+ added.push({ field: edge.field, to: edge.to });
222
+ }
223
+ const edgeFields = [...inherited, ...added];
224
+ const relocated = { ...base.facts, relocates: base.facts.name, edgeFields };
225
+ // Two spellings, not one with an optional `locus`: `KindFacts` is a union discriminated
226
+ // on the locus, so the moved case must carry the `at` locus as its own literal branch.
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.
233
+ return kind(facts, { render: base.render });
234
+ }
82
235
  export function embeddedMemberValue(init) {
83
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
+ }
84
244
  const render = definition?.render;
85
245
  const edgeFields = definition?.facts.edgeFields;
86
246
  return {
87
- kind: definition?.key ?? init.kind,
247
+ kind: kindKey,
88
248
  key: init.key,
89
249
  leaves: init.leaves,
90
250
  collections: init.collections ?? {},
@@ -92,3 +252,18 @@ export function embeddedMemberValue(init) {
92
252
  ...(edgeFields !== undefined ? { edgeFields } : {}),
93
253
  };
94
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
+ }