@dtmd/temper 0.0.16 → 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.
- package/dist/src/declarations.d.ts +3 -3
- package/dist/src/declarations.js +134 -17
- package/dist/src/emit.d.ts +28 -6
- package/dist/src/emit.js +113 -26
- package/dist/src/generated/ClauseRow.d.ts +8 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/kind.d.ts +217 -13
- package/dist/src/kind.js +76 -6
- package/dist/src/member-address.d.ts +102 -0
- package/dist/src/member-address.js +114 -0
- package/dist/src/prose.d.ts +7 -7
- package/dist/src/prose.js +11 -12
- package/package.json +3 -3
|
@@ -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
|
|
110
|
-
* one of these
|
|
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
|
*/
|
package/dist/src/declarations.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
296
|
-
pending.push(
|
|
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 =
|
|
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 =
|
|
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
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
746
|
-
* one of these
|
|
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
|
*/
|
package/dist/src/emit.d.ts
CHANGED
|
@@ -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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
182
|
-
* point at
|
|
183
|
-
*
|
|
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
|
-
|
|
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(`${
|
|
200
|
-
`
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,12 +585,63 @@ 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
|
|
553
|
-
*
|
|
554
|
-
* spells a member address, so an edge field and a mention name
|
|
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.
|
|
594
|
+
*
|
|
595
|
+
* A projected member's address is its file, so two at one address are a collision and
|
|
596
|
+
* refuse loud. A registration member's address is its *group key* — a `hook` registers on
|
|
597
|
+
* its event, and Claude Code admits any number of matcher groups per event — so several
|
|
598
|
+
* legitimately share one address and the table keeps the last composed (the pre-0.0.16
|
|
599
|
+
* reading; an edge targeting that shared address resolves ambiguously, the open fork
|
|
600
|
+
* `(hook-member-identity)` in `.flume/plan/open-questions.md`). Refusing them broke
|
|
601
|
+
* `emit` on every harness with two hooks on one event (0.0.16).
|
|
555
602
|
*/
|
|
556
603
|
function memberTable(harness) {
|
|
557
|
-
|
|
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
|
+
]);
|
|
610
|
+
for (const member of harness.members.filter(isRegistration)) {
|
|
611
|
+
table.set(hostAddress(member.kind, member.name), member);
|
|
612
|
+
}
|
|
613
|
+
return table;
|
|
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];
|
|
558
645
|
}
|
|
559
646
|
/** The harness's projected members as payload members, deterministically kind-then-name ordered. */
|
|
560
647
|
function orderedMembers(harness, options) {
|
|
@@ -564,7 +651,7 @@ function orderedMembers(harness, options) {
|
|
|
564
651
|
.map((member) => ({
|
|
565
652
|
kind: member.kind,
|
|
566
653
|
name: member.name,
|
|
567
|
-
host: member.host &&
|
|
654
|
+
host: member.host && hostAddress(member.host.kind, member.host.name),
|
|
568
655
|
// The generated row carries a mutable field list; the member's is read-only,
|
|
569
656
|
// so copy each pair into a fresh tuple — the same values, a shape the row accepts.
|
|
570
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
|
|
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
|
/**
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/src/kind.d.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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
|
+
}
|
package/dist/src/prose.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
61
|
-
* discovery locus (an `at`-locus kind) may name a member discovered on disk, so
|
|
62
|
-
* owns the verdict.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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:
|
|
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
|
|
23
|
-
* discovery locus (an `at`-locus kind) may name a member discovered on disk, so
|
|
24
|
-
* owns the verdict.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
30
|
-
|
|
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.
|
|
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.
|
|
56
|
-
"@dtmd/temper-win32-x64": "0.0.
|
|
55
|
+
"@dtmd/temper-linux-x64": "0.0.18",
|
|
56
|
+
"@dtmd/temper-win32-x64": "0.0.18"
|
|
57
57
|
}
|
|
58
58
|
}
|