UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

831 lines • 29.2 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { decodeText, encodeText } from "../../../../shell/bytes.js"; import { parseDateMode, showDate } from "./dates.js"; import { GitError, UnsupportedFieldError } from "./errors.js"; import { FULL_SHA } from "./format.js"; import { mappedIdentity } from "./mailmap.js"; import { RefKind, } from "./types.js"; /** The fewest hex digits an abbreviated id keeps, whatever width is asked. */ export const MINIMUM_ABBREV = 4; const INT_LIMIT = 2n ** 31n; const UINT_LIMIT = 2n ** 32n; // The prefixes and suffixes a short ref name is read under, in the order // gitrevisions(7) lists them: the git directory, then refs/, tags, branches, // remote-tracking branches and a remote's HEAD. const SHORT_NAME_RULES = [ ['', ''], ['refs/', ''], ['refs/tags/', ''], ['refs/heads/', ''], ['refs/remotes/', ''], ['refs/remotes/', '/HEAD'], ]; // The lines a signature in a commit or tag message opens with. const SIGNATURE_MARKERS = [ '-----BEGIN PGP SIGNATURE-----', '-----BEGIN PGP MESSAGE-----', '-----BEGIN SSH SIGNATURE-----', '-----BEGIN SIGNED MESSAGE-----', ]; const PEELED_SUFFIX = '^{}'; // git's fields this build refuses rather than renders: what a ref's storage // says (deltabase, objectsize:disk, flag), a walk or a signature check // (describe, ahead-behind, is-base, signature), push configuration, trailer // parsing and colour. const UNSUPPORTED = [ 'ahead-behind', 'color', 'deltabase', 'describe', 'flag', 'is-base', 'push', 'signature', 'trailers', ]; const IDENT_ROLES = ['author', 'committer', 'tagger']; const NAME_FIELDS = ['refname', 'symref']; const PERSON_ROLES = [...IDENT_ROLES, 'creator']; const ID_FIELDS = ['objectname', 'tree', 'parent']; const ALIGN_POSITIONS = ['left', 'middle', 'right']; const TRACK_WORDS = ['track', 'trackshort', 'remotename', 'remoteref']; const EMAIL_WORDS = ['trim', 'localpart', 'mailmap']; const REF_FIELDS = new Set(['HEAD', 'worktreepath', 'upstream', ...NAME_FIELDS]); const MESSAGE_FIELDS = new Set(['subject', 'body', 'contents']); const OBJECTLESS = new Set([ 'refname', 'symref', 'upstream', 'worktreepath', 'HEAD', 'objectname', 'align', 'if', 'then', 'else', 'end', ]); function field(text, name, deref, argument) { return { text, name, deref, argument, variant: '', count: 0, options: new Set(), date: null }; } function withField(base, change) { return { ...base, ...change }; } /** * A decimal integer as a C `int` reads it: leading white space and a sign * allowed, nothing after the digits, nothing past 32 bits. */ export function cInt(text) { const found = /^[ \t\n\v\f\r]*([+-]?[0-9]+)$/.exec(text); if (found === null) return null; const value = BigInt(found[1] ?? ''); return value >= -INT_LIMIT && value < INT_LIMIT ? Number(value) : null; } /** * A count as a C `unsigned` reads it, a minus refused: past 31 bits it comes * back negative, as the `int` it is stored in reads. */ export function cUint(text) { const found = /^[ \t\n\v\f\r]*\+?([0-9]+)$/.exec(text); if (found === null) return null; const value = BigInt(found[1] ?? ''); if (value >= UINT_LIMIT) return null; return Number(value >= INT_LIMIT ? value - UINT_LIMIT : value); } /** * How an argument refusal names its field: with the `*` for a name, an id or * a person field, without it for the rest. */ function label(f) { const starred = [...NAME_FIELDS, 'upstream', 'push', ...ID_FIELDS].includes(f.name) || f.name.endsWith('name') || f.name.endsWith('email'); return f.deref && starred ? `*${f.name}` : f.name; } function unrecognized(f, argument) { return new GitError(`unrecognized %(${label(f)}) argument: ${argument}`); } function argumentsRefused(f) { if (f.argument) throw new GitError(`%(${f.name}) does not take arguments`); } /** `short`, `lstrip=<n>` (`strip=<n>`) or `rstrip=<n>`. */ function nameVariant(f, argument) { if (argument === 'short') return withField(f, { variant: 'short' }); for (const [prefix, variant] of [ ['lstrip=', 'lstrip'], ['strip=', 'lstrip'], ['rstrip=', 'rstrip'], ]) { if (argument.startsWith(prefix)) { const value = argument.slice(prefix.length); const count = cInt(value); if (count === null) throw new GitError(`Integer value expected refname:${variant}=${value}`); return withField(f, { variant, count }); } } throw unrecognized(f, argument); } function parseName(f) { return f.argument ? nameVariant(f, f.argument) : f; } /** * Each of `track`, `trackshort`, `remotename` and `remoteref` replaces the * one before it and `nobracket` is a switch; any other word makes the whole * argument one name variant. */ function parseUpstream(f) { const argument = f.argument; if (!argument) return f; let variant = ''; const options = new Set(); for (const word of argument.split(',')) { if (TRACK_WORDS.includes(word)) variant = word; else if (word === 'nobracket') options.add(word); else return nameVariant(f, argument); } return withField(f, { variant, options }); } function parseId(f) { const argument = f.argument; if (!argument) return f; if (argument === 'short') return withField(f, { variant: 'short', count: 0 }); if (argument.startsWith('short=')) { const value = argument.slice('short='.length); const width = cUint(value); if (width === null || width === 0) { throw new GitError(`positive value expected '${value}' in %(${f.text})`); } // A width past 31 bits wraps negative and reads as plain short. return withField(f, { variant: 'short', count: Math.max(width, 0) }); } throw unrecognized(f, argument); } function parseObjectsize(f) { if (!f.argument) return f; if (f.argument === 'disk') throw new UnsupportedFieldError(f.text); throw new GitError(`unrecognized %(objectsize) argument: ${f.argument}`); } function parseRaw(f) { if (!f.argument) return f; if (f.argument === 'size') return withField(f, { variant: 'size' }); throw new GitError(`unrecognized %(raw) argument: ${f.argument}`); } function parseRest(f) { argumentsRefused(f); throw new GitError(`this command reject atom %(${f.text})`); } function parseBare(f) { argumentsRefused(f); return f; } /** `body` refuses an argument, and prints nothing at all after an empty one. */ function parseBody(f) { argumentsRefused(f); return f.argument === null ? f : withField(f, { variant: 'blank' }); } function parseIgnoring(f) { return withField(f, { argument: null }); } /** * `author`, `committer` and `tagger` print nothing at all once they have a * colon, whatever follows it. */ function parsePerson(f) { return f.argument !== null ? withField(f, { variant: 'blank' }) : f; } function parsePersonName(f) { if (!f.argument) return f; if (f.argument === 'mailmap') return withField(f, { options: new Set(['mailmap']) }); throw unrecognized(f, f.argument); } /** * A comma list of `trim`, `localpart` and `mailmap`, each word read off the * front of what is left; the refusal names that rest. */ function parseEmail(f) { let rest = f.argument; if (!rest) return f; const options = new Set(); for (;;) { const word = EMAIL_WORDS.find((w) => rest?.startsWith(w)); if (word === undefined) throw unrecognized(f, rest); options.add(word); rest = rest.slice(word.length); if (!rest) break; if (!rest.startsWith(',')) throw unrecognized(f, rest); rest = rest.slice(1); } return withField(f, { options }); } function parseDate(f) { // The style is checked where a date is shown, so a listing whose refs // carry no such date never refuses it. return withField(f, { date: f.argument }); } function parseSubject(f) { if (!f.argument) return f; if (f.argument === 'sanitize') return withField(f, { variant: 'sanitize' }); throw new GitError(`unrecognized %(subject) argument: ${f.argument}`); } function parseContents(f) { const argument = f.argument; if (!argument) return f; if (['subject', 'body', 'signature', 'size'].includes(argument)) { return withField(f, { variant: argument }); } if (argument === 'trailers' || argument.startsWith('trailers:')) { throw new UnsupportedFieldError(f.text); } if (argument.startsWith('lines=')) { const count = cUint(argument.slice('lines='.length)); if (count === null) throw new GitError(`positive value expected contents:${argument}`); return withField(f, { variant: 'lines', count: Math.max(count, 0) }); } throw new GitError(`unrecognized %(contents) argument: ${argument}`); } function alignWidth(text) { const value = cUint(text); return value === null || value < 0 ? null : value; } /** `<width>` and `<position>` in either order, by name or bare, the last of each winning. */ function parseAlign(f) { if (!f.argument) throw new GitError('expected format: %(align:<width>,<position>)'); let width = null; let position = 'left'; for (const word of f.argument.split(',')) { if (word.startsWith('position=')) { const value = word.slice('position='.length); if (!ALIGN_POSITIONS.includes(value)) throw new GitError(`unrecognized position:${value}`); position = value; } else if (word.startsWith('width=')) { const value = word.slice('width='.length); width = alignWidth(value); if (width === null) throw new GitError(`unrecognized width:${value}`); } else if (ALIGN_POSITIONS.includes(word)) { position = word; } else { const found = alignWidth(word); if (found === null) throw new GitError(`unrecognized %(align) argument: ${word}`); width = found; } } if (width === null) throw new GitError('positive width expected with the %(align) atom'); return withField(f, { variant: position, count: width }); } function parseIf(f) { const argument = f.argument; if (!argument) return withField(f, { argument: null }); for (const word of ['equals', 'notequals']) { if (argument.startsWith(`${word}=`)) return withField(f, { variant: word }); } throw new GitError(`unrecognized %(if) argument: ${argument}`); } /** * A field this build does not render, refused as unsupported once git would * accept its argument; an argument git refuses is refused in git's words * first. */ function parseUnsupported(f) { const name = f.name; if (name === 'deltabase') { argumentsRefused(f); } else if (name === 'is-base' || name === 'ahead-behind') { if (!f.argument) throw new GitError(`expected format: %(${name}:<committish>)`); } else if (name === 'color') { if (!f.argument) throw new GitError('expected format: %(color:<color>)'); } else if (name === 'push') { parseUpstream(f); } throw new UnsupportedFieldError(f.text); } const PARSERS = new Map([ ['refname', parseName], ['symref', parseName], ['upstream', parseUpstream], ['worktreepath', parseIgnoring], ['HEAD', parseBare], ['objecttype', parseBare], ['objectsize', parseObjectsize], ['objectname', parseId], ['tree', parseId], ['parent', parseId], ['numparent', parseIgnoring], ['object', parseIgnoring], ['type', parseIgnoring], ['tag', parseIgnoring], ['raw', parseRaw], ['rest', parseRest], ['creator', parseIgnoring], ['creatordate', parseDate], ['subject', parseSubject], ['body', parseBody], ['contents', parseContents], ['align', parseAlign], ['if', parseIf], ['then', parseIgnoring], ['else', parseIgnoring], ['end', parseIgnoring], ...UNSUPPORTED.map((name) => [name, parseUnsupported]), ...IDENT_ROLES.flatMap((role) => [ [role, parsePerson], [`${role}name`, parsePersonName], [`${role}email`, parseEmail], [`${role}date`, parseDate], ]), ]); /** * Read one field as a format's `%(...)` or a sort key spells it. * * @throws GitError a name git does not have, or an argument it refuses * @throws UnsupportedFieldError a field git has that this build does not render */ export function parseField(text) { const deref = text.startsWith('*'); const body = deref ? text.slice(1) : text; const colon = body.indexOf(':'); const name = colon === -1 ? body : body.slice(0, colon); if (!name && colon === -1) throw new GitError(`malformed field name: ${text}`); const parser = PARSERS.get(name); if (parser === undefined) throw new GitError(`unknown field name: ${text}`); return parser(field(text, name, deref, colon === -1 ? null : body.slice(colon + 1))); } /** Whether a field reads the ref's object, not only its name. */ export function needsObject(f) { return !OBJECTLESS.has(f.name) || (f.deref && f.name === 'objectname'); } /** * The shortest name that still reads back as `name`. Each way of spelling a * short name, most specific first, gives a candidate; one is taken when no * other way (`strict`), or no way rev-parse tries before it (loose), turns it * into another ref that exists. */ export function shortenRef(name, known, strict = true) { for (let at = SHORT_NAME_RULES.length - 1; at > 0; at--) { const [prefix, suffix] = SHORT_NAME_RULES[at] ?? ['', '']; if (!name.startsWith(prefix) || !name.endsWith(suffix)) continue; const short = name.slice(prefix.length, name.length - suffix.length); if (!short) continue; const rivals = strict ? SHORT_NAME_RULES : SHORT_NAME_RULES.slice(0, at); const taken = rivals.some(([p, s], i) => { const spelled = `${p}${short}${s}`; return i !== at && spelled !== name && known.has(spelled); }); if (!taken) return short; } return name; } /** * `name` less `count` components from the front; a negative count keeps that * many from the back instead. */ export function lstripRef(name, count) { const parts = name.split('/'); if (count < 0) return parts.slice(Math.max(parts.length + count, 0)).join('/'); return count < parts.length ? parts.slice(count).join('/') : ''; } /** * `name` less `count` components from the back; a negative count keeps that * many from the front instead. */ export function rstripRef(name, count) { const parts = name.split('/'); if (count < 0) return -count < parts.length ? parts.slice(0, -count).join('/') : name; return count < parts.length ? parts.slice(0, parts.length - count).join('/') : ''; } function nameText(f, name, ctx) { if (f.variant === 'short') return shortenRef(name, ctx.known, ctx.strict); if (f.variant === 'lstrip') return lstripRef(name, f.count); if (f.variant === 'rstrip') return rstripRef(name, f.count); return name; } /** * The instant and offset an identity line records: the seconds since the * epoch and the offset in minutes east of UTC, null when the line has no date * after its email. The date follows the email's `>` and a space: seconds, then * an optional `+hhmm` offset. */ export function identDate(ident) { const close = ident.lastIndexOf('>'); if (close === -1) return null; const found = /^ +([0-9]+)(?: +([+-])([0-9]{2})([0-9]{2}))?/.exec(ident.slice(close + 1)); if (found === null) return null; const seconds = Number(found[1]); if (found[2] === undefined) return [seconds, 0]; const offset = Number(found[3]) * 60 + Number(found[4]); return [seconds, found[2] === '-' ? -offset : offset]; } const PARSED = new WeakMap(); /** An object's header lines and its message. */ function headers(obj) { const cached = PARSED.get(obj); if (cached !== undefined) return cached; const text = decodeText(obj.raw); const gap = text.indexOf('\n\n'); let head = gap === -1 ? text : text.slice(0, gap); if (gap === -1 && text.endsWith('\n')) head = text.slice(0, -1); const message = gap === -1 ? '' : text.slice(gap + 2); const headers = []; for (const line of head.split('\n')) { if (!line || line.startsWith(' ')) continue; const space = line.indexOf(' '); headers.push(space === -1 ? [line, ''] : [line.slice(0, space), line.slice(space + 1)]); } const result = { headers, message, messageBytes: encodeText(message).length }; PARSED.set(obj, result); return result; } function header(obj, key) { return headers(obj).headers.find(([k]) => k === key)?.[1] ?? null; } /** Where the last signature line starts, the message's length when no line opens one. */ function signatureAt(message) { let at = message.length; let start = 0; while (start < message.length) { const end = message.indexOf('\n', start); if (SIGNATURE_MARKERS.some((marker) => message.startsWith(marker, start))) at = start; start = end === -1 ? message.length : end + 1; } return at; } function blank(line) { return line === '' || line === '\r'; } function lineEnd(message, at, sig) { const end = message.indexOf('\n', at); return end === -1 || end >= sig ? -1 : end; } /** * Split a message as the message fields read it: where the subject starts, * the subject, where the body starts and where the signature starts. The * subject is the first paragraph after any empty lines, its lines joined with * spaces (a carriage return before each newline dropped); the body starts at * the next line that is not empty. A signature cuts both short where its line * starts. */ export function messageParts(message) { const sig = signatureAt(message); let at = 0; while (at < sig) { const end = lineEnd(message, at, sig); if (end === -1 || !blank(message.slice(at, end))) break; at = end + 1; } const start = at; const lines = []; while (at < sig) { const end = lineEnd(message, at, sig); const line = message.slice(at, end === -1 ? sig : end); if (blank(line)) break; lines.push(line.endsWith('\r') ? line.slice(0, -1) : line); at = end === -1 ? sig : end + 1; } while (at < sig) { const end = lineEnd(message, at, sig); if (end === -1 || !blank(message.slice(at, end))) break; at = end + 1; } return [start, lines.join(' '), at, sig]; } function titleChar(ch) { return /^[A-Za-z0-9._]$/.test(ch); } /** * A subject as a file name: letters, digits, `.` and `_` kept, each run of * anything else one `-` between them, a run of dots one dot, and trailing dots * and dashes dropped. */ export function sanitizeSubject(subject) { const out = []; let gap = false; for (const ch of subject) { if (titleChar(ch)) { if (gap && out.length) out.push('-'); gap = false; if (ch === '.' && out[out.length - 1] === '.') continue; out.push(ch); } else { gap = true; } } return out.join('').replace(/[.-]+$/, ''); } /** `contents:lines=<n>`: the first lines from the subject on, each after the first indented four spaces. */ function messageLines(message, start, sig, count) { const lines = []; let at = start; while (at < sig && lines.length < count) { const end = lineEnd(message, at, sig); lines.push(message.slice(at, end === -1 ? sig : end)); at = end === -1 ? sig : end + 1; } return lines.join('\n '); } function requestedWidth(f, ctx) { return Math.min(Math.max(f.count || ctx.abbrev, MINIMUM_ABBREV), FULL_SHA); } function abbreviated(f, oid, ctx) { if (f.variant !== 'short') return oid; const width = requestedWidth(f, ctx); return oid.slice(0, Math.max(width, ctx.abbreviations.get(oid) ?? width)); } function shownObject(f, item) { return f.deref ? item.peeled : item.obj; } /** The ids an id field shows for a ref. */ function ids(f, item) { if (f.name === 'objectname') { if (f.deref) return item.peeled !== null ? [item.peeled.oid] : []; return [item.oid]; } const obj = shownObject(f, item); if (obj?.type !== 'commit') return []; return headers(obj) .headers.filter(([k]) => k === f.name) .map(([, v]) => v); } /** * The ids a listing abbreviates, each at the narrowest width any of its fields * asks, for one pass that widens them to be unique. */ export function abbreviationRequests(fields, items, ctx) { const widths = new Map(); for (const f of fields) { if (!ID_FIELDS.includes(f.name) || f.variant !== 'short') continue; const width = requestedWidth(f, ctx); for (const item of items) { for (const oid of ids(f, item)) widths.set(oid, Math.min(widths.get(oid) ?? width, width)); } } return widths; } function upstreamText(f, item, ctx) { const up = item.upstream; if (up === null || item.kind !== RefKind.BRANCH) return ''; if (f.variant === 'remotename') return up.remote; if (f.variant === 'remoteref') return up.merge; if (f.variant === 'trackshort') { if (up.gone) return ''; if (up.ahead && up.behind) return '<>'; return up.ahead ? '>' : up.behind ? '<' : '='; } if (f.variant === 'track') { const state = up.gone ? 'gone' : [ ['ahead', up.ahead], ['behind', up.behind], ] .filter(([, count]) => count) .map(([word, count]) => `${String(word)} ${String(count)}`) .join(', '); if (!state) return ''; return f.options.has('nobracket') ? state : `[${state}]`; } return nameText(f, up.ref, ctx); } function personRole(name) { for (const role of PERSON_ROLES) { if (name.startsWith(role)) return [role, name.slice(role.length)]; } return [name, '']; } /** * The identity line a person field reads, null when the object has none: * `creator` is a commit's committer and a tag's tagger. */ function person(obj, role) { if (obj === null || (obj.type !== 'commit' && obj.type !== 'tag')) return null; const key = role === 'creator' ? (obj.type === 'commit' ? 'committer' : 'tagger') : role; return header(obj, key); } /** * An identity's name and its email, brackets included: the name is what * comes before the space ahead of `<`, empty without one. */ function splitIdent(ident) { const space = ident.indexOf(' <'); const name = space !== -1 ? ident.slice(0, space) : ''; const lt = ident.indexOf('<'); if (lt === -1) return [name, '']; const gt = ident.indexOf('>', lt); return [name, gt === -1 ? ident.slice(lt) : ident.slice(lt, gt + 1)]; } function mapped(ident, ctx) { const [name, email] = splitIdent(ident); if (!ctx.mailmap.length) return [name, email]; return splitIdent(mappedIdentity(`${name} ${email}`, ctx.mailmap)); } function value(text, number = null) { return { text, number }; } function personValue(f, item, ctx) { const [role, part] = personRole(f.name); const ident = person(shownObject(f, item), role); if (ident === null) return value('', part === 'date' ? 0 : null); if (part === '') return value(f.variant === 'blank' ? '' : ident); if (part === 'date') { const mode = f.date === null ? ctx.date : parseDateMode(f.date, ctx.date); const found = identDate(ident); if (found === null) return value('', 0); const [seconds, offset] = found; return value(showDate(seconds, offset, mode), seconds); } const [name, email] = f.options.has('mailmap') ? mapped(ident, ctx) : splitIdent(ident); if (part === 'name') return value(name); let shown = email; if (f.options.has('trim') || f.options.has('localpart')) { shown = shown.replace(/^</, '').replace(/>$/, ''); if (f.options.has('localpart')) shown = shown.split('@')[0] ?? ''; } return value(shown); } function messageValue(f, item) { const obj = shownObject(f, item); if (obj === null || (obj.type !== 'commit' && obj.type !== 'tag')) { // A size nothing has is nothing to print, and 0 to sort by. return value('', f.variant === 'size' ? 0 : null); } const { message, messageBytes } = headers(obj); const [start, subject, body, sig] = messageParts(message); const variant = f.variant; if (f.name === 'subject' || variant === 'subject') { return value(variant === 'sanitize' ? sanitizeSubject(subject) : subject); } if (f.name === 'body') return value(variant === 'blank' ? '' : message.slice(body)); if (variant === 'body') return value(message.slice(body, sig)); if (variant === 'signature') return value(message.slice(sig)); // The contents start at the subject: empty lines before it are no part of // them, each one byte. if (variant === 'size') return value(String(messageBytes - start), messageBytes - start); if (variant === 'lines') return value(messageLines(message, start, sig, f.count)); return value(message.slice(start)); } function objectValue(f, item, ctx) { const name = f.name; if (ID_FIELDS.includes(name)) { return value(ids(f, item) .map((oid) => abbreviated(f, oid, ctx)) .join(' ')); } const obj = shownObject(f, item); if (obj === null) { const counted = name === 'objectsize' || name === 'numparent' || f.variant === 'size'; return value('', counted ? 0 : null); } if (name === 'objecttype') return value(obj.type); if (name === 'objectsize') return value(String(obj.raw.length), obj.raw.length); if (name === 'raw') { if (f.variant === 'size') return value(String(obj.raw.length), obj.raw.length); return value(decodeText(obj.raw)); } if (name === 'numparent') { if (obj.type !== 'commit') return value('', 0); const count = headers(obj).headers.filter(([k]) => k === 'parent').length; return value(String(count), count); } if (obj.type !== 'tag') return value(''); return value(header(obj, name) ?? ''); } function refValue(f, item, ctx) { const name = f.name; if (name === 'HEAD') return value(item.name === ctx.head ? '*' : ' '); if (name === 'worktreepath') return value(item.worktree); if (name === 'upstream') return value(upstreamText(f, item, ctx)); let text; if (item.kind === RefKind.DETACHED && name === 'refname') text = ctx.headDescription; else if (name === 'symref') text = item.symref ? nameText(f, item.symref, ctx) : ''; else text = nameText(f, item.name, ctx); return value(f.deref ? `${text}${PEELED_SUFFIX}` : text); } /** * What one field prints for one ref, and what it sorts by. * * @throws GitError a date style git does not have, met on a date */ export function fieldValue(f, item, ctx) { if (REF_FIELDS.has(f.name)) return refValue(f, item, ctx); if (MESSAGE_FIELDS.has(f.name)) return messageValue(f, item); if (PERSON_ROLES.includes(personRole(f.name)[0])) return personValue(f, item, ctx); return objectValue(f, item, ctx); } /** Whether a listing reads the mailmap. */ export function mailmapped(fields) { return fields.some((f) => f.options.has('mailmap')); } //# sourceMappingURL=ref_fields.js.map