Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

153 changes: 153 additions & 0 deletions public/analyst-workflow.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Analytic Workflow Builder – Clean Layout</title>
<style>
:root {
--bg:#f5f7fa; --panel:#fff; --border:#d0d7e2; --text:#1f2937; --muted:#6b7280;
--accent:#3b5b86; --accent-soft:#eaf0f8; --radius:12px; --gap:14px;
}
body { margin:0; font-family:system-ui, sans-serif; background:var(--bg); color:var(--text);}
.page { max-width:1400px; margin:auto; padding:16px; }
.header { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
.tabs { display:flex; gap:8px; margin-bottom:12px; }
.tab { padding:8px 12px; border:1px solid var(--border); border-radius:8px; cursor:pointer; background:var(--panel);}
.tab.active { background:var(--accent); color:#fff; }
.layout { display:grid; grid-template-columns:240px 1fr 320px; gap:var(--gap); }
.panel { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:12px; }
.hidden { display:none; }
.step { border:1px dashed var(--border); padding:10px; border-radius:8px; margin-bottom:10px; }
.bottom-drawer { margin-top:12px; border:1px solid var(--border); border-radius:var(--radius); padding:12px; background:var(--panel);}
button { padding:6px 10px; border-radius:6px; border:1px solid var(--border); cursor:pointer; }
.primary { background:var(--accent); color:white; border-color:var(--accent);}
</style>
</head>
<body>
<div class="page">

<div class="header">
<div>
<h2 style="margin:0;">Analytic Workflow Builder</h2>
<small class="muted">Build, manage, and run conditional query pipelines</small>
</div>
<div>
<button>Save</button>
<button>Export</button>
<button class="primary">Run</button>
</div>
</div>

<div class="tabs">
<div class="tab active" onclick="showTab('pipelines')">Pipelines</div>
<div class="tab" onclick="showTab('data')">Data</div>
<div class="tab" onclick="showTab('queries')">Queries</div>
<div class="tab" onclick="showTab('run')">Run</div>
</div>

<!-- PIPELINES -->
<div id="pipelines" class="tab-content">
<div class="layout">

<!-- LEFT: Pipeline list -->
<div class="panel">
<strong>Pipelines</strong>
<div class="step">QA Review</div>
<div class="step">Lead Expansion</div>
<div class="step">Data Transformation</div>
<button style="margin-top:8px;">+ New</button>
</div>

<!-- CENTER: Steps -->
<div class="panel">
<div style="display:flex; justify-content:space-between;">
<strong>Workflow Steps</strong>
<button onclick="addStep()">+ Add Step</button>
</div>

<div class="step" onclick="selectStep(this)">
step_1 · ASK → true/false
</div>
<div class="step" onclick="selectStep(this)">
step_2 · SELECT → rows/no rows
</div>

</div>

<!-- RIGHT: Inspector -->
<div id="inspector" class="panel hidden">
<strong>Step Inspector</strong>
<div style="margin-top:10px;">
<label>Step ID</label>
<div class="panel">step_1</div>
</div>
<div style="margin-top:10px;">
<label>Query</label>
<div class="panel">qa_has_version_iri</div>
</div>
<div style="margin-top:10px;">
<label>Branching</label>
<div class="panel">true → step_2<br>false → step_3</div>
</div>
</div>

</div>
</div>

<!-- DATA -->
<div id="data" class="tab-content hidden">
<div class="panel">
<strong>Data Management</strong>
<p class="muted">Load RDF into IndexedDB and inspect graphs.</p>
<button>Upload RDF</button>
<button>View Graphs</button>
</div>
</div>

<!-- QUERIES -->
<div id="queries" class="tab-content hidden">
<div class="panel">
<strong>Query Registry</strong>
<div class="step">qa_has_version_iri (ASK)</div>
<div class="step">select_associates (SELECT)</div>
<div class="step">construct_roles (CONSTRUCT)</div>
</div>
</div>

<!-- RUN -->
<div id="run" class="tab-content hidden">
<div class="panel">
<strong>Run Pipeline</strong>
<p>Select dataset and execute pipeline.</p>
<button class="primary">Run</button>
</div>

<div class="bottom-drawer">
<strong>Execution Trace</strong>
<p class="muted">Appears after run</p>
</div>
</div>

</div>

<script>
function showTab(id) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
document.getElementById(id).classList.remove('hidden');

document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
}

function selectStep(el) {
document.getElementById('inspector').classList.remove('hidden');
}

function addStep() {
alert('Add step UI goes here');
}
</script>

</body>
</html>
65 changes: 53 additions & 12 deletions public/app/axiolotl-inference.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,36 @@ function quadKey(q) {
].join('¦');
}

function quadHasBlankNode(q) {
return q.subject.termType === 'BlankNode' ||
q.object.termType === 'BlankNode' ||
q.graph.termType === 'BlankNode';
}

function looseQuadKey(q) {
const subj =
q.subject.termType === 'BlankNode'
? 'BlankNode:_'
: `${q.subject.termType}:${q.subject.value}`;

const obj =
q.object.termType === 'BlankNode'
? 'BlankNode:_'
: `${q.object.termType}:${q.object.value}:${q.object.language || ''}:${q.object.datatype?.value || ''}`;

const graph =
q.graph.termType === 'BlankNode'
? 'BlankNode:_'
: `${q.graph.termType}:${q.graph.value || ''}`;

return [
subj,
`${q.predicate.termType}:${q.predicate.value}`,
obj,
graph
].join('¦');
}

function canBeSubject(term) {
return !!term && (term.termType === 'NamedNode' || term.termType === 'BlankNode');
}
Expand Down Expand Up @@ -412,7 +442,7 @@ async function inferUntilStable(rules) {
// ---- 3) Seed closures from existing dataset ----
let totalAdded = 0;
let pass = 0;
const MAX_PASSES = 100;
const MAX_PASSES = 30;

const seedSeenBefore = seen.size;
processQueues();
Expand All @@ -421,6 +451,11 @@ async function inferUntilStable(rules) {

inferenceInfo(`[inferUntilStable] Seed closures added ${seedAdded} triples.`);

const rulesFiltered = rules.filter(r => r !== 'subclassof' && r !== 'subpropertyof');
if (rules.length !== rulesFiltered.length) {
inferenceInfo('[inferUntilStable] SPARQL disabled for subclassof/subpropertyof; using JS closures instead.');
}

let changed = true;

while (changed) {
Expand All @@ -434,17 +469,19 @@ async function inferUntilStable(rules) {

inferenceInfo(`[inferUntilStable] Starting pass ${pass}...`);

const rulesFiltered = rules.filter(r => r !== 'subclassof' && r !== 'subpropertyof');
if (rules.length !== rulesFiltered.length) {
inferenceInfo('[inferUntilStable] Skipping SPARQL for subclassof/subpropertyof (handled by JS closures)');
}

for (const rule of rulesFiltered) {
const constructQuery = getConstructQueryForRule(rule);
if (!constructQuery) continue;

const newQuads = await runRuleOnce(rule, rdfjsStore);

const blankCount = newQuads.filter(quadHasBlankNode).length;
const looseCount = new Set(newQuads.map(looseQuadKey)).size;

inferenceInfo(
`[inferUntilStable] Pass ${pass}, rule "${rule}": raw=${newQuads.length}, withBlank=${blankCount}, looseUnique=${looseCount}`
);

const batch = [];
const batchSeen = new Set();
for (const q of newQuads) {
Expand All @@ -460,26 +497,30 @@ async function inferUntilStable(rules) {

const beforeClosure = seen.size;
processQueues();
const closureAdded = seen.size - beforeClosure;
const propagatedAdded = seen.size - beforeClosure;

const ruleAdded = directAdded + closureAdded;
passAdded += ruleAdded;
const totalRuleAdded = directAdded + propagatedAdded;

inferenceInfo(
`[inferUntilStable] Pass ${pass}, rule "${rule}": direct=${directAdded}, propagated=${closureAdded}, total=${ruleAdded}`
`[inferUntilStable] Pass ${pass}, rule "${rule}": direct=${directAdded}, propagated=${propagatedAdded}, total=${totalRuleAdded}`
);

if (totalRuleAdded > 0) {
passAdded += totalRuleAdded;
totalAdded += totalRuleAdded;
changed = true;
}
}

if (passAdded > 0) {
totalAdded += passAdded;
changed = true;
inferenceInfo(`[inferUntilStable] Completed pass ${pass}: added ${passAdded} triples.`);
} else {
inferenceInfo(`[inferUntilStable] Completed pass ${pass}: no new triples. Stable.`);
}
}

const overlayCount = overlayStore.getQuads(null, null, null, null).length;

inferenceInfo(
`[inferUntilStable] Completed inference. Passes=${pass}, total new triples=${totalAdded}, overlay triples=${overlayCount}`
);
Expand Down
52 changes: 45 additions & 7 deletions public/app/axiolotl-query.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
// toastSuccess
// toastError
// commonSPARQLPrefixes
// commonMIMEType
// getSelectedOutputMime (depends on commonMIMEType)
// downloadText


Expand Down Expand Up @@ -279,9 +277,17 @@ function handleDownloadPreview(format = 'text/turtle') {
// Get user choice of where to save inferred triples
function getSaveTarget() {
const isNamed = document.getElementById('save-target-named')?.checked;

if (isNamed) {
return { mode: 'named', graphIRI: makeNamedGraphIRI('http://example.org/inferred') };
const iriForOverlay = document.getElementById('iri-for-save-target')?.value;

return {
mode: 'named',
// Uses iriForOverlay if it has a value, otherwise generates one
graphIRI: iriForOverlay?.trim() ? iriForOverlay : makeNamedGraphIRI('http://example.org/inferred')
};
}

return { mode: 'default', graphIRI: null };
}

Expand Down Expand Up @@ -366,7 +372,10 @@ async function exportInferredOverlay() {
const ext = ({
'text/turtle': 'ttl',
'application/n-triples': 'nt',
'application/n-quads': 'nq'
'application/n-quads': 'nq',
'application/trig': 'trig',
'application/ld+json': 'jsonld',
'application/rdf+xml': 'rdf'
})[mime] || 'ttl';

downloadText(`inferred-${timestampUTC()}.${ext}`, text, mime);
Expand Down Expand Up @@ -417,9 +426,7 @@ function addNewFileRow() {
function toggleReasonerButtons() {
const useDB = document.getElementById('reasoner-source-indexeddb')?.checked;
const saveBtn = document.getElementById('save-inferred-to-db');
const insertBtn = document.getElementById('insert-inferred-to-endpoint');
if (saveBtn) saveBtn.style.display = useDB ? '' : 'none';
if (insertBtn) insertBtn.style.display = useDB ? 'none' : '';
}
// run at load + when radios change
['reasoner-source-indexeddb','reasoner-source-endpoint'].forEach(id=>{
Expand All @@ -430,7 +437,6 @@ toggleReasonerButtons();
// Event handlers
document.getElementById('run-inference')?.addEventListener('click', runInference);
document.getElementById('save-inferred-to-db')?.addEventListener('click', saveInferredTriplesToDB);
document.getElementById('insert-inferred-to-endpoint')?.addEventListener('click', insertInferredTriplesIntoEndpoint);
document.getElementById('export-inferred')?.addEventListener('click', async () => {
await exportInferredOverlay();
});
Expand Down Expand Up @@ -655,6 +661,17 @@ const $writeOpts = document.getElementById('write-options');
});
});

const stashInferenceDefaultGraphMode = document.getElementById('save-target-default');
const stashInferenceNamedGraphMode = document.getElementById('save-target-named');
const nameSaveTargetEl = document.getElementById('name-save-target');

[stashInferenceDefaultGraphMode, stashInferenceNamedGraphMode].forEach(el=>{
el?.addEventListener('change', ()=>{
const namedGraphOptionChosen = stashInferenceNamedGraphMode?.checked;
if (nameSaveTargetEl) nameSaveTargetEl.style.display = namedGraphOptionChosen ? 'block' : 'none';
});
});

/**
* Validate that query kind matches UI mode. Returns {ok:boolean,reason?:string}
* Pure; logs for developer visibility.
Expand Down Expand Up @@ -1329,6 +1346,27 @@ function renderQueryError(err) {
resultsDiv.innerHTML = html;
}

// Drop-down id="output-format" has values: Turtle, n-Triples, JSON-LD, RDF/XML
const commonMIMEType = {
'Turtle': 'text/turtle',
'n-Triples': 'application/n-triples',
'JSON-LD': 'application/ld+json',
'RDF/XML': 'application/rdf+xml',
'N-Quads': 'application/n-quads',
'TriG': 'application/trig',
'SPARQL Results JSON': 'application/sparql-results+json',
'SPARQL Results XML': 'application/sparql-results+xml',
'SPARQL Update': 'application/sparql-update',
'SPARQL Query': 'application/sparql-query',
};

// Utility to get selected output MIME type from dropdown
function getSelectedOutputMime() {
const sel = document.getElementById('output-format');
const label = sel?.value || 'Turtle';
return commonMIMEType[label] || 'text/turtle';
}

// Make callable from other modules if they need it
window.renderQueryError = renderQueryError;

Expand Down
Loading
Loading