@inflexa-ai/harness 0.28.1 → 0.29.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.
- package/dist/app/data-profile-orientation.d.ts +52 -27
- package/dist/app/data-profile-orientation.js +331 -92
- package/dist/app/data-profile-view.d.ts +70 -0
- package/dist/app/data-profile-view.js +123 -0
- package/dist/app/synthesize-run.js +1 -1
- package/dist/contracts/data-profile.d.ts +276 -25
- package/dist/contracts/index.d.ts +3 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/profile-vocabulary.d.ts +103 -0
- package/dist/contracts/profile-vocabulary.js +449 -0
- package/dist/contracts/target-dossier.d.ts +21 -21
- package/dist/execution/input-signature.d.ts +16 -3
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/post-step-pipeline.js +1 -1
- package/dist/execution/staged-input.d.ts +4 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -0
- package/dist/input-scan/clustering.d.ts +63 -0
- package/dist/input-scan/clustering.js +208 -0
- package/dist/input-scan/companions.d.ts +30 -0
- package/dist/input-scan/companions.js +138 -0
- package/dist/input-scan/decoder/container_readout.py +171 -0
- package/dist/input-scan/detect-sets.d.ts +24 -0
- package/dist/input-scan/detect-sets.js +279 -0
- package/dist/input-scan/enrich.d.ts +62 -19
- package/dist/input-scan/enrich.js +132 -187
- package/dist/input-scan/formats.d.ts +13 -5
- package/dist/input-scan/formats.js +13 -5
- package/dist/input-scan/index.d.ts +10 -4
- package/dist/input-scan/index.js +7 -4
- package/dist/input-scan/markers.d.ts +21 -0
- package/dist/input-scan/markers.js +89 -0
- package/dist/input-scan/menu.d.ts +127 -0
- package/dist/input-scan/menu.js +0 -0
- package/dist/input-scan/quarantine.d.ts +17 -0
- package/dist/input-scan/quarantine.js +69 -0
- package/dist/input-scan/readout-budget.d.ts +32 -0
- package/dist/input-scan/readout-budget.js +61 -0
- package/dist/input-scan/readout.d.ts +33 -0
- package/dist/input-scan/readout.js +336 -0
- package/dist/input-scan/scan.d.ts +5 -4
- package/dist/input-scan/scan.js +5 -5
- package/dist/input-scan/set-types.d.ts +209 -0
- package/dist/input-scan/set-types.js +15 -0
- package/dist/input-scan/templates.d.ts +93 -0
- package/dist/input-scan/templates.js +369 -0
- package/dist/input-scan/tokens.d.ts +94 -0
- package/dist/input-scan/tokens.js +267 -0
- package/dist/input-scan/tuning.d.ts +55 -0
- package/dist/input-scan/tuning.js +55 -0
- package/dist/input-scan/types.d.ts +2 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +8 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +3 -2
- package/dist/prompts/sandbox/data-profiler.d.ts +10 -1
- package/dist/prompts/sandbox/data-profiler.js +197 -53
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +10 -7
- package/dist/schemas/data-profile-schemas.d.ts +943 -80
- package/dist/schemas/data-profile-schemas.js +300 -115
- package/dist/state/data-profile.d.ts +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/vector-store.d.ts +13 -0
- package/dist/state/vector-store.js +8 -0
- package/dist/tasks/data-profile-absorb.d.ts +78 -0
- package/dist/tasks/data-profile-absorb.js +285 -0
- package/dist/tasks/data-profile-index.d.ts +34 -25
- package/dist/tasks/data-profile-index.js +160 -59
- package/dist/tasks/data-profile-resolve.d.ts +72 -0
- package/dist/tasks/data-profile-resolve.js +639 -0
- package/dist/tasks/data-profile.d.ts +16 -5
- package/dist/tasks/data-profile.js +270 -131
- package/dist/tools/research/inspect-data-profile.d.ts +48 -22
- package/dist/tools/research/inspect-data-profile.js +73 -54
- package/dist/tools/workspace/scan-inputs.d.ts +4 -4
- package/dist/tools/workspace/scan-inputs.js +13 -14
- package/dist/tools/workspace/workspace-search.d.ts +11 -2
- package/dist/tools/workspace/workspace-search.js +44 -16
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +21 -21
- package/package.json +5 -3
- package/dist/input-scan/coverage.d.ts +0 -22
- package/dist/input-scan/coverage.js +0 -85
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage 3 — name-template mining within one directory context.
|
|
3
|
+
*
|
|
4
|
+
* Literal versus slot is decided by per-position distinct counts: a position whose
|
|
5
|
+
* token is the same in every member is literal text, and one that varies is a slot.
|
|
6
|
+
* Groups that differ in exactly one position, or only in extension, merge — variance
|
|
7
|
+
* inside a set is a property of the set, never a partition key.
|
|
8
|
+
*
|
|
9
|
+
* Proliferation is bounded three ways, because a template per file is the same failure
|
|
10
|
+
* as no templates at all: a prefix trie claims residual singletons that share literal
|
|
11
|
+
* text, a family collapse claims singletons that share only a token-kind sequence
|
|
12
|
+
* (one template for the family costs less than one template each), and a catch-all
|
|
13
|
+
* claims the rest where the repetition across directories is itself the structure.
|
|
14
|
+
*/
|
|
15
|
+
import { DATE_SYMBOL, DIGIT_SYMBOL, ID_SYMBOL, classifyValues, commonAffixes, isDateToken, maskAffix, skeletonOf, tokenizeName } from "./tokens.js";
|
|
16
|
+
import { MAX_SLOT_SAMPLE_VALUES, MIN_AFFIX_LENGTH, MIN_CATCH_ALL_MEMBERS, MIN_FAMILY_SET_MEMBERS, MIN_PREFIX_SET_MEMBERS } from "./tuning.js";
|
|
17
|
+
function toItem(file) {
|
|
18
|
+
const { tokens, suffix } = tokenizeName(file.name);
|
|
19
|
+
return { file, tokens, suffix, skeleton: skeletonOf(tokens) };
|
|
20
|
+
}
|
|
21
|
+
function newGroup(kind, seed) {
|
|
22
|
+
return { kind, tokenKinds: [], prefixTokens: [], suffixes: new Map(), items: [], ...seed };
|
|
23
|
+
}
|
|
24
|
+
function firstPass(items) {
|
|
25
|
+
const groups = new Map();
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
const key = `${item.skeleton} ${item.suffix}`;
|
|
28
|
+
let group = groups.get(key);
|
|
29
|
+
if (!group) {
|
|
30
|
+
group = newGroup("tokens", { tokenKinds: item.tokens.map((token) => token.kind), suffix: item.suffix });
|
|
31
|
+
groups.set(key, group);
|
|
32
|
+
}
|
|
33
|
+
group.items.push(item);
|
|
34
|
+
group.suffixes.set(item.suffix, (group.suffixes.get(item.suffix) ?? 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
return [...groups.values()];
|
|
37
|
+
}
|
|
38
|
+
function distinctCounts(group) {
|
|
39
|
+
if (!group.distinct) {
|
|
40
|
+
group.distinct = group.tokenKinds.map((_, position) => {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const item of group.items)
|
|
43
|
+
seen.add(item.tokens[position]?.value ?? "");
|
|
44
|
+
return seen.size;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return group.distinct;
|
|
48
|
+
}
|
|
49
|
+
function isVariable(group, position) {
|
|
50
|
+
if (group.tokenKinds[position] === "id")
|
|
51
|
+
return true;
|
|
52
|
+
return distinctCounts(group)[position] > 1;
|
|
53
|
+
}
|
|
54
|
+
function mergeGroups(groups) {
|
|
55
|
+
const head = groups[0];
|
|
56
|
+
const suffixes = new Map();
|
|
57
|
+
const items = [];
|
|
58
|
+
for (const group of groups) {
|
|
59
|
+
items.push(...group.items);
|
|
60
|
+
for (const [suffix, count] of group.suffixes)
|
|
61
|
+
suffixes.set(suffix, (suffixes.get(suffix) ?? 0) + count);
|
|
62
|
+
}
|
|
63
|
+
return { kind: head.kind, tokenKinds: head.tokenKinds, prefixTokens: head.prefixTokens, suffix: head.suffix, suffixes, items };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Merge groups differing in exactly one alphabetic or numeric position to a fixpoint,
|
|
67
|
+
* then fold groups whose stems agree once their slots are wildcarded.
|
|
68
|
+
*
|
|
69
|
+
* Without the first pass a categorical position reads as unrelated groups, and the
|
|
70
|
+
* agent is handed the very split it exists to decide against. Without the second,
|
|
71
|
+
* members that differ only in compression become two sets that a reader has to notice
|
|
72
|
+
* are one.
|
|
73
|
+
*/
|
|
74
|
+
function mergeVariants(groups) {
|
|
75
|
+
let current = [...groups];
|
|
76
|
+
for (;;) {
|
|
77
|
+
const families = new Map();
|
|
78
|
+
for (const group of current) {
|
|
79
|
+
const key = `${group.tokenKinds.join(",")} ${group.suffix}`;
|
|
80
|
+
const bucket = families.get(key);
|
|
81
|
+
if (bucket)
|
|
82
|
+
bucket.push(group);
|
|
83
|
+
else
|
|
84
|
+
families.set(key, [group]);
|
|
85
|
+
}
|
|
86
|
+
const next = [];
|
|
87
|
+
let merged = false;
|
|
88
|
+
for (const family of families.values()) {
|
|
89
|
+
if (family.length === 1) {
|
|
90
|
+
next.push(family[0]);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const positions = family[0].tokenKinds.length;
|
|
94
|
+
let mergedHere = false;
|
|
95
|
+
for (let p = 0; p < positions && !mergedHere; p++) {
|
|
96
|
+
const kind = family[0].tokenKinds[p];
|
|
97
|
+
if (kind !== "alpha" && kind !== "digit")
|
|
98
|
+
continue;
|
|
99
|
+
const buckets = new Map();
|
|
100
|
+
for (const group of family) {
|
|
101
|
+
// Every member of a group shares its literal tokens, so the first row
|
|
102
|
+
// identifies the whole group at this position.
|
|
103
|
+
const row = group.items[0].tokens;
|
|
104
|
+
const key = row.map((token, i) => (i === p || isVariable(group, i) ? "" : token.value)).join(" ");
|
|
105
|
+
const bucket = buckets.get(key);
|
|
106
|
+
if (bucket)
|
|
107
|
+
bucket.push(group);
|
|
108
|
+
else
|
|
109
|
+
buckets.set(key, [group]);
|
|
110
|
+
}
|
|
111
|
+
if ([...buckets.values()].every((bucket) => bucket.length === 1))
|
|
112
|
+
continue;
|
|
113
|
+
for (const bucket of buckets.values())
|
|
114
|
+
next.push(bucket.length === 1 ? bucket[0] : mergeGroups(bucket));
|
|
115
|
+
mergedHere = true;
|
|
116
|
+
merged = true;
|
|
117
|
+
}
|
|
118
|
+
if (!mergedHere)
|
|
119
|
+
next.push(...family);
|
|
120
|
+
}
|
|
121
|
+
current = next;
|
|
122
|
+
if (!merged)
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
const byStem = new Map();
|
|
126
|
+
for (const group of current) {
|
|
127
|
+
const row = group.items[0].tokens;
|
|
128
|
+
const key = `${group.tokenKinds.join(",")} ${row.map((token, i) => (isVariable(group, i) ? "" : token.value)).join(" ")}`;
|
|
129
|
+
const bucket = byStem.get(key);
|
|
130
|
+
if (bucket)
|
|
131
|
+
bucket.push(group);
|
|
132
|
+
else
|
|
133
|
+
byStem.set(key, [group]);
|
|
134
|
+
}
|
|
135
|
+
return [...byStem.values()].map((bucket) => (bucket.length === 1 ? bucket[0] : mergeGroups(bucket)));
|
|
136
|
+
}
|
|
137
|
+
function symbolOf(token) {
|
|
138
|
+
if (token.kind === "id")
|
|
139
|
+
return ID_SYMBOL;
|
|
140
|
+
if (token.kind === "digit")
|
|
141
|
+
return DIGIT_SYMBOL;
|
|
142
|
+
if (token.kind === "date")
|
|
143
|
+
return DATE_SYMBOL;
|
|
144
|
+
return token.value;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Claim residual singletons by shared literal prefix, deepest node first.
|
|
148
|
+
*
|
|
149
|
+
* Children are claimed before their parent, so a specific prefix wins over the general
|
|
150
|
+
* one it extends, and a prefix earns a set only once it covers enough files to be
|
|
151
|
+
* cheaper than the singletons it replaces.
|
|
152
|
+
*/
|
|
153
|
+
function minePrefixes(residue) {
|
|
154
|
+
const bySuffix = new Map();
|
|
155
|
+
for (const item of residue) {
|
|
156
|
+
const bucket = bySuffix.get(item.suffix);
|
|
157
|
+
if (bucket)
|
|
158
|
+
bucket.push(item);
|
|
159
|
+
else
|
|
160
|
+
bySuffix.set(item.suffix, [item]);
|
|
161
|
+
}
|
|
162
|
+
const sets = [];
|
|
163
|
+
const rest = [];
|
|
164
|
+
for (const [suffix, items] of bySuffix) {
|
|
165
|
+
if (items.length < MIN_PREFIX_SET_MEMBERS) {
|
|
166
|
+
rest.push(...items);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const root = { children: new Map(), items: [] };
|
|
170
|
+
for (const item of items) {
|
|
171
|
+
let node = root;
|
|
172
|
+
node.items.push(item);
|
|
173
|
+
for (const token of item.tokens) {
|
|
174
|
+
const key = symbolOf(token);
|
|
175
|
+
let child = node.children.get(key);
|
|
176
|
+
if (!child) {
|
|
177
|
+
child = { children: new Map(), items: [], token };
|
|
178
|
+
node.children.set(key, child);
|
|
179
|
+
}
|
|
180
|
+
child.items.push(item);
|
|
181
|
+
node = child;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const taken = new Set();
|
|
185
|
+
const claim = (node, prefixTokens) => {
|
|
186
|
+
for (const child of node.children.values())
|
|
187
|
+
claim(child, [...prefixTokens, child.token]);
|
|
188
|
+
const free = node.items.filter((item) => !taken.has(item));
|
|
189
|
+
const literals = prefixTokens.filter((token) => token.kind === "alpha").length;
|
|
190
|
+
if (free.length < MIN_PREFIX_SET_MEMBERS || literals < 1)
|
|
191
|
+
return;
|
|
192
|
+
for (const item of free)
|
|
193
|
+
taken.add(item);
|
|
194
|
+
const suffixes = new Map([[suffix, free.length]]);
|
|
195
|
+
sets.push(newGroup("prefix", { prefixTokens, suffix, suffixes, items: free }));
|
|
196
|
+
};
|
|
197
|
+
claim(root, []);
|
|
198
|
+
rest.push(...items.filter((item) => !taken.has(item)));
|
|
199
|
+
}
|
|
200
|
+
return { sets, rest };
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Collapse singletons that share a token-kind sequence into one set.
|
|
204
|
+
*
|
|
205
|
+
* A flat directory of differently-named tables has one shape even though no two names
|
|
206
|
+
* differ in a single position. One template for the family is a shorter description
|
|
207
|
+
* than one template each, which is the only reason to prefer it — so it applies only
|
|
208
|
+
* once the family is large enough to pay for the generality it gives up.
|
|
209
|
+
*/
|
|
210
|
+
function collapseFamilies(residue) {
|
|
211
|
+
const families = new Map();
|
|
212
|
+
for (const item of residue) {
|
|
213
|
+
const key = `${item.tokens.map((token) => token.kind).join(",")} ${item.suffix}`;
|
|
214
|
+
const bucket = families.get(key);
|
|
215
|
+
if (bucket)
|
|
216
|
+
bucket.push(item);
|
|
217
|
+
else
|
|
218
|
+
families.set(key, [item]);
|
|
219
|
+
}
|
|
220
|
+
const sets = [];
|
|
221
|
+
const rest = [];
|
|
222
|
+
for (const items of families.values()) {
|
|
223
|
+
if (items.length < MIN_FAMILY_SET_MEMBERS) {
|
|
224
|
+
rest.push(...items);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const head = items[0];
|
|
228
|
+
sets.push(newGroup("family", {
|
|
229
|
+
tokenKinds: head.tokens.map((token) => token.kind),
|
|
230
|
+
suffix: head.suffix,
|
|
231
|
+
suffixes: new Map([[head.suffix, items.length]]),
|
|
232
|
+
items: [...items],
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
return { sets, rest };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Fold what is left of a cross-directory context into one set.
|
|
239
|
+
*
|
|
240
|
+
* Where the same directory shape repeats, that repetition is the structure, and the
|
|
241
|
+
* residual names inside it are one set whatever they are called. Extension
|
|
242
|
+
* disagreement is carried as the format census, never used to split.
|
|
243
|
+
*/
|
|
244
|
+
export function catchAll(residue, hasDirectorySlot) {
|
|
245
|
+
if (!hasDirectorySlot || residue.length < MIN_CATCH_ALL_MEMBERS)
|
|
246
|
+
return { sets: [], rest: [...residue] };
|
|
247
|
+
const suffixes = new Map();
|
|
248
|
+
for (const item of residue)
|
|
249
|
+
suffixes.set(item.suffix, (suffixes.get(item.suffix) ?? 0) + 1);
|
|
250
|
+
return {
|
|
251
|
+
sets: [newGroup("catch-all", { suffix: [...suffixes.keys()].sort((a, b) => a.localeCompare(b, "en")).join(","), suffixes, items: [...residue] })],
|
|
252
|
+
rest: [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Mine one directory context: every file whose directory path instantiates one template. */
|
|
256
|
+
export function mineContext(files) {
|
|
257
|
+
const groups = mergeVariants(firstPass(files.map(toItem)));
|
|
258
|
+
const sets = [];
|
|
259
|
+
const singletons = [];
|
|
260
|
+
for (const group of groups) {
|
|
261
|
+
if (group.items.length >= 2)
|
|
262
|
+
sets.push(group);
|
|
263
|
+
else
|
|
264
|
+
singletons.push(group.items[0]);
|
|
265
|
+
}
|
|
266
|
+
const prefixes = minePrefixes(singletons);
|
|
267
|
+
sets.push(...prefixes.sets);
|
|
268
|
+
const families = collapseFamilies(prefixes.rest);
|
|
269
|
+
sets.push(...families.sets);
|
|
270
|
+
return { sets, residue: families.rest };
|
|
271
|
+
}
|
|
272
|
+
/** Describe one varying position: its class, its cardinality, and a bounded value sample. */
|
|
273
|
+
export function describeSlot(rawValues, location, index) {
|
|
274
|
+
const distinct = [...new Set(rawValues)];
|
|
275
|
+
let { prefix, suffix } = commonAffixes(distinct);
|
|
276
|
+
if (prefix.length < MIN_AFFIX_LENGTH)
|
|
277
|
+
prefix = "";
|
|
278
|
+
if (suffix.length < MIN_AFFIX_LENGTH)
|
|
279
|
+
suffix = "";
|
|
280
|
+
// A number and a date are each one value: slicing a shared year off them would
|
|
281
|
+
// report a date as a literal prefix plus a pattern.
|
|
282
|
+
if (distinct.every((value) => /^[0-9]*$/.test(value)) || distinct.every((value) => isDateToken(value))) {
|
|
283
|
+
prefix = "";
|
|
284
|
+
suffix = "";
|
|
285
|
+
}
|
|
286
|
+
const strip = (value) => value.slice(prefix.length, suffix ? value.length - suffix.length : undefined);
|
|
287
|
+
const cores = distinct.map(strip);
|
|
288
|
+
const observed = classifyValues(cores);
|
|
289
|
+
const sorted = [...cores].sort((a, b) => a.localeCompare(b, "en"));
|
|
290
|
+
return {
|
|
291
|
+
location,
|
|
292
|
+
index,
|
|
293
|
+
tokenClass: observed.tokenClass,
|
|
294
|
+
...(observed.width !== undefined ? { width: observed.width } : {}),
|
|
295
|
+
...(observed.minLength !== undefined ? { minLength: observed.minLength, maxLength: observed.maxLength } : {}),
|
|
296
|
+
...(observed.skeleton !== undefined ? { skeleton: observed.skeleton } : {}),
|
|
297
|
+
...(prefix ? { prefix } : {}),
|
|
298
|
+
...(suffix ? { suffix } : {}),
|
|
299
|
+
distinctValues: distinct.length,
|
|
300
|
+
sampleValues: sorted.slice(0, MAX_SLOT_SAMPLE_VALUES),
|
|
301
|
+
values: cores,
|
|
302
|
+
memberValues: rawValues.map(strip),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** The template token a slot renders as. */
|
|
306
|
+
export function slotToken(slot) {
|
|
307
|
+
switch (slot.tokenClass) {
|
|
308
|
+
case "digits-fixed":
|
|
309
|
+
return `<digits:${slot.width}>`;
|
|
310
|
+
case "pattern":
|
|
311
|
+
return `<pattern:${slot.skeleton}>`;
|
|
312
|
+
default:
|
|
313
|
+
return `<${slot.tokenClass}>`;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function affixText(slot, affix) {
|
|
317
|
+
return slot.tokenClass === "opaque-id" ? maskAffix(affix) : affix;
|
|
318
|
+
}
|
|
319
|
+
function suffixLiteral(group) {
|
|
320
|
+
if (group.suffixes.size <= 1)
|
|
321
|
+
return group.suffix ? `.${group.suffix}` : "";
|
|
322
|
+
const suffixes = [...group.suffixes.keys()].sort((a, b) => a.localeCompare(b, "en"));
|
|
323
|
+
return `.{${suffixes.join(",")}}`;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The name half of a set's template: its literal text, its slots, and the order they
|
|
327
|
+
* appear in. Directory slots are prepended by the assembly, which knows the context.
|
|
328
|
+
*/
|
|
329
|
+
export function describeGroup(group) {
|
|
330
|
+
const slots = [];
|
|
331
|
+
const segments = [];
|
|
332
|
+
const pushSlot = (slot) => {
|
|
333
|
+
if (slot.prefix)
|
|
334
|
+
segments.push({ kind: "literal", text: affixText(slot, slot.prefix) });
|
|
335
|
+
segments.push({ kind: "slot", slot: slots.length });
|
|
336
|
+
if (slot.suffix)
|
|
337
|
+
segments.push({ kind: "literal", text: affixText(slot, slot.suffix) });
|
|
338
|
+
slots.push(slot);
|
|
339
|
+
};
|
|
340
|
+
if (group.kind === "catch-all") {
|
|
341
|
+
pushSlot(describeSlot(group.items.map((item) => item.file.name), "name", 0));
|
|
342
|
+
return { slots, segments };
|
|
343
|
+
}
|
|
344
|
+
if (group.kind === "prefix") {
|
|
345
|
+
segments.push({ kind: "literal", text: group.prefixTokens.map((token) => token.value).join("") });
|
|
346
|
+
const rests = group.items.map((item) => item.tokens
|
|
347
|
+
.slice(group.prefixTokens.length)
|
|
348
|
+
.map((token) => token.value)
|
|
349
|
+
.join(""));
|
|
350
|
+
pushSlot(describeSlot(rests, "name", group.prefixTokens.length));
|
|
351
|
+
const literal = suffixLiteral(group);
|
|
352
|
+
if (literal)
|
|
353
|
+
segments.push({ kind: "literal", text: literal });
|
|
354
|
+
return { slots, segments };
|
|
355
|
+
}
|
|
356
|
+
for (let i = 0; i < group.tokenKinds.length; i++) {
|
|
357
|
+
const values = group.items.map((item) => item.tokens[i]?.value ?? "");
|
|
358
|
+
const distinct = new Set(values);
|
|
359
|
+
if (distinct.size === 1 && group.tokenKinds[i] !== "id") {
|
|
360
|
+
segments.push({ kind: "literal", text: [...distinct][0] });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
pushSlot(describeSlot(values, "name", i));
|
|
364
|
+
}
|
|
365
|
+
const literal = suffixLiteral(group);
|
|
366
|
+
if (literal)
|
|
367
|
+
segments.push({ kind: "literal", text: literal });
|
|
368
|
+
return { slots, segments };
|
|
369
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenisation and the token-class lattice the detected-set pipeline mines over.
|
|
3
|
+
*
|
|
4
|
+
* A name splits on the boundaries that carry structure — the dotted suffix chain, and
|
|
5
|
+
* runs of letters, digits, and separators inside each dot-part — except where a run is
|
|
6
|
+
* one indivisible token. A machine-issued opaque identifier and a delimited date are
|
|
7
|
+
* each one token, because splitting them yields positions whose values never repeat,
|
|
8
|
+
* and a position whose values never repeat fragments the template space into one
|
|
9
|
+
* template per file.
|
|
10
|
+
*
|
|
11
|
+
* The lattice orders classes by specificity: a value set is described by the most
|
|
12
|
+
* specific class every one of its values satisfies.
|
|
13
|
+
*/
|
|
14
|
+
export type TokenKind = "alpha" | "digit" | "delim" | "id" | "date";
|
|
15
|
+
export interface NameToken {
|
|
16
|
+
readonly kind: TokenKind;
|
|
17
|
+
readonly value: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const ID_SYMBOL = "ID";
|
|
20
|
+
export declare const DIGIT_SYMBOL = "#";
|
|
21
|
+
export declare const DATE_SYMBOL = "@";
|
|
22
|
+
export declare function basenameOf(path: string): string;
|
|
23
|
+
export declare function dirnameOf(path: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Split a basename into its stem and the terminal chain of recognised extensions.
|
|
26
|
+
*
|
|
27
|
+
* The stem may itself contain dots: only a trailing run of known extension tokens is
|
|
28
|
+
* the extension, so the categorical tokens of a dotted suffix chain stay minable.
|
|
29
|
+
*/
|
|
30
|
+
export declare function splitStem(fileName: string): {
|
|
31
|
+
stem: string;
|
|
32
|
+
suffix: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A machine-issued identifier: a UUID, a fixed-length hex digest, or a long
|
|
36
|
+
* class-mixed base64url token that is structurally random.
|
|
37
|
+
*
|
|
38
|
+
* Randomness shows either as case flipping inside letter runs or as digit
|
|
39
|
+
* fragmentation, and either signal alone suffices — a letter-heavy random token has
|
|
40
|
+
* few digit runs, and a digit-heavy one has few letters to flip. Separator density
|
|
41
|
+
* vetoes the second signal only: snake_case is how a person writes a name, and a
|
|
42
|
+
* person's name is not an identifier however many digits it carries.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isOpaqueId(value: string): boolean;
|
|
45
|
+
/** A calendar date written compactly or with a single separator: `20260824`, `2026-08-24`. */
|
|
46
|
+
export declare function isDateToken(value: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Tokenise one path segment — a directory name, or one dot-part of a stem.
|
|
49
|
+
*
|
|
50
|
+
* A maximal base64url-charset run is tested whole before it is split, which is what
|
|
51
|
+
* lets a long identifier collapse to one position where masking digit runs cannot. A
|
|
52
|
+
* date embedded in a longer run is lifted out whole for the same reason: split into
|
|
53
|
+
* year, month, and day it becomes three positions that vary together, and the template
|
|
54
|
+
* space grows by the product rather than by one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function tokenizeSegment(segment: string): NameToken[];
|
|
57
|
+
/** Tokenise a full basename. Dot-parts of the stem are joined by explicit delimiter tokens. */
|
|
58
|
+
export declare function tokenizeName(fileName: string): {
|
|
59
|
+
tokens: NameToken[];
|
|
60
|
+
suffix: string;
|
|
61
|
+
};
|
|
62
|
+
/** The masked form of a token run: variable material becomes its class symbol. */
|
|
63
|
+
export declare function skeletonOf(tokens: readonly NameToken[]): string;
|
|
64
|
+
export declare function maskSegment(segment: string): string;
|
|
65
|
+
export type SlotTokenClass = "constant" | "date" | "digits-fixed" | "digits" | "hex" | "opaque-id" | "word" | "pattern" | "any";
|
|
66
|
+
export interface ValueClass {
|
|
67
|
+
readonly tokenClass: SlotTokenClass;
|
|
68
|
+
/** Digit width, when every value is a fixed-width number. */
|
|
69
|
+
readonly width?: number;
|
|
70
|
+
readonly minLength?: number;
|
|
71
|
+
readonly maxLength?: number;
|
|
72
|
+
/** The shared masked form, when the class is `pattern`. */
|
|
73
|
+
readonly skeleton?: string;
|
|
74
|
+
}
|
|
75
|
+
/** Place a set of observed values on the lattice, most specific class first. */
|
|
76
|
+
export declare function classifyValues(values: readonly string[]): ValueClass;
|
|
77
|
+
/**
|
|
78
|
+
* The longest prefix and suffix shared by every value.
|
|
79
|
+
*
|
|
80
|
+
* A greedy identifier run swallows the literal text fused to it; recovering the shared
|
|
81
|
+
* affixes puts that text back in the template where a reader can see it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function commonAffixes(values: readonly string[]): {
|
|
84
|
+
prefix: string;
|
|
85
|
+
suffix: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Mask class-mixed runs inside a literal affix of an identifier position.
|
|
89
|
+
*
|
|
90
|
+
* The affix was recovered by comparing identifier values, so a run that looks like
|
|
91
|
+
* identifier material almost certainly is: it reached the template only because every
|
|
92
|
+
* member happened to share it.
|
|
93
|
+
*/
|
|
94
|
+
export declare function maskAffix(affix: string): string;
|