Compose freeform Truthtrance facts
Verify and publish checkpoint container / container (push) Successful in 11m2s
Verify and publish checkpoint container / container (push) Successful in 11m2s
This commit is contained in:
1 parent
cc9cc0945d
commit
593ff33ca8
10 files changed
+306
-4
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import { parseFreeformFactV2 } from './truthtrance-freeform-facts-v2';
|
||||
import { parseFreeformFactV3 } from './truthtrance-freeform-facts-v3';
|
||||
import { parseFreeformFactV4 } from './truthtrance-freeform-facts-v4';
|
||||
import { parseTruthFreeformFactsV5 } from './truthtrance-freeform-facts-v5';
|
||||
import { parseFreeformFactV6 } from './truthtrance-freeform-facts-v6';
|
||||
import { TRUTH_FACT_MAX_TERMS } from './truth-fact-expression';
|
||||
import type { TruthFact } from './truthtrance';
|
||||
|
||||
type Compound = Extract<TruthFact, { kind: 'and' | 'or' }>;
|
||||
type Separator = { start: number; end: number; kind: 'and' | 'or' };
|
||||
|
||||
/** Recognize only a uniquely partitioned, flat current-fact expression.
|
||||
* Existing whole questions retain their original parser version. Each atom
|
||||
* uses the established public grammar; this parser never inspects game state. */
|
||||
export function parseTruthFreeformFactsV7(text: string): Compound | null {
|
||||
if (typeof text !== 'string' || text.length > 500 || /[\r\n]/.test(text)) return null;
|
||||
const input = text.trim();
|
||||
if (parseFreeformFactV2(input) || parseFreeformFactV3(input) || parseFreeformFactV4(input) ||
|
||||
parseTruthFreeformFactsV5(input) || parseFreeformFactV6(input)) return null;
|
||||
const body = input.endsWith('?') ? input.slice(0, -1).trimEnd() : input;
|
||||
if (!body || body.includes('?')) return null;
|
||||
const separators: Separator[] = [...body.matchAll(/[ \t]+(and|or)[ \t]+/gi)].map(match => ({
|
||||
start: match.index!, end: match.index! + match[0].length,
|
||||
kind: match[1].toLowerCase() as 'and' | 'or',
|
||||
}));
|
||||
// Keep the search bounded even if a malicious input contains many words
|
||||
// that resemble connectors. A separator may still be part of a card name.
|
||||
if (!separators.length || separators.length > 24) return null;
|
||||
const shorthand = /^do[ \t]+you[ \t]+have[ \t]+/i.test(body);
|
||||
const atomCache = new Map<string, TruthFact | null>();
|
||||
const atom = (raw: string, first: boolean): TruthFact | null => {
|
||||
const segment = raw.trim();
|
||||
if (!segment) return null;
|
||||
const key = `${first ? 'first' : 'later'}:${segment}`;
|
||||
if (atomCache.has(key)) return atomCache.get(key)!;
|
||||
let qualified = segment;
|
||||
if (!first && /^not[ \t]+have[ \t]+/i.test(segment)) qualified = `Do you ${segment}`;
|
||||
else if (!first && /^have[ \t]+/i.test(segment)) qualified = `Do you ${segment}`;
|
||||
else if (!first && shorthand && !/^(?:do|did|is)\b/i.test(segment)) qualified = `Do you have ${segment}`;
|
||||
const fact = parseFreeformFactV3(qualified) ?? parseFreeformFactV4(qualified) ?? parseFreeformFactV6(qualified);
|
||||
const result = fact && fact.kind !== 'and' && fact.kind !== 'or' ? fact : null;
|
||||
atomCache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
// Retain at most two paths per suffix; two prove ambiguity. Memoization
|
||||
// bounds repeated exploration even when many separators form card names.
|
||||
const memo = new Map<string, TruthFact[][]>();
|
||||
const suffix = (offset: number, kind: 'and' | 'or', count: number): TruthFact[][] => {
|
||||
const key = `${offset}:${kind}:${count}`;
|
||||
const cached = memo.get(key); if (cached) return cached;
|
||||
const results: TruthFact[][] = [];
|
||||
const final = atom(body.slice(offset), false);
|
||||
if (final && count + 1 <= TRUTH_FACT_MAX_TERMS) results.push([final]);
|
||||
if (count + 1 < TRUTH_FACT_MAX_TERMS) for (const sep of separators) {
|
||||
if (sep.start <= offset || sep.kind !== kind) continue;
|
||||
const head = atom(body.slice(offset, sep.start), false);
|
||||
if (!head) continue;
|
||||
for (const tail of suffix(sep.end, kind, count + 1)) {
|
||||
results.push([head, ...tail]);
|
||||
if (results.length >= 2) break;
|
||||
}
|
||||
if (results.length >= 2) break;
|
||||
}
|
||||
memo.set(key, results);
|
||||
return results;
|
||||
};
|
||||
const matches: Compound[] = [];
|
||||
for (const sep of separators) {
|
||||
const first = atom(body.slice(0, sep.start), true);
|
||||
if (!first) continue;
|
||||
for (const tail of suffix(sep.end, sep.kind, 1)) {
|
||||
matches.push({kind: sep.kind, terms: [first, ...tail]});
|
||||
if (matches.length >= 2) return null;
|
||||
}
|
||||
}
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
Reference in new issue
Block a user