@sonordev/contracts 2.3.0 → 2.5.0

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.
@@ -0,0 +1,321 @@
1
+ 'use strict';
2
+
3
+ // src/contracts/schema-placeholders.ts
4
+ var RESERVED_EXAMPLE_DOMAINS = ["example.com", "example.net", "example.org"];
5
+ var RESERVED_EXAMPLE_TLD = "example";
6
+ var PLACEHOLDER_NAMES = /* @__PURE__ */ new Set([
7
+ "example",
8
+ "example business",
9
+ "example company",
10
+ "example organization",
11
+ "your business",
12
+ "your business name",
13
+ "your company",
14
+ "your company name",
15
+ "your organization",
16
+ "your organization name",
17
+ "business name",
18
+ "company name",
19
+ "organization name",
20
+ "your name",
21
+ "lorem ipsum"
22
+ ]);
23
+ var NAME_KEYS = /* @__PURE__ */ new Set([
24
+ "name",
25
+ "legalName",
26
+ "alternateName",
27
+ "author",
28
+ "creator",
29
+ "publisher",
30
+ "brand",
31
+ "provider",
32
+ "seller",
33
+ "manufacturer",
34
+ "organizer",
35
+ "founder"
36
+ ]);
37
+ var PHONE_KEYS = /* @__PURE__ */ new Set(["telephone", "faxNumber"]);
38
+ var URI_TEMPLATE_KEYS = /* @__PURE__ */ new Set(["target", "urlTemplate"]);
39
+ var BODY_TEXT_KEYS = /* @__PURE__ */ new Set(["articleBody"]);
40
+ var ANNOTATION_KEYS = /* @__PURE__ */ new Set(["note", "notes", "_note", "_notes", "_comment", "_comments", "__comment", "$comment"]);
41
+ var STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["@context", "@type", "@id", "position"]);
42
+ var MUSTACHE_SLOT = /\{\{\s*[#^/>&]?\s*[A-Za-z_$][\w$.[\]-]*\s*\}\}/;
43
+ var JSX_EXPRESSION_VALUE = /^\{\s*[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*|\[[^\]]*\])+[^{}]*\}$/;
44
+ var JSX_MEMBER_IN_TEXT = /\{\s*[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+\s*\}/;
45
+ var BRACKET_SLOT_VALUE = /^\[\s*[A-Za-z][^[\]"{}<>]{0,80}\]$/;
46
+ var BRACKET_SLOT_IN_TEXT = /\[\s*((?:your|insert your|enter your|add your)\s+[^[\]]{1,60}?|(?:(?:business|company|organization|brand|full|first|last|resident|client|customer|author)\s+)?(?:name|phone(?:\s+number)?|telephone|email(?:\s+address)?|(?:street\s+)?address|city|state|zip(?:\s+code)?|postal\s+code|website|url|logo|date))\s*\](?!\()/gi;
47
+ var REPLACE_MARKER = /\bREPLACE(?:_WITH_\w*|_ME\b)/;
48
+ var UPPER_SLOT_VALUE = /^YOUR_[A-Z0-9_]+$/;
49
+ var MAX_DEPTH = 40;
50
+ var MAX_EVIDENCE = 5;
51
+ var EVIDENCE_VALUE_LENGTH = 120;
52
+ function withoutSchemaPlaceholders(value) {
53
+ const state = { dropped: [], notes: [] };
54
+ if (value === null || value === void 0) return { value: null, ...state };
55
+ const result = visitRoot(value, state);
56
+ return { value: result, ...state };
57
+ }
58
+ function findSchemaPlaceholders(value) {
59
+ return withoutSchemaPlaceholders(value).dropped;
60
+ }
61
+ var REASON_TEXT = {
62
+ example_domain: "is on a reserved example domain",
63
+ placeholder_phone: "is a placeholder phone number",
64
+ placeholder_name: "is a placeholder name",
65
+ template_token: "is an unfilled template slot",
66
+ annotation: "is an AI note, not schema",
67
+ emptied: "held only placeholders"
68
+ };
69
+ function describeSchemaPlaceholder(node) {
70
+ const where = `${node.type || "Node"} at ${node.path}`;
71
+ const first = node.evidence[0];
72
+ const detail = first ? `${first.key} ${JSON.stringify(first.value)} ${REASON_TEXT[first.reason]}` : REASON_TEXT[node.reasons[0]];
73
+ return node.reasons.includes("emptied") ? `${where} has nothing real left: ${detail}` : `${where}: ${detail}`;
74
+ }
75
+ function visitRoot(value, state) {
76
+ if (Array.isArray(value)) {
77
+ const out = visitArray(value, "$", state, 0, true);
78
+ return out.removed && out.value.length === 0 ? null : out.value;
79
+ }
80
+ if (!isPlainObject(value)) return value;
81
+ if (isGraphWrapper(value)) {
82
+ const out = visitContainer(value, "$", state, 0);
83
+ return out.removedChild && !hasContentBeyond(out.value, /* @__PURE__ */ new Set(["@context"])) ? null : out.value;
84
+ }
85
+ const visit = visitNode(value, "$", state, 0);
86
+ return visit.keep ? visit.value : null;
87
+ }
88
+ function visitNode(node, path, state, depth) {
89
+ if (isAnnotationOnly(node)) {
90
+ state.dropped.push({ path, type: null, reasons: ["annotation"], evidence: annotationEvidence(node) });
91
+ return { keep: false };
92
+ }
93
+ const evidence = [];
94
+ collectEvidence(node, evidence, depth);
95
+ if (evidence.length) {
96
+ state.dropped.push({ path, type: typeOf(node), reasons: unique(evidence.map((e) => e.reason)), evidence });
97
+ return { keep: false };
98
+ }
99
+ const droppedMark = state.dropped.length;
100
+ const notesMark = state.notes.length;
101
+ const pruned = visitContainer(node, path, state, depth);
102
+ if (pruned.removedChild && !hasContentBeyond(pruned.value, STRUCTURAL_KEYS)) {
103
+ const children = state.dropped.splice(droppedMark);
104
+ state.notes.length = notesMark;
105
+ state.dropped.push({
106
+ path,
107
+ type: typeOf(node),
108
+ reasons: unique(["emptied", ...children.flatMap((c) => c.reasons)]),
109
+ evidence: children.flatMap((c) => c.evidence).slice(0, MAX_EVIDENCE)
110
+ });
111
+ return { keep: false };
112
+ }
113
+ return { keep: true, value: pruned.value };
114
+ }
115
+ function visitContainer(obj, path, state, depth) {
116
+ if (depth >= MAX_DEPTH) return { value: obj, removedChild: false };
117
+ let changed = false;
118
+ let removedChild = false;
119
+ const out = {};
120
+ for (const [key, child] of Object.entries(obj)) {
121
+ if (key === "@context") {
122
+ out[key] = child;
123
+ continue;
124
+ }
125
+ if (ANNOTATION_KEYS.has(key)) {
126
+ state.notes.push({ path, key });
127
+ changed = true;
128
+ continue;
129
+ }
130
+ const childPath = stepKey(path, key);
131
+ if (Array.isArray(child)) {
132
+ const pruned = visitArray(child, childPath, state, depth + 1, key === "@graph");
133
+ if (pruned.removed) removedChild = true;
134
+ if (pruned.value !== child) changed = true;
135
+ if (pruned.removed && pruned.value.length === 0) continue;
136
+ out[key] = pruned.value;
137
+ continue;
138
+ }
139
+ if (isPlainObject(child)) {
140
+ if (isNestedNode(child)) {
141
+ const visit = visitNode(child, childPath, state, depth + 1);
142
+ if (!visit.keep) {
143
+ changed = true;
144
+ removedChild = true;
145
+ continue;
146
+ }
147
+ if (visit.value !== child) changed = true;
148
+ out[key] = visit.value;
149
+ continue;
150
+ }
151
+ const pruned = visitContainer(child, childPath, state, depth + 1);
152
+ if (pruned.removedChild) removedChild = true;
153
+ if (pruned.value !== child) changed = true;
154
+ out[key] = pruned.value;
155
+ continue;
156
+ }
157
+ out[key] = child;
158
+ }
159
+ return { value: changed ? out : obj, removedChild };
160
+ }
161
+ function visitArray(items, path, state, depth, units) {
162
+ if (depth >= MAX_DEPTH) return { value: items, removed: false };
163
+ let changed = false;
164
+ let removed = false;
165
+ const out = [];
166
+ items.forEach((item, index) => {
167
+ const itemPath = `${path}[${index}]`;
168
+ if (Array.isArray(item)) {
169
+ const pruned = visitArray(item, itemPath, state, depth + 1, units);
170
+ if (pruned.removed) removed = true;
171
+ if (pruned.value !== item) changed = true;
172
+ out.push(pruned.value);
173
+ return;
174
+ }
175
+ if (isPlainObject(item) && (units || isNestedNode(item))) {
176
+ const visit = visitNode(item, itemPath, state, depth + 1);
177
+ if (!visit.keep) {
178
+ changed = true;
179
+ removed = true;
180
+ return;
181
+ }
182
+ if (visit.value !== item) changed = true;
183
+ out.push(visit.value);
184
+ return;
185
+ }
186
+ if (isPlainObject(item)) {
187
+ const pruned = visitContainer(item, itemPath, state, depth + 1);
188
+ if (pruned.removedChild) removed = true;
189
+ if (pruned.value !== item) changed = true;
190
+ out.push(pruned.value);
191
+ return;
192
+ }
193
+ out.push(item);
194
+ });
195
+ return { value: changed ? out : items, removed };
196
+ }
197
+ function collectEvidence(holder, out, depth) {
198
+ for (const [key, value] of Object.entries(holder)) {
199
+ if (key === "@context" || ANNOTATION_KEYS.has(key)) continue;
200
+ collectFromValue(holder, key, value, out, depth);
201
+ }
202
+ }
203
+ function collectFromValue(holder, key, value, out, depth) {
204
+ if (depth >= MAX_DEPTH || out.length >= MAX_EVIDENCE) return;
205
+ if (typeof value === "string") {
206
+ for (const reason of placeholderReasons(key, value, holder)) {
207
+ if (out.length >= MAX_EVIDENCE) return;
208
+ out.push({ reason, key, value: cutForLog(value) });
209
+ }
210
+ return;
211
+ }
212
+ if (Array.isArray(value)) {
213
+ for (const item of value) collectFromValue(holder, key, item, out, depth + 1);
214
+ return;
215
+ }
216
+ if (isPlainObject(value) && !isNestedNode(value)) collectEvidence(value, out, depth + 1);
217
+ }
218
+ function placeholderReasons(key, raw, holder) {
219
+ const value = raw.trim();
220
+ if (!value) return [];
221
+ const reasons = [];
222
+ const host = urlHost(value);
223
+ if (host && isReservedExampleHost(host)) reasons.push("example_domain");
224
+ if ((PHONE_KEYS.has(key) || /^tel:/i.test(value)) && isPlaceholderPhone(value)) reasons.push("placeholder_phone");
225
+ if (NAME_KEYS.has(key) && !("value" in holder) && PLACEHOLDER_NAMES.has(normalizeName(value))) {
226
+ reasons.push("placeholder_name");
227
+ }
228
+ if (isTemplateSlot(key, value)) reasons.push("template_token");
229
+ return reasons;
230
+ }
231
+ function isTemplateSlot(key, value) {
232
+ if (JSX_EXPRESSION_VALUE.test(value) || BRACKET_SLOT_VALUE.test(value) || UPPER_SLOT_VALUE.test(value)) return true;
233
+ if (REPLACE_MARKER.test(value)) return true;
234
+ if (!BODY_TEXT_KEYS.has(key)) {
235
+ if (MUSTACHE_SLOT.test(value)) return true;
236
+ for (const match of value.matchAll(BRACKET_SLOT_IN_TEXT)) {
237
+ if (/^[A-Z]/.test(match[1])) return true;
238
+ }
239
+ }
240
+ return !URI_TEMPLATE_KEYS.has(key) && isUrlOrPath(value) && JSX_MEMBER_IN_TEXT.test(value);
241
+ }
242
+ function urlHost(value) {
243
+ const mailto = /^mailto:[^@\s?]+@([^?\s#/]+)/i.exec(value);
244
+ if (mailto) return mailto[1];
245
+ const url = /^(?:[a-z][a-z0-9+.-]*:)?\/\/(?:[^/?#@\s]*@)?([^/?#:\s]+)/i.exec(value);
246
+ if (url) return url[1];
247
+ const email = /^[^\s@/:]+@([^\s@/:?#]+)$/.exec(value);
248
+ if (email) return email[1];
249
+ const bare = /^((?:[a-z0-9-]+\.)+[a-z][a-z0-9-]*)\.?(?::\d+)?(?:[/?#]\S*)?$/i.exec(value);
250
+ return bare ? bare[1] : null;
251
+ }
252
+ function isReservedExampleHost(host) {
253
+ const h = host.toLowerCase().replace(/\.$/, "");
254
+ if (h === RESERVED_EXAMPLE_TLD || h.endsWith(`.${RESERVED_EXAMPLE_TLD}`)) return true;
255
+ return RESERVED_EXAMPLE_DOMAINS.some((domain) => h === domain || h.endsWith(`.${domain}`));
256
+ }
257
+ function isUrlOrPath(value) {
258
+ return /^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(value) || value.startsWith("/");
259
+ }
260
+ function isPlaceholderPhone(raw) {
261
+ const value = raw.replace(/^tel:/i, "").trim();
262
+ if (/^[+\d\s().xX-]+$/.test(value) && (value.match(/x/gi)?.length ?? 0) >= 4) return true;
263
+ let digits = value.replace(/\D/g, "");
264
+ if (digits.length === 11 && digits.startsWith("1")) digits = digits.slice(1);
265
+ if (digits.length >= 7 && /^0+$/.test(digits)) return true;
266
+ if (digits === "1234567890" || digits === "1234567") return true;
267
+ const local = digits.length === 10 ? digits.slice(3) : digits.length === 7 ? digits : null;
268
+ return local !== null && /^55501\d\d$/.test(local);
269
+ }
270
+ function normalizeName(value) {
271
+ return value.replace(/\s+/g, " ").trim().replace(/[.,]+$/, "").toLowerCase();
272
+ }
273
+ function isPlainObject(value) {
274
+ return typeof value === "object" && value !== null && !Array.isArray(value);
275
+ }
276
+ function isNestedNode(value) {
277
+ return "@type" in value || "@id" in value || isAnnotationOnly(value);
278
+ }
279
+ function isGraphWrapper(value) {
280
+ return "@graph" in value && !("@type" in value) && !("@id" in value);
281
+ }
282
+ function isAnnotationOnly(value) {
283
+ if ("@type" in value || "@id" in value || "@graph" in value) return false;
284
+ const keys = Object.keys(value).filter((key) => key !== "@context");
285
+ return keys.length > 0 && keys.every((key) => ANNOTATION_KEYS.has(key));
286
+ }
287
+ function annotationEvidence(node) {
288
+ return Object.entries(node).filter(([key]) => ANNOTATION_KEYS.has(key)).slice(0, MAX_EVIDENCE).map(([key, value]) => ({
289
+ reason: "annotation",
290
+ key,
291
+ value: cutForLog(typeof value === "string" ? value : JSON.stringify(value) ?? "")
292
+ }));
293
+ }
294
+ function hasContentBeyond(obj, ignored) {
295
+ return Object.entries(obj).some(([key, value]) => !ignored.has(key) && hasContent(value));
296
+ }
297
+ function hasContent(value) {
298
+ if (value === null || value === void 0 || value === "") return false;
299
+ if (Array.isArray(value)) return value.length > 0;
300
+ if (isPlainObject(value)) return Object.keys(value).length > 0;
301
+ return true;
302
+ }
303
+ function typeOf(node) {
304
+ const type = node["@type"];
305
+ if (typeof type === "string") return type;
306
+ if (Array.isArray(type)) return type.filter((t) => typeof t === "string").join(",") || null;
307
+ return null;
308
+ }
309
+ function stepKey(path, key) {
310
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
311
+ }
312
+ function unique(values) {
313
+ return [...new Set(values)];
314
+ }
315
+ function cutForLog(value) {
316
+ return value.length > EVIDENCE_VALUE_LENGTH ? `${value.slice(0, EVIDENCE_VALUE_LENGTH)}...` : value;
317
+ }
318
+
319
+ exports.describeSchemaPlaceholder = describeSchemaPlaceholder;
320
+ exports.findSchemaPlaceholders = findSchemaPlaceholders;
321
+ exports.withoutSchemaPlaceholders = withoutSchemaPlaceholders;
@@ -0,0 +1,317 @@
1
+ // src/contracts/schema-placeholders.ts
2
+ var RESERVED_EXAMPLE_DOMAINS = ["example.com", "example.net", "example.org"];
3
+ var RESERVED_EXAMPLE_TLD = "example";
4
+ var PLACEHOLDER_NAMES = /* @__PURE__ */ new Set([
5
+ "example",
6
+ "example business",
7
+ "example company",
8
+ "example organization",
9
+ "your business",
10
+ "your business name",
11
+ "your company",
12
+ "your company name",
13
+ "your organization",
14
+ "your organization name",
15
+ "business name",
16
+ "company name",
17
+ "organization name",
18
+ "your name",
19
+ "lorem ipsum"
20
+ ]);
21
+ var NAME_KEYS = /* @__PURE__ */ new Set([
22
+ "name",
23
+ "legalName",
24
+ "alternateName",
25
+ "author",
26
+ "creator",
27
+ "publisher",
28
+ "brand",
29
+ "provider",
30
+ "seller",
31
+ "manufacturer",
32
+ "organizer",
33
+ "founder"
34
+ ]);
35
+ var PHONE_KEYS = /* @__PURE__ */ new Set(["telephone", "faxNumber"]);
36
+ var URI_TEMPLATE_KEYS = /* @__PURE__ */ new Set(["target", "urlTemplate"]);
37
+ var BODY_TEXT_KEYS = /* @__PURE__ */ new Set(["articleBody"]);
38
+ var ANNOTATION_KEYS = /* @__PURE__ */ new Set(["note", "notes", "_note", "_notes", "_comment", "_comments", "__comment", "$comment"]);
39
+ var STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["@context", "@type", "@id", "position"]);
40
+ var MUSTACHE_SLOT = /\{\{\s*[#^/>&]?\s*[A-Za-z_$][\w$.[\]-]*\s*\}\}/;
41
+ var JSX_EXPRESSION_VALUE = /^\{\s*[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*|\[[^\]]*\])+[^{}]*\}$/;
42
+ var JSX_MEMBER_IN_TEXT = /\{\s*[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+\s*\}/;
43
+ var BRACKET_SLOT_VALUE = /^\[\s*[A-Za-z][^[\]"{}<>]{0,80}\]$/;
44
+ var BRACKET_SLOT_IN_TEXT = /\[\s*((?:your|insert your|enter your|add your)\s+[^[\]]{1,60}?|(?:(?:business|company|organization|brand|full|first|last|resident|client|customer|author)\s+)?(?:name|phone(?:\s+number)?|telephone|email(?:\s+address)?|(?:street\s+)?address|city|state|zip(?:\s+code)?|postal\s+code|website|url|logo|date))\s*\](?!\()/gi;
45
+ var REPLACE_MARKER = /\bREPLACE(?:_WITH_\w*|_ME\b)/;
46
+ var UPPER_SLOT_VALUE = /^YOUR_[A-Z0-9_]+$/;
47
+ var MAX_DEPTH = 40;
48
+ var MAX_EVIDENCE = 5;
49
+ var EVIDENCE_VALUE_LENGTH = 120;
50
+ function withoutSchemaPlaceholders(value) {
51
+ const state = { dropped: [], notes: [] };
52
+ if (value === null || value === void 0) return { value: null, ...state };
53
+ const result = visitRoot(value, state);
54
+ return { value: result, ...state };
55
+ }
56
+ function findSchemaPlaceholders(value) {
57
+ return withoutSchemaPlaceholders(value).dropped;
58
+ }
59
+ var REASON_TEXT = {
60
+ example_domain: "is on a reserved example domain",
61
+ placeholder_phone: "is a placeholder phone number",
62
+ placeholder_name: "is a placeholder name",
63
+ template_token: "is an unfilled template slot",
64
+ annotation: "is an AI note, not schema",
65
+ emptied: "held only placeholders"
66
+ };
67
+ function describeSchemaPlaceholder(node) {
68
+ const where = `${node.type || "Node"} at ${node.path}`;
69
+ const first = node.evidence[0];
70
+ const detail = first ? `${first.key} ${JSON.stringify(first.value)} ${REASON_TEXT[first.reason]}` : REASON_TEXT[node.reasons[0]];
71
+ return node.reasons.includes("emptied") ? `${where} has nothing real left: ${detail}` : `${where}: ${detail}`;
72
+ }
73
+ function visitRoot(value, state) {
74
+ if (Array.isArray(value)) {
75
+ const out = visitArray(value, "$", state, 0, true);
76
+ return out.removed && out.value.length === 0 ? null : out.value;
77
+ }
78
+ if (!isPlainObject(value)) return value;
79
+ if (isGraphWrapper(value)) {
80
+ const out = visitContainer(value, "$", state, 0);
81
+ return out.removedChild && !hasContentBeyond(out.value, /* @__PURE__ */ new Set(["@context"])) ? null : out.value;
82
+ }
83
+ const visit = visitNode(value, "$", state, 0);
84
+ return visit.keep ? visit.value : null;
85
+ }
86
+ function visitNode(node, path, state, depth) {
87
+ if (isAnnotationOnly(node)) {
88
+ state.dropped.push({ path, type: null, reasons: ["annotation"], evidence: annotationEvidence(node) });
89
+ return { keep: false };
90
+ }
91
+ const evidence = [];
92
+ collectEvidence(node, evidence, depth);
93
+ if (evidence.length) {
94
+ state.dropped.push({ path, type: typeOf(node), reasons: unique(evidence.map((e) => e.reason)), evidence });
95
+ return { keep: false };
96
+ }
97
+ const droppedMark = state.dropped.length;
98
+ const notesMark = state.notes.length;
99
+ const pruned = visitContainer(node, path, state, depth);
100
+ if (pruned.removedChild && !hasContentBeyond(pruned.value, STRUCTURAL_KEYS)) {
101
+ const children = state.dropped.splice(droppedMark);
102
+ state.notes.length = notesMark;
103
+ state.dropped.push({
104
+ path,
105
+ type: typeOf(node),
106
+ reasons: unique(["emptied", ...children.flatMap((c) => c.reasons)]),
107
+ evidence: children.flatMap((c) => c.evidence).slice(0, MAX_EVIDENCE)
108
+ });
109
+ return { keep: false };
110
+ }
111
+ return { keep: true, value: pruned.value };
112
+ }
113
+ function visitContainer(obj, path, state, depth) {
114
+ if (depth >= MAX_DEPTH) return { value: obj, removedChild: false };
115
+ let changed = false;
116
+ let removedChild = false;
117
+ const out = {};
118
+ for (const [key, child] of Object.entries(obj)) {
119
+ if (key === "@context") {
120
+ out[key] = child;
121
+ continue;
122
+ }
123
+ if (ANNOTATION_KEYS.has(key)) {
124
+ state.notes.push({ path, key });
125
+ changed = true;
126
+ continue;
127
+ }
128
+ const childPath = stepKey(path, key);
129
+ if (Array.isArray(child)) {
130
+ const pruned = visitArray(child, childPath, state, depth + 1, key === "@graph");
131
+ if (pruned.removed) removedChild = true;
132
+ if (pruned.value !== child) changed = true;
133
+ if (pruned.removed && pruned.value.length === 0) continue;
134
+ out[key] = pruned.value;
135
+ continue;
136
+ }
137
+ if (isPlainObject(child)) {
138
+ if (isNestedNode(child)) {
139
+ const visit = visitNode(child, childPath, state, depth + 1);
140
+ if (!visit.keep) {
141
+ changed = true;
142
+ removedChild = true;
143
+ continue;
144
+ }
145
+ if (visit.value !== child) changed = true;
146
+ out[key] = visit.value;
147
+ continue;
148
+ }
149
+ const pruned = visitContainer(child, childPath, state, depth + 1);
150
+ if (pruned.removedChild) removedChild = true;
151
+ if (pruned.value !== child) changed = true;
152
+ out[key] = pruned.value;
153
+ continue;
154
+ }
155
+ out[key] = child;
156
+ }
157
+ return { value: changed ? out : obj, removedChild };
158
+ }
159
+ function visitArray(items, path, state, depth, units) {
160
+ if (depth >= MAX_DEPTH) return { value: items, removed: false };
161
+ let changed = false;
162
+ let removed = false;
163
+ const out = [];
164
+ items.forEach((item, index) => {
165
+ const itemPath = `${path}[${index}]`;
166
+ if (Array.isArray(item)) {
167
+ const pruned = visitArray(item, itemPath, state, depth + 1, units);
168
+ if (pruned.removed) removed = true;
169
+ if (pruned.value !== item) changed = true;
170
+ out.push(pruned.value);
171
+ return;
172
+ }
173
+ if (isPlainObject(item) && (units || isNestedNode(item))) {
174
+ const visit = visitNode(item, itemPath, state, depth + 1);
175
+ if (!visit.keep) {
176
+ changed = true;
177
+ removed = true;
178
+ return;
179
+ }
180
+ if (visit.value !== item) changed = true;
181
+ out.push(visit.value);
182
+ return;
183
+ }
184
+ if (isPlainObject(item)) {
185
+ const pruned = visitContainer(item, itemPath, state, depth + 1);
186
+ if (pruned.removedChild) removed = true;
187
+ if (pruned.value !== item) changed = true;
188
+ out.push(pruned.value);
189
+ return;
190
+ }
191
+ out.push(item);
192
+ });
193
+ return { value: changed ? out : items, removed };
194
+ }
195
+ function collectEvidence(holder, out, depth) {
196
+ for (const [key, value] of Object.entries(holder)) {
197
+ if (key === "@context" || ANNOTATION_KEYS.has(key)) continue;
198
+ collectFromValue(holder, key, value, out, depth);
199
+ }
200
+ }
201
+ function collectFromValue(holder, key, value, out, depth) {
202
+ if (depth >= MAX_DEPTH || out.length >= MAX_EVIDENCE) return;
203
+ if (typeof value === "string") {
204
+ for (const reason of placeholderReasons(key, value, holder)) {
205
+ if (out.length >= MAX_EVIDENCE) return;
206
+ out.push({ reason, key, value: cutForLog(value) });
207
+ }
208
+ return;
209
+ }
210
+ if (Array.isArray(value)) {
211
+ for (const item of value) collectFromValue(holder, key, item, out, depth + 1);
212
+ return;
213
+ }
214
+ if (isPlainObject(value) && !isNestedNode(value)) collectEvidence(value, out, depth + 1);
215
+ }
216
+ function placeholderReasons(key, raw, holder) {
217
+ const value = raw.trim();
218
+ if (!value) return [];
219
+ const reasons = [];
220
+ const host = urlHost(value);
221
+ if (host && isReservedExampleHost(host)) reasons.push("example_domain");
222
+ if ((PHONE_KEYS.has(key) || /^tel:/i.test(value)) && isPlaceholderPhone(value)) reasons.push("placeholder_phone");
223
+ if (NAME_KEYS.has(key) && !("value" in holder) && PLACEHOLDER_NAMES.has(normalizeName(value))) {
224
+ reasons.push("placeholder_name");
225
+ }
226
+ if (isTemplateSlot(key, value)) reasons.push("template_token");
227
+ return reasons;
228
+ }
229
+ function isTemplateSlot(key, value) {
230
+ if (JSX_EXPRESSION_VALUE.test(value) || BRACKET_SLOT_VALUE.test(value) || UPPER_SLOT_VALUE.test(value)) return true;
231
+ if (REPLACE_MARKER.test(value)) return true;
232
+ if (!BODY_TEXT_KEYS.has(key)) {
233
+ if (MUSTACHE_SLOT.test(value)) return true;
234
+ for (const match of value.matchAll(BRACKET_SLOT_IN_TEXT)) {
235
+ if (/^[A-Z]/.test(match[1])) return true;
236
+ }
237
+ }
238
+ return !URI_TEMPLATE_KEYS.has(key) && isUrlOrPath(value) && JSX_MEMBER_IN_TEXT.test(value);
239
+ }
240
+ function urlHost(value) {
241
+ const mailto = /^mailto:[^@\s?]+@([^?\s#/]+)/i.exec(value);
242
+ if (mailto) return mailto[1];
243
+ const url = /^(?:[a-z][a-z0-9+.-]*:)?\/\/(?:[^/?#@\s]*@)?([^/?#:\s]+)/i.exec(value);
244
+ if (url) return url[1];
245
+ const email = /^[^\s@/:]+@([^\s@/:?#]+)$/.exec(value);
246
+ if (email) return email[1];
247
+ const bare = /^((?:[a-z0-9-]+\.)+[a-z][a-z0-9-]*)\.?(?::\d+)?(?:[/?#]\S*)?$/i.exec(value);
248
+ return bare ? bare[1] : null;
249
+ }
250
+ function isReservedExampleHost(host) {
251
+ const h = host.toLowerCase().replace(/\.$/, "");
252
+ if (h === RESERVED_EXAMPLE_TLD || h.endsWith(`.${RESERVED_EXAMPLE_TLD}`)) return true;
253
+ return RESERVED_EXAMPLE_DOMAINS.some((domain) => h === domain || h.endsWith(`.${domain}`));
254
+ }
255
+ function isUrlOrPath(value) {
256
+ return /^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(value) || value.startsWith("/");
257
+ }
258
+ function isPlaceholderPhone(raw) {
259
+ const value = raw.replace(/^tel:/i, "").trim();
260
+ if (/^[+\d\s().xX-]+$/.test(value) && (value.match(/x/gi)?.length ?? 0) >= 4) return true;
261
+ let digits = value.replace(/\D/g, "");
262
+ if (digits.length === 11 && digits.startsWith("1")) digits = digits.slice(1);
263
+ if (digits.length >= 7 && /^0+$/.test(digits)) return true;
264
+ if (digits === "1234567890" || digits === "1234567") return true;
265
+ const local = digits.length === 10 ? digits.slice(3) : digits.length === 7 ? digits : null;
266
+ return local !== null && /^55501\d\d$/.test(local);
267
+ }
268
+ function normalizeName(value) {
269
+ return value.replace(/\s+/g, " ").trim().replace(/[.,]+$/, "").toLowerCase();
270
+ }
271
+ function isPlainObject(value) {
272
+ return typeof value === "object" && value !== null && !Array.isArray(value);
273
+ }
274
+ function isNestedNode(value) {
275
+ return "@type" in value || "@id" in value || isAnnotationOnly(value);
276
+ }
277
+ function isGraphWrapper(value) {
278
+ return "@graph" in value && !("@type" in value) && !("@id" in value);
279
+ }
280
+ function isAnnotationOnly(value) {
281
+ if ("@type" in value || "@id" in value || "@graph" in value) return false;
282
+ const keys = Object.keys(value).filter((key) => key !== "@context");
283
+ return keys.length > 0 && keys.every((key) => ANNOTATION_KEYS.has(key));
284
+ }
285
+ function annotationEvidence(node) {
286
+ return Object.entries(node).filter(([key]) => ANNOTATION_KEYS.has(key)).slice(0, MAX_EVIDENCE).map(([key, value]) => ({
287
+ reason: "annotation",
288
+ key,
289
+ value: cutForLog(typeof value === "string" ? value : JSON.stringify(value) ?? "")
290
+ }));
291
+ }
292
+ function hasContentBeyond(obj, ignored) {
293
+ return Object.entries(obj).some(([key, value]) => !ignored.has(key) && hasContent(value));
294
+ }
295
+ function hasContent(value) {
296
+ if (value === null || value === void 0 || value === "") return false;
297
+ if (Array.isArray(value)) return value.length > 0;
298
+ if (isPlainObject(value)) return Object.keys(value).length > 0;
299
+ return true;
300
+ }
301
+ function typeOf(node) {
302
+ const type = node["@type"];
303
+ if (typeof type === "string") return type;
304
+ if (Array.isArray(type)) return type.filter((t) => typeof t === "string").join(",") || null;
305
+ return null;
306
+ }
307
+ function stepKey(path, key) {
308
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
309
+ }
310
+ function unique(values) {
311
+ return [...new Set(values)];
312
+ }
313
+ function cutForLog(value) {
314
+ return value.length > EVIDENCE_VALUE_LENGTH ? `${value.slice(0, EVIDENCE_VALUE_LENGTH)}...` : value;
315
+ }
316
+
317
+ export { describeSchemaPlaceholder, findSchemaPlaceholders, withoutSchemaPlaceholders };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Sentences contract: how prose is split into sentences, so the APIs that cut
3
+ * an excerpt, a carousel slide or a summary out of an article agree on where
4
+ * a sentence ends. The APIs import it as `@sonordev/contracts/sentences`.
5
+ *
6
+ * Lossless: joining the result with spaces gives back every word of the input,
7
+ * in order. The usual one-liner, `text.match(/[^.!?]+[.!?]+(?=\s|$)/g)`, isn't.
8
+ * A period it can't use, like the first one in "9 p.m. call", makes match()
9
+ * skip ahead and silently drop every word before its next match, so a
10
+ * sentence about a 9 p.m. call comes back as "m.". Don't re-fork a regex for
11
+ * this.
12
+ *
13
+ * A sentence ends at . ! or ? (and any closing quote or bracket after it)
14
+ * followed by whitespace, then an optional opening quote or bracket, then
15
+ * A to Z or 0 to 9. Never before a lowercase word or a symbol, and only ASCII
16
+ * capitals count. It doesn't end at:
17
+ * - an abbreviation: a dotted one made of letters ("p.m.", "U.S.", "e.g.",
18
+ * "Ph.D."), a single letter ("J.", "Plan A."), or a short form listed below
19
+ * (titles, months, days, company suffixes, street suffixes);
20
+ * - a label that has a number after it ("No. 5", "Fig. 2", "Ste. 200");
21
+ * - a list number ("1.", "Step 2.", "Do three things: 1.").
22
+ * A piece with no letter in it ("...", "1.") isn't a sentence and joins the
23
+ * next one. A "!" or "?" is never held back by an abbreviation (only a period
24
+ * is), though the same start-of-sentence rule applies after it.
25
+ *
26
+ * Prices, versions, domains and email addresses ("$4.50", "v2.1",
27
+ * "example.com") are not abbreviations: a sentence can end after them.
28
+ *
29
+ * Where it is unsure it leans toward not splitting, because a sentence that
30
+ * runs long costs less than a lost word. A period after a short form that
31
+ * isn't listed ends a sentence like any other.
32
+ *
33
+ * It reads sentences, not layout. A newline or a list marker doesn't start a
34
+ * new sentence by itself, so split text into lines or paragraphs first when
35
+ * those matter, then split each one.
36
+ */
37
+ /**
38
+ * Splits prose into trimmed sentences without dropping or reordering a word.
39
+ * Empty, blank and nullish input give `[]`, never `null`.
40
+ *
41
+ * One pass, linear in the length of the text: each run of . ! ? (and the
42
+ * closing quotes or brackets after it) is looked at once, an abbreviation or
43
+ * list-marker check reads only the last few characters, and the check for a
44
+ * letter reads each character once.
45
+ */
46
+ export declare function splitSentences(text: string | null | undefined): string[];