@pattern-algebra/core 0.0.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/README.md +571 -0
- package/dist/automaton/complement.d.ts +20 -0
- package/dist/automaton/complement.d.ts.map +1 -0
- package/dist/automaton/complement.js +36 -0
- package/dist/automaton/complement.js.map +1 -0
- package/dist/automaton/complement.test.d.ts +2 -0
- package/dist/automaton/complement.test.d.ts.map +1 -0
- package/dist/automaton/complement.test.js +114 -0
- package/dist/automaton/complement.test.js.map +1 -0
- package/dist/automaton/determinize.d.ts +41 -0
- package/dist/automaton/determinize.d.ts.map +1 -0
- package/dist/automaton/determinize.js +310 -0
- package/dist/automaton/determinize.js.map +1 -0
- package/dist/automaton/determinize.test.d.ts +2 -0
- package/dist/automaton/determinize.test.d.ts.map +1 -0
- package/dist/automaton/determinize.test.js +134 -0
- package/dist/automaton/determinize.test.js.map +1 -0
- package/dist/automaton/emptiness.d.ts +41 -0
- package/dist/automaton/emptiness.d.ts.map +1 -0
- package/dist/automaton/emptiness.js +262 -0
- package/dist/automaton/emptiness.js.map +1 -0
- package/dist/automaton/emptiness.test.d.ts +2 -0
- package/dist/automaton/emptiness.test.d.ts.map +1 -0
- package/dist/automaton/emptiness.test.js +154 -0
- package/dist/automaton/emptiness.test.js.map +1 -0
- package/dist/automaton/index.d.ts +10 -0
- package/dist/automaton/index.d.ts.map +1 -0
- package/dist/automaton/index.js +11 -0
- package/dist/automaton/index.js.map +1 -0
- package/dist/automaton/intersect.d.ts +35 -0
- package/dist/automaton/intersect.d.ts.map +1 -0
- package/dist/automaton/intersect.js +302 -0
- package/dist/automaton/intersect.js.map +1 -0
- package/dist/automaton/pattern-algebra.d.ts +62 -0
- package/dist/automaton/pattern-algebra.d.ts.map +1 -0
- package/dist/automaton/pattern-algebra.js +309 -0
- package/dist/automaton/pattern-algebra.js.map +1 -0
- package/dist/automaton/pattern-algebra.test.d.ts +2 -0
- package/dist/automaton/pattern-algebra.test.d.ts.map +1 -0
- package/dist/automaton/pattern-algebra.test.js +223 -0
- package/dist/automaton/pattern-algebra.test.js.map +1 -0
- package/dist/compile/automaton-builder.d.ts +47 -0
- package/dist/compile/automaton-builder.d.ts.map +1 -0
- package/dist/compile/automaton-builder.js +211 -0
- package/dist/compile/automaton-builder.js.map +1 -0
- package/dist/compile/compiler.d.ts +32 -0
- package/dist/compile/compiler.d.ts.map +1 -0
- package/dist/compile/compiler.js +47 -0
- package/dist/compile/compiler.js.map +1 -0
- package/dist/compile/index.d.ts +8 -0
- package/dist/compile/index.d.ts.map +1 -0
- package/dist/compile/index.js +8 -0
- package/dist/compile/index.js.map +1 -0
- package/dist/compile/quick-reject.d.ts +28 -0
- package/dist/compile/quick-reject.d.ts.map +1 -0
- package/dist/compile/quick-reject.js +147 -0
- package/dist/compile/quick-reject.js.map +1 -0
- package/dist/containment/analysis.d.ts +60 -0
- package/dist/containment/analysis.d.ts.map +1 -0
- package/dist/containment/analysis.js +378 -0
- package/dist/containment/analysis.js.map +1 -0
- package/dist/containment/containment.d.ts +23 -0
- package/dist/containment/containment.d.ts.map +1 -0
- package/dist/containment/containment.js +681 -0
- package/dist/containment/containment.js.map +1 -0
- package/dist/containment/containment.test.d.ts +2 -0
- package/dist/containment/containment.test.d.ts.map +1 -0
- package/dist/containment/containment.test.js +209 -0
- package/dist/containment/containment.test.js.map +1 -0
- package/dist/containment/index.d.ts +7 -0
- package/dist/containment/index.d.ts.map +1 -0
- package/dist/containment/index.js +7 -0
- package/dist/containment/index.js.map +1 -0
- package/dist/core-alpha.d.ts +1253 -0
- package/dist/core-beta.d.ts +1253 -0
- package/dist/core-public.d.ts +1253 -0
- package/dist/core-unstripped.d.ts +1253 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/match/index.d.ts +8 -0
- package/dist/match/index.d.ts.map +1 -0
- package/dist/match/index.js +8 -0
- package/dist/match/index.js.map +1 -0
- package/dist/match/matcher.d.ts +40 -0
- package/dist/match/matcher.d.ts.map +1 -0
- package/dist/match/matcher.js +256 -0
- package/dist/match/matcher.js.map +1 -0
- package/dist/match/matcher.test.d.ts +2 -0
- package/dist/match/matcher.test.d.ts.map +1 -0
- package/dist/match/matcher.test.js +185 -0
- package/dist/match/matcher.test.js.map +1 -0
- package/dist/match/path-utils.d.ts +132 -0
- package/dist/match/path-utils.d.ts.map +1 -0
- package/dist/match/path-utils.js +223 -0
- package/dist/match/path-utils.js.map +1 -0
- package/dist/match/path-utils.test.d.ts +2 -0
- package/dist/match/path-utils.test.d.ts.map +1 -0
- package/dist/match/path-utils.test.js +193 -0
- package/dist/match/path-utils.test.js.map +1 -0
- package/dist/match/segment-matcher.d.ts +25 -0
- package/dist/match/segment-matcher.d.ts.map +1 -0
- package/dist/match/segment-matcher.js +267 -0
- package/dist/match/segment-matcher.js.map +1 -0
- package/dist/parse/brace-expansion.d.ts +34 -0
- package/dist/parse/brace-expansion.d.ts.map +1 -0
- package/dist/parse/brace-expansion.js +294 -0
- package/dist/parse/brace-expansion.js.map +1 -0
- package/dist/parse/brace-expansion.test.d.ts +2 -0
- package/dist/parse/brace-expansion.test.d.ts.map +1 -0
- package/dist/parse/brace-expansion.test.js +105 -0
- package/dist/parse/brace-expansion.test.js.map +1 -0
- package/dist/parse/index.d.ts +8 -0
- package/dist/parse/index.d.ts.map +1 -0
- package/dist/parse/index.js +8 -0
- package/dist/parse/index.js.map +1 -0
- package/dist/parse/parser.d.ts +15 -0
- package/dist/parse/parser.d.ts.map +1 -0
- package/dist/parse/parser.js +526 -0
- package/dist/parse/parser.js.map +1 -0
- package/dist/parse/parser.test.d.ts +2 -0
- package/dist/parse/parser.test.d.ts.map +1 -0
- package/dist/parse/parser.test.js +266 -0
- package/dist/parse/parser.test.js.map +1 -0
- package/dist/parse/validator.d.ts +30 -0
- package/dist/parse/validator.d.ts.map +1 -0
- package/dist/parse/validator.js +115 -0
- package/dist/parse/validator.js.map +1 -0
- package/dist/parse/validator.test.d.ts +2 -0
- package/dist/parse/validator.test.d.ts.map +1 -0
- package/dist/parse/validator.test.js +45 -0
- package/dist/parse/validator.test.js.map +1 -0
- package/dist/types/ast.d.ts +158 -0
- package/dist/types/ast.d.ts.map +1 -0
- package/dist/types/ast.js +2 -0
- package/dist/types/ast.js.map +1 -0
- package/dist/types/automaton.d.ts +150 -0
- package/dist/types/automaton.d.ts.map +1 -0
- package/dist/types/automaton.js +2 -0
- package/dist/types/automaton.js.map +1 -0
- package/dist/types/containment.d.ts +257 -0
- package/dist/types/containment.d.ts.map +1 -0
- package/dist/types/containment.js +5 -0
- package/dist/types/containment.js.map +1 -0
- package/dist/types/errors.d.ts +37 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +24 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern containment checking.
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*/
|
|
5
|
+
import { matchPath } from '../match/matcher';
|
|
6
|
+
/**
|
|
7
|
+
* Check if pattern A is contained within pattern B.
|
|
8
|
+
*
|
|
9
|
+
* A ⊆ B means: every path that matches A also matches B.
|
|
10
|
+
*
|
|
11
|
+
* Uses a hybrid approach:
|
|
12
|
+
* 1. Structural analysis for quick checks
|
|
13
|
+
* 2. Sample-based testing for validation
|
|
14
|
+
* 3. Automaton operations for complex cases (when available)
|
|
15
|
+
*
|
|
16
|
+
* @param a - First compiled pattern
|
|
17
|
+
* @param b - Second compiled pattern
|
|
18
|
+
* @returns Containment result with explanation data
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export function checkContainment(a, b) {
|
|
23
|
+
// Use structural analysis for containment checking
|
|
24
|
+
const { isSubset, isSuperset, counterexample, reverseCounterexample } = checkContainmentStructural(a, b);
|
|
25
|
+
const isEqual = isSubset && isSuperset;
|
|
26
|
+
const hasOverlap = checkHasOverlap(a, b);
|
|
27
|
+
let relationship;
|
|
28
|
+
if (isEqual) {
|
|
29
|
+
relationship = 'equal';
|
|
30
|
+
}
|
|
31
|
+
else if (isSubset) {
|
|
32
|
+
relationship = 'subset';
|
|
33
|
+
}
|
|
34
|
+
else if (isSuperset) {
|
|
35
|
+
relationship = 'superset';
|
|
36
|
+
}
|
|
37
|
+
else if (!hasOverlap) {
|
|
38
|
+
relationship = 'disjoint';
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
relationship = 'overlapping';
|
|
42
|
+
}
|
|
43
|
+
// Build explanation
|
|
44
|
+
const explanation = buildExplanation(a, b, relationship, counterexample, reverseCounterexample);
|
|
45
|
+
return {
|
|
46
|
+
patternA: a.source,
|
|
47
|
+
patternB: b.source,
|
|
48
|
+
isSubset,
|
|
49
|
+
isSuperset,
|
|
50
|
+
isEqual,
|
|
51
|
+
hasOverlap,
|
|
52
|
+
relationship,
|
|
53
|
+
counterexample,
|
|
54
|
+
reverseCounterexample,
|
|
55
|
+
explanation,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Structural containment check.
|
|
60
|
+
*/
|
|
61
|
+
function checkContainmentStructural(a, b) {
|
|
62
|
+
// Generate test paths from pattern A and check if they match B
|
|
63
|
+
const aPaths = generateTestPaths(a, 20);
|
|
64
|
+
const bPaths = generateTestPaths(b, 20);
|
|
65
|
+
let aSubsetB = true;
|
|
66
|
+
let bSubsetA = true;
|
|
67
|
+
let counterexample;
|
|
68
|
+
let reverseCounterexample;
|
|
69
|
+
// Check if all A paths match B
|
|
70
|
+
for (const path of aPaths) {
|
|
71
|
+
if (!matchPath(path, b)) {
|
|
72
|
+
aSubsetB = false;
|
|
73
|
+
counterexample = path;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Check if all B paths match A
|
|
78
|
+
for (const path of bPaths) {
|
|
79
|
+
if (!matchPath(path, a)) {
|
|
80
|
+
bSubsetA = false;
|
|
81
|
+
reverseCounterexample = path;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Additional structural checks
|
|
86
|
+
if (aSubsetB) {
|
|
87
|
+
// Verify with depth analysis
|
|
88
|
+
if (a.isUnbounded && !b.isUnbounded) {
|
|
89
|
+
// A can go deeper than B allows
|
|
90
|
+
aSubsetB = false;
|
|
91
|
+
counterexample = generateDeepPath(a, (b.maxSegments ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (bSubsetA) {
|
|
95
|
+
if (b.isUnbounded && !a.isUnbounded) {
|
|
96
|
+
bSubsetA = false;
|
|
97
|
+
reverseCounterexample = generateDeepPath(b, (a.maxSegments ?? 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
isSubset: aSubsetB,
|
|
102
|
+
isSuperset: bSubsetA,
|
|
103
|
+
counterexample,
|
|
104
|
+
reverseCounterexample,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if patterns have any overlap.
|
|
109
|
+
*/
|
|
110
|
+
function checkHasOverlap(a, b) {
|
|
111
|
+
// Generate paths from A and check if they match B
|
|
112
|
+
const aPaths = generateTestPaths(a, 10);
|
|
113
|
+
for (const path of aPaths) {
|
|
114
|
+
if (matchPath(path, b)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Generate paths from B and check if they match A
|
|
119
|
+
const bPaths = generateTestPaths(b, 10);
|
|
120
|
+
for (const path of bPaths) {
|
|
121
|
+
if (matchPath(path, a)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Try to generate paths that combine constraints from both patterns
|
|
126
|
+
const combinedPaths = generateCombinedPaths(a, b, 10);
|
|
127
|
+
for (const path of combinedPaths) {
|
|
128
|
+
if (matchPath(path, a) && matchPath(path, b)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Generate paths that might match both patterns by combining their constraints.
|
|
136
|
+
*/
|
|
137
|
+
function generateCombinedPaths(a, b, count) {
|
|
138
|
+
const paths = [];
|
|
139
|
+
// Extract constraints from both patterns, removing leading slashes
|
|
140
|
+
const aPrefix = a.quickReject.requiredPrefix?.replace(/^\//, '') ?? '';
|
|
141
|
+
const bPrefix = b.quickReject.requiredPrefix?.replace(/^\//, '') ?? '';
|
|
142
|
+
const aSuffix = a.quickReject.requiredSuffix?.replace(/^\//, '') ?? '';
|
|
143
|
+
const bSuffix = b.quickReject.requiredSuffix?.replace(/^\//, '') ?? '';
|
|
144
|
+
// Extract file extensions and base names from suffixes
|
|
145
|
+
const extractFileInfo = (suffix) => {
|
|
146
|
+
if (!suffix)
|
|
147
|
+
return { basename: '', ext: '' };
|
|
148
|
+
const lastDot = suffix.lastIndexOf('.');
|
|
149
|
+
if (lastDot > 0) {
|
|
150
|
+
return { basename: suffix.slice(0, lastDot), ext: suffix.slice(lastDot) };
|
|
151
|
+
}
|
|
152
|
+
return { basename: suffix, ext: '' };
|
|
153
|
+
};
|
|
154
|
+
// Build candidate paths
|
|
155
|
+
const prefixes = [aPrefix, bPrefix].filter(Boolean);
|
|
156
|
+
if (prefixes.length === 0)
|
|
157
|
+
prefixes.push('');
|
|
158
|
+
// If we have suffixes like "index.ts", extract the components
|
|
159
|
+
const suffixInfoA = extractFileInfo(aSuffix);
|
|
160
|
+
const suffixInfoB = extractFileInfo(bSuffix);
|
|
161
|
+
// File basenames to try
|
|
162
|
+
const basenames = [suffixInfoA.basename || 'index', suffixInfoB.basename || 'index', 'file', 'test'].filter(Boolean);
|
|
163
|
+
// Extensions to try
|
|
164
|
+
const extensions = [suffixInfoA.ext, suffixInfoB.ext, '.ts', '.js'].filter(Boolean);
|
|
165
|
+
for (const prefix of prefixes) {
|
|
166
|
+
for (const basename of basenames) {
|
|
167
|
+
for (const ext of extensions) {
|
|
168
|
+
// Build path: /prefix/basename.ext
|
|
169
|
+
const filename = basename + ext;
|
|
170
|
+
const path = prefix ? `/${prefix}/${filename}` : `/${filename}`;
|
|
171
|
+
if (!paths.includes(path)) {
|
|
172
|
+
paths.push(path);
|
|
173
|
+
}
|
|
174
|
+
// Also try with subdirectory
|
|
175
|
+
if (prefix) {
|
|
176
|
+
const deepPath = `/${prefix}/sub/${filename}`;
|
|
177
|
+
if (!paths.includes(deepPath)) {
|
|
178
|
+
paths.push(deepPath);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (paths.length >= count)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (paths.length >= count)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (paths.length >= count)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
return paths.slice(0, count);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Generate test paths that match a pattern.
|
|
194
|
+
*/
|
|
195
|
+
function generateTestPaths(pattern, count) {
|
|
196
|
+
const paths = [];
|
|
197
|
+
const ast = pattern.ast;
|
|
198
|
+
if (ast.root.type === 'alternation') {
|
|
199
|
+
// Generate paths for each branch
|
|
200
|
+
for (const branch of ast.root.branches) {
|
|
201
|
+
paths.push(...generatePathsFromSequence(branch, Math.ceil(count / ast.root.branches.length)));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
paths.push(...generatePathsFromSequence(ast.root, count));
|
|
206
|
+
}
|
|
207
|
+
return paths.slice(0, count);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Generate paths from a segment sequence.
|
|
211
|
+
*/
|
|
212
|
+
function generatePathsFromSequence(sequence, count) {
|
|
213
|
+
const paths = [];
|
|
214
|
+
// Generate base path
|
|
215
|
+
const basePath = generateBasePath(sequence.segments);
|
|
216
|
+
paths.push(basePath);
|
|
217
|
+
// Generate variations
|
|
218
|
+
for (let i = 1; i < count && paths.length < count; i++) {
|
|
219
|
+
const variation = generatePathVariation(sequence.segments, i);
|
|
220
|
+
if (variation && !paths.includes(variation)) {
|
|
221
|
+
paths.push(variation);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return paths;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Generate a base path from segments.
|
|
228
|
+
*/
|
|
229
|
+
function generateBasePath(segments) {
|
|
230
|
+
const parts = [];
|
|
231
|
+
for (const segment of segments) {
|
|
232
|
+
parts.push(generateSegmentValue(segment, 0));
|
|
233
|
+
}
|
|
234
|
+
return '/' + parts.join('/');
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Generate a path variation.
|
|
238
|
+
*/
|
|
239
|
+
function generatePathVariation(segments, variationIndex) {
|
|
240
|
+
const parts = [];
|
|
241
|
+
let usedVariation = false;
|
|
242
|
+
for (let i = 0; i < segments.length; i++) {
|
|
243
|
+
const segment = segments[i];
|
|
244
|
+
if (segment.type === 'globstar' && !usedVariation) {
|
|
245
|
+
// For globstar, always add at least one segment (globstar requires at least 1)
|
|
246
|
+
// Then optionally add more based on variation
|
|
247
|
+
const extraCount = 1 + (variationIndex % 3);
|
|
248
|
+
for (let j = 0; j < extraCount; j++) {
|
|
249
|
+
parts.push(`dir${j}`);
|
|
250
|
+
}
|
|
251
|
+
usedVariation = true;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
parts.push(generateSegmentValue(segment, usedVariation ? 0 : variationIndex));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return '/' + parts.join('/');
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Generate a value that matches a segment pattern.
|
|
261
|
+
*/
|
|
262
|
+
function generateSegmentValue(segment, variation) {
|
|
263
|
+
switch (segment.type) {
|
|
264
|
+
case 'literal':
|
|
265
|
+
return segment.value;
|
|
266
|
+
case 'globstar':
|
|
267
|
+
return 'subdir';
|
|
268
|
+
case 'wildcard': {
|
|
269
|
+
// Generate based on pattern
|
|
270
|
+
const pattern = segment.pattern;
|
|
271
|
+
if (pattern.endsWith('.ts')) {
|
|
272
|
+
return `file${variation}.ts`;
|
|
273
|
+
}
|
|
274
|
+
if (pattern.endsWith('.js')) {
|
|
275
|
+
return `file${variation}.js`;
|
|
276
|
+
}
|
|
277
|
+
if (pattern.startsWith('test-')) {
|
|
278
|
+
return `test-${variation}`;
|
|
279
|
+
}
|
|
280
|
+
return `match${variation}`;
|
|
281
|
+
}
|
|
282
|
+
case 'charclass':
|
|
283
|
+
// Pick a character from the class
|
|
284
|
+
if (segment.ranges.length > 0) {
|
|
285
|
+
const range = segment.ranges[0];
|
|
286
|
+
return range.start;
|
|
287
|
+
}
|
|
288
|
+
return segment.chars[0] || 'x';
|
|
289
|
+
case 'composite':
|
|
290
|
+
return `composite${variation}`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Generate a path with a specific depth.
|
|
295
|
+
*/
|
|
296
|
+
function generateDeepPath(pattern, depth) {
|
|
297
|
+
const ast = pattern.ast;
|
|
298
|
+
const segments = [];
|
|
299
|
+
if (ast.root.type === 'sequence') {
|
|
300
|
+
for (const seg of ast.root.segments) {
|
|
301
|
+
if (seg.type === 'globstar') {
|
|
302
|
+
// Fill with enough segments to reach target depth
|
|
303
|
+
while (segments.length < depth - 1) {
|
|
304
|
+
segments.push('deep');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
segments.push(generateSegmentValue(seg, 0));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Ensure we reach target depth
|
|
313
|
+
while (segments.length < depth) {
|
|
314
|
+
segments.push('extra');
|
|
315
|
+
}
|
|
316
|
+
return '/' + segments.join('/');
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Build a detailed explanation of the containment result.
|
|
320
|
+
*/
|
|
321
|
+
function buildExplanation(a, b, relationship, counterexample, reverseCounterexample) {
|
|
322
|
+
const failureReasons = determineFailureReasons(a, b, relationship, counterexample);
|
|
323
|
+
const segmentComparison = buildSegmentComparison(a, b);
|
|
324
|
+
const structuralDiffs = buildStructuralDifferences(a, b);
|
|
325
|
+
const witnesses = buildWitnesses(a, b, counterexample, reverseCounterexample);
|
|
326
|
+
return {
|
|
327
|
+
failureReasons,
|
|
328
|
+
segmentComparison,
|
|
329
|
+
structuralDiffs,
|
|
330
|
+
witnesses,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Determine the high-level reasons why containment fails.
|
|
335
|
+
*/
|
|
336
|
+
function determineFailureReasons(a, b, relationship, counterexample) {
|
|
337
|
+
if (relationship === 'subset' || relationship === 'equal') {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
const reasons = [];
|
|
341
|
+
// Check depth mismatch
|
|
342
|
+
if (a.isUnbounded !== b.isUnbounded) {
|
|
343
|
+
reasons.push('depth_mismatch');
|
|
344
|
+
}
|
|
345
|
+
else if (!a.isUnbounded && !b.isUnbounded) {
|
|
346
|
+
if (a.maxSegments > b.maxSegments || a.minSegments < b.minSegments) {
|
|
347
|
+
reasons.push('depth_mismatch');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Check prefix mismatch
|
|
351
|
+
if (a.quickReject.requiredPrefix !== b.quickReject.requiredPrefix) {
|
|
352
|
+
if (!b.quickReject.requiredPrefix || !a.quickReject.requiredPrefix?.startsWith(b.quickReject.requiredPrefix)) {
|
|
353
|
+
reasons.push('prefix_mismatch');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Check suffix mismatch
|
|
357
|
+
if (a.quickReject.requiredSuffix !== b.quickReject.requiredSuffix) {
|
|
358
|
+
if (!b.quickReject.requiredSuffix || !a.quickReject.requiredSuffix?.endsWith(b.quickReject.requiredSuffix)) {
|
|
359
|
+
reasons.push('suffix_mismatch');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Check for segment-level mismatches using counterexample
|
|
363
|
+
if (counterexample && reasons.length === 0) {
|
|
364
|
+
// If we have a counterexample but no obvious structural reason,
|
|
365
|
+
// it's likely a segment-level mismatch
|
|
366
|
+
reasons.push('segment_mismatch');
|
|
367
|
+
}
|
|
368
|
+
return reasons;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Build segment-by-segment comparison.
|
|
372
|
+
*/
|
|
373
|
+
function buildSegmentComparison(a, b) {
|
|
374
|
+
const entries = [];
|
|
375
|
+
const maxPositions = Math.max(a.maxSegments ?? 10, b.maxSegments ?? 10, a.minSegments, b.minSegments);
|
|
376
|
+
// For simplicity, compare the first few positions
|
|
377
|
+
// A full implementation would analyze the automaton structure
|
|
378
|
+
for (let pos = 0; pos < Math.min(maxPositions, 5); pos++) {
|
|
379
|
+
const constraintA = getConstraintAtPosition(a, pos);
|
|
380
|
+
const constraintB = getConstraintAtPosition(b, pos);
|
|
381
|
+
const aSubsetOfB = isConstraintSubset(constraintA, constraintB);
|
|
382
|
+
const difference = aSubsetOfB ? undefined : describeConstraintDifference(constraintA, constraintB);
|
|
383
|
+
entries.push({
|
|
384
|
+
position: pos,
|
|
385
|
+
patternAAllows: constraintA,
|
|
386
|
+
patternBAllows: constraintB,
|
|
387
|
+
aSubsetOfB,
|
|
388
|
+
difference,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return entries;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Get the constraint at a given segment position.
|
|
395
|
+
*/
|
|
396
|
+
function getConstraintAtPosition(pattern, position) {
|
|
397
|
+
const ast = pattern.ast;
|
|
398
|
+
if (ast.root.type !== 'sequence') {
|
|
399
|
+
// For alternations, return a more general constraint
|
|
400
|
+
return {
|
|
401
|
+
type: 'any',
|
|
402
|
+
optional: true,
|
|
403
|
+
repeatable: false,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const segments = ast.root.segments;
|
|
407
|
+
if (position >= segments.length) {
|
|
408
|
+
// Past the end of the pattern
|
|
409
|
+
if (pattern.isUnbounded) {
|
|
410
|
+
return { type: 'any_sequence', optional: true, repeatable: true };
|
|
411
|
+
}
|
|
412
|
+
return { type: 'end', optional: false, repeatable: false };
|
|
413
|
+
}
|
|
414
|
+
const segment = segments[position];
|
|
415
|
+
switch (segment.type) {
|
|
416
|
+
case 'literal':
|
|
417
|
+
return {
|
|
418
|
+
type: 'literal',
|
|
419
|
+
literalValue: segment.value,
|
|
420
|
+
optional: false,
|
|
421
|
+
repeatable: false,
|
|
422
|
+
};
|
|
423
|
+
case 'wildcard':
|
|
424
|
+
return {
|
|
425
|
+
type: 'wildcard',
|
|
426
|
+
wildcardPattern: segment.pattern,
|
|
427
|
+
optional: false,
|
|
428
|
+
repeatable: false,
|
|
429
|
+
};
|
|
430
|
+
case 'globstar':
|
|
431
|
+
return {
|
|
432
|
+
type: 'any_sequence',
|
|
433
|
+
optional: true,
|
|
434
|
+
repeatable: true,
|
|
435
|
+
};
|
|
436
|
+
case 'charclass':
|
|
437
|
+
return {
|
|
438
|
+
type: 'charclass',
|
|
439
|
+
charclassDescription: describeCharClass(segment),
|
|
440
|
+
optional: false,
|
|
441
|
+
repeatable: false,
|
|
442
|
+
};
|
|
443
|
+
case 'composite':
|
|
444
|
+
return {
|
|
445
|
+
type: 'wildcard',
|
|
446
|
+
wildcardPattern: 'composite',
|
|
447
|
+
optional: false,
|
|
448
|
+
repeatable: false,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Describe a character class for display.
|
|
454
|
+
*/
|
|
455
|
+
function describeCharClass(segment) {
|
|
456
|
+
const desc = segment.negated ? 'not ' : '';
|
|
457
|
+
const parts = [];
|
|
458
|
+
if (segment.chars) {
|
|
459
|
+
parts.push(`[${segment.chars}]`);
|
|
460
|
+
}
|
|
461
|
+
for (const range of segment.ranges) {
|
|
462
|
+
parts.push(`${range.start}-${range.end}`);
|
|
463
|
+
}
|
|
464
|
+
return desc + parts.join(', ');
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Check if constraint A is a subset of constraint B.
|
|
468
|
+
*/
|
|
469
|
+
function isConstraintSubset(a, b) {
|
|
470
|
+
const aType = a.type;
|
|
471
|
+
const bType = b.type;
|
|
472
|
+
// any_sequence contains everything
|
|
473
|
+
if (bType === 'any_sequence')
|
|
474
|
+
return true;
|
|
475
|
+
if (aType === 'any_sequence')
|
|
476
|
+
return false;
|
|
477
|
+
// any contains any single segment (except any_sequence which is handled above)
|
|
478
|
+
if (bType === 'any')
|
|
479
|
+
return true;
|
|
480
|
+
// end only contains end
|
|
481
|
+
if (bType === 'end')
|
|
482
|
+
return aType === 'end';
|
|
483
|
+
if (aType === 'end')
|
|
484
|
+
return false;
|
|
485
|
+
// literal is only subset if same literal or B is wildcard/any
|
|
486
|
+
if (aType === 'literal') {
|
|
487
|
+
if (bType === 'literal')
|
|
488
|
+
return a.literalValue === b.literalValue;
|
|
489
|
+
if (bType === 'wildcard')
|
|
490
|
+
return true;
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
// wildcard is subset of wildcard only if patterns align
|
|
494
|
+
if (aType === 'wildcard' && bType === 'wildcard') {
|
|
495
|
+
// Simplified: check if patterns look compatible
|
|
496
|
+
return true; // Would need regex analysis for accuracy
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Describe the difference between two constraints.
|
|
502
|
+
*/
|
|
503
|
+
function describeConstraintDifference(a, b) {
|
|
504
|
+
if (a.type === 'any_sequence' && b.type !== 'any_sequence') {
|
|
505
|
+
return 'A allows unlimited depth, B is bounded';
|
|
506
|
+
}
|
|
507
|
+
if (a.type === 'literal' && b.type === 'literal' && a.literalValue !== b.literalValue) {
|
|
508
|
+
return `A requires "${a.literalValue}", B requires "${b.literalValue}"`;
|
|
509
|
+
}
|
|
510
|
+
if (a.type === 'wildcard' && b.type === 'literal') {
|
|
511
|
+
return `A allows any matching segment, B requires exact "${b.literalValue}"`;
|
|
512
|
+
}
|
|
513
|
+
return 'Constraint mismatch';
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Build structural differences summary.
|
|
517
|
+
*/
|
|
518
|
+
function buildStructuralDifferences(a, b) {
|
|
519
|
+
const depthDifference = buildDepthComparison(a, b);
|
|
520
|
+
const prefixDifference = buildPrefixComparison(a, b);
|
|
521
|
+
const suffixDifference = buildSuffixComparison(a, b);
|
|
522
|
+
const anchoringDifference = buildAnchoringComparison(a, b);
|
|
523
|
+
return {
|
|
524
|
+
depthDifference,
|
|
525
|
+
prefixDifference,
|
|
526
|
+
suffixDifference,
|
|
527
|
+
anchoringDifference,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function buildDepthComparison(a, b) {
|
|
531
|
+
const aMax = a.maxSegments ?? 'unbounded';
|
|
532
|
+
const bMax = b.maxSegments ?? 'unbounded';
|
|
533
|
+
const differ = a.minSegments !== b.minSegments || aMax !== bMax;
|
|
534
|
+
let explanation;
|
|
535
|
+
if (differ) {
|
|
536
|
+
if (a.isUnbounded && !b.isUnbounded) {
|
|
537
|
+
explanation = `A can match paths of any depth, B is limited to ${bMax} segments`;
|
|
538
|
+
}
|
|
539
|
+
else if (!a.isUnbounded && b.isUnbounded) {
|
|
540
|
+
explanation = `A is limited to ${aMax} segments, B can match any depth`;
|
|
541
|
+
}
|
|
542
|
+
else if (a.minSegments !== b.minSegments) {
|
|
543
|
+
explanation = `A requires at least ${a.minSegments} segments, B requires ${b.minSegments}`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
differ,
|
|
548
|
+
patternAMin: a.minSegments,
|
|
549
|
+
patternAMax: aMax,
|
|
550
|
+
patternBMin: b.minSegments,
|
|
551
|
+
patternBMax: bMax,
|
|
552
|
+
explanation,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function buildPrefixComparison(a, b) {
|
|
556
|
+
const prefixA = a.quickReject.requiredPrefix;
|
|
557
|
+
const prefixB = b.quickReject.requiredPrefix;
|
|
558
|
+
const differ = prefixA !== prefixB;
|
|
559
|
+
let explanation;
|
|
560
|
+
if (differ) {
|
|
561
|
+
if (prefixA && prefixB) {
|
|
562
|
+
explanation = `A requires prefix "${prefixA}", B requires "${prefixB}"`;
|
|
563
|
+
}
|
|
564
|
+
else if (prefixA) {
|
|
565
|
+
explanation = `A requires prefix "${prefixA}", B has no prefix requirement`;
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
explanation = `A has no prefix requirement, B requires "${prefixB}"`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
differ,
|
|
573
|
+
patternAPrefix: prefixA,
|
|
574
|
+
patternBPrefix: prefixB,
|
|
575
|
+
explanation,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function buildSuffixComparison(a, b) {
|
|
579
|
+
const suffixA = a.quickReject.requiredSuffix;
|
|
580
|
+
const suffixB = b.quickReject.requiredSuffix;
|
|
581
|
+
const differ = suffixA !== suffixB;
|
|
582
|
+
let explanation;
|
|
583
|
+
if (differ) {
|
|
584
|
+
if (suffixA && suffixB) {
|
|
585
|
+
explanation = `A requires suffix "${suffixA}", B requires "${suffixB}"`;
|
|
586
|
+
}
|
|
587
|
+
else if (suffixA) {
|
|
588
|
+
explanation = `A requires suffix "${suffixA}", B has no suffix requirement`;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
explanation = `A has no suffix requirement, B requires "${suffixB}"`;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
differ,
|
|
596
|
+
patternASuffix: suffixA,
|
|
597
|
+
patternBSuffix: suffixB,
|
|
598
|
+
explanation,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function buildAnchoringComparison(a, b) {
|
|
602
|
+
const aAbsolute = a.ast.isAbsolute;
|
|
603
|
+
const bAbsolute = b.ast.isAbsolute;
|
|
604
|
+
const differ = aAbsolute !== bAbsolute;
|
|
605
|
+
let explanation;
|
|
606
|
+
if (differ) {
|
|
607
|
+
explanation = aAbsolute ? 'A is an absolute pattern, B is relative' : 'A is a relative pattern, B is absolute';
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
differ,
|
|
611
|
+
patternAAbsolute: aAbsolute,
|
|
612
|
+
patternBAbsolute: bAbsolute,
|
|
613
|
+
explanation,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Build witness paths for the containment result.
|
|
618
|
+
*/
|
|
619
|
+
function buildWitnesses(a, b, counterexample, reverseCounterexample) {
|
|
620
|
+
const witnesses = [];
|
|
621
|
+
// Add counterexample if present
|
|
622
|
+
if (counterexample) {
|
|
623
|
+
witnesses.push({
|
|
624
|
+
path: counterexample,
|
|
625
|
+
matchesA: true,
|
|
626
|
+
matchesB: false,
|
|
627
|
+
category: 'counterexample',
|
|
628
|
+
divergenceIndex: findDivergenceIndex(counterexample, a, b),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
// Add reverse counterexample if present
|
|
632
|
+
if (reverseCounterexample) {
|
|
633
|
+
witnesses.push({
|
|
634
|
+
path: reverseCounterexample,
|
|
635
|
+
matchesA: false,
|
|
636
|
+
matchesB: true,
|
|
637
|
+
category: 'reverse_counterexample',
|
|
638
|
+
divergenceIndex: findDivergenceIndex(reverseCounterexample, a, b),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
// Try to find a shared example
|
|
642
|
+
const sharedExample = findSharedExample(a, b);
|
|
643
|
+
if (sharedExample) {
|
|
644
|
+
witnesses.push({
|
|
645
|
+
path: sharedExample,
|
|
646
|
+
matchesA: true,
|
|
647
|
+
matchesB: true,
|
|
648
|
+
category: 'shared',
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return witnesses;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Find the segment index where patterns diverge for a path.
|
|
655
|
+
*/
|
|
656
|
+
function findDivergenceIndex(_path, _a, _b) {
|
|
657
|
+
// This would require tracing through both automata
|
|
658
|
+
// For simplicity, return undefined
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Find an example path that matches both patterns.
|
|
663
|
+
*/
|
|
664
|
+
function findSharedExample(a, b) {
|
|
665
|
+
// Generate paths from A and check if any match B
|
|
666
|
+
const aPaths = generateTestPaths(a, 10);
|
|
667
|
+
for (const path of aPaths) {
|
|
668
|
+
if (matchPath(path, b)) {
|
|
669
|
+
return path;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Generate paths from B and check if any match A
|
|
673
|
+
const bPaths = generateTestPaths(b, 10);
|
|
674
|
+
for (const path of bPaths) {
|
|
675
|
+
if (matchPath(path, a)) {
|
|
676
|
+
return path;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=containment.js.map
|