@dtmd/temper 0.0.7 → 0.0.8
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/README.md +84 -53
- package/bin/temper.js +0 -0
- package/dist/src/assembly.d.ts +15 -1
- package/dist/src/assembly.js +2 -1
- package/dist/src/builtins.d.ts +682 -66
- package/dist/src/builtins.js +664 -92
- package/dist/src/claude-code.d.ts +2 -2
- package/dist/src/claude-code.js +1 -1
- package/dist/src/contract.d.ts +180 -29
- package/dist/src/contract.js +128 -15
- package/dist/src/declarations.d.ts +72 -5
- package/dist/src/declarations.js +389 -107
- package/dist/src/dial.d.ts +75 -0
- package/dist/src/dial.js +82 -0
- package/dist/src/emit.d.ts +35 -1
- package/dist/src/emit.js +369 -63
- package/dist/src/generated/AssemblyFactRow.d.ts +2 -2
- package/dist/src/generated/BoundRow.d.ts +2 -2
- package/dist/src/generated/ClauseRow.d.ts +73 -3
- package/dist/src/generated/CollectionAddressRow.d.ts +4 -0
- package/dist/src/generated/EmbeddedMember.d.ts +3 -3
- package/dist/src/generated/FeatureValue.d.ts +2 -2
- package/dist/src/generated/Features.d.ts +52 -3
- package/dist/src/generated/KindFactRow.d.ts +24 -7
- package/dist/src/generated/MentionRow.d.ts +5 -3
- package/dist/src/generated/NestedMemberRow.d.ts +32 -0
- package/dist/src/generated/PayloadMember.d.ts +6 -0
- package/dist/src/generated/RequirementRow.d.ts +4 -2
- package/dist/src/generated/SatisfiesRow.d.ts +2 -1
- package/dist/src/generated/Shape.d.ts +15 -0
- package/dist/src/generated/Shape.js +2 -0
- package/dist/src/generated/TemplateRow.d.ts +24 -0
- package/dist/src/generated/TemplateRow.js +2 -0
- package/dist/src/generated/ValueType.d.ts +11 -2
- package/dist/src/generated/Verifier.d.ts +20 -0
- package/dist/src/generated/Verifier.js +2 -0
- package/dist/src/generated/index.d.ts +3 -0
- package/dist/src/index.d.ts +8 -8
- package/dist/src/index.js +3 -3
- package/dist/src/kind.d.ts +135 -29
- package/dist/src/kind.js +35 -8
- package/dist/src/prose.d.ts +81 -25
- package/dist/src/prose.js +91 -21
- package/package.json +2 -2
package/dist/src/emit.js
CHANGED
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
|
-
import { renderText, resolveLeaf } from "./prose.js";
|
|
13
|
+
import { checkMentions, isTextSpan, renderText, resolveLeaf } from "./prose.js";
|
|
14
14
|
import { permissionUnion } from "./needs.js";
|
|
15
|
-
import { compareStrings, compileDeclarations, declaredAddresses, declaredRequirements, encodeSeam, registrationRows, settingsRows, } from "./declarations.js";
|
|
15
|
+
import { compareStrings, compileDeclarations, declaredAddresses, declaredAtLocusKinds, declaredRequirements, encodeSeam, placementKey, registrationRows, settingsRows, tapHookRows, } from "./declarations.js";
|
|
16
|
+
/** The {@link MentionScope} a set of {@link ResolveOptions} names — its two sets, each defaulting to empty. */
|
|
17
|
+
function scopeOf(options) {
|
|
18
|
+
return {
|
|
19
|
+
mentionable: options.mentionable ?? new Set(),
|
|
20
|
+
deferrableKinds: options.deferrableKinds ?? new Set(),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
16
23
|
/**
|
|
17
24
|
* TOML-quote a leaf's authored text into a basic-string literal — the escapes
|
|
18
25
|
* `toml_edit`'s parser reads back (backslash, quote, and the C0 control set),
|
|
@@ -49,9 +56,167 @@ function tomlString(text) {
|
|
|
49
56
|
}
|
|
50
57
|
return out + '"';
|
|
51
58
|
}
|
|
59
|
+
/** Join path parts with `/`, dropping the empties and the `.` root a root-locus kind carries. */
|
|
60
|
+
function joinSlash(...parts) {
|
|
61
|
+
return parts.filter((part) => part !== "" && part !== ".").join("/");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `name` spliced through `pattern`'s single `*` — the one name-through-a-glob map, shared
|
|
65
|
+
* by a flat `at` glob and a host template's path pattern. A `*`-free pattern is a fixed
|
|
66
|
+
* path, left verbatim. `starredSegment` admits a single-`*` segment glob whose one `*` stars
|
|
67
|
+
* a whole leading directory segment (a starred-segment kind's locus), landing `<name>/<file>`;
|
|
68
|
+
* every other caller passes `false`, where a `/` beside the `*` is a stray directory the
|
|
69
|
+
* splice cannot place.
|
|
70
|
+
*
|
|
71
|
+
* # Throws
|
|
72
|
+
* If `pattern` carries a `*` yet is neither single-star nor single-segment (and not the
|
|
73
|
+
* admitted leading-segment case): the splice would leave a stray literal `*` or directory
|
|
74
|
+
* segment behind.
|
|
75
|
+
*/
|
|
76
|
+
function spliceName(kindName, pattern, name, starredSegment) {
|
|
77
|
+
const stars = pattern.split("*").length - 1;
|
|
78
|
+
const leadingSegment = starredSegment && stars === 1 && pattern.startsWith("*/");
|
|
79
|
+
if (stars > 0 && !leadingSegment && (stars > 1 || pattern.includes("/"))) {
|
|
80
|
+
throw new Error(`kind \`${kindName}\`: glob \`${pattern}\` is neither a single-segment single-\`*\` ` +
|
|
81
|
+
`pattern nor an any-depth \`**\` glob — a member name splices through neither.`);
|
|
82
|
+
}
|
|
83
|
+
return pattern.replace("*", name);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The unit `host`'s file children compose their paths under — a directory unit's own
|
|
87
|
+
* directory, since a template's path pattern is relative to the parent's unit.
|
|
88
|
+
*
|
|
89
|
+
* # Throws
|
|
90
|
+
* If the host owns no directory unit: a lone file has no interior for a child to sit in.
|
|
91
|
+
*/
|
|
92
|
+
function hostUnit(host, context) {
|
|
93
|
+
if (host.facts.locus.kind === "at" && host.facts.unitShape === "directory") {
|
|
94
|
+
return joinSlash(host.facts.locus.root, host.name);
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`${context}: its host \`${host.kind}:${host.name}\` owns no directory unit — a template's ` +
|
|
97
|
+
`path pattern is relative to the host's unit, and a lone file has no interior for a ` +
|
|
98
|
+
`child to sit in (specs/model/representation.md, "locus").`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A nested file child's harness-relative locus: its host member's unit joined with the
|
|
102
|
+
* host template's path pattern, its own name spliced through the pattern. The pattern is
|
|
103
|
+
* the host kind's declared fact and the child kind governs no glob, so one home owns the
|
|
104
|
+
* path and no child contends with its host's own locus.
|
|
105
|
+
*
|
|
106
|
+
* # Throws
|
|
107
|
+
* If the child names no host, or its host's kind templates no file layer for the child's
|
|
108
|
+
* kind — there is no pattern to compose against.
|
|
109
|
+
*/
|
|
110
|
+
function nestedFilePath(member) {
|
|
111
|
+
const context = `member \`${member.name}\` of kind \`${member.kind}\``;
|
|
112
|
+
const host = member.host;
|
|
113
|
+
if (host === undefined) {
|
|
114
|
+
throw new Error(`${context}: a nested file child names the host its path composes under.`);
|
|
115
|
+
}
|
|
116
|
+
const template = (host.facts.templates ?? []).find((layer) => layer.kind.key === member.kind && layer.path !== undefined);
|
|
117
|
+
if (template?.path === undefined) {
|
|
118
|
+
throw new Error(`${context}: its host \`${host.kind}:${host.name}\` templates no file layer for kind ` +
|
|
119
|
+
`\`${member.kind}\` — the path pattern is the host kind's declared fact, and there is ` +
|
|
120
|
+
`none to compose against (specs/model/representation.md, "locus").`);
|
|
121
|
+
}
|
|
122
|
+
return joinSlash(hostUnit(host, context), spliceName(member.kind, template.path, member.name, false));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The harness-relative locus `member` projects onto: a directory unit lands its entry
|
|
126
|
+
* file under `<root>/<name>/`; a lone file splices the name through the glob's single
|
|
127
|
+
* `*` (an any-depth glob — a memory kind's `**\/CLAUDE.md` — lands the root `<name>.md`,
|
|
128
|
+
* and a `*`-free glob is a fixed path left verbatim); a nested file child composes its
|
|
129
|
+
* path under its host's unit ({@link nestedFilePath}). The engine derives the same locus
|
|
130
|
+
* from the same facts (`src/drift.rs`'s `member_projection_path`); the two must agree,
|
|
131
|
+
* since a hook's rendered link is written from this side and reaped from that one.
|
|
132
|
+
*
|
|
133
|
+
* # Throws
|
|
134
|
+
* If the kind is embedded (no standalone projection), or the member's glob or host
|
|
135
|
+
* template pattern maps its name to no one path ({@link spliceName},
|
|
136
|
+
* {@link nestedFilePath}).
|
|
137
|
+
*/
|
|
138
|
+
function projectionPath(member) {
|
|
139
|
+
const facts = member.facts;
|
|
140
|
+
if (facts.locus.kind === "embedded") {
|
|
141
|
+
throw new Error(`kind \`${facts.name}\` is embedded — its members live inside a host body and ` +
|
|
142
|
+
`carry no standalone projection (specs/model/representation.md, "locus").`);
|
|
143
|
+
}
|
|
144
|
+
if (facts.locus.kind === "nested-file")
|
|
145
|
+
return nestedFilePath(member);
|
|
146
|
+
const { root, glob } = facts.locus;
|
|
147
|
+
if (facts.unitShape === "directory") {
|
|
148
|
+
const slash = glob.indexOf("/");
|
|
149
|
+
return joinSlash(root, member.name, slash < 0 ? glob : glob.slice(slash + 1));
|
|
150
|
+
}
|
|
151
|
+
if (glob.includes("**"))
|
|
152
|
+
return joinSlash(root, `${member.name}.md`);
|
|
153
|
+
const starredSegment = facts.unitShape === "starred-segment";
|
|
154
|
+
return joinSlash(root, spliceName(facts.name, glob, member.name, starredSegment));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* `to`'s path as read from the document at `from` — the shared leading segments drop
|
|
158
|
+
* and each of `from`'s remaining directory segments becomes a `..`, so a rendered link
|
|
159
|
+
* resolves from wherever the host member's own projection lands.
|
|
160
|
+
*/
|
|
161
|
+
function relativeProjection(from, to) {
|
|
162
|
+
const fromDirs = from.split("/").slice(0, -1);
|
|
163
|
+
const toParts = to.split("/");
|
|
164
|
+
let shared = 0;
|
|
165
|
+
while (shared < fromDirs.length && shared < toParts.length - 1 && fromDirs[shared] === toParts[shared]) {
|
|
166
|
+
shared += 1;
|
|
167
|
+
}
|
|
168
|
+
return [...fromDirs.slice(shared).map(() => ".."), ...toParts.slice(shared)].join("/");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The closed, engine-derived facts about one embedded value's edge-field targets,
|
|
172
|
+
* keyed by edge field: the declaring kind names which leaves are addresses, and each
|
|
173
|
+
* address resolves against the program's composed members — the same table a mention
|
|
174
|
+
* resolves against. The facts are read off the resolved target, so a format that
|
|
175
|
+
* selects them renders a reference true by construction; the four are the whole set.
|
|
176
|
+
*
|
|
177
|
+
* An unfilled leaf is no edge, so it contributes no entry: requiredness is the kind's
|
|
178
|
+
* own field schema, which fails in the author's program at compose time.
|
|
179
|
+
*
|
|
180
|
+
* # Throws
|
|
181
|
+
* If a filled leaf names no composed member, or names one that owns no projection to
|
|
182
|
+
* point at. An edge target cannot defer to the gate the way a bare mention may: the
|
|
183
|
+
* reference is written now, and there is nothing true to write.
|
|
184
|
+
*/
|
|
185
|
+
function edgeTargetFacts(host, value, leaves, options) {
|
|
186
|
+
const targets = {};
|
|
187
|
+
const context = `member \`${host.name}\`: embedded value \`${value.key}\` of kind \`${value.kind}\``;
|
|
188
|
+
for (const edge of value.edgeFields ?? []) {
|
|
189
|
+
const address = leaves[edge.field];
|
|
190
|
+
if (address === undefined || address === "")
|
|
191
|
+
continue;
|
|
192
|
+
// A one-element `to` set resolves a bare address within its one kind; a
|
|
193
|
+
// multi-element set reads the kind-qualified `kind:name` the author wrote
|
|
194
|
+
// (`EdgeField.to`). An already-qualified address carries its own colon, so
|
|
195
|
+
// only a bare leaf is lifted to `${edge.to[0]}:${address}` for the lookup.
|
|
196
|
+
const lookup = edge.to.length === 1 && !address.includes(":") ? `${edge.to[0]}:${address}` : address;
|
|
197
|
+
const target = options.members?.get(lookup);
|
|
198
|
+
if (target === undefined) {
|
|
199
|
+
throw new Error(`${context}: edge field \`${edge.field}\` names \`${address}\`, which resolves to no ` +
|
|
200
|
+
`composed member — an edge target's facts are derived, never fabricated ` +
|
|
201
|
+
`(specs/model/pipeline.md, "Emit", the "Refusing" bullet).`);
|
|
202
|
+
}
|
|
203
|
+
if (!isProjected(target)) {
|
|
204
|
+
throw new Error(`${context}: edge field \`${edge.field}\` names \`${address}\`, which owns no ` +
|
|
205
|
+
`projection to reference (specs/model/representation.md, "locus").`);
|
|
206
|
+
}
|
|
207
|
+
targets[edge.field] = {
|
|
208
|
+
name: target.name,
|
|
209
|
+
address: lookup,
|
|
210
|
+
kind: target.kind,
|
|
211
|
+
path: relativeProjection(projectionPath(host), projectionPath(target)),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return targets;
|
|
215
|
+
}
|
|
52
216
|
/**
|
|
53
217
|
* Resolve one embedded member's value's leaves — top-level and each
|
|
54
|
-
* collection entry's — to their final stored strings
|
|
218
|
+
* collection entry's — to their final stored strings, and derive its edge fields'
|
|
219
|
+
* target facts off the resolved leaves: a `Text`-authored leaf
|
|
55
220
|
* resolves the way `resolveBody` resolves a member-level `Text` body (mention
|
|
56
221
|
* resolution-checked against `mentionable`, loud on a dangling address); a
|
|
57
222
|
* bare-string leaf is unchanged. The one resolution point shared by the
|
|
@@ -59,23 +224,30 @@ function tomlString(text) {
|
|
|
59
224
|
* embedded-kind leaf mention never depends on whether the kind declares
|
|
60
225
|
* `render` (`pipeline.md`, "Emit", the "Refusing" bullet).
|
|
61
226
|
*/
|
|
62
|
-
function resolveMemberLeaves(value,
|
|
227
|
+
function resolveMemberLeaves(host, value, options) {
|
|
228
|
+
const scope = scopeOf(options);
|
|
63
229
|
const context = (childPath) => `member.${value.kind} ${value.key}: leaf \`${childPath}\``;
|
|
64
230
|
const leaves = {};
|
|
65
231
|
for (const [key, leaf] of Object.entries(value.leaves)) {
|
|
66
|
-
leaves[key] = resolveLeaf(leaf,
|
|
232
|
+
leaves[key] = resolveLeaf(leaf, scope, context(key));
|
|
67
233
|
}
|
|
68
234
|
const collections = {};
|
|
69
235
|
for (const [collection, entries] of Object.entries(value.collections)) {
|
|
70
236
|
collections[collection] = entries.map((entry) => {
|
|
71
237
|
const entryLeaves = {};
|
|
72
238
|
for (const [leaf, text] of Object.entries(entry.leaves)) {
|
|
73
|
-
entryLeaves[leaf] = resolveLeaf(text,
|
|
239
|
+
entryLeaves[leaf] = resolveLeaf(text, scope, context(`${collection}.${entry.key}.${leaf}`));
|
|
74
240
|
}
|
|
75
241
|
return { key: entry.key, leaves: entryLeaves };
|
|
76
242
|
});
|
|
77
243
|
}
|
|
78
|
-
return {
|
|
244
|
+
return {
|
|
245
|
+
kind: value.kind,
|
|
246
|
+
key: value.key,
|
|
247
|
+
leaves,
|
|
248
|
+
collections,
|
|
249
|
+
targets: edgeTargetFacts(host, value, leaves, options),
|
|
250
|
+
};
|
|
79
251
|
}
|
|
80
252
|
/**
|
|
81
253
|
* Render one resolved embedded member's interior TOML: its top-level leaves,
|
|
@@ -101,24 +273,163 @@ function renderMemberToml(value) {
|
|
|
101
273
|
return lines.join("\n");
|
|
102
274
|
}
|
|
103
275
|
/**
|
|
104
|
-
* Render one embedded member's value to its
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
276
|
+
* Render one embedded member's value to its projected block. A `render`-less
|
|
277
|
+
* kind projects the default `[collection.entry]` TOML view wrapped in a
|
|
278
|
+
* `member.<kind> <key>` fence, byte-unchanged. A kind that declares a `render`
|
|
279
|
+
* hook projects the hook's output directly, with no fence: an embedded format
|
|
280
|
+
* is writer-only and unconstrained when its host is composed (`representation.md`,
|
|
281
|
+
* "kind") — the engine never reads the block back (nested-member facts ride the
|
|
282
|
+
* lock, `pipeline.md`, "The lock"), so the fence is cosmetic and a hook that
|
|
283
|
+
* already renders readable markdown should not be re-buried in a code fence.
|
|
284
|
+
* Leaves resolve once (`resolveMemberLeaves`) before either path sees them, so a
|
|
109
285
|
* hook receives plain strings, never a raw `Text` leaf.
|
|
110
286
|
*/
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return `\`\`\`member.${value.kind} ${value.key}\n${
|
|
287
|
+
function renderMemberBlock(host, value, options) {
|
|
288
|
+
const resolved = resolveMemberLeaves(host, value, options);
|
|
289
|
+
if (value.render !== undefined)
|
|
290
|
+
return value.render(resolved);
|
|
291
|
+
return `\`\`\`member.${value.kind} ${value.key}\n${renderMemberToml(resolved)}\n\`\`\``;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* A recording view of a resolved value: every read of an edge field's key — off the
|
|
295
|
+
* derived `targets` facts or off the `leaves` that authored its address — is collected
|
|
296
|
+
* into `placed`. Those two are the whole surface an edge's data can reach a format
|
|
297
|
+
* through, so a format that touches neither placed nothing.
|
|
298
|
+
*
|
|
299
|
+
* Placement is observed as *selection*, which bounds the check in one direction only: a
|
|
300
|
+
* format that reads an edge and discards it reads as placed. That keeps the predicate
|
|
301
|
+
* free of false positives, which is what earns it the gate — a format that never names
|
|
302
|
+
* the edge, the case the check exists for, is caught exactly.
|
|
303
|
+
*/
|
|
304
|
+
function recordingView(resolved, edgeFields, placed) {
|
|
305
|
+
const watch = (record) => new Proxy(record, {
|
|
306
|
+
get(target, property, receiver) {
|
|
307
|
+
if (typeof property === "string" && edgeFields.has(property))
|
|
308
|
+
placed.add(property);
|
|
309
|
+
return Reflect.get(target, property, receiver);
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
return { ...resolved, leaves: watch(resolved.leaves), targets: watch(resolved.targets) };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The declared edge fields one embedded value's format placed, sorted — the fact a
|
|
316
|
+
* `format-places-edges` clause decides over, since the engine never sees a format and
|
|
317
|
+
* never reads a rendering back. A `render`-less kind takes the default TOML view, which
|
|
318
|
+
* writes every leaf, so every edge the value fills is placed by construction; a kind that
|
|
319
|
+
* declares one runs the hook against a {@link recordingView} and reports what it
|
|
320
|
+
* selected.
|
|
321
|
+
*
|
|
322
|
+
* The obligation ranges over the edges this value *fills*, never its kind's whole
|
|
323
|
+
* declared set: an unfilled field is no edge, so a format cannot omit it. `undefined`
|
|
324
|
+
* when the value fills none — there is nothing to place, so the row records nothing
|
|
325
|
+
* rather than an empty column on every ordinary value.
|
|
326
|
+
*
|
|
327
|
+
* This renders the value a second time, the way `nestedMemberRow` reads its leaves a
|
|
328
|
+
* second time: a hook is pure (emit double-verifies its own bytes), so the observing
|
|
329
|
+
* render and the projecting one cannot disagree.
|
|
330
|
+
*/
|
|
331
|
+
function placedEdges(host, value, options) {
|
|
332
|
+
if ((value.edgeFields ?? []).length === 0)
|
|
333
|
+
return undefined;
|
|
334
|
+
const resolved = resolveMemberLeaves(host, value, options);
|
|
335
|
+
// `targets` carries exactly the filled edge fields — an unfilled one derives no facts.
|
|
336
|
+
const edgeFields = new Set(Object.keys(resolved.targets));
|
|
337
|
+
if (edgeFields.size === 0)
|
|
338
|
+
return undefined;
|
|
339
|
+
if (value.render === undefined)
|
|
340
|
+
return [...edgeFields].sort(compareStrings);
|
|
341
|
+
const placed = new Set();
|
|
342
|
+
value.render(recordingView(resolved, edgeFields, placed));
|
|
343
|
+
return [...placed].sort(compareStrings);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Every composed embedded value's placed edge fields, keyed by the value's
|
|
347
|
+
* {@link placementKey} — what `emit` hands {@link compileDeclarations} so each
|
|
348
|
+
* `nested_member` row carries its own format's placement record. Iterates exactly the
|
|
349
|
+
* values `nestedMemberRows` does, so every edge-bearing row it builds has an observation.
|
|
350
|
+
*/
|
|
351
|
+
export function edgePlacements(harness, options) {
|
|
352
|
+
const placements = new Map();
|
|
353
|
+
for (const member of harness.members) {
|
|
354
|
+
if (member.prose?.kind !== "blocks")
|
|
355
|
+
continue;
|
|
356
|
+
for (const value of member.prose.values) {
|
|
357
|
+
if (isTextSpan(value))
|
|
358
|
+
continue;
|
|
359
|
+
const placed = placedEdges(member, value, options);
|
|
360
|
+
if (placed !== undefined) {
|
|
361
|
+
placements.set(placementKey(`${member.kind}:${member.name}`, value.kind, value.key), placed);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return placements;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The line count of a rendered block, matching the engine's `str::lines()`: a single
|
|
369
|
+
* trailing newline is absorbed (a block and the same block plus one `\n` span the same),
|
|
370
|
+
* and an empty block spans none. Kept in step with `src/extract.rs`'s file-side count so a
|
|
371
|
+
* budget reads one member the same whether it is a file or an embedded projection.
|
|
372
|
+
*/
|
|
373
|
+
function renderedLineCount(block) {
|
|
374
|
+
if (block.length === 0)
|
|
375
|
+
return 0;
|
|
376
|
+
const body = block.endsWith("\n") ? block.slice(0, -1) : block;
|
|
377
|
+
return body.split("\n").length;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Every composed embedded value's rendered extent — the line and character count of the
|
|
381
|
+
* block `emit` projected for it — keyed by its {@link placementKey}, what `emit` hands
|
|
382
|
+
* {@link compileDeclarations} so each `nested_member` row carries the span an `extent`
|
|
383
|
+
* clause budgets. Iterates exactly the values {@link edgePlacements} does, rendering each
|
|
384
|
+
* through the same {@link renderMemberBlock} the body projection uses (a hook is pure, so
|
|
385
|
+
* the measured render and the projected one cannot disagree), never a second renderer.
|
|
386
|
+
*
|
|
387
|
+
* A value the SDK composes is always rendered here, so it always captures a span; a value
|
|
388
|
+
* no format rendered — an embedded member read off a layout host's source — is lowered by
|
|
389
|
+
* the engine, not this pass, and reaches its row with no span (the `placed_edges`
|
|
390
|
+
* distinction between an observed empty and an unobserved absence).
|
|
391
|
+
*/
|
|
392
|
+
export function renderedExtents(harness, options) {
|
|
393
|
+
const extents = new Map();
|
|
394
|
+
for (const member of harness.members) {
|
|
395
|
+
if (member.prose?.kind !== "blocks")
|
|
396
|
+
continue;
|
|
397
|
+
for (const value of member.prose.values) {
|
|
398
|
+
if (isTextSpan(value))
|
|
399
|
+
continue;
|
|
400
|
+
const block = renderMemberBlock(member, value, options);
|
|
401
|
+
extents.set(placementKey(`${member.kind}:${member.name}`, value.kind, value.key), {
|
|
402
|
+
lines: renderedLineCount(block),
|
|
403
|
+
// Unicode scalar values, matching Rust's `chars().count()` — iterating a string
|
|
404
|
+
// yields code points, so a surrogate pair counts once, the way it does file-side.
|
|
405
|
+
chars: [...block].length,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return extents;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Render a member-level `Text` body to its final bytes: its mentions are
|
|
413
|
+
* resolution-checked against `scope` ({@link checkMentions}: loud on a dangling
|
|
414
|
+
* address, a discovery-locus one deferred; `context` naming the host) and the
|
|
415
|
+
* display rule applied, each include slot left standing for the engine to splice.
|
|
416
|
+
* Shared by a `text` body and a composed body's prose spans, so a narrative span
|
|
417
|
+
* resolves the identical way a member-level `text` body does.
|
|
418
|
+
*
|
|
419
|
+
* # Throws
|
|
420
|
+
* If a mention names no declared value and has no discovery locus.
|
|
421
|
+
*/
|
|
422
|
+
function renderTextBody(prose, scope, context) {
|
|
423
|
+
checkMentions(prose.mentions, scope, context);
|
|
424
|
+
return renderText(prose);
|
|
116
425
|
}
|
|
117
426
|
/**
|
|
118
427
|
* Resolve a member's prose to its final body bytes: a `file()` asset is read in
|
|
119
428
|
* byte-for-byte; a `text` body's mentions are resolution-checked (loud on a
|
|
120
|
-
* dangling address) and rendered by the one display rule; a `blocks()`
|
|
121
|
-
* renders each
|
|
429
|
+
* dangling address) and rendered by the one display rule; a `blocks()` composed
|
|
430
|
+
* body renders each child in authored order — a prose span as its resolved words
|
|
431
|
+
* (`renderTextBody`), an embedded member as a `member.<kind> <key>` TOML fence
|
|
432
|
+
* (or, for a kind with a `render` hook, the hook's fence-free markdown). The words
|
|
122
433
|
* are never reworded.
|
|
123
434
|
*
|
|
124
435
|
* # Throws
|
|
@@ -138,30 +449,31 @@ function resolveBody(member, options) {
|
|
|
138
449
|
`(looked at \`${assetPath}\`).`, { cause });
|
|
139
450
|
}
|
|
140
451
|
}
|
|
452
|
+
const scope = scopeOf(options);
|
|
141
453
|
if (prose.kind === "blocks") {
|
|
142
|
-
|
|
454
|
+
const context = `member \`${member.name}\``;
|
|
455
|
+
return (prose.values
|
|
456
|
+
.map((value) => isTextSpan(value) ? renderTextBody(value, scope, context) : renderMemberBlock(member, value, options))
|
|
457
|
+
.join("\n\n") + "\n");
|
|
143
458
|
}
|
|
144
|
-
|
|
145
|
-
for (const mention of prose.mentions) {
|
|
146
|
-
if (!mentionable.has(mention.target.address)) {
|
|
147
|
-
throw new Error(`member \`${member.name}\`: mention of \`${mention.target.address}\` resolves to no ` +
|
|
148
|
-
`declared value — a mention cannot dangle (specs/model/contract.md).`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return renderText(prose);
|
|
459
|
+
return renderTextBody(prose, scope, `member \`${member.name}\``);
|
|
152
460
|
}
|
|
153
461
|
/**
|
|
154
|
-
* The
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
462
|
+
* The one declare-side refusal emit runs before it produces a byte: a
|
|
463
|
+
* `satisfies` claim naming no declared requirement (a dangling join).
|
|
464
|
+
*
|
|
465
|
+
* Fill enforcement — every `required` requirement has ≥1 satisfier — is the
|
|
466
|
+
* engine's, not the SDK's: it lands over the composed members' `satisfies`
|
|
467
|
+
* *plus* the fill rows emit derives from a layout document's `satisfies` edge
|
|
468
|
+
* slot, which the SDK never reads. A pre-flight over composed `satisfies`
|
|
469
|
+
* alone would spuriously refuse a requirement a layout host fills, so the SDK
|
|
470
|
+
* implements no semantics here and defers to the engine's requirement clause.
|
|
158
471
|
*
|
|
159
472
|
* # Throws
|
|
160
|
-
* On a dangling `satisfies` join
|
|
473
|
+
* On a dangling `satisfies` join.
|
|
161
474
|
*/
|
|
162
475
|
function refuseBrokenSource(harness) {
|
|
163
476
|
const requirements = declaredRequirements(harness);
|
|
164
|
-
const filled = new Set();
|
|
165
477
|
for (const member of harness.members) {
|
|
166
478
|
for (const name of member.satisfies) {
|
|
167
479
|
if (!requirements.has(name)) {
|
|
@@ -169,25 +481,6 @@ function refuseBrokenSource(harness) {
|
|
|
169
481
|
`harness-level or member-published requirement declares — a dangling join ` +
|
|
170
482
|
`(specs/model/pipeline.md, "Emit", the "Refusing" bullet).`);
|
|
171
483
|
}
|
|
172
|
-
filled.add(name);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const requiredSources = [];
|
|
176
|
-
for (const [name, requirement] of Object.entries(harness.require)) {
|
|
177
|
-
if (requirement.required)
|
|
178
|
-
requiredSources.push([name, "the assembly"]);
|
|
179
|
-
}
|
|
180
|
-
for (const member of harness.members) {
|
|
181
|
-
for (const [name, requirement] of Object.entries(member.requires)) {
|
|
182
|
-
if (requirement.required)
|
|
183
|
-
requiredSources.push([name, `member \`${member.name}\``]);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
for (const [name, source] of requiredSources) {
|
|
187
|
-
if (!filled.has(name)) {
|
|
188
|
-
throw new Error(`required requirement \`${name}\` (declared by ${source}) is filled by no member's ` +
|
|
189
|
-
`\`satisfies\` — an unfilled required requirement ` +
|
|
190
|
-
`(specs/model/pipeline.md, "Emit", the "Refusing" bullet).`);
|
|
191
484
|
}
|
|
192
485
|
}
|
|
193
486
|
}
|
|
@@ -200,12 +493,12 @@ function isRegistration(member) {
|
|
|
200
493
|
return member.facts.shape === "fields";
|
|
201
494
|
}
|
|
202
495
|
/**
|
|
203
|
-
* A member is projected iff
|
|
204
|
-
*
|
|
205
|
-
* standalone projection.
|
|
496
|
+
* A member is projected iff it owns a file — at its kind's governed glob or composed
|
|
497
|
+
* under its host's unit — and is not a fields-only registration member. An embedded
|
|
498
|
+
* member and a registration member each carry no standalone projection.
|
|
206
499
|
*/
|
|
207
500
|
function isProjected(member) {
|
|
208
|
-
return member.facts.locus.kind
|
|
501
|
+
return member.facts.locus.kind !== "embedded" && !isRegistration(member);
|
|
209
502
|
}
|
|
210
503
|
/**
|
|
211
504
|
* The resolved absolute path of a `file()` prose asset, or `undefined` for
|
|
@@ -224,16 +517,18 @@ function fileSourcePath(member) {
|
|
|
224
517
|
return fileURLToPath(new URL(prose.path, prose.moduleUrl));
|
|
225
518
|
}
|
|
226
519
|
/**
|
|
227
|
-
* The harness's
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
520
|
+
* The harness's registration write facts as the public {@link RegistrationFact} view —
|
|
521
|
+
* the seam's own `registration` rows mapped to the nested `collectionAddress` shape the
|
|
522
|
+
* `EmitResult` sibling exposes, so the two cannot disagree on what a manifest carries.
|
|
523
|
+
* The fields-only registration members ({@link registrationRows}) and the memberless tap
|
|
524
|
+
* hooks a telemetry verifier synthesizes ({@link tapHookRows}) fold in together, the same
|
|
525
|
+
* union `compileDeclarations` writes into `declarations.registrations`.
|
|
231
526
|
*
|
|
232
527
|
* # Throws
|
|
233
528
|
* If a fields-only member declares no collection address — it surfaces in no manifest.
|
|
234
529
|
*/
|
|
235
530
|
function registrationFacts(harness) {
|
|
236
|
-
return registrationRows(harness).map((row) => ({
|
|
531
|
+
return [...registrationRows(harness), ...tapHookRows(harness)].map((row) => ({
|
|
237
532
|
kind: row.kind,
|
|
238
533
|
key: row.key,
|
|
239
534
|
collectionAddress: { manifest: row.manifest, keyPath: row.key_path },
|
|
@@ -249,6 +544,14 @@ function registrationFacts(harness) {
|
|
|
249
544
|
function settingsResidue(harness) {
|
|
250
545
|
return settingsRows(harness).map((row) => ({ manifest: row.manifest, key: row.key, value: row.value }));
|
|
251
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* The harness's composed members by `kind:name` address — the table an embedded value's
|
|
549
|
+
* edge field resolves its target against. Keyed the identical way {@link declaredAddresses}
|
|
550
|
+
* spells a member address, so an edge field and a mention name a member the same way.
|
|
551
|
+
*/
|
|
552
|
+
function memberTable(harness) {
|
|
553
|
+
return new Map(harness.members.map((member) => [`${member.kind}:${member.name}`, member]));
|
|
554
|
+
}
|
|
252
555
|
/** The harness's projected members as payload members, deterministically kind-then-name ordered. */
|
|
253
556
|
function orderedMembers(harness, options) {
|
|
254
557
|
return [...harness.members]
|
|
@@ -257,6 +560,7 @@ function orderedMembers(harness, options) {
|
|
|
257
560
|
.map((member) => ({
|
|
258
561
|
kind: member.kind,
|
|
259
562
|
name: member.name,
|
|
563
|
+
host: member.host && `${member.host.kind}:${member.host.name}`,
|
|
260
564
|
// The generated row carries a mutable field list; the member's is read-only,
|
|
261
565
|
// so copy each pair into a fresh tuple — the same values, a shape the row accepts.
|
|
262
566
|
fields: member.fields.map(([name, value]) => [name, value]),
|
|
@@ -275,10 +579,12 @@ export function emit(harness) {
|
|
|
275
579
|
refuseBrokenSource(harness);
|
|
276
580
|
const resolve = {
|
|
277
581
|
mentionable: declaredAddresses(harness),
|
|
582
|
+
deferrableKinds: declaredAtLocusKinds(harness),
|
|
583
|
+
members: memberTable(harness),
|
|
278
584
|
};
|
|
279
585
|
const compile = () => {
|
|
280
586
|
const members = orderedMembers(harness, resolve);
|
|
281
|
-
const declarations = compileDeclarations(harness);
|
|
587
|
+
const declarations = compileDeclarations(harness, edgePlacements(harness, resolve), renderedExtents(harness, resolve));
|
|
282
588
|
return {
|
|
283
589
|
declarations,
|
|
284
590
|
members,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A node-scope clause row's scalar bound — `min_len`'s `min`, `max_len`/`
|
|
2
|
+
* A node-scope clause row's scalar bound — `min_len`'s `min`, `max_len`/`extent`'s
|
|
3
3
|
* `max`, each endpoint optional so the row carries only what the predicate declared.
|
|
4
4
|
*/
|
|
5
5
|
export type BoundRow = {
|
|
@@ -8,7 +8,7 @@ export type BoundRow = {
|
|
|
8
8
|
*/
|
|
9
9
|
min?: number;
|
|
10
10
|
/**
|
|
11
|
-
* The inclusive upper bound, when the predicate declares one (`max_len`/`
|
|
11
|
+
* The inclusive upper bound, when the predicate declares one (`max_len`/`extent`).
|
|
12
12
|
*/
|
|
13
13
|
max?: number;
|
|
14
14
|
};
|