Try the Sample Commands
Explore recorded sample responses in this browser demo. Choose a command or type 'help' to see whatβs available.
Clinical form specifications often begin in Excel. CRF.xl works from that starting point, turning workbook grids into form definitions and ODM-XML while checking the dependencies between calculation rules.
The approach: Office.js reads the workbook, a dependency graph checks formula relationships, and Web Workers generate the clinical form exports.
Reported project measurements: Sub-10ms UI thread responsiveness during 10,000+ item parsing via asynchronous chunking workers; 100% strict compliance verification against CDISC ODM 1.3.2 foundation schemas; automated 21 CFR Part 11 audit trails with cryptographic state digests.
Decoupled Office.js Taskpane & Domain Layer: Clean separation between Office.js Taskpane React components and the core domain layer (src/taskpane/core/). The orchestration pipeline relies on a composable middleware runner pattern for data transformations and validation gates.
Web Worker Pipeline: Resource-heavy parsing, AST construction, and XML serialization are offloaded to dedicated Web Workers (engine.worker.ts, customxml-worker.ts), preserving interactive UI responsiveness in constrained Office Webview runtimes.
DAG Dependency Graph: Abstract Syntax Tree (AST) validation for conditional logic and form item display skips uses topological DAG traversal (dag-validator.ts, rules-parser.ts) to detect circular dependencies and unresolvable item references at authoring time.
flowchart TD
subgraph IngestionLayer [Office.js Client Ingestion]
A[Excel Taskpane UI / React] -->|Trigger Ingestion/Sync| B[App Orchestrator]
B --> C[Office.js Chunking Ingestion Layer]
end
subgraph WebWorkerPipeline [Web Worker Processing Engine]
C -->|Transfer ArrayBuffer/Chunks| D[Web Worker Engine]
D --> E[Excel/Rules Parser & AST Generator]
E --> F[DAG Dependency & Circular Ref Validator]
F --> G[CDISC Controlled Terminology Resolver]
G --> H[Serialization Pipeline]
end
H -->|Emits ODM-XML / aCRF| I[Office.js CustomXML / Blob Exporter]
H -->|Validation Errors| J[Interactive Grid Error Annotator]
src/taskpane/core/dag-validator.ts)
// Topological DAG Validation Solver for Clinical Skip & Derivation Logic
interface RuleNode {
id: string;
dependsOn: string[];
}
function detectCircularLogicDependencies(nodes: RuleNode[]): string[][] {
const adjList = new Map<string, string[]>();
nodes.forEach((n) => adjList.set(n.id, n.dependsOn || []));
const visited = new Set<string>();
const recStack = new Set<string>();
const cycles: string[][] = [];
function dfs(current: string, path: string[]) {
visited.add(current);
recStack.add(current);
const neighbors = adjList.get(current) || [];
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
dfs(neighbor, [...path, neighbor]);
} else if (recStack.has(neighbor)) {
const cycleStartIndex = path.indexOf(neighbor);
cycles.push([...path.slice(cycleStartIndex), neighbor]);
}
}
recStack.delete(current);
}
for (const node of nodes) {
if (!visited.has(node.id)) {
dfs(node.id, [node.id]);
}
}
return cycles;
}
src/taskpane/office/excel-reader.ts)
// Non-blocking chunked reading of large clinical protocol worksheets in Office.js
async function readWorkbookInChunks(
context: Excel.RequestContext,
sheetName: string,
chunkSize: number = 500,
onChunk: (rows: any[][], progress: number) => Promise<void>
): Promise<void> {
const sheet = context.workbook.worksheets.getItem(sheetName);
const usedRange = sheet.getUsedRange();
usedRange.load(["rowCount", "columnCount", "values"]);
await context.sync();
const totalRows = usedRange.rowCount;
for (let offset = 0; offset < totalRows; offset += chunkSize) {
const end = Math.min(offset + chunkSize, totalRows);
const chunkValues = usedRange.values.slice(offset, end);
const progress = Math.round((end / totalRows) * 100);
await onChunk(chunkValues, progress);
}
}
src/taskpane/export/odm-serializer.ts)
// Regulatory-grade CDISC ODM-XML 1.3.2 Snapshot Serializer
function serializeODMStudy(study: {
oid: string;
name: string;
protocolId: string;
forms: Array<{ oid: string; name: string; itemGroups: Array<{ oid: string }> }>;
}): string {
const formsXml = study.forms
.map(
(f) =>
`<FormDef OID="${f.oid}" Name="${f.name}" Repeating="No">` +
f.itemGroups.map((g) => `<ItemGroupRef ItemGroupOID="${g.oid}" Mandatory="Yes"/>`).join("") +
`</FormDef>`
)
.join("");
return (
`<?xml version="1.0" encoding="UTF-8"?>\n` +
`<ODM xmlns="http://www.cdisc.org/ns/odm/v1.3" ODMVersion="1.3.2" FileType="Snapshot">\n` +
` <Study OID="${study.oid}">\n` +
` <GlobalVariables>\n` +
` <StudyName>${study.name}</StudyName>\n` +
` <ProtocolName>${study.protocolId}</ProtocolName>\n` +
` </GlobalVariables>\n` +
` <MetaDataVersion OID="MDV.1" Name="CDASH 2.2 Specification">\n` +
` ${formsXml}\n` +
` </MetaDataVersion>\n` +
` </Study>\n` +
`</ODM>`
);
}