@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.
@@ -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,20 +62,31 @@ 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,
67
69
  count: predicate.key === "count"
68
70
  ? { min: predicate.args?.min ?? 0, max: predicate.args?.max ?? Number.MAX_SAFE_INTEGER }
69
71
  : undefined,
70
- target: predicate.key === "membership" ? predicate.target : undefined,
72
+ // The requirement-name column `membership`'s allowed-value target and
73
+ // `reached-from`'s closure roots share — one naming scheme for "the requirement
74
+ // whose satisfiers this clause reads" (`src/contract.rs` `predicate_from_row`).
75
+ target: predicate.key === "membership" || predicate.key === "reached-from"
76
+ ? predicate.target
77
+ : undefined,
71
78
  degree: predicate.key === "degree"
72
79
  ? {
73
80
  incoming: edgeBoundArgs(predicate.args, "incoming"),
74
81
  outgoing: edgeBoundArgs(predicate.args, "outgoing"),
75
82
  }
76
83
  : undefined,
84
+ // The by-incidence field set rides its own shared column, not the direction-only
85
+ // `degree` bound — the slot `reached-from`'s via set joins it. Copied into a
86
+ // fresh array: the predicate's set is read-only, the row's column is not.
87
+ fields: (predicate.key === "degree" || predicate.key === "reached-from") && predicate.fields
88
+ ? [...predicate.fields]
89
+ : undefined,
77
90
  gate: predicate.key === "mention-reachable" ? predicate.gate : undefined,
78
91
  value_type: predicate.key === "type" && predicate.value_type ? [...predicate.value_type] : undefined,
79
92
  shape: predicate.key === "shape" ? predicate.shape : undefined,
@@ -103,6 +116,57 @@ function clauseRow(clause, kind) {
103
116
  : undefined,
104
117
  };
105
118
  }
119
+ /**
120
+ * The `field` column for one predicate: the field it names, or — for the four
121
+ * predicates that name a *section*, a field *set*, or a *requirement* rather than a
122
+ * field — an identity synthesized from the arguments the row already carries.
123
+ *
124
+ * `section_contains`, `require_sections`, `degree` and `reached-from` set no `field`,
125
+ * and the column is what emit stamps a clause's label from (`stamp_clause_label`,
126
+ * `src/drift.rs`), so reading `Predicate.field` folds every clause of one of those
127
+ * predicates on one kind into one label — two rows wearing one label, which
128
+ * admissibility refuses as a malformed lock. Synthesizing here keeps the whole fix at
129
+ * the lowering: the Rust reader reconstructs all four predicates from the
130
+ * `section`/`sections`/`fields`/`target` columns and never from this one, so nothing
131
+ * round-trips through the synthesized text.
132
+ */
133
+ function clauseField(predicate) {
134
+ if (predicate.key === "section_contains") {
135
+ const { section } = predicate;
136
+ return section === undefined ? undefined : `${section.heading}.${section.marker}`;
137
+ }
138
+ if (predicate.key === "require_sections") {
139
+ // Joined with `+` rather than the label's own `.`, so the segment reads as the
140
+ // set it is and two different heading lists cannot fold to one label.
141
+ return predicate.sections?.join("+");
142
+ }
143
+ if (predicate.key === "degree") {
144
+ // A `degree` clause names no field of its own: its by-incidence filter is what
145
+ // distinguishes two bounds on one kind, so the filter is the segment — sorted and
146
+ // `+`-joined by the rule directly above, so two field lists spelling one set land
147
+ // one address and two different sets never fold to one. An unfiltered bound adds
148
+ // no segment: it ranges over every edge at the member, so two of them on one kind
149
+ // are a redundancy the author collapses, never a distinction an address must hold.
150
+ return predicate.fields === undefined ? undefined : [...predicate.fields].sort().join("+");
151
+ }
152
+ if (predicate.key === "reached-from") {
153
+ // A `reached-from` clause names no field either: its identity is the pair it walks.
154
+ // The roots requirement leads, and a declared via set follows after a `.` — the
155
+ // two-argument `section_contains` precedent — sorted and `+`-joined by `degree`'s
156
+ // rule directly above. So two closures rooted at one requirement over different
157
+ // arcs land two addresses instead of folding into one, and two spellings of one via
158
+ // set land one. An unfiltered closure adds no second segment: the roots are its
159
+ // whole identity, and two of them on one kind are a redundancy the author
160
+ // collapses.
161
+ const { target } = predicate;
162
+ if (target === undefined)
163
+ return undefined;
164
+ return predicate.fields === undefined
165
+ ? target
166
+ : `${target}.${[...predicate.fields].sort().join("+")}`;
167
+ }
168
+ return predicate.field;
169
+ }
106
170
  /** `min_len`/`max_len`/`extent`'s scalar bound off their shared `min`/`max`
107
171
  * args keys — `undefined` for every other predicate, and for these three when
108
172
  * neither endpoint is present. */
@@ -240,6 +304,16 @@ function collectionAddressRow(facts) {
240
304
  entry_shape: facts.collectionAddress.entryShape,
241
305
  };
242
306
  }
307
+ /**
308
+ * Lower a kind's exhaustive leaf-set witness into the row's `leaves` column: the
309
+ * witness's own keys, in the order its author declared them (`kind.ts`'s `LeafSet`,
310
+ * decision 0053 — the type is the declaration, and TypeScript erases it, so the record is
311
+ * how the corpus hands the key set over). `undefined` for a kind declaring no witness, so
312
+ * its row omits the column.
313
+ */
314
+ function leafSetRow(leaves) {
315
+ return leaves === undefined ? undefined : Object.keys(leaves);
316
+ }
243
317
  /**
244
318
  * One kind's fact row — an `at` locus supplies `governs_root`/`governs_glob` and any
245
319
  * other locus neither (a nested-file kind's path composes from its host's unit and the
@@ -247,7 +321,8 @@ function collectionAddressRow(facts) {
247
321
  * A file locus's `commitment` class rides the same spelling, absent for the committed
248
322
  * default. `templates` names the embedded kinds the corpus admits over it, and `content`
249
323
  * lowers a declared layout (absent for a `file`-content kind). A registration kind
250
- * extends the row with its `shape` marker and `collection_address`. Advisory
324
+ * extends the row with its `shape` marker and `collection_address`. A declared leaf-set
325
+ * witness lowers to `leaves` ([`leafSetRow`]), absent for a kind declaring none. Advisory
251
326
  * `guidance`/`cite` pair rides alongside, locus-optional so an embedded kind's own
252
327
  * counsel reaches the lock the same way a nested-file kind's already does (decision
253
328
  * 0045) — callable for any locus; [`kindFactKindsInPlay`] decides which embedded kinds
@@ -267,11 +342,95 @@ function kindFactRow(facts, admissions) {
267
342
  templates: templatesFor(facts, admissions),
268
343
  content: contentRow(facts.content),
269
344
  shape: facts.shape,
345
+ leaves: leafSetRow(facts.leaves),
270
346
  collection_address: collectionAddressRow(facts),
271
347
  guidance: facts.guidance,
272
348
  cite: facts.cite,
273
349
  };
274
350
  }
351
+ /**
352
+ * The locus a colliding kind is declared at, for {@link resolveNameCollision}'s refusal:
353
+ * an `at` kind names the path its members are found at, and every other locus names
354
+ * itself — a kind that governs no glob has no path of its own to name.
355
+ */
356
+ function locusLabel(facts) {
357
+ const { locus } = facts;
358
+ if (locus.kind !== "at")
359
+ return `the \`${locus.kind}\` locus`;
360
+ return `\`${locus.root === "." ? locus.glob : `${locus.root}/${locus.glob}`}\``;
361
+ }
362
+ /**
363
+ * A kind's facts with the three faces a relocation may diverge from its base on erased:
364
+ * the `relocates` marker, the appended `edgeFields`, and an `at` locus's moved root and
365
+ * glob (`kind.ts`'s `KindRelocation`). Two values agreeing here are one kind up to a
366
+ * relocation delta; the delta's own faces are checked on their own terms.
367
+ */
368
+ function relocationInvariants(facts) {
369
+ const bare = { ...facts };
370
+ delete bare.relocates;
371
+ delete bare.edgeFields;
372
+ if (facts.locus.kind === "at")
373
+ bare.locus = { ...facts.locus, root: "", glob: "" };
374
+ return bare;
375
+ }
376
+ /**
377
+ * Whether `relocation` was derived from `base` — the provenance the relocation-wins rule
378
+ * rests on. The marker names the base, `relocate` only ever *appends* edge fields (so the
379
+ * base's are a prefix of the relocation's), and every other fact rides through unchanged:
380
+ * the base's facts are a subset by construction, which is exactly what makes keeping the
381
+ * relocation lossless. Checked rather than assumed, because the marker carries a name and
382
+ * a name is what is in dispute — a value bearing it whose facts are not the other's
383
+ * superset is a third kind of the same name, and dropping *that* would be the very silent
384
+ * loss this decision exists to end.
385
+ */
386
+ function isRelocationOf(relocation, base) {
387
+ if (relocation.relocates !== base.name || base.relocates !== undefined)
388
+ return false;
389
+ const inherited = base.edgeFields ?? [];
390
+ const declared = relocation.edgeFields ?? [];
391
+ return (declared.length >= inherited.length &&
392
+ isDeepStrictEqual(declared.slice(0, inherited.length), inherited) &&
393
+ isDeepStrictEqual(relocationInvariants(relocation), relocationInvariants(base)));
394
+ }
395
+ /**
396
+ * Which of two `KindFacts` values sharing one name is the kind in play — a decision that
397
+ * must not depend on the order the two arrived in, since {@link kindsInPlay} draws its
398
+ * facts from `members`, then `expect`, then `admit`, then embedded templates, and a kind
399
+ * legitimately reaches it through any of them.
400
+ *
401
+ * A **relocation** wins over its base whichever way round they arrive
402
+ * ({@link isRelocationOf} proves the provenance): the base's facts are the relocation's
403
+ * minus the delta, so keeping the base would silently drop the relocation's added edge
404
+ * fields and moved locus. Two values that are structurally equal are one kind declared
405
+ * twice — keep the first. Anything else is a genuine collision and refuses: kind identity
406
+ * travels by import, never by string (`representation.md`, "kind"), while the lock is a
407
+ * string-keyed medium, so the second kind has no name to reach the engine
408
+ * under and dropping it is a silent loss of everything it declared. Two relocations of
409
+ * one base collide the same way — each is still a kind of the base's own name.
410
+ *
411
+ * # Throws
412
+ * On a collision, naming the kind and both loci — the two declarations an author has to
413
+ * go look at.
414
+ */
415
+ function resolveNameCollision(held, arriving) {
416
+ // At most one holds: each direction demands the other value carry no marker at all.
417
+ if (isRelocationOf(held, arriving))
418
+ return held;
419
+ if (isRelocationOf(arriving, held))
420
+ return arriving;
421
+ if (isDeepStrictEqual(held, arriving))
422
+ return held;
423
+ throw new Error(held.relocates !== undefined && arriving.relocates !== undefined
424
+ ? `two relocations of kind \`${held.name}\` are in play, at ${locusLabel(held)} and ` +
425
+ `${locusLabel(arriving)}, and they diverge. A relocation is still a kind of its base's ` +
426
+ `name, so a second one collides with the first: relocate the base once and import that ` +
427
+ `one value wherever it is used (specs/model/representation.md, "kind").`
428
+ : `two kinds named \`${held.name}\` are in play, at ${locusLabel(held)} and ` +
429
+ `${locusLabel(arriving)}. Kind identity travels by import, never by string, and the lock ` +
430
+ `is keyed by name — the second kind has no name of its own to reach the engine under. ` +
431
+ `Import the declared kind rather than redeclaring it; to move a built-in to another root ` +
432
+ `or add an edge field to it, use \`relocate()\` (specs/model/representation.md, "kind").`);
433
+ }
275
434
  /**
276
435
  * Every kind in play, at any locus — member kinds ∪ expect kinds ∪ their embedded
277
436
  * children — name-sorted, so every family derived from it inherits one stable order.
@@ -285,15 +444,24 @@ function kindFactRow(facts, admissions) {
285
444
  * Only *embedded* children are drawn in. A path-carrying template is the nested-file
286
445
  * layer, whose child owns a unit and reaches the lock through `expect` like any other
287
446
  * unit kind; pulling one in here would forge it a kind-fact row it never declared.
447
+ *
448
+ * Two facts values arriving under one name are decided by {@link resolveNameCollision},
449
+ * never by arrival order: the deduped facts are what `assemblyFactRows` derives every
450
+ * `edge` row from, so first-wins would drop a relocation's added edge on the floor
451
+ * whenever the base happened to be named first.
288
452
  */
289
453
  function kindsInPlay(harness) {
290
454
  const byName = new Map();
291
455
  const pending = [];
292
456
  const admit = (facts) => {
293
- if (byName.has(facts.name))
457
+ const held = byName.get(facts.name);
458
+ if (held === facts)
459
+ return;
460
+ const winner = held === undefined ? facts : resolveNameCollision(held, facts);
461
+ if (winner === held)
294
462
  return;
295
- byName.set(facts.name, facts);
296
- pending.push(facts);
463
+ byName.set(facts.name, winner);
464
+ pending.push(winner);
297
465
  };
298
466
  for (const member of harness.members)
299
467
  admit(member.facts);
@@ -317,13 +485,20 @@ function atLocusKindsInPlay(allKinds) {
317
485
  /**
318
486
  * The distinct kinds in play that take a kind-fact row: every non-embedded locus
319
487
  * unconditionally (a nested-file kind owns a file the engine must place, and places it
320
- * off its row, though it governs no glob to be discovered at), plus an embedded kind
321
- * only when it declares `guidance` or `cite` of its own (decision 0045) an embedded
322
- * kind with neither has nothing for the row to carry, and its members already reach the
323
- * corpus through their host's `templates` column alone (`kindsInPlay`), never the row.
488
+ * off its row, though it governs no glob to be discovered at), plus an embedded kind that
489
+ * has something of its own for a row to carry: `guidance` or `cite` (decision 0045), or a
490
+ * declared leaf set (decision 0053). The leaf set is on this list for the reason the
491
+ * column exists it teaches what a child carries *before* the surface holds a member of
492
+ * it, and an embedded kind taking no row never reaches the lock to teach it. An embedded
493
+ * kind declaring none of the three has nothing for the row to carry, and its members
494
+ * already reach the corpus through their host's `templates` column alone (`kindsInPlay`),
495
+ * never the row.
324
496
  */
325
497
  function kindFactKindsInPlay(allKinds) {
326
- return allKinds.filter((facts) => facts.locus.kind !== "embedded" || facts.guidance !== undefined || facts.cite !== undefined);
498
+ return allKinds.filter((facts) => facts.locus.kind !== "embedded" ||
499
+ facts.guidance !== undefined ||
500
+ facts.cite !== undefined ||
501
+ facts.leaves !== undefined);
327
502
  }
328
503
  /** The requirement rows — assembly `require` and every member's `requires`, one namespace. */
329
504
  function requirementRows(harness) {
@@ -388,7 +563,7 @@ function assemblyFactRows(harness, kinds) {
388
563
  function satisfiesRows(harness) {
389
564
  const rows = [];
390
565
  for (const member of harness.members) {
391
- const address = `${member.kind}:${member.name}`;
566
+ const address = hostAddress(member.kind, member.name);
392
567
  for (const requirement of member.satisfies) {
393
568
  rows.push({ member: address, requirement });
394
569
  }
@@ -409,7 +584,7 @@ function satisfiesRows(harness) {
409
584
  function mentionRows(harness) {
410
585
  const rows = [];
411
586
  for (const member of harness.members) {
412
- const address = `${member.kind}:${member.name}`;
587
+ const address = hostAddress(member.kind, member.name);
413
588
  if (member.prose?.kind === "text") {
414
589
  for (const mention of member.prose.mentions) {
415
590
  rows.push({ member: address, target: mention.target.address });
@@ -435,13 +610,11 @@ function mentionRows(harness) {
435
610
  * contribute — top-level leaves addressed by their bare field name, a
436
611
  * collection entry's leaves addressed `<collection>.<entry>.<field>` (one layer
437
612
  * 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.
613
+ * structural address ({@link leafAddress}). A bare-string leaf names no mention.
441
614
  */
442
615
  function embeddedLeafMentionRows(hostName, value) {
443
616
  const rows = [];
444
- const addressed = (childPath) => `${hostName}/${value.kind}/${value.key}/${childPath}`;
617
+ const addressed = (childPath) => leafAddress(hostName, value.kind, value.key, childPath);
445
618
  for (const [field, leaf] of Object.entries(value.leaves)) {
446
619
  if (typeof leaf === "string")
447
620
  continue;
@@ -478,7 +651,7 @@ function includeRows(harness) {
478
651
  rows.push({ member: address, source_path: fileURLToPath(new URL(include.path, include.moduleUrl)) });
479
652
  };
480
653
  for (const member of harness.members) {
481
- const address = `${member.kind}:${member.name}`;
654
+ const address = hostAddress(member.kind, member.name);
482
655
  if (member.prose?.kind === "text") {
483
656
  for (const include of member.prose.includes)
484
657
  push(address, include);
@@ -494,6 +667,27 @@ function includeRows(harness) {
494
667
  }
495
668
  return rows;
496
669
  }
670
+ /**
671
+ * The `input` rows — every member's declared inputs, in member-then-authored order.
672
+ * Each carries the declaring member's `kind:name` address and the input's path resolved
673
+ * against the stating module ({@link fileURLToPath} over the input's own `moduleUrl`),
674
+ * never the workspace — exactly as an include's is; the engine reads and fingerprints
675
+ * it, and splices nothing.
676
+ *
677
+ * Unlike an include, an input pairs with no body slot, so nothing downstream depends on
678
+ * this order — the authored one is kept anyway, so a re-emit is byte-stable and the
679
+ * author reads their own declarations back.
680
+ */
681
+ function inputRows(harness) {
682
+ const rows = [];
683
+ for (const member of harness.members) {
684
+ const address = hostAddress(member.kind, member.name);
685
+ for (const declared of member.inputs) {
686
+ rows.push({ member: address, source_path: fileURLToPath(new URL(declared.path, declared.moduleUrl)) });
687
+ }
688
+ }
689
+ return rows;
690
+ }
497
691
  /**
498
692
  * One composed embedded value's key in an {@link EdgePlacements} table — its host's
499
693
  * `kind:name` address plus the value's own kind and key, the same triple the
@@ -591,7 +785,7 @@ function nestedMemberRows(harness, admissions, scope, placements, extents) {
591
785
  for (const member of harness.members) {
592
786
  if (member.prose?.kind !== "blocks")
593
787
  continue;
594
- const host = `${member.kind}:${member.name}`;
788
+ const host = hostAddress(member.kind, member.name);
595
789
  for (const value of member.prose.values) {
596
790
  if (isTextSpan(value))
597
791
  continue;
@@ -729,22 +923,22 @@ export function declaredRequirements(harness) {
729
923
  export function declaredAddresses(harness) {
730
924
  const set = declaredRequirements(harness);
731
925
  for (const member of harness.members) {
732
- set.add(`${member.kind}:${member.name}`);
926
+ set.add(hostAddress(member.kind, member.name));
733
927
  if (member.prose?.kind !== "blocks")
734
928
  continue;
735
929
  for (const value of member.prose.values) {
736
930
  if (isTextSpan(value))
737
931
  continue;
738
- set.add(`${member.kind}:${member.name}/${value.kind}/${value.key}`);
932
+ set.add(nestedAddress(hostAddress(member.kind, member.name), value.kind, value.key));
739
933
  }
740
934
  }
741
935
  return set;
742
936
  }
743
937
  /**
744
938
  * 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
939
+ * dangling mention is measured against (`prose.ts`'s `defersToGate`): a mention whose
940
+ * address is a host address of one of these and names no composed value defers to
941
+ * `check`, while a mention naming no declared kind refuses at emit. Member kinds ∪ `expect` kinds; an embedded
748
942
  * kind is excluded — its members are composed within a host, never discovered, so a
749
943
  * flat `kind:name` mention of one has no discovery locus to defer to.
750
944
  */
@@ -774,6 +968,15 @@ export function compileDeclarations(harness, placements, extents) {
774
968
  clauses.push(clauseRow(clause, binding.kind.key));
775
969
  }
776
970
  }
971
+ // The root member's own clauses — the third source of a clause row, beside `expect`'s
972
+ // kind-keyed rows and a requirement's nested ones. They lower with **no** `kind`
973
+ // column: that absence at the top level is the discriminator
974
+ // `compose::root_contract_from_rows` reads, and `drift::stamp_clause_labels` addresses
975
+ // them under the `root` owner segment. Declaration order within the array, appended
976
+ // past the kind-sorted `expect` rows — a fixed position, so double emit is byte-stable.
977
+ for (const clause of harness.contract) {
978
+ clauses.push(clauseRow(clause, undefined));
979
+ }
777
980
  return {
778
981
  kinds: kindFactKindsInPlay(allKinds).map((facts) => kindFactRow(facts, admissions)),
779
982
  clauses,
@@ -782,6 +985,7 @@ export function compileDeclarations(harness, placements, extents) {
782
985
  satisfies: satisfiesRows(harness),
783
986
  mentions: mentionRows(harness),
784
987
  includes: includeRows(harness),
988
+ inputs: inputRows(harness),
785
989
  nested_members: nestedMemberRows(harness, admissions, mentionScope(harness), placements, extents),
786
990
  registrations: [...registrationRows(harness), ...tapHookRows(harness)],
787
991
  settings: settingsRows(harness),
@@ -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