prune
This commit is contained in:
@@ -7,17 +7,16 @@ const XSD = 'http://www.w3.org/2001/XMLSchema#';
|
||||
const XSD_DATE = XSD + 'date';
|
||||
const XSD_DATETIME = XSD + 'dateTime';
|
||||
|
||||
// The validator. checkSpec(spec, profile, maxPathDepth) runs the named checks
|
||||
// in order and returns the FIRST error as a one-element array (empty if ok).
|
||||
// Every message names the thing that is wrong in the profile's own words and
|
||||
// says what to write instead. No database call anywhere in this file — that is
|
||||
// what lets an LLM iterate for free.
|
||||
// The validator. checkSpec runs the checks in order and returns the first
|
||||
// error as a one-element array (empty if ok). Every message names the thing
|
||||
// that is wrong in the profile's own words and says what to write instead.
|
||||
// No database call anywhere in this file.
|
||||
|
||||
const ANYOF_MAX_TERMS = 24;
|
||||
const ANYOF_MAX_TERM_LENGTH = 100;
|
||||
|
||||
// 1 + 2. the profile names itself and is one we have. knownProfiles is the
|
||||
// Map app.js loaded at boot; omit it and the check is skipped (fixtures).
|
||||
// The spec names a profile we have. knownProfiles is the profile Map loaded
|
||||
// at boot; the check is skipped when it is omitted.
|
||||
function profileNamed(spec, knownProfiles) {
|
||||
if (!spec.profileUri) {
|
||||
return `no rep:profile named. Available: ${knownProfiles ? [...knownProfiles.values()].map(p => p.title).join(', ') : '(none)'} .`;
|
||||
@@ -28,7 +27,7 @@ function profileNamed(spec, knownProfiles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3 + 4. the target class is in the profile
|
||||
// The target class is in the profile
|
||||
function targetClass(spec, profile) {
|
||||
if (!spec.targetClass) {
|
||||
return `no sh:targetClass. In "${profile.title}" you can list: ${shapeList(profile)}.`;
|
||||
@@ -43,13 +42,13 @@ function shapeList(profile) {
|
||||
return profile.shapes.map(s => s.label || s.targetClass).join(', ');
|
||||
}
|
||||
|
||||
// 5. columns exist
|
||||
// Columns exist
|
||||
function columnsPresent(spec) {
|
||||
if (!spec.columns.length) return 'no columns. Add at least one rep:columns entry.';
|
||||
return null;
|
||||
}
|
||||
|
||||
// 6. every column has a label
|
||||
// Every column has a label
|
||||
function columnLabel(spec) {
|
||||
for (const col of spec.columns) {
|
||||
if (!col.label) return 'a column has no rdfs:label.';
|
||||
@@ -57,7 +56,7 @@ function columnLabel(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 7. labels unique
|
||||
// Labels are unique
|
||||
function columnLabelsUnique(spec) {
|
||||
const seen = new Map();
|
||||
for (const col of spec.columns) {
|
||||
@@ -69,7 +68,7 @@ function columnLabelsUnique(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 8 + 9 + 10. every path resolves, columns end on a value, depth capped
|
||||
// Every path resolves, columns end on a value, depth capped
|
||||
function pathsResolve(spec, profile, maxPathDepth) {
|
||||
const startShapeUri = entityForClass(profile, spec.targetClass);
|
||||
for (const col of spec.columns) {
|
||||
@@ -85,7 +84,7 @@ function pathsResolve(spec, profile, maxPathDepth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 11. sh:min/sh:max only on numbers and dates
|
||||
// sh:min/sh:max only on numbers and dates
|
||||
function collectMinMaxTyped(spec, profile) {
|
||||
const startShapeUri = entityForClass(profile, spec.targetClass);
|
||||
for (const col of spec.columns) {
|
||||
@@ -108,7 +107,7 @@ function collectMinMaxTyped(spec, profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 13. at most one rep:row column
|
||||
// At most one rep:row column
|
||||
function oneRowColumn(spec) {
|
||||
const rowCols = spec.columns.filter(c => c.collect === REP + 'row');
|
||||
if (rowCols.length > 1) {
|
||||
@@ -118,7 +117,7 @@ function oneRowColumn(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 12 + 14. columns carry no constraints; known collect mode
|
||||
// Columns carry no constraints; known collect mode
|
||||
function columnPure(spec) {
|
||||
for (const col of spec.columns) {
|
||||
if (Object.keys(col.constraints || {}).length) {
|
||||
@@ -132,7 +131,7 @@ function columnPure(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 15. no label inside a filter
|
||||
// No label inside a filter
|
||||
function filterLabel(spec) {
|
||||
for (const filter of spec.filters) {
|
||||
if ((filter.constraints || {}).label) {
|
||||
@@ -142,7 +141,7 @@ function filterLabel(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 15b. a filter with a path but no constraint selects everything; the model
|
||||
// A filter with a path but no constraint selects everything; the model
|
||||
// almost certainly meant to filter. Refuse and say what to add.
|
||||
function filterConstraint(spec) {
|
||||
const MEANINGFUL = ['minCount', 'maxCount', 'hasValue', 'in', 'minInclusive',
|
||||
@@ -157,7 +156,7 @@ function filterConstraint(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 16. sh:maxCount above 0 needs counting
|
||||
// sh:maxCount above 0 needs counting
|
||||
function maxCountZero(spec) {
|
||||
for (const [i, filter] of spec.filters.entries()) {
|
||||
const c = filter.constraints;
|
||||
@@ -171,7 +170,7 @@ function maxCountZero(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 17 + 18. rep:anyOf size limits
|
||||
// rep:anyOf size limits
|
||||
function anyOfLimits(spec) {
|
||||
for (const [i, filter] of spec.filters.entries()) {
|
||||
const c = filter.constraints;
|
||||
@@ -191,7 +190,7 @@ function anyOfLimits(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 19. sh:in mixes URIs and literals
|
||||
// sh:in must not mix URIs and literals
|
||||
function inHomogeneous(spec) {
|
||||
for (const filter of spec.filters) {
|
||||
const c = filter.constraints;
|
||||
@@ -204,7 +203,7 @@ function inHomogeneous(spec) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 20. typed literals that will not parse
|
||||
// Typed literals must parse
|
||||
function literalsParse(spec) {
|
||||
const constraintTerms = [];
|
||||
for (const filter of spec.filters) {
|
||||
@@ -259,8 +258,8 @@ export function checkSpec(spec, profile, maxPathDepth, knownProfiles = null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// The shape a column's path lands in: follow sh:node through every hop but
|
||||
// the last. walk() has already validated the path, so any gap means the walk
|
||||
// The shape a path lands in: follow sh:node through every hop but the
|
||||
// last. walk() has already validated the path, so a gap here means the walk
|
||||
// reported the real error and this can bail.
|
||||
function shapeAfter(profile, startShapeUri, hops) {
|
||||
let shapeUri = startShapeUri;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { mkdirSync, writeFileSync } from 'fs';
|
||||
|
||||
// Rows to a CSV file. CSV_SEPARATOR is ';' by default.
|
||||
// A cell is quoted only when it holds the separator, a quote or a newline —
|
||||
// this matches the shape of today's loket-report-generation-service output.
|
||||
// A quote inside a cell is doubled, per RFC 4180.
|
||||
// Rows to a CSV file. CSV_SEPARATOR is ';' by default. A cell is quoted
|
||||
// only when it holds the separator, a quote or a newline; a quote inside a
|
||||
// cell is doubled, per RFC 4180.
|
||||
|
||||
const CSV_SEPARATOR = process.env.CSV_SEPARATOR || ';';
|
||||
const SHARE_DIR = process.env.SHARE_DIR || '/share';
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { sparqlEscapeUri, sparqlEscapeString, sparqlEscapeDateTime } from '../db.js';
|
||||
|
||||
// cogs:Job + task:Task, written by us. No other service is involved.
|
||||
// Triples follow §6 of plan-opus-5.iteration-1-4.md; the shape follows
|
||||
// config/resources/master-job-domain.lisp.
|
||||
// Every write carries mu-call-scope-id (REPORT_SCOPE) so the delta notifier
|
||||
// lets jobs-controller ignore our statuses — it throws on unknown operations.
|
||||
// All functions take the update function to use: scopedUpdate(sparql, extra),
|
||||
// which sends the captured groups plus this scope header.
|
||||
// cogs:Job + task:Task, written by us; no other service is involved. Every
|
||||
// write carries mu-call-scope-id (REPORT_SCOPE) so the delta notifier lets
|
||||
// jobs-controller ignore our statuses — it throws on unknown operations.
|
||||
|
||||
const REPORT_SCOPE = 'http://redpencil.data.gift/id/concept/muScope/report-generation';
|
||||
const JOB_BASE = process.env.JOB_URI_BASE || 'http://redpencil.data.gift/id/job/';
|
||||
@@ -32,9 +28,7 @@ const TASK = 'http://redpencil.data.gift/vocabularies/tasks/';
|
||||
const OSLC_MESSAGE = 'http://open-services.net/ns/core#message';
|
||||
const OSLC_ERROR_URI = 'http://open-services.net/ns/core#Error';
|
||||
|
||||
// updateFn(sparql, extraHeaders) — app.js passes scopedUpdate, which sends
|
||||
// the captured groups; the scope header is added here so every job write is
|
||||
// opt-out-able for the delta notifier.
|
||||
// Adds the scope header to every job write.
|
||||
function withScope(updateFn) {
|
||||
return async sparql => updateFn(sparql, { 'mu-call-scope-id': REPORT_SCOPE });
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||
const OWL_ONTOLOGY = 'http://www.w3.org/2002/07/owl#Ontology';
|
||||
|
||||
export async function loadProfiles(dir) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const fs = await import('fs');
|
||||
// eslint-disable-next-line no-undef
|
||||
const path = await import('path');
|
||||
const files = fs.readdirSync(dir).filter(f => f.endsWith('.ttl')).sort();
|
||||
const profiles = new Map();
|
||||
|
||||
@@ -5,9 +5,9 @@ import { sessionUpdate, sparqlEscapeUri, sparqlEscapeString, sparqlEscapeInt,
|
||||
|
||||
const REPORT_CLASS = process.env.REPORT_CLASS || 'http://lblod.data.gift/vocabularies/reporting/Report';
|
||||
|
||||
// Write the file to /share, then register it once: a logical file
|
||||
// (http://data.lblod.info/files/<uuid>) plus a physical share:// file
|
||||
// pointing back with nie:dataSource. The logical URI is what you link to.
|
||||
// Register the CSV: a logical file (http://data.lblod.info/files/<uuid>) plus
|
||||
// a physical share:// file pointing back with nie:dataSource. The logical
|
||||
// URI is what you link to.
|
||||
export async function registerFile(sessionUpdateFn, fileName, filePath) {
|
||||
const size = statSync(filePath).size;
|
||||
const uuid = randomUUID();
|
||||
@@ -36,8 +36,7 @@ export async function registerFile(sessionUpdateFn, fileName, filePath) {
|
||||
return logicalUri;
|
||||
}
|
||||
|
||||
// One report pointing at the logical file URI. Insert without naming a graph;
|
||||
// the auth layer places the triples from the caller's session.
|
||||
// One report pointing at the logical file URI.
|
||||
export async function registerReport(sessionUpdateFn, title, fileUri, extra = {}) {
|
||||
const uuid = randomUUID();
|
||||
const now = new Date();
|
||||
|
||||
@@ -5,11 +5,9 @@ import { rowsToCsv, writeCsv } from './csv.js';
|
||||
import { registerFile, registerReport } from './report.js';
|
||||
import { touch } from './job.js';
|
||||
|
||||
// Ties the six steps together. `query` and `update` are the template's own
|
||||
// helpers: they attach the caller's session from the request context, which
|
||||
// lives for the whole run (also past the 202), so mu-authorization keeps
|
||||
// deciding graphs and visibility. Returns { reportUri, fileUri, rowCount }.
|
||||
// The task is touched once per column group so the dashboard shows movement.
|
||||
// Ties the steps together: seed → columns → assemble → CSV → register.
|
||||
// Returns { reportUri, fileUri, rowCount }. The task is touched once per
|
||||
// column group so the dashboard shows movement.
|
||||
|
||||
export async function run(query, update, parsed, profile, title, taskUri, extra = {}) {
|
||||
const subjects = await seed(query, parsed);
|
||||
@@ -29,8 +27,8 @@ export async function run(query, update, parsed, profile, title, taskUri, extra
|
||||
return { reportUri, fileUri, rowCount: rows.length - 1, filePath };
|
||||
}
|
||||
|
||||
// Exported for run_report: the file name must be known before the run starts,
|
||||
// so the chat can show a pending card named after the report.
|
||||
// The file name must be known before the run starts, so the chat can show a
|
||||
// pending card named after the report.
|
||||
export function slug(title) {
|
||||
return (title || 'report').toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { sparqlEscapeUri, sparqlEscapeString, sparqlEscapeDateTime, sparqlEscapeDate } from '../db.js';
|
||||
|
||||
// Subject selection. One paged query per page; stops at ROW_LIMIT.
|
||||
// No GRAPH, no property paths, every hop its own variable.
|
||||
|
||||
const ROW_LIMIT = Number(process.env.ROW_LIMIT || 200000);
|
||||
const PAGE_SIZE = Number(process.env.SEED_PAGE_SIZE || 5000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user