Spaces:
Running
Running
Download data.js from Battam/agentic-env-catalog: direct link, hf CLI and curl.
- Browser
- Download file 13.9 kB
-
https://huggingface.co/spaces/Battam/agentic-env-catalog/resolve/main/data.js
- Command line
-
hf download hf://spaces/Battam/agentic-env-catalog/data.js
-
curl -L -o data.js https://huggingface.co/spaces/Battam/agentic-env-catalog/resolve/main/data.js
13.9 kB
| // Reading the index in the browser. Every file comes from one commit of the dataset, so every number agrees. | |
| import { asyncBufferFromUrl, parquetMetadataAsync, parquetReadObjects } from 'https://cdn.jsdelivr.net/npm/hyparquet@1.31.5/+esm'; | |
| import { compressors } from 'https://cdn.jsdelivr.net/npm/hyparquet-compressors@1.1.2/+esm'; | |
| export const REPO = 'Battam/agentic-env-index'; | |
| export const HF = 'https://huggingface.co'; | |
| export const DATASET_URL = `${HF}/datasets/${REPO}`; | |
| export const fileUrl = (sha, path) => `${DATASET_URL}/resolve/${sha}/${path}`; | |
| export const num = (v) => (v == null ? null : typeof v === 'bigint' ? Number(v) : Number(v)); | |
| async function getJson(url) { | |
| const r = await fetch(url); | |
| if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`); | |
| return r.json(); | |
| } | |
| async function getText(url) { | |
| const r = await fetch(url); | |
| if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`); | |
| return r.text(); | |
| } | |
| /** A small file read whole (the meta tables): one request instead of a size probe and ranges. */ | |
| export async function readParquet(url, options = {}) { | |
| const r = await fetch(url); | |
| if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`); | |
| const file = await r.arrayBuffer(); | |
| return parquetReadObjects({ file, compressors, utf8: false, ...options }); | |
| } | |
| export async function parquetRows(url) { | |
| const file = await asyncBufferFromUrl({ url }); | |
| return num((await parquetMetadataAsync(file)).num_rows); | |
| } | |
| /** The columns of cols that the file has, read with hyparquet (byte ranges); range: {rowStart, rowEnd}. */ | |
| export async function readColumns(url, cols, range = {}) { | |
| const file = await asyncBufferFromUrl({ url }); | |
| const metadata = await parquetMetadataAsync(file); | |
| const names = new Set(metadata.schema.slice(1).map((s) => s.name)); | |
| const columns = cols.filter((c) => names.has(c)); | |
| const rows = await parquetReadObjects({ file, metadata, columns, compressors, utf8: false, ...range }); | |
| return { rows, total: num(metadata.num_rows) }; | |
| } | |
| /** The commit to read: ?rev=<sha or branch>, else main. Returns {sha, date, files}. */ | |
| export async function loadRevision(rev) { | |
| const j = await getJson(`${HF}/api/datasets/${REPO}/revision/${encodeURIComponent(rev || 'main')}`); | |
| if (!/^[0-9a-f]{40}$/.test(j.sha || '')) throw new Error('the Hub gave no commit id'); | |
| return { sha: j.sha, date: j.lastModified || null, files: (j.siblings || []).map((s) => s.rfilename) }; | |
| } | |
| /** Batch folders of one source with the date of their last commit (Hub tree API). */ | |
| export async function loadBatchDates(sha, source) { | |
| const j = await getJson(`${HF}/api/datasets/${REPO}/tree/${sha}/data/${encodeURIComponent(source)}?expand=true`); | |
| const out = new Map(); | |
| for (const e of j) { | |
| if (e.type !== 'directory') continue; | |
| out.set(e.path.split('/').pop(), e.lastCommit?.date || null); | |
| } | |
| return out; | |
| } | |
| // ---- the classification contract (#252): domain is a list of codes (D1 to D10, with leaves), or multi, or | |
| // unclassified; form is one id; names come from meta/vocabulary.json. Without them everything shows as unclassified. | |
| const pick = (o, keys) => { | |
| if (!o || typeof o !== 'object') return undefined; | |
| for (const k of keys) if (o[k] != null) return o[k]; | |
| return undefined; | |
| }; | |
| const NAME_KEYS = ['name', 'label', 'title', 'en']; | |
| const CODE_KEYS = ['code', 'id', 'key', 'value']; | |
| const KIDS_KEYS = ['children', 'leaves', 'subdomains', 'items']; | |
| const ABOUT_KEYS = ['description', 'about', 'definition']; | |
| function vocabSection(node) { | |
| const names = new Map(); | |
| const about = new Map(); | |
| const order = []; | |
| const add = (code, name, text) => { | |
| if (code == null) return; | |
| code = String(code); | |
| if (!names.has(code)) order.push(code); | |
| if (typeof name === 'string' && name.trim()) names.set(code, name.trim()); | |
| else if (!names.has(code)) names.set(code, null); | |
| if (typeof text === 'string' && text.trim()) about.set(code, text.trim()); | |
| }; | |
| const walk = (n) => { | |
| if (!n) return; | |
| if (Array.isArray(n)) { | |
| for (const it of n) { | |
| if (typeof it === 'string') add(it, null); | |
| else if (it && typeof it === 'object') { | |
| add(pick(it, CODE_KEYS), pick(it, NAME_KEYS), pick(it, ABOUT_KEYS)); | |
| walk(pick(it, KIDS_KEYS)); | |
| } | |
| } | |
| } else if (typeof n === 'object') { | |
| for (const [k, v] of Object.entries(n)) { | |
| if (typeof v === 'string') add(k, v); | |
| else if (v && typeof v === 'object') { | |
| add(k, pick(v, NAME_KEYS), pick(v, ABOUT_KEYS)); | |
| walk(pick(v, KIDS_KEYS)); | |
| } else add(k, null); | |
| } | |
| } | |
| }; | |
| walk(node); | |
| return { names, about, order }; | |
| } | |
| // meta/vocabulary.json (#304): domains [{code, name, about, parent}], forms [{id, about}]. A domain shows its name; a | |
| // form has no name, so it shows its id written as words (test-suites -> test suites). | |
| export function parseVocabulary(j) { | |
| const form = vocabSection(pick(j, ['form', 'forms'])); | |
| for (const [id, name] of form.names) if (!name) form.names.set(id, id.replace(/[-_]+/g, ' ')); | |
| return { domain: vocabSection(pick(j, ['domain', 'domains'])), form }; | |
| } | |
| export const topOf = (code) => String(code).split('.')[0]; | |
| function domainOf(v) { | |
| let list = v; | |
| if (typeof list === 'string') list = [list]; | |
| if (list && typeof list.toArray === 'function') list = list.toArray(); | |
| list = Array.isArray(list) ? list.map(String).map((s) => s.trim()).filter(Boolean) : []; | |
| if (!list.length || list.includes('unclassified')) return { kind: 'none', codes: [] }; | |
| if (list.includes('multi')) return { kind: 'multi', codes: [] }; | |
| return { kind: 'codes', codes: [...new Set(list)] }; | |
| } | |
| // ---- small parsers of the index's own text files | |
| const intOf = (s) => Number(String(s).replace(/,/g, '')); | |
| /** CHANGELOG.md -> [{date, title, kind, sourcesAdded, sourcesRemoved, rowsAdded, rowsRemoved, sources, rows}] */ | |
| export function parseChangelog(md) { | |
| const out = []; | |
| const parts = md.split(/^## /m).slice(1); | |
| for (const part of parts) { | |
| const [head, ...lines] = part.split('\n'); | |
| const m = head.match(/^(\d{4}-\d{2}-\d{2}):\s*(.+)$/); | |
| if (!m) continue; | |
| const e = { date: m[1], title: m[2].trim(), kind: 'other', sourcesAdded: null, sourcesRemoved: null, rowsAdded: 0, rowsRemoved: 0 }; | |
| const body = lines.join('\n'); | |
| const start = body.match(/lists ([\d,]+) sources with ([\d,]+) rows/); | |
| if (start) { | |
| e.kind = 'start'; | |
| e.sources = intOf(start[1]); | |
| e.rows = intOf(start[2]); | |
| } | |
| const sa = body.match(/^- Sources added \((\d+)\)/m); | |
| const sr = body.match(/^- Sources removed \((\d+)\)/m); | |
| if (sa || sr) { | |
| e.kind = 'update'; | |
| e.sourcesAdded = sa ? Number(sa[1]) : 0; | |
| e.sourcesRemoved = sr ? Number(sr[1]) : 0; | |
| } | |
| for (const r of body.matchAll(/^\s+- \S+: .*?\(([\d,]+) to ([\d,]+)\)\s*$/gm)) { | |
| const d = intOf(r[2]) - intOf(r[1]); | |
| if (d > 0) e.rowsAdded += d; | |
| else e.rowsRemoved -= d; | |
| } | |
| out.push(e); | |
| } | |
| return out; | |
| } | |
| /** README.md, section "Not included, and why": category -> requirement sentence. */ | |
| export function parseRequirements(md) { | |
| const out = new Map(); | |
| const sec = md.split(/^## /m).find((s) => s.startsWith('Not included')); | |
| if (!sec) return out; | |
| for (const m of sec.matchAll(/^\| `([a-z0-9_]+)` \| (.+?) \| \d+ \| \d+ \|\s*$/gm)) out.set(m[1], m[2].trim()); | |
| return out; | |
| } | |
| const LINK_RE = /^(\d{4}-\d{2}-\d{2}): ([\d,]+) of ([\d,]+) reachable/; | |
| function linkOf(text) { | |
| if (!text) return { kind: 'unchecked' }; | |
| const m = String(text).match(LINK_RE); | |
| if (m) { | |
| const ok = intOf(m[2]); | |
| const total = intOf(m[3]); | |
| return { kind: ok >= total ? 'ok' : 'partial', ok, total, date: m[1] }; | |
| } | |
| if (/^needs /.test(text)) return { kind: 'login', text: String(text) }; | |
| return { kind: 'other', text: String(text) }; | |
| } | |
| /** What every row of a source allows, from sources.json license_rows: 'yes' (every row), 'cond' (yes or | |
| * conditional for every row) or 'no' (some rows are no or unknown). */ | |
| function permission(lr) { | |
| if (!lr) return 'no'; | |
| const keys = Object.entries(lr).filter(([, v]) => num(v?.rows) > 0).map(([k]) => k); | |
| if (!keys.length) return 'no'; | |
| if (keys.every((k) => k === 'yes')) return 'yes'; | |
| if (keys.every((k) => k === 'yes' || k === 'conditional')) return 'cond'; | |
| return 'no'; | |
| } | |
| async function readCatalog(url) { | |
| if (/\.json(\?|$)/i.test(url)) { | |
| const j = await getJson(url); | |
| return Array.isArray(j) ? j : j.sources || j.rows || []; | |
| } | |
| return readParquet(url); | |
| } | |
| /** Everything the page shows, from one commit. params: URLSearchParams (rev, catalog, vocabulary). */ | |
| export async function loadIndex(params, onNote) { | |
| const rev = await loadRevision(params.get('rev')); | |
| const at = (p) => fileUrl(rev.sha, p); | |
| const catalogUrl = params.get('catalog') || at('meta/catalog.parquet'); | |
| const has = (p) => rev.files.includes(p); | |
| const vocabUrl = params.get('vocabulary') || (has('meta/vocabulary.json') ? at('meta/vocabulary.json') : null); | |
| const [catalog, sourcesJson, coverage, batches, notIncluded, changelog, readme, vocabJson] = await Promise.all([ | |
| readCatalog(catalogUrl), | |
| getJson(at('meta/sources.json')), | |
| getJson(at('meta/coverage.json')), | |
| readParquet(at('meta/batches.parquet')).catch(() => []), | |
| readParquet(at('meta/not_included.parquet')).catch(() => []), | |
| getText(at('CHANGELOG.md')).catch(() => ''), | |
| getText(at('README.md')).catch(() => ''), | |
| vocabUrl ? getJson(vocabUrl).catch((e) => { onNote?.(`The vocabulary could not be read (${e.message}); codes are shown as they are.`); return null; }) : null, | |
| ]); | |
| const vocab = parseVocabulary(vocabJson || {}); | |
| const srcInfo = new Map((sourcesJson.sources || []).map((e) => [e.source, e])); | |
| const cov = coverage.by_source || {}; | |
| const batchesBy = new Map(); | |
| for (const b of batches) { | |
| if (!batchesBy.has(b.source)) batchesBy.set(b.source, []); | |
| batchesBy.get(b.source).push(b); | |
| } | |
| const filesBy = new Map(); | |
| for (const f of rev.files) { | |
| const m = f.match(/^data\/([^/]+)\/([^/]+)\/[^/]+\.parquet$/); | |
| if (!m) continue; | |
| if (!filesBy.has(m[1])) filesBy.set(m[1], []); | |
| filesBy.get(m[1]).push({ path: f, batch: m[2] }); | |
| } | |
| const sources = catalog.map((r) => { | |
| const info = srcInfo.get(r.source) || {}; | |
| const lic = r.license || r.licence || {}; | |
| const dom = domainOf(r.domain); | |
| const rows = num(r.files ?? info.rows) || 0; | |
| const bytes = num(r.bytes); | |
| const form = r.form == null || r.form === '' || r.form === 'unclassified' ? null : String(r.form); | |
| // A unit is one unit group (a test case, one workbook); files are counted apart. Once the catalog carries the | |
| // columns units (distinct units; a raw corpus counts rows), answers (units that have an answer) and files (files | |
| // of every role), they are read as they are. Until then: rows of a collection carry roles (sources.json unit: unit, | |
| // companion, answer) and such a source is task-ready; its units are its answer files when it has any (one answer | |
| // per unit group), else its unit rows. Every other source is a raw corpus: each row is one unit to mine. | |
| const roles = info.unit || {}; | |
| const has = (k) => r[k] != null && r[k] !== '' && Number.isFinite(num(r[k])); | |
| const answers = has('answers') ? num(r.answers) : num(roles.answer) || 0; | |
| const isTask = num(roles.unit) > 0 || answers > 0 || (has('units') && num(r.units) < rows); | |
| const units = has('units') ? num(r.units) : isTask ? (answers || num(roles.unit)) : rows; | |
| const lr = info.license_rows?.redistribution_ok || {}; | |
| const shipRows = (num(lr.yes?.rows) || 0) + (num(lr.conditional?.rows) || 0); | |
| const shipShare = rows ? Math.min(1, shipRows / rows) : 0; | |
| return { | |
| isTask, | |
| readyUnits: isTask ? units : 0, | |
| rawRows: isTask ? 0 : rows, | |
| fromCatalog: has('units'), | |
| roles: { unit: num(roles.unit) || 0, companion: num(roles.companion) || 0, answer: answers }, | |
| kinds: Object.keys(roles).filter((k) => !['unit', 'companion', 'answer'].includes(k)), | |
| units, | |
| answered: answers, | |
| shipShare, | |
| id: r.source, | |
| title: r.title || r.source, | |
| description: r.description || info.description || '', | |
| summary: r.summary || '', | |
| unit: r.unit || '', | |
| status: r.status || null, | |
| reasons: Array.isArray(r.status_reasons) ? r.status_reasons : r.status_reasons?.toArray?.() || [], | |
| domainKind: dom.kind, | |
| codes: dom.codes, | |
| tops: [...new Set(dom.codes.map(topOf))], | |
| form, | |
| tier: lic.tier || 'unknown', | |
| ship: lic.ship || null, | |
| spdx: lic.spdx || null, | |
| licenceNote: lic.note || null, | |
| contamination: r.contamination || {}, | |
| pii: r.pii || {}, | |
| rows, | |
| bytes: bytes && bytes > 0 ? bytes : null, | |
| // distinct content (#364): bytes minus the sizes of the rows meta/duplicates.parquet lists; null when not recorded | |
| distinct: has('distinct_bytes') ? num(r.distinct_bytes) : null, | |
| rowsNoSize: num(r.files_without_size) || 0, | |
| topTypes: Array.isArray(r.top_types) ? r.top_types : r.top_types?.toArray?.() || [], | |
| needsAuth: r.needs_auth || null, | |
| link: linkOf(r.link_check), | |
| linkText: r.link_check || null, | |
| train: permission(info.license_rows?.training_ok), | |
| redist: permission(info.license_rows?.redistribution_ok), | |
| info, | |
| cov: cov[r.source] || null, | |
| batches: batchesBy.get(r.source) || [], | |
| files: (filesBy.get(r.source) || []).sort((a, b) => a.path.localeCompare(b.path)), | |
| }; | |
| }); | |
| return { | |
| sha: rev.sha, | |
| date: rev.date, | |
| overrides: { catalog: params.get('catalog'), vocabulary: params.get('vocabulary') }, | |
| hasVocabulary: Boolean(vocabJson), | |
| vocab, | |
| sources, | |
| coverage, | |
| notIncluded, | |
| requirements: parseRequirements(readme), | |
| changelog: parseChangelog(changelog), | |
| }; | |
| } | |