CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
CI (SIAX Cloud) / integration (push) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / security (push) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / integration (pull_request) Successful in 33s
Security Scan (SIAX Cloud) / security (push) Successful in 33s
CI (SIAX Cloud) / quality (push) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 58s
The local model returns 12500.00 as a JSON number, which the contract rejects as a non-string decimal. A numeric value now matches a source span numerically and the delivered value is rewritten to that span's decimal string (12500.00), so precision is preserved instead of rounded. Exact string equality is still required for string decimals, and ambiguous formats still go to review.
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
/**
|
|
* Deterministic, locale-aware value parsing used for evidence checks.
|
|
*
|
|
* The model sees text; code decides whether a source span really supports a
|
|
* value. These helpers never guess when a format is ambiguous: a span that can
|
|
* be read two ways yields no match, and the field goes to review.
|
|
*/
|
|
|
|
function isCalendarDate(year: number, month: number, day: number): boolean {
|
|
if (year < 1 || month < 1 || month > 12 || day < 1) return false;
|
|
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
return day <= days[month - 1]!;
|
|
}
|
|
|
|
const pad = (n: number): string => String(n).padStart(2, '0');
|
|
|
|
/** Returns every unambiguous ISO rendering of the span (empty when ambiguous). */
|
|
export function dateCandidates(text: string): string[] {
|
|
const t = text.trim();
|
|
const iso = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
|
|
if (iso) {
|
|
const [y, m, d] = [Number(iso[1]), Number(iso[2]), Number(iso[3])];
|
|
return isCalendarDate(y, m, d) ? [`${y}-${pad(m)}-${pad(d)}`] : [];
|
|
}
|
|
const compact = /^(\d{4})(\d{2})(\d{2})$/.exec(t);
|
|
if (compact) {
|
|
const [y, m, d] = [Number(compact[1]), Number(compact[2]), Number(compact[3])];
|
|
return isCalendarDate(y, m, d) ? [`${y}-${pad(m)}-${pad(d)}`] : [];
|
|
}
|
|
const numeric = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4})$/.exec(t);
|
|
if (numeric) {
|
|
const a = Number(numeric[1]);
|
|
const b = Number(numeric[2]);
|
|
const y = Number(numeric[3]);
|
|
// Both day-first and month-first are possible for <=12/<=12: no match.
|
|
if (a <= 12 && b <= 12) return [];
|
|
const [day, month] = a > 12 ? [a, b] : [b, a];
|
|
return isCalendarDate(y, month, day) ? [`${y}-${pad(month)}-${pad(day)}`] : [];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/** Returns every unambiguous decimal rendering of the span. */
|
|
export function decimalCandidates(text: string): string[] {
|
|
const t = text.trim().replace(/[\s\u00a0\u202f]/g, '');
|
|
if (!/^[+-]?[\d.,]+$/.test(t) || !/\d/.test(t)) return [];
|
|
const sign = t.startsWith('-') ? '-' : '';
|
|
const body = t.replace(/^[+-]/, '');
|
|
const hasComma = body.includes(',');
|
|
const hasDot = body.includes('.');
|
|
const out = new Set<string>();
|
|
const add = (value: string) => {
|
|
if (/^(0|[1-9]\d*)(\.\d+)?$/.test(value)) out.add(`${sign}${value}`);
|
|
};
|
|
if (hasComma && hasDot) {
|
|
const lastComma = body.lastIndexOf(',');
|
|
const lastDot = body.lastIndexOf('.');
|
|
if (lastComma > lastDot) add(body.replace(/\./g, '').replace(',', '.'));
|
|
else add(body.replace(/,/g, ''));
|
|
} else if (hasComma) {
|
|
const parts = body.split(',');
|
|
if (parts.length === 2 && parts[1]!.length <= 2) add(`${parts[0]}.${parts[1]}`);
|
|
add(body.replace(/,/g, ''));
|
|
} else if (hasDot) {
|
|
const parts = body.split('.');
|
|
if (parts.length === 2 && parts[1]!.length <= 2) add(body);
|
|
add(body.replace(/\./g, ''));
|
|
} else {
|
|
add(body);
|
|
}
|
|
return [...out];
|
|
}
|
|
|
|
/**
|
|
* Canonical decimal string: no exponent, no leading '+', no trailing zeros in
|
|
* the fraction. Used only for equality checks; the delivered value keeps its
|
|
* original precision.
|
|
*/
|
|
export function canonicalDecimal(value: string): string | undefined {
|
|
const match = /^([+-]?)(0|[1-9]\d*)(?:\.(\d+))?$/.exec(value.trim());
|
|
if (!match) return undefined;
|
|
const sign = match[1] === '-' ? '-' : '';
|
|
const fraction = (match[3] ?? '').replace(/0+$/, '');
|
|
return `${sign}${match[2]}${fraction ? `.${fraction}` : ''}`;
|
|
}
|