@dtmd/temper 0.0.17 → 0.0.18

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.
@@ -106,9 +106,9 @@ export declare function declaredRequirements(harness: Harness): Set<string>;
106
106
  export declare function declaredAddresses(harness: Harness): Set<string>;
107
107
  /**
108
108
  * Every discoverable (`at`-locus) kind the program declares — the deferral signal a
109
- * dangling mention is measured against (`prose.ts`'s `defersToGate`): a mention naming
110
- * one of these whose member is not a composed value defers to `check`, while a mention
111
- * naming no declared kind refuses at emit. Member kinds ∪ `expect` kinds; an embedded
109
+ * dangling mention is measured against (`prose.ts`'s `defersToGate`): a mention whose
110
+ * address is a host address of one of these and names no composed value defers to
111
+ * `check`, while a mention naming no declared kind refuses at emit. Member kinds ∪ `expect` kinds; an embedded
112
112
  * kind is excluded — its members are composed within a host, never discovered, so a
113
113
  * flat `kind:name` mention of one has no discovery locus to defer to.
114
114
  */
@@ -9,8 +9,10 @@
9
9
  * in lockstep.
10
10
  */
11
11
  import { fileURLToPath } from "node:url";
12
+ import { isDeepStrictEqual } from "node:util";
12
13
  import { isTextSpan, resolveLeaf } from "./prose.js";
13
14
  import { SETTINGS_MANIFEST, TELEMETRY_EVENT_HOOKS, hook, tapHookRegistration } from "./builtins.js";
15
+ import { hostAddress, leafAddress, nestedAddress } from "./member-address.js";
14
16
  /**
15
17
  * Compile one `Clause` into its lock row: the shared `key`/`field`/`severity`/
16
18
  * `guidance`/`cite` columns — the clause's four channels surviving erasure
@@ -60,7 +62,7 @@ function clauseRow(clause, kind) {
60
62
  return {
61
63
  kind,
62
64
  predicate: predicate.key,
63
- field: predicate.field,
65
+ field: clauseField(predicate),
64
66
  severity: clause.severity,
65
67
  guidance: clause.guidance,
66
68
  cite: clause.cite,
@@ -103,6 +105,31 @@ function clauseRow(clause, kind) {
103
105
  : undefined,
104
106
  };
105
107
  }
108
+ /**
109
+ * The `field` column for one predicate: the field it names, or — for the two
110
+ * predicates that name a *section* rather than a field — an identity synthesized
111
+ * from the arguments the row already carries.
112
+ *
113
+ * `section_contains` and `require_sections` set no `field`, and the column is what
114
+ * emit stamps a clause's label from (`stamp_clause_label`, `src/drift.rs`), so
115
+ * reading `Predicate.field` folds every clause of either predicate on one kind
116
+ * into one label — two rows wearing one label, which admissibility refuses as a
117
+ * malformed lock. Synthesizing here keeps the whole fix at the lowering: the Rust
118
+ * reader reconstructs both predicates from the `section`/`sections` columns and
119
+ * never from this one, so nothing round-trips through the synthesized text.
120
+ */
121
+ function clauseField(predicate) {
122
+ if (predicate.key === "section_contains") {
123
+ const { section } = predicate;
124
+ return section === undefined ? undefined : `${section.heading}.${section.marker}`;
125
+ }
126
+ if (predicate.key === "require_sections") {
127
+ // Joined with `+` rather than the label's own `.`, so the segment reads as the
128
+ // set it is and two different heading lists cannot fold to one label.
129
+ return predicate.sections?.join("+");
130
+ }
131
+ return predicate.field;
132
+ }
106
133
  /** `min_len`/`max_len`/`extent`'s scalar bound off their shared `min`/`max`
107
134
  * args keys — `undefined` for every other predicate, and for these three when
108
135
  * neither endpoint is present. */
@@ -272,6 +299,89 @@ function kindFactRow(facts, admissions) {
272
299
  cite: facts.cite,
273
300
  };
274
301
  }
302
+ /**
303
+ * The locus a colliding kind is declared at, for {@link resolveNameCollision}'s refusal:
304
+ * an `at` kind names the path its members are found at, and every other locus names
305
+ * itself — a kind that governs no glob has no path of its own to name.
306
+ */
307
+ function locusLabel(facts) {
308
+ const { locus } = facts;
309
+ if (locus.kind !== "at")
310
+ return `the \`${locus.kind}\` locus`;
311
+ return `\`${locus.root === "." ? locus.glob : `${locus.root}/${locus.glob}`}\``;
312
+ }
313
+ /**
314
+ * A kind's facts with the three faces a relocation may diverge from its base on erased:
315
+ * the `relocates` marker, the appended `edgeFields`, and an `at` locus's moved root and
316
+ * glob (`kind.ts`'s `KindRelocation`). Two values agreeing here are one kind up to a
317
+ * relocation delta; the delta's own faces are checked on their own terms.
318
+ */
319
+ function relocationInvariants(facts) {
320
+ const bare = { ...facts };
321
+ delete bare.relocates;
322
+ delete bare.edgeFields;
323
+ if (facts.locus.kind === "at")
324
+ bare.locus = { ...facts.locus, root: "", glob: "" };
325
+ return bare;
326
+ }
327
+ /**
328
+ * Whether `relocation` was derived from `base` — the provenance the relocation-wins rule
329
+ * rests on. The marker names the base, `relocate` only ever *appends* edge fields (so the
330
+ * base's are a prefix of the relocation's), and every other fact rides through unchanged:
331
+ * the base's facts are a subset by construction, which is exactly what makes keeping the
332
+ * relocation lossless. Checked rather than assumed, because the marker carries a name and
333
+ * a name is what is in dispute — a value bearing it whose facts are not the other's
334
+ * superset is a third kind of the same name, and dropping *that* would be the very silent
335
+ * loss this decision exists to end.
336
+ */
337
+ function isRelocationOf(relocation, base) {
338
+ if (relocation.relocates !== base.name || base.relocates !== undefined)
339
+ return false;
340
+ const inherited = base.edgeFields ?? [];
341
+ const declared = relocation.edgeFields ?? [];
342
+ return (declared.length >= inherited.length &&
343
+ isDeepStrictEqual(declared.slice(0, inherited.length), inherited) &&
344
+ isDeepStrictEqual(relocationInvariants(relocation), relocationInvariants(base)));
345
+ }
346
+ /**
347
+ * Which of two `KindFacts` values sharing one name is the kind in play — a decision that
348
+ * must not depend on the order the two arrived in, since {@link kindsInPlay} draws its
349
+ * facts from `members`, then `expect`, then `admit`, then embedded templates, and a kind
350
+ * legitimately reaches it through any of them.
351
+ *
352
+ * A **relocation** wins over its base whichever way round they arrive
353
+ * ({@link isRelocationOf} proves the provenance): the base's facts are the relocation's
354
+ * minus the delta, so keeping the base would silently drop the relocation's added edge
355
+ * fields and moved locus. Two values that are structurally equal are one kind declared
356
+ * twice — keep the first. Anything else is a genuine collision and refuses: kind identity
357
+ * travels by import, never by string (`representation.md`, "kind"), while the lock is a
358
+ * string-keyed medium, so the second kind has no name to reach the engine
359
+ * under and dropping it is a silent loss of everything it declared. Two relocations of
360
+ * one base collide the same way — each is still a kind of the base's own name.
361
+ *
362
+ * # Throws
363
+ * On a collision, naming the kind and both loci — the two declarations an author has to
364
+ * go look at.
365
+ */
366
+ function resolveNameCollision(held, arriving) {
367
+ // At most one holds: each direction demands the other value carry no marker at all.
368
+ if (isRelocationOf(held, arriving))
369
+ return held;
370
+ if (isRelocationOf(arriving, held))
371
+ return arriving;
372
+ if (isDeepStrictEqual(held, arriving))
373
+ return held;
374
+ throw new Error(held.relocates !== undefined && arriving.relocates !== undefined
375
+ ? `two relocations of kind \`${held.name}\` are in play, at ${locusLabel(held)} and ` +
376
+ `${locusLabel(arriving)}, and they diverge. A relocation is still a kind of its base's ` +
377
+ `name, so a second one collides with the first: relocate the base once and import that ` +
378
+ `one value wherever it is used (specs/model/representation.md, "kind").`
379
+ : `two kinds named \`${held.name}\` are in play, at ${locusLabel(held)} and ` +
380
+ `${locusLabel(arriving)}. Kind identity travels by import, never by string, and the lock ` +
381
+ `is keyed by name — the second kind has no name of its own to reach the engine under. ` +
382
+ `Import the declared kind rather than redeclaring it; to move a built-in to another root ` +
383
+ `or add an edge field to it, use \`relocate()\` (specs/model/representation.md, "kind").`);
384
+ }
275
385
  /**
276
386
  * Every kind in play, at any locus — member kinds ∪ expect kinds ∪ their embedded
277
387
  * children — name-sorted, so every family derived from it inherits one stable order.
@@ -285,15 +395,24 @@ function kindFactRow(facts, admissions) {
285
395
  * Only *embedded* children are drawn in. A path-carrying template is the nested-file
286
396
  * layer, whose child owns a unit and reaches the lock through `expect` like any other
287
397
  * unit kind; pulling one in here would forge it a kind-fact row it never declared.
398
+ *
399
+ * Two facts values arriving under one name are decided by {@link resolveNameCollision},
400
+ * never by arrival order: the deduped facts are what `assemblyFactRows` derives every
401
+ * `edge` row from, so first-wins would drop a relocation's added edge on the floor
402
+ * whenever the base happened to be named first.
288
403
  */
289
404
  function kindsInPlay(harness) {
290
405
  const byName = new Map();
291
406
  const pending = [];
292
407
  const admit = (facts) => {
293
- if (byName.has(facts.name))
408
+ const held = byName.get(facts.name);
409
+ if (held === facts)
410
+ return;
411
+ const winner = held === undefined ? facts : resolveNameCollision(held, facts);
412
+ if (winner === held)
294
413
  return;
295
- byName.set(facts.name, facts);
296
- pending.push(facts);
414
+ byName.set(facts.name, winner);
415
+ pending.push(winner);
297
416
  };
298
417
  for (const member of harness.members)
299
418
  admit(member.facts);
@@ -388,7 +507,7 @@ function assemblyFactRows(harness, kinds) {
388
507
  function satisfiesRows(harness) {
389
508
  const rows = [];
390
509
  for (const member of harness.members) {
391
- const address = `${member.kind}:${member.name}`;
510
+ const address = hostAddress(member.kind, member.name);
392
511
  for (const requirement of member.satisfies) {
393
512
  rows.push({ member: address, requirement });
394
513
  }
@@ -409,7 +528,7 @@ function satisfiesRows(harness) {
409
528
  function mentionRows(harness) {
410
529
  const rows = [];
411
530
  for (const member of harness.members) {
412
- const address = `${member.kind}:${member.name}`;
531
+ const address = hostAddress(member.kind, member.name);
413
532
  if (member.prose?.kind === "text") {
414
533
  for (const mention of member.prose.mentions) {
415
534
  rows.push({ member: address, target: mention.target.address });
@@ -435,13 +554,11 @@ function mentionRows(harness) {
435
554
  * contribute — top-level leaves addressed by their bare field name, a
436
555
  * collection entry's leaves addressed `<collection>.<entry>.<field>` (one layer
437
556
  * deep, matching the row's own shape) — each row keyed to the leaf's own
438
- * structural address, the `<member>/<kind>/<key>/<child-path>` grammar
439
- * `src/read.rs`'s `parse_leaf_address` resolves. A bare-string leaf names no
440
- * mention.
557
+ * structural address ({@link leafAddress}). A bare-string leaf names no mention.
441
558
  */
442
559
  function embeddedLeafMentionRows(hostName, value) {
443
560
  const rows = [];
444
- const addressed = (childPath) => `${hostName}/${value.kind}/${value.key}/${childPath}`;
561
+ const addressed = (childPath) => leafAddress(hostName, value.kind, value.key, childPath);
445
562
  for (const [field, leaf] of Object.entries(value.leaves)) {
446
563
  if (typeof leaf === "string")
447
564
  continue;
@@ -478,7 +595,7 @@ function includeRows(harness) {
478
595
  rows.push({ member: address, source_path: fileURLToPath(new URL(include.path, include.moduleUrl)) });
479
596
  };
480
597
  for (const member of harness.members) {
481
- const address = `${member.kind}:${member.name}`;
598
+ const address = hostAddress(member.kind, member.name);
482
599
  if (member.prose?.kind === "text") {
483
600
  for (const include of member.prose.includes)
484
601
  push(address, include);
@@ -591,7 +708,7 @@ function nestedMemberRows(harness, admissions, scope, placements, extents) {
591
708
  for (const member of harness.members) {
592
709
  if (member.prose?.kind !== "blocks")
593
710
  continue;
594
- const host = `${member.kind}:${member.name}`;
711
+ const host = hostAddress(member.kind, member.name);
595
712
  for (const value of member.prose.values) {
596
713
  if (isTextSpan(value))
597
714
  continue;
@@ -729,22 +846,22 @@ export function declaredRequirements(harness) {
729
846
  export function declaredAddresses(harness) {
730
847
  const set = declaredRequirements(harness);
731
848
  for (const member of harness.members) {
732
- set.add(`${member.kind}:${member.name}`);
849
+ set.add(hostAddress(member.kind, member.name));
733
850
  if (member.prose?.kind !== "blocks")
734
851
  continue;
735
852
  for (const value of member.prose.values) {
736
853
  if (isTextSpan(value))
737
854
  continue;
738
- set.add(`${member.kind}:${member.name}/${value.kind}/${value.key}`);
855
+ set.add(nestedAddress(hostAddress(member.kind, member.name), value.kind, value.key));
739
856
  }
740
857
  }
741
858
  return set;
742
859
  }
743
860
  /**
744
861
  * Every discoverable (`at`-locus) kind the program declares — the deferral signal a
745
- * dangling mention is measured against (`prose.ts`'s `defersToGate`): a mention naming
746
- * one of these whose member is not a composed value defers to `check`, while a mention
747
- * naming no declared kind refuses at emit. Member kinds ∪ `expect` kinds; an embedded
862
+ * dangling mention is measured against (`prose.ts`'s `defersToGate`): a mention whose
863
+ * address is a host address of one of these and names no composed value defers to
864
+ * `check`, while a mention naming no declared kind refuses at emit. Member kinds ∪ `expect` kinds; an embedded
748
865
  * kind is excluded — its members are composed within a host, never discovered, so a
749
866
  * flat `kind:name` mention of one has no discovery locus to defer to.
750
867
  */
@@ -9,10 +9,30 @@
9
9
  * run.
10
10
  */
11
11
  import type { Harness } from "./assembly.js";
12
- import type { Member } from "./kind.js";
12
+ import type { EmbeddedMemberValue, Member } from "./kind.js";
13
13
  import type { Declarations } from "./declarations.js";
14
14
  import type { PayloadMember } from "./generated/index.js";
15
15
  export type { PayloadMember } from "./generated/index.js";
16
+ /**
17
+ * One composed embedded value as an edge target, carried with the host member whose body
18
+ * it lives in: an embedded member owns no file, so every path fact about it is derived
19
+ * from its host's own projection.
20
+ */
21
+ export interface EmbeddedTarget {
22
+ /** The member whose composed body carries the value — the projection it lives in. */
23
+ readonly host: Member;
24
+ /** The composed value itself. */
25
+ readonly value: EmbeddedMemberValue;
26
+ }
27
+ /**
28
+ * What one address in the member table names: a top-level composed member, or the
29
+ * embedded values one nested spelling reaches. The nested list carries one element for
30
+ * a full `<host-address>/<kind>/<key>` address, and one per host for a bare `kind:key` —
31
+ * a bare key names a nested member only when a single host carries it, and resolution
32
+ * refuses the rest as ambiguous rather than picking one: uniqueness is the resolver's
33
+ * bar, not the grammar's.
34
+ */
35
+ export type EdgeTarget = Member | readonly EmbeddedTarget[];
16
36
  /** What a mention may resolve against at emit. */
17
37
  export interface ResolveOptions {
18
38
  /** The addresses a mention may name — resolution-checked; a mention cannot dangle. */
@@ -23,12 +43,14 @@ export interface ResolveOptions {
23
43
  */
24
44
  readonly deferrableKinds?: ReadonlySet<string>;
25
45
  /**
26
- * The program's composed members by `kind:name` address — what an embedded value's
27
- * edge field resolves against to derive its target facts. An edge target never defers
28
- * to the gate the way a bare mention may: the facts are rendered into the projection
29
- * now, so an unresolved one has nothing true to place.
46
+ * The program's composed members by address — what an embedded value's edge field
47
+ * resolves against to derive its target facts. Top-level members index at their
48
+ * `kind:name` address, and each composed embedded value at both of its own spellings:
49
+ * its full `<host-address>/<kind>/<key>` address and its bare `kind:key`.
50
+ * An edge target never defers to the gate the way a bare mention may: the facts are
51
+ * rendered into the projection now, so an unresolved one has nothing true to place.
30
52
  */
31
- readonly members?: ReadonlyMap<string, Member>;
53
+ readonly members?: ReadonlyMap<string, EdgeTarget>;
32
54
  }
33
55
  /**
34
56
  * One fields-only registration member erased for the manifest write face: its key
package/dist/src/emit.js CHANGED
@@ -13,6 +13,7 @@ import { readFileSync } from "node:fs";
13
13
  import { checkMentions, isTextSpan, renderText, resolveLeaf } from "./prose.js";
14
14
  import { permissionUnion } from "./needs.js";
15
15
  import { compareStrings, compileDeclarations, declaredAddresses, declaredAtLocusKinds, declaredRequirements, encodeSeam, placementKey, registrationRows, settingsRows, tapHookRows, uniqueMap, } from "./declarations.js";
16
+ import { bareLookupKey, edgeLookupKey, hostAddress, nestedAddress } from "./member-address.js";
16
17
  /** The {@link MentionScope} a set of {@link ResolveOptions} names — its two sets, each defaulting to empty. */
17
18
  function scopeOf(options) {
18
19
  return {
@@ -93,7 +94,7 @@ function hostUnit(host, context) {
93
94
  if (host.facts.locus.kind === "at" && host.facts.unitShape === "directory") {
94
95
  return joinSlash(host.facts.locus.root, host.name);
95
96
  }
96
- throw new Error(`${context}: its host \`${host.kind}:${host.name}\` owns no directory unit — a template's ` +
97
+ throw new Error(`${context}: its host \`${hostAddress(host.kind, host.name)}\` owns no directory unit — a template's ` +
97
98
  `path pattern is relative to the host's unit, and a lone file has no interior for a ` +
98
99
  `child to sit in (specs/model/representation.md, "locus").`);
99
100
  }
@@ -115,7 +116,7 @@ function nestedFilePath(member) {
115
116
  }
116
117
  const template = (host.facts.templates ?? []).find((layer) => layer.kind.key === member.kind && layer.path !== undefined);
117
118
  if (template?.path === undefined) {
118
- throw new Error(`${context}: its host \`${host.kind}:${host.name}\` templates no file layer for kind ` +
119
+ throw new Error(`${context}: its host \`${hostAddress(host.kind, host.name)}\` templates no file layer for kind ` +
119
120
  `\`${member.kind}\` — the path pattern is the host kind's declared fact, and there is ` +
120
121
  `none to compose against (specs/model/representation.md, "locus").`);
121
122
  }
@@ -178,9 +179,10 @@ function relativeProjection(from, to) {
178
179
  * own field schema, which fails in the author's program at compose time.
179
180
  *
180
181
  * # Throws
181
- * If a filled leaf names no composed member, or names one that owns no projection to
182
- * point at. An edge target cannot defer to the gate the way a bare mention may: the
183
- * reference is written now, and there is nothing true to write.
182
+ * If a filled leaf names no composed member, names one that owns no projection to
183
+ * point at, or names a bare nested key several hosts carry
184
+ * ({@link resolvedTargetFacts}). An edge target cannot defer to the gate the way a bare
185
+ * mention may: the reference is written now, and there is nothing true to write.
184
186
  */
185
187
  function edgeTargetFacts(host, value, leaves, options) {
186
188
  const targets = {};
@@ -189,22 +191,40 @@ function edgeTargetFacts(host, value, leaves, options) {
189
191
  const address = leaves[edge.field];
190
192
  if (address === undefined || address === "")
191
193
  continue;
192
- // A one-element `to` set resolves a bare address within its one kind; a
193
- // multi-element set reads the kind-qualified `kind:name` the author wrote
194
- // (`EdgeField.to`). An already-qualified address carries its own colon, so
195
- // only a bare leaf is lifted to `${edge.to[0]}:${address}` for the lookup.
196
- const lookup = edge.to.length === 1 && !address.includes(":") ? `${edge.to[0]}:${address}` : address;
194
+ const lookup = edgeLookupKey(address, edge.to);
197
195
  const target = options.members?.get(lookup);
196
+ const reference = `${context}: edge field \`${edge.field}\` names \`${address}\``;
198
197
  if (target === undefined) {
199
- throw new Error(`${context}: edge field \`${edge.field}\` names \`${address}\`, which resolves to no ` +
200
- `composed member an edge target's facts are derived, never fabricated ` +
201
- `(specs/model/pipeline.md, "Emit", the "Refusing" bullet).`);
198
+ throw new Error(`${reference}, which resolves to no composed member an edge target's facts are ` +
199
+ `derived, never fabricated (specs/model/pipeline.md, "Emit", the "Refusing" bullet).`);
202
200
  }
203
- if (!isProjected(target)) {
204
- throw new Error(`${context}: edge field \`${edge.field}\` names \`${address}\`, which owns no ` +
205
- `projection to reference (specs/model/representation.md, "locus").`);
206
- }
207
- targets[edge.field] = {
201
+ targets[edge.field] = resolvedTargetFacts(host, target, lookup, reference);
202
+ }
203
+ return targets;
204
+ }
205
+ /** Whether an address named the nested index — the embedded values one spelling reaches. */
206
+ function isNested(target) {
207
+ return Array.isArray(target);
208
+ }
209
+ /**
210
+ * The four derived facts one resolved edge target contributes, read off the target
211
+ * itself and never off the citing instance. A top-level member answers with its own
212
+ * identity and its own projection; an embedded value answers with its own kind and key,
213
+ * its canonical `<host-address>/<kind>/<key>` address (whichever spelling the leaf
214
+ * authored), and its *host's* projection — an embedded member owns no file, so the file
215
+ * its rendering lands in is the host's.
216
+ *
217
+ * # Throws
218
+ * If a bare `kind:key` is carried by more than one host — an ambiguous address names
219
+ * nothing, and the full spelling is what tells the carriers apart — or if the target,
220
+ * or the host carrying it, owns no projection to point at.
221
+ */
222
+ function resolvedTargetFacts(host, target, lookup, reference) {
223
+ const noProjection = () => new Error(`${reference}, which owns no projection to reference (specs/model/representation.md, "locus").`);
224
+ if (!isNested(target)) {
225
+ if (!isProjected(target))
226
+ throw noProjection();
227
+ return {
208
228
  name: target.name,
209
229
  address: lookup,
210
230
  kind: target.kind,
@@ -212,7 +232,23 @@ function edgeTargetFacts(host, value, leaves, options) {
212
232
  repoRootedPath: projectionPath(target),
213
233
  };
214
234
  }
215
- return targets;
235
+ if (target.length > 1) {
236
+ const hosts = target.map((carrier) => `\`${hostAddress(carrier.host.kind, carrier.host.name)}\``).join(", ");
237
+ throw new Error(`${reference}, a bare key ${target.length} hosts carry (${hosts}) — a nested member's address ` +
238
+ `composes through its host, so spell the whole \`<host-address>/<kind>/<key>\` ` +
239
+ `(specs/model/representation.md, "member").`);
240
+ }
241
+ const { host: carrier, value } = target[0];
242
+ if (!isProjected(carrier))
243
+ throw noProjection();
244
+ const carrierPath = projectionPath(carrier);
245
+ return {
246
+ name: value.key,
247
+ address: nestedAddress(hostAddress(carrier.kind, carrier.name), value.kind, value.key),
248
+ kind: value.kind,
249
+ path: relativeProjection(projectionPath(host), carrierPath),
250
+ repoRootedPath: carrierPath,
251
+ };
216
252
  }
217
253
  /**
218
254
  * Resolve one embedded member's value's leaves — top-level and each
@@ -359,7 +395,7 @@ function edgePlacements(harness, options) {
359
395
  continue;
360
396
  const placed = placedEdges(member, value, options);
361
397
  if (placed !== undefined) {
362
- entries.push([placementKey(`${member.kind}:${member.name}`, value.kind, value.key), placed]);
398
+ entries.push([placementKey(hostAddress(member.kind, member.name), value.kind, value.key), placed]);
363
399
  }
364
400
  }
365
401
  }
@@ -400,7 +436,7 @@ function renderedExtents(harness, options) {
400
436
  continue;
401
437
  const block = renderMemberBlock(member, value, options);
402
438
  entries.push([
403
- placementKey(`${member.kind}:${member.name}`, value.kind, value.key),
439
+ placementKey(hostAddress(member.kind, member.name), value.kind, value.key),
404
440
  {
405
441
  lines: renderedLineCount(block),
406
442
  // Unicode scalar values, matching Rust's `chars().count()` — iterating a string
@@ -549,9 +585,12 @@ function settingsResidue(harness) {
549
585
  return settingsRows(harness).map((row) => ({ manifest: row.manifest, key: row.key, value: row.value }));
550
586
  }
551
587
  /**
552
- * The harness's composed members by `kind:name` address — the table an embedded value's
553
- * edge field resolves its target against. Keyed the identical way {@link declaredAddresses}
554
- * spells a member address, so an edge field and a mention name a member the same way.
588
+ * The harness's composed members by address — the table an embedded value's edge field
589
+ * resolves its target against. A top-level member keys the identical way
590
+ * {@link declaredAddresses} spells a member address, so an edge field and a mention name
591
+ * a member the same way; a nested member keys under both of its own spellings
592
+ * ({@link nestedTargets}), so an embedded edge target resolves at emit as a top-level one
593
+ * does.
555
594
  *
556
595
  * A projected member's address is its file, so two at one address are a collision and
557
596
  * refuse loud. A registration member's address is its *group key* — a `hook` registers on
@@ -562,14 +601,48 @@ function settingsResidue(harness) {
562
601
  * `emit` on every harness with two hooks on one event (0.0.16).
563
602
  */
564
603
  function memberTable(harness) {
565
- const table = uniqueMap(harness.members
566
- .filter((member) => !isRegistration(member))
567
- .map((member) => [`${member.kind}:${member.name}`, member]));
604
+ const table = uniqueMap([
605
+ ...harness.members
606
+ .filter((member) => !isRegistration(member))
607
+ .map((member) => [hostAddress(member.kind, member.name), member]),
608
+ ...nestedTargets(harness),
609
+ ]);
568
610
  for (const member of harness.members.filter(isRegistration)) {
569
- table.set(`${member.kind}:${member.name}`, member);
611
+ table.set(hostAddress(member.kind, member.name), member);
570
612
  }
571
613
  return table;
572
614
  }
615
+ /**
616
+ * Every composed embedded value as member-table entries, under both of its spellings: the
617
+ * full `<host-address>/<kind>/<key>` address, whose one carrier is the host whose body
618
+ * composed it — two of those coincident are a malformed lock, refused by the shared
619
+ * {@link uniqueMap} the way two members at one address are — and the bare `kind:key`,
620
+ * whose entry carries *every* host that spells it. A bare key names a nested member only
621
+ * when a single host carries it; several is ambiguous, refused by name at resolution
622
+ * ({@link resolvedTargetFacts}) rather than here, since uniqueness is the resolver's bar
623
+ * and not the corpus's — one key two hosts carry and nothing cites still composes.
624
+ */
625
+ function nestedTargets(harness) {
626
+ const qualified = [];
627
+ const bare = new Map();
628
+ for (const member of harness.members) {
629
+ if (member.prose?.kind !== "blocks")
630
+ continue;
631
+ for (const value of member.prose.values) {
632
+ if (isTextSpan(value))
633
+ continue;
634
+ const target = { host: member, value };
635
+ qualified.push([nestedAddress(hostAddress(member.kind, member.name), value.kind, value.key), [target]]);
636
+ const key = bareLookupKey(value.kind, value.key);
637
+ const carriers = bare.get(key);
638
+ if (carriers === undefined)
639
+ bare.set(key, [target]);
640
+ else
641
+ carriers.push(target);
642
+ }
643
+ }
644
+ return [...qualified, ...bare];
645
+ }
573
646
  /** The harness's projected members as payload members, deterministically kind-then-name ordered. */
574
647
  function orderedMembers(harness, options) {
575
648
  return [...harness.members]
@@ -578,7 +651,7 @@ function orderedMembers(harness, options) {
578
651
  .map((member) => ({
579
652
  kind: member.kind,
580
653
  name: member.name,
581
- host: member.host && `${member.host.kind}:${member.host.name}`,
654
+ host: member.host && hostAddress(member.host.kind, member.host.name),
582
655
  // The generated row carries a mutable field list; the member's is read-only,
583
656
  // so copy each pair into a fresh tuple — the same values, a shape the row accepts.
584
657
  fields: member.fields.map(([name, value]) => [name, value]),
@@ -49,7 +49,14 @@ export type ClauseRow = {
49
49
  */
50
50
  predicate: string;
51
51
  /**
52
- * The field (or marker) the predicate constrains, when it names one.
52
+ * The clause's **compiled label** segment ([`crate::contract::clause_label`]) the
53
+ * field the predicate constrains where it names one, and otherwise the identity the
54
+ * seam synthesizes from the arguments the row already carries (`section_contains`
55
+ * and `require_sections` name no field, so `clauseField` in
56
+ * `sdk/src/declarations.ts` lowers a `<heading>.<marker>` / joined-sections segment
57
+ * here instead). `None` where the predicate needs no segment to label uniquely. A
58
+ * synthesized segment labels only: those two predicates are reconstructed from
59
+ * their own `section`/`sections` columns, never round-tripped through this one.
53
60
  */
54
61
  field?: string;
55
62
  /**
@@ -18,11 +18,11 @@ export type { Capability } from "./needs.js";
18
18
  export { bash, capability, permissionUnion } from "./needs.js";
19
19
  export type { Charset, Clause, ExtentUnit, Predicate, Requirement, Severity, Verifier } from "./contract.js";
20
20
  export { allowedChars, clause, closedKeys, count, degree, deny, enumOf, extent, forbiddenKeys, formatPlacesEdges, globValid, maxLen, membership, mentionReachable, minLen, mustDefine, nameMatchesDir, optional, range, required, requireSections, requirement, script, sectionContains, shape, telemetry, type, unique, uniqueName, when, } from "./contract.js";
21
- export type { CollectionAddress, EdgeField, EdgeTargetFacts, EmbeddedMemberCollectionEntry, EmbeddedMemberValue, Format, KindDefinition, KindFacts, KindOptions, Layout, LayoutRegion, Locus, Member, MemberInit, Registration, ResolvedEmbeddedMemberCollectionEntry, ResolvedEmbeddedMemberValue, Shape, Template, UnitShape, } from "./kind.js";
22
- export { embeddedMemberValue, kind } from "./kind.js";
21
+ export type { CollectionAddress, EdgeField, EdgeTargetFacts, EmbeddedMemberCollectionEntry, EmbeddedMemberValue, Format, KindDefinition, KindFacts, KindOptions, KindRelocation, Layout, LayoutRegion, Locus, Member, MemberInit, Registration, ResolvedEmbeddedMemberCollectionEntry, ResolvedEmbeddedMemberValue, Shape, Template, UnitShape, } from "./kind.js";
22
+ export { embeddedMemberValue, kind, relocate } from "./kind.js";
23
23
  export type { Admission, EnforcementMode, ExpectBinding, Harness } from "./assembly.js";
24
24
  export { harness } from "./assembly.js";
25
- export type { EmitResult, RegistrationFact, ResolveOptions, SettingsResidue } from "./emit.js";
25
+ export type { EdgeTarget, EmbeddedTarget, EmitResult, RegistrationFact, ResolveOptions, SettingsResidue, } from "./emit.js";
26
26
  export { emit } from "./emit.js";
27
27
  export type { Dial, DialEntry } from "./dial.js";
28
28
  export { dial, dialDefaultContract } from "./dial.js";
package/dist/src/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  export { blocks, file, include, mentionOf, renderText, text } from "./prose.js";
16
16
  export { bash, capability, permissionUnion } from "./needs.js";
17
17
  export { allowedChars, clause, closedKeys, count, degree, deny, enumOf, extent, forbiddenKeys, formatPlacesEdges, globValid, maxLen, membership, mentionReachable, minLen, mustDefine, nameMatchesDir, optional, range, required, requireSections, requirement, script, sectionContains, shape, telemetry, type, unique, uniqueName, when, } from "./contract.js";
18
- export { embeddedMemberValue, kind } from "./kind.js";
18
+ export { embeddedMemberValue, kind, relocate } from "./kind.js";
19
19
  export { harness } from "./assembly.js";
20
20
  export { emit } from "./emit.js";
21
21
  export { dial, dialDefaultContract } from "./dial.js";
@@ -169,15 +169,30 @@ export interface Template {
169
169
  readonly path?: string;
170
170
  }
171
171
  /** The seven facts of a kind's runtime residue. */
172
- export interface KindFacts {
172
+ export type KindFacts = {
173
173
  /** Fact 1, label — the compiled debug label findings speak; the kind's name. */
174
174
  readonly name: string;
175
175
  /** The declared provider authority, when the kind qualifies by one. */
176
176
  readonly provider?: string;
177
+ /**
178
+ * The built-in kind this facts value **relocates** — set only by {@link relocate},
179
+ * from the base kind's own name, never authored. It is the authoring layer's
180
+ * *provenance* fact: the value was derived from the imported built-in, so a
181
+ * second same-named kind in play is a sanctioned relocation rather than a name
182
+ * collision. It never reaches a kind-fact row — the lock reader holds no base
183
+ * value to compare against and re-decides *structurally* instead
184
+ * (`src/compose.rs`'s `row_relocates_builtin`).
185
+ */
186
+ readonly relocates?: string;
177
187
  /** Fact 2, locus — where members live, and for a file locus whether their documents
178
188
  * are committed: a `local` commitment class declares the kind reviewed and its
179
189
  * members' documents not. */
180
- readonly locus: Locus;
190
+ readonly locus: {
191
+ readonly kind: "at";
192
+ readonly root: string;
193
+ readonly glob: string;
194
+ readonly commitment?: "local";
195
+ };
181
196
  /** Fact 3a, projection — the artifact format; omitted for a kind that declares none. */
182
197
  readonly format?: Format;
183
198
  /** Fact 3b, projection — the on-disk unit shape. */
@@ -186,15 +201,15 @@ export interface KindFacts {
186
201
  * the world reaches a member (never rivals — a member is live if any one is). */
187
202
  readonly registration: readonly Registration[];
188
203
  /**
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
- */
204
+ * The frontmatter key the member's name writes under. For `unitShape:
205
+ * "named-field"` this is the id **source** — the declared field a member's
206
+ * identity is read from (an agent's `name`), never the filename or directory.
207
+ * For `"directory"` it is a projection-order detail only (a skill's `name`
208
+ * still writes into frontmatter, but identity is the directory name); absent
209
+ * when identity is the file stem and no field carries it (a rule), or the
210
+ * starred directory segment (`"starred-segment"`) — both path-derived, never a
211
+ * field.
212
+ */
198
213
  readonly identityField?: string;
199
214
  /** Fact 5, edge fields — the kind's fields that are references to other members. */
200
215
  readonly edgeFields?: readonly EdgeField[];
@@ -216,7 +231,123 @@ export interface KindFacts {
216
231
  /** External-fact source backing the guidance — a doc URL plus retrieved date,
217
232
  * carried as data. */
218
233
  readonly cite?: string;
219
- }
234
+ } | {
235
+ /** Fact 1, label — the compiled debug label findings speak; the kind's name. */
236
+ readonly name: string;
237
+ /** The declared provider authority, when the kind qualifies by one. */
238
+ readonly provider?: string;
239
+ /**
240
+ * The built-in kind this facts value **relocates** — set only by {@link relocate},
241
+ * from the base kind's own name, never authored. It is the authoring layer's
242
+ * *provenance* fact: the value was derived from the imported built-in, so a
243
+ * second same-named kind in play is a sanctioned relocation rather than a name
244
+ * collision. It never reaches a kind-fact row — the lock reader holds no base
245
+ * value to compare against and re-decides *structurally* instead
246
+ * (`src/compose.rs`'s `row_relocates_builtin`).
247
+ */
248
+ readonly relocates?: string;
249
+ /** Fact 2, locus — where members live, and for a file locus whether their documents
250
+ * are committed: a `local` commitment class declares the kind reviewed and its
251
+ * members' documents not. */
252
+ readonly locus: {
253
+ readonly kind: "embedded";
254
+ };
255
+ /** Fact 3a, projection — the artifact format; omitted for a kind that declares none. */
256
+ readonly format?: Format;
257
+ /** Fact 3b, projection — the on-disk unit shape. */
258
+ readonly unitShape: UnitShape;
259
+ /** Fact 4, registration — embedded members register nothing. */
260
+ readonly registration: readonly [];
261
+ /**
262
+ * The frontmatter key the member's name writes under. For `unitShape:
263
+ * "named-field"` this is the id **source** — the declared field a member's
264
+ * identity is read from (an agent's `name`), never the filename or directory.
265
+ * For `"directory"` it is a projection-order detail only (a skill's `name`
266
+ * still writes into frontmatter, but identity is the directory name); absent
267
+ * when identity is the file stem and no field carries it (a rule), or the
268
+ * starred directory segment (`"starred-segment"`) — both path-derived, never a
269
+ * field.
270
+ */
271
+ readonly identityField?: string;
272
+ /** Fact 5, edge fields — the kind's fields that are references to other members. */
273
+ readonly edgeFields?: readonly EdgeField[];
274
+ /** Fact 6, content — a declared {@link Layout} over the body's heading tree; absent
275
+ * leaves the kind `file`-content (one verbatim prose body, the default). */
276
+ readonly content?: Layout;
277
+ /** Fact 6b, content — the fields-only body shape (`"fields"`, no body slot); absent
278
+ * leaves the kind body-bearing (`file` or a {@link Layout}). */
279
+ readonly shape?: Shape;
280
+ /** The registration member's {@link CollectionAddress} — which manifest and key path
281
+ * its registration surfaces at; absent for a kind that owns its own file locus. */
282
+ readonly collectionAddress?: CollectionAddress;
283
+ /** Fact 7, template — one {@link Template} per inner layer of nested members the kind
284
+ * hosts; absent for a kind that nests nothing. */
285
+ readonly templates?: readonly Template[];
286
+ /** Advisory authoring counsel for the kind as a whole — teaching at authoring time via
287
+ * `schema` hover or `explain`, carrying no predicate or severity (decision 0045). */
288
+ readonly guidance?: string;
289
+ /** External-fact source backing the guidance — a doc URL plus retrieved date,
290
+ * carried as data. */
291
+ readonly cite?: string;
292
+ } | {
293
+ /** Fact 1, label — the compiled debug label findings speak; the kind's name. */
294
+ readonly name: string;
295
+ /** The declared provider authority, when the kind qualifies by one. */
296
+ readonly provider?: string;
297
+ /**
298
+ * The built-in kind this facts value **relocates** — set only by {@link relocate},
299
+ * from the base kind's own name, never authored. It is the authoring layer's
300
+ * *provenance* fact: the value was derived from the imported built-in, so a
301
+ * second same-named kind in play is a sanctioned relocation rather than a name
302
+ * collision. It never reaches a kind-fact row — the lock reader holds no base
303
+ * value to compare against and re-decides *structurally* instead
304
+ * (`src/compose.rs`'s `row_relocates_builtin`).
305
+ */
306
+ readonly relocates?: string;
307
+ /** Fact 2, locus — where members live, and for a file locus whether their documents
308
+ * are committed: a `local` commitment class declares the kind reviewed and its
309
+ * members' documents not. */
310
+ readonly locus: {
311
+ readonly kind: "nested-file";
312
+ };
313
+ /** Fact 3a, projection — the artifact format; omitted for a kind that declares none. */
314
+ readonly format?: Format;
315
+ /** Fact 3b, projection — the on-disk unit shape. */
316
+ readonly unitShape: UnitShape;
317
+ /** Fact 4, registration — nested-file members register nothing. */
318
+ readonly registration: readonly [];
319
+ /**
320
+ * The frontmatter key the member's name writes under. For `unitShape:
321
+ * "named-field"` this is the id **source** — the declared field a member's
322
+ * identity is read from (an agent's `name`), never the filename or directory.
323
+ * For `"directory"` it is a projection-order detail only (a skill's `name`
324
+ * still writes into frontmatter, but identity is the directory name); absent
325
+ * when identity is the file stem and no field carries it (a rule), or the
326
+ * starred directory segment (`"starred-segment"`) — both path-derived, never a
327
+ * field.
328
+ */
329
+ readonly identityField?: string;
330
+ /** Fact 5, edge fields — the kind's fields that are references to other members. */
331
+ readonly edgeFields?: readonly EdgeField[];
332
+ /** Fact 6, content — a declared {@link Layout} over the body's heading tree; absent
333
+ * leaves the kind `file`-content (one verbatim prose body, the default). */
334
+ readonly content?: Layout;
335
+ /** Fact 6b, content — the fields-only body shape (`"fields"`, no body slot); absent
336
+ * leaves the kind body-bearing (`file` or a {@link Layout}). */
337
+ readonly shape?: Shape;
338
+ /** The registration member's {@link CollectionAddress} — which manifest and key path
339
+ * its registration surfaces at; absent for a kind that owns its own file locus. */
340
+ readonly collectionAddress?: CollectionAddress;
341
+ /** Fact 7, template — one {@link Template} per inner layer of nested members the kind
342
+ * hosts; absent for a kind that nests nothing. */
343
+ readonly templates?: readonly Template[];
344
+ /** Advisory authoring counsel for the kind as a whole — teaching at authoring time via
345
+ * `schema` hover or `explain`, carrying no predicate or severity (decision 0045). */
346
+ readonly guidance?: string;
347
+ /** External-fact source backing the guidance — a doc URL plus retrieved date,
348
+ * carried as data. */
349
+ readonly cite?: string;
350
+ };
220
351
  /**
221
352
  * One authored member — a typed value in the library. Kind identity travels by
222
353
  * import (`facts`), never by string; the
@@ -285,6 +416,73 @@ export interface KindOptions {
285
416
  * it builds, since it is erased before a member reaches emit.
286
417
  */
287
418
  export declare function kind<T extends object>(facts: KindFacts, options?: KindOptions): KindDefinition<T>;
419
+ /**
420
+ * A **relocation delta** — the facts a relocated built-in kind diverges from its base
421
+ * on. Two faces, either or both:
422
+ *
423
+ * - `edgeFields`, *added* to whatever the base already declares (never replacing them,
424
+ * which would drop a shipped kind's own edges silently). Each added field names a key
425
+ * of the relocated kind's typed surface `T`, so an edge can never be declared over a
426
+ * field the kind does not carry.
427
+ * - `governs`, *replacing* the base's `at` locus root and glob — moving where the kind's
428
+ * members are found, the one fact the engine's own overlay exists to apply
429
+ * (`src/compose.rs`'s `overlay_builtin_kind`).
430
+ *
431
+ * Every other fact — format, unit shape, registration, content, templates — rides
432
+ * through unchanged, which is exactly what makes the emitted row still read as a
433
+ * relocation rather than a name collision on the reading side.
434
+ */
435
+ export interface KindRelocation<T> {
436
+ /** The edge fields this relocation adds, each over a field of the kind's own surface. */
437
+ readonly edgeFields?: readonly {
438
+ readonly field: keyof T & string;
439
+ readonly to: readonly [string, ...string[]];
440
+ }[];
441
+ /**
442
+ * The locus this relocation moves the kind's members to — root and glob, the two
443
+ * columns the lock reader's overlay writes back. A file locus's `commitment` class is
444
+ * deliberately absent: the overlay writes `Governs { root, glob }` and nothing else, so
445
+ * a `commitment` delta would author a fact the reader drops in silence — a built-in's
446
+ * commitment class stays the built-in's.
447
+ */
448
+ readonly governs?: {
449
+ readonly root: string;
450
+ readonly glob: string;
451
+ };
452
+ }
453
+ /**
454
+ * **Relocate** a built-in kind: the sanctioned way an adopting corpus moves a kind it
455
+ * does not own to its own root, adds an edge field to it, or both. Returns a fresh
456
+ * constructor over the widened typed surface `T` (the base's fields plus the added edge
457
+ * fields, spelled by the caller as one interface), carrying the base's facts with
458
+ * `delta`'s edge fields appended, `delta`'s `governs` in place of the base's locus root
459
+ * and glob, and the base's own {@link KindDefinition.render} hook preserved. Ownership,
460
+ * not privilege — a relocated built-in is an ordinary kind value from here on, and its
461
+ * added edge reaches the lock as an assembly `edge` row keyed by `from`, exactly as any
462
+ * kind's does (`declarations.ts`), never as a column on a kind-fact row.
463
+ *
464
+ * A moved locus, by contrast, *is* a kind-fact row column pair: the emitted row carries
465
+ * the delta's `governs_root`/`governs_glob` while `format`, `unit_shape` and
466
+ * `registration` stay the base's — which is precisely the three-fact test the lock
467
+ * reader applies before overlaying the row onto its compiled-in built-in
468
+ * (`src/compose.rs`'s `row_relocates_builtin`), so the engine reads members at the new
469
+ * root rather than treating the row as a colliding kind.
470
+ *
471
+ * The produced facts carry `relocates`, naming the base — the marker that tells a
472
+ * legitimate relocation from a genuine name collision when two same-named kinds are in
473
+ * play. Identity travels by import: the base is the imported built-in value, so the
474
+ * provenance is proven here rather than inferred downstream.
475
+ *
476
+ * # Throws
477
+ * If an added edge field re-declares one the base already carries, or one another
478
+ * entry of the same delta already added — two `edge` rows over one `<from, field>`
479
+ * cross-wire the graph instead of declaring one relationship. If a `governs` delta
480
+ * names a base whose locus is not `at` ({@link relocatedLocus}). And if the delta
481
+ * declares neither face, which would mint a marker-bearing clone of the base — a second
482
+ * same-named kind diverging on nothing, which is a name collision spelled as a
483
+ * relocation.
484
+ */
485
+ export declare function relocate<T extends object>(base: KindDefinition<any>, delta: KindRelocation<T>): KindDefinition<T>;
288
486
  /**
289
487
  * One entry in a sibling collection: its own key plus its leaf fields
290
488
  * (`rejected."baked-projection"`) — an ordered list element, never a positional
@@ -390,8 +588,14 @@ export interface ResolvedEmbeddedMemberValue {
390
588
  * composed value's shape. A bare string names a kind whose facts are out of reach, so
391
589
  * such a value renders with no target facts.
392
590
  */
591
+ export declare function embeddedMemberValue<T extends object>(init: {
592
+ kind: KindDefinition<T>;
593
+ key: string;
594
+ leaves: Readonly<Record<keyof T, string | Text>>;
595
+ collections?: EmbeddedMemberValue["collections"];
596
+ }): EmbeddedMemberValue;
393
597
  export declare function embeddedMemberValue(init: {
394
- kind: string | KindDefinition<any>;
598
+ kind: string;
395
599
  key: string;
396
600
  leaves: Readonly<Record<string, string | Text>>;
397
601
  collections?: EmbeddedMemberValue["collections"];
package/dist/src/kind.js CHANGED
@@ -72,13 +72,83 @@ export function kind(facts, options = {}) {
72
72
  return Object.assign(construct, { facts, key: facts.name, render: options.render });
73
73
  }
74
74
  /**
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.
75
+ * The locus a `governs` delta moves the base to: the base's `at` root and glob replaced,
76
+ * every other locus fact (its commitment class among them) riding through.
77
+ *
78
+ * # Throws
79
+ * If the base's locus is not `at` an embedded or nested-file kind governs no glob at
80
+ * all (its members compose their paths from a host's unit, or own no file), so there is
81
+ * no locus to move and its row carries no `governs` columns to move it to.
82
+ */
83
+ function relocatedLocus(base, governs) {
84
+ if (base.locus.kind !== "at") {
85
+ throw new Error(`relocating kind \`${base.name}\`: a \`governs\` delta moves the ` +
86
+ `path glob a kind's members are found at, and this kind's locus is \`${base.locus.kind}\` ` +
87
+ `— it governs no glob of its own. Drop the \`governs\` face of the delta ` +
88
+ `(specs/model/representation.md, "locus").`);
89
+ }
90
+ return { ...base.locus, root: governs.root, glob: governs.glob };
91
+ }
92
+ /**
93
+ * **Relocate** a built-in kind: the sanctioned way an adopting corpus moves a kind it
94
+ * does not own to its own root, adds an edge field to it, or both. Returns a fresh
95
+ * constructor over the widened typed surface `T` (the base's fields plus the added edge
96
+ * fields, spelled by the caller as one interface), carrying the base's facts with
97
+ * `delta`'s edge fields appended, `delta`'s `governs` in place of the base's locus root
98
+ * and glob, and the base's own {@link KindDefinition.render} hook preserved. Ownership,
99
+ * not privilege — a relocated built-in is an ordinary kind value from here on, and its
100
+ * added edge reaches the lock as an assembly `edge` row keyed by `from`, exactly as any
101
+ * kind's does (`declarations.ts`), never as a column on a kind-fact row.
102
+ *
103
+ * A moved locus, by contrast, *is* a kind-fact row column pair: the emitted row carries
104
+ * the delta's `governs_root`/`governs_glob` while `format`, `unit_shape` and
105
+ * `registration` stay the base's — which is precisely the three-fact test the lock
106
+ * reader applies before overlaying the row onto its compiled-in built-in
107
+ * (`src/compose.rs`'s `row_relocates_builtin`), so the engine reads members at the new
108
+ * root rather than treating the row as a colliding kind.
109
+ *
110
+ * The produced facts carry `relocates`, naming the base — the marker that tells a
111
+ * legitimate relocation from a genuine name collision when two same-named kinds are in
112
+ * play. Identity travels by import: the base is the imported built-in value, so the
113
+ * provenance is proven here rather than inferred downstream.
114
+ *
115
+ * # Throws
116
+ * If an added edge field re-declares one the base already carries, or one another
117
+ * entry of the same delta already added — two `edge` rows over one `<from, field>`
118
+ * cross-wire the graph instead of declaring one relationship. If a `governs` delta
119
+ * names a base whose locus is not `at` ({@link relocatedLocus}). And if the delta
120
+ * declares neither face, which would mint a marker-bearing clone of the base — a second
121
+ * same-named kind diverging on nothing, which is a name collision spelled as a
122
+ * relocation.
81
123
  */
124
+ export function relocate(base, delta) {
125
+ const declaredEdges = delta.edgeFields ?? [];
126
+ if (declaredEdges.length === 0 && delta.governs === undefined) {
127
+ throw new Error(`relocating kind \`${base.facts.name}\`: a relocation declares at least one diverging ` +
128
+ `fact — \`edgeFields\`, \`governs\`, or both. A delta declaring neither mints a ` +
129
+ `second kind of the base's own name that diverges on nothing, which reads as a name ` +
130
+ `collision rather than a relocation (specs/model/representation.md, "kind").`);
131
+ }
132
+ const locus = delta.governs === undefined ? undefined : relocatedLocus(base.facts, delta.governs);
133
+ const inherited = base.facts.edgeFields ?? [];
134
+ const claimed = new Set(inherited.map((edge) => edge.field));
135
+ const added = [];
136
+ for (const edge of declaredEdges) {
137
+ if (claimed.has(edge.field)) {
138
+ throw new Error(`relocating kind \`${base.facts.name}\`: edge field \`${edge.field}\` is already ` +
139
+ `declared, and a second declaration of one field cross-wires the graph rather than ` +
140
+ `adding a relationship (specs/model/representation.md, "kind").`);
141
+ }
142
+ claimed.add(edge.field);
143
+ added.push({ field: edge.field, to: edge.to });
144
+ }
145
+ const edgeFields = [...inherited, ...added];
146
+ const relocated = { ...base.facts, relocates: base.facts.name, edgeFields };
147
+ // Two spellings, not one with an optional `locus`: `KindFacts` is a union discriminated
148
+ // on the locus, so the moved case must carry the `at` locus as its own literal branch.
149
+ const facts = locus === undefined ? relocated : { ...relocated, locus };
150
+ return kind(facts, { render: base.render });
151
+ }
82
152
  export function embeddedMemberValue(init) {
83
153
  const definition = typeof init.kind === "string" ? undefined : init.kind;
84
154
  const render = definition?.render;
@@ -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
+ }
@@ -19,8 +19,8 @@ export interface Mentionable {
19
19
  }
20
20
  /**
21
21
  * 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.
22
+ * {@link hostAddress}, its bare name the display text — the convention every corpus
23
+ * repeats to cite a member from prose.
24
24
  */
25
25
  export declare function mentionOf(member: Member): Mentionable;
26
26
  /** One authored interpolation: position in the template plus its target. */
@@ -57,11 +57,11 @@ export interface MentionScope {
57
57
  }
58
58
  /**
59
59
  * 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.
60
+ * emit: a host address ({@link parseHostAddress}) whose kind is one the program declares
61
+ * at a discovery locus (an `at`-locus kind) may name a member discovered on disk, so
62
+ * `check` owns the verdict. Anything the grammar's reader answers `undefined` for a
63
+ * segmented embedded address, a bare requirement name, a name-less `kind:` names no
64
+ * discoverable member, and stays a dangling refusal however deferrable its head reads.
65
65
  */
66
66
  export declare function defersToGate(address: string, deferrableKinds: ReadonlySet<string>): boolean;
67
67
  /**
package/dist/src/prose.js CHANGED
@@ -9,27 +9,26 @@
9
9
  * **include** (an {@link Include}) pulls the target file's bytes into the host's emitted
10
10
  * projection. Both are authored per word and resolution-checked at emit, never mined.
11
11
  */
12
+ import { hostAddress, parseHostAddress } from "./member-address.js";
12
13
  /**
13
14
  * 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.
15
+ * {@link hostAddress}, its bare name the display text — the convention every corpus
16
+ * repeats to cite a member from prose.
16
17
  */
17
18
  export function mentionOf(member) {
18
- return { address: `${member.kind}:${member.name}`, display: member.name };
19
+ return { address: hostAddress(member.kind, member.name), display: member.name };
19
20
  }
20
21
  /**
21
22
  * 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.
23
+ * emit: a host address ({@link parseHostAddress}) whose kind is one the program declares
24
+ * at a discovery locus (an `at`-locus kind) may name a member discovered on disk, so
25
+ * `check` owns the verdict. Anything the grammar's reader answers `undefined` for a
26
+ * segmented embedded address, a bare requirement name, a name-less `kind:` names no
27
+ * discoverable member, and stays a dangling refusal however deferrable its head reads.
27
28
  */
28
29
  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));
30
+ const host = parseHostAddress(address);
31
+ return host !== undefined && deferrableKinds.has(host.kind);
33
32
  }
34
33
  /**
35
34
  * Refuse a mention whose address neither resolves against the scope's `mentionable`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtmd/temper",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
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.18",
56
+ "@dtmd/temper-win32-x64": "0.0.18"
57
57
  }
58
58
  }