15 — DAG-Based Munāsakhat Implementation: Automatic Re-expression and الجدول المعلق Rendering
15 — DAG-Based Munāsakhat Implementation: Automatic Re-expression and الجدول المعلق Rendering
Section titled “15 — DAG-Based Munāsakhat Implementation: Automatic Re-expression and الجدول المعلق Rendering”Core claim: The classical الجدول المعلق (hanging table) for Munāsakhat is a presentation layer over the estate flow tree structure (findings/11-extension-formalism.md). Automatic re-expression of each heir’s kinship vector relative to each sub-deceased, combined with per-step trace capture, yields the exact data needed to render tables matching the classical sources without manual interpretation.
Implementation: Two new public APIs in
@faraid/api:
solveChainFromDAG(spec, deceasedSequence, config)— automatic re-expression via family DAGrenderShabaka(result)— structured table output;renderShabakaMarkdown(table)for CLI/docs
1. The Gap: Flat API vs. DAG-Based Resolution
Section titled “1. The Gap: Flat API vs. DAG-Based Resolution”1.1 The Problem: Manual Re-expression
Section titled “1.1 The Problem: Manual Re-expression”The original solveChain() API accepts a flat sequence of heir sets keyed by HeirType:
interface ChainSolverInput { initialHeirs: HeirInput[]; // R0: {"Mother": 1, "BrotherFull": 2} deaths: Array<{ dying: HeirType; // "Mother" heirs: HeirInput[]; // R1: {"Son": 2} ← caller MUST translate }>; config?: MadhhabConfig;}The re-expression gap: When John dies leaving {Mother, 2 Brothers}, then Mother dies, the caller must manually translate:
- “In Mother’s case, John’s brothers become Sons (not BrotherFull)”
- “John is deceased, so he has no role in Mother’s sub-mas’ala”
This is error-prone. The translator must:
- Know which persons are related to the sub-deceased
- Recompute each person’s 5-tuple relative to the new reference point
- Verify no duplicates (identical tuples must be merged)
- Handle edge cases (gender-sequence validity, grandmother rules)
Result: Correctness depends on the caller’s understanding of kinship re-expression — the exact problem findings/11-extension-formalism.md §4.1 identifies.
1.2 The Solution: DAG-Based API
Section titled “1.2 The Solution: DAG-Based API”interface DagChainSolverInput { spec: FamilySpec; // DAG structure + spouses deceasedSequence: string[]; // ["John", "Mother"] config?: MadhhabConfig;}
function solveChainFromDAG(input: DagChainSolverInput): DagChainSolverResult { // 1. Validate inputs // 2. Call resolveChainFromDAGWithTrace (core's automatic re-expression) // 3. Annotate steps with re-expressed HeirType labels // 4. Return full trace + final shares}Automatic re-expression: The function:
- Maintains a family DAG (nodes = persons, edges = kinship)
- For each sub-deceased, recomputes every heir’s 5-tuple via BFS path analysis (findings/01 §3)
- Aggregates identical tuples (avoiding duplicates)
- Passes clean heir sets to the core pipeline $F$
The caller just provides the DAG and the sequence of deaths. Re-expression is deterministic and mathematically rigorous.
2. Per-Step Trace Infrastructure
Section titled “2. Per-Step Trace Infrastructure”2.1 Data Structures
Section titled “2.1 Data Structures”ChainStepTrace (in @faraid/core, extensions/chain.ts):
export interface ChainStepTrace { step: number; // 0 (primary R0), 1 (first sub-deceased), ... deceasedId?: string; // undefined for step 0 subBase: bigint; // R_i.B (asl or ʿawl for this problem) scale: bigint; // B_after / gcd(sahm, B) — the ×N factor multiplier: bigint; // (sahm × scale) / subBase baseAfter: bigint; // jāmiʿa after this merge (before final GCD) subShares: Map<string, bigint>; // raw shares within R_i sharesAfter: Map<string, bigint>; // running jāmiʿa shares (accumulated)}
export function evaluateChainTrace( initialBase: bigint, initialShares: Map<string, bigint>, subProblems: Array<{ deceasedId: string; subBase: bigint; subShares: Map<string, bigint>; }>): { base: bigint; shares: Map<string, bigint>; trace: ChainStepTrace[];};Per-step Heir Re-expression (in @faraid/api, types.ts):
export interface DagChainSubHeir { nodeId: string; // physical person id type: HeirType | 'Unknown'; // RE-EXPRESSED relative to sub-deceased label: HeirLabel; // { ar: "ابن", en: "Son" } subSahm: bigint; // share within R_i (out of subBase) fard?: string; // "½" / "⅙" if entitled to fixed fraction role: 'fard' | 'asaba' | 'asaba_bil_ghayr' | 'asaba_maa_ghayr' | 'unknown';}
export interface DagChainStep { step: number; deceasedId?: string; subBase: bigint; // R_i.B scale: bigint; // ×N in the classical table multiplier: bigint; baseAfter: bigint; subHeirs: DagChainSubHeir[]; // heirs of THIS step's sub-deceased sharesAfter: Array<{ nodeId: string; type: HeirType | 'Unknown'; // type relative to ORIGINAL deceased label: HeirLabel; sahm: bigint; // accumulated share in jāmiʿa }>;}
export interface DagChainSolverResult { base: bigint; // final jāmiʿa (musahhah) shares: Array<{ nodeId: string; type: HeirType | 'Unknown'; // stable across steps label: HeirLabel; sahm: bigint; // final share }>; steps: DagChainStep[]; config?: MadhhabConfig;}2.2 The Trace Algorithm
Section titled “2.2 The Trace Algorithm”Algorithm: Per-Step Trace Capture
The core resolveChainFromDAGWithTrace() function:
Input: dag, deceasedSequence = [d_0, d_1, ..., d_n], configOutput: { base, shares, trace, perStepHeirs, perStepResults }
1. Solve F(H_{d_0}) → (B_0, shares_0) Record: trace[0] ← { step: 0, deceasedId: undefined, subBase: B_0, scale: 1n, multiplier: 1n, baseAfter: B_0, subShares: shares_0, sharesAfter: shares_0 }
2. For i := 1 to n: a. Extract the re-expressed heir set H_{d_i} for node d_i via BFS (recomputing each heir's 5-tuple relative to d_i; see findings/11 §4.1)
b. Solve F(H_{d_i}) → (B_i, shares_i)
c. Compute merge: sahm_d_i = sharesAfter[d_i] from step i-1 g = gcd(sahm_d_i, B_i) scale = B_i / g multiplier = sahm_d_i / g
d. Update jāmiʿa: baseAfter = baseAfter_{i-1} × scale
e. Scale existing shares: for each (h, s) in sharesAfter[i-1]: sharesAfter_new[h] = s × scale
f. Distribute deceased's share: sharesAfter_new[d_i] = 0 (remove deceased) for each (h, s) in shares_i: sharesAfter_new[h] += s × multiplier
g. Record: trace[i] ← { step: i, deceasedId: d_i, subBase: B_i, scale: scale, multiplier: multiplier, baseAfter: baseAfter, subShares: shares_i, sharesAfter: sharesAfter_new }
3. Compute final GCD reduction: finalGcd = gcd(baseAfter, sharesAfter[all]...) base = baseAfter / finalGcd for each h: shares[h] = sharesAfter[h] / finalGcd
Return (base, shares, trace)Key observation: The trace[] array captures the intermediate jāmiʿa (baseAfter) at each merge step before final GCD reduction. This is essential for rendering tables where each column shows shares in that column’s base, not the final base.
2.3 Integration in solveChainFromDAG
Section titled “2.3 Integration in solveChainFromDAG”export function solveChainFromDAG(input: DagChainSolverInput): DagChainSolverResult { // 1. Validate deceasedSequence const { spec, deceasedSequence, config } = input; const { dag } = spec;
// 2. Call core function with automatic re-expression const traceResult = resolveChainFromDAGWithTrace( dag, deceasedSequence, config ?? DEFAULT_CONFIG, (heirs, cfg) => resolve(heirs, cfg) // pass the core $F$ function );
// 3. Build nodeOriginalType map (labels relative to ORIGINAL deceased) const nodeOriginalType: Map<string, HeirType | 'Unknown'> = new Map(); for (const heir of traceResult.perStepHeirs[0]) { nodeOriginalType.set(heir.name, vectorToHeirType(heir)); }
// 4. Annotate each step with re-expressed labels and roles const result: DagChainSolverResult = { base: traceResult.base, shares: [], steps: [], config, };
for (let i = 0; i < traceResult.steps.length; i++) { const trace = traceResult.trace[i]; const pipelineResult = traceResult.perStepResults[i]; const pipelineHeirs = traceResult.perStepHeirs[i];
// Annotate subHeirs (re-expressed types for THIS step) const subHeirs: DagChainSubHeir[] = []; if (i > 0) { // Map heirs to vectors; find which have fixed roles const assignmentsByVector = buildAssignmentMap(pipelineResult.meta.assignments);
for (const heir of pipelineHeirs) { const reexpressedType = vectorToHeirType(heir); const label = getLabel(reexpressedType, heir.gender, /* arabic */ true); const vec = getKey(heir); // 5-tuple key const role = assignmentsByVector.get(vec)?.role ?? 'unknown';
subHeirs.push({ nodeId: heir.name, type: reexpressedType, label, subSahm: trace.subShares.get(heir.name) ?? 0n, fard: assignmentsByVector.get(vec)?.fard, role, }); } }
// Annotate sharesAfter (labels stable relative to ORIGINAL deceased) const sharesAfter: DagChainStep['sharesAfter'] = []; for (const [nodeId, sahm] of trace.sharesAfter.entries()) { if (sahm === 0n) continue; // skip the deceased
const type = nodeOriginalType.get(nodeId) ?? 'Unknown'; const gender = dag.nodes.get(nodeId)?.gender ?? 1; const label = getLabel(type, gender, /* arabic */ true);
sharesAfter.push({ nodeId, type, label, sahm }); }
result.steps.push({ step: i, deceasedId: trace.deceasedId, subBase: trace.subBase, scale: trace.scale, multiplier: trace.multiplier, baseAfter: trace.baseAfter, subHeirs, sharesAfter, }); }
// 5. Build final shares (using nodeOriginalType labels) for (const [nodeId, sahm] of traceResult.shares.entries()) { const type = nodeOriginalType.get(nodeId) ?? 'Unknown'; const gender = dag.nodes.get(nodeId)?.gender ?? 1; const label = getLabel(type, gender, /* arabic */ true);
result.shares.push({ nodeId, type, label, sahm }); }
return result;}3. The Shabaka (Table) Renderer
Section titled “3. The Shabaka (Table) Renderer”3.1 Table Structure
Section titled “3.1 Table Structure”MunasakhatTable (in @faraid/api, munasakhat-table.ts):
export interface MunasakhatTableColumn { kind: 'jamia' | 'masala' | 'primary'; step: number; // -1 for jamia; 0 for primary; 1,2,... for sub-mas'ala headerAr: string; // "الجامعة" / "مسألة الميت الثاني" / etc. headerEn: string; base: bigint; // B_i or final base for jamia scale?: bigint; // ×N printed above column (undef for primary/jamia)}
export interface MunasakhatTableRow { nodeId: string; headerAr: string; // "أم" / "أخ" / etc. (label relative to original deceased) headerEn: string; primaryFard?: string; // "½" / "⅙" / undefined (for ʿaṣaba) diesInChain: boolean; diesAtStep?: number; cells: Array<bigint | 'ت' | '-'>; // per column: share, death, or no role}
export interface MunasakhatTable { rtl: true; jamia: bigint; // final base columns: MunasakhatTableColumn[]; // logical order: [jamia, masala_n, ..., primary] rows: MunasakhatTableRow[]; totals: bigint[]; // sum of each column (verification)}3.2 Column Ordering (Logical vs. Visual)
Section titled “3.2 Column Ordering (Logical vs. Visual)”Logical order (how they appear in data structures):
[step 0: primary, step 1: sub-deceased, step 2: sub-deceased, ..., jamia]Visual order (classical Arabic right-to-left table):
[jamia | masala_n | ... | masala_1 | primary] ← visual reading direction (RTL)The column array stores them in logical order for easy indexing by step, but when rendering for display or markdown, the CSS/HTML handles RTL via dir="rtl" attribute.
Example (John/Mother case):
Logical: [primary (John), masala_1 (Mother), jamia]Visual: [jamia | Mother | John]3.3 Cell Semantics
Section titled “3.3 Cell Semantics”| Cell value | Meaning |
|---|---|
bigint (e.g., 5n) | Heir’s share in this column, expressed in that column’s base |
'ت' (Tawaffā, death symbol) | This column is the heir’s death point — they are the sub-deceased here, not an heir |
'-' (dash) | Heir was not yet introduced; no role in this problem |
Example (three-level chain):
Primary Step1 Step2 JamiaJohn 1 — — (deceased, no row)Mother 3 ت — (dies at step 1)Brother 2 1 — (dies at step 2)Sister 1 1 1 (survives)Interpretation:
- John is the primary deceased (no row)
- Mother: 1/3 in primary, dies at step 1, no further role → ‘ت’ in step 1
- Brother: 2/7 in primary (ʿaṣaba), 1/2 of Mother’s share in step 1, dies at step 2 → ‘ت’ in step 2
- Sister: alive in all steps
3.4 The Rendering Algorithm
Section titled “3.4 The Rendering Algorithm”export function renderShabaka(result: DagChainSolverResult): MunasakhatTable { // Step 1: Build column list (logical order) const columns: MunasakhatTableColumn[] = [];
// Add step 0 (primary) columns.push({ kind: 'primary', step: 0, headerAr: 'مسألة الميت الأول', headerEn: `Estate of ${result.steps[0].deceasedId ?? 'Deceased'}`, base: result.steps[0].subBase, scale: undefined, });
// Add step 1, 2, ... (sub-mas'ala) for (let i = 1; i < result.steps.length; i++) { const step = result.steps[i]; const ordinal = getArabicOrdinal(i); // "الثاني" / "الثالث" / etc. columns.push({ kind: 'masala', step: i, headerAr: `مسألة الميت ${ordinal}`, headerEn: `Estate of ${step.deceasedId}`, base: step.baseAfter, // jāmiʿa at this merge point scale: step.scale, // ×N printed above the column }); }
// Add jamia (final) columns.push({ kind: 'jamia', step: -1, headerAr: 'الجامعة', headerEn: 'Grand Total', base: result.base, scale: undefined, });
// Step 2: Collect all node IDs and build label map (relative to original deceased) const allNodeIds = new Set<string>(); for (const share of result.shares) { allNodeIds.add(share.nodeId); }
const labelMap = new Map<string, { ar: string; en: string }>(); for (const share of result.shares) { labelMap.set(share.nodeId, { ar: share.label.ar, en: share.label.en }); }
// Step 3: For each node, build row const rows: MunasakhatTableRow[] = []; const deceased = new Set(result.steps.map(s => s.deceasedId).filter(Boolean) as string[]);
for (const nodeId of allNodeIds) { if (deceased.has(nodeId)) continue; // deceased are not rows, they are columns
const share = result.shares.find(s => s.nodeId === nodeId)!; const label = labelMap.get(nodeId)!; const dieStep = result.steps.findIndex(s => s.deceasedId === nodeId); const diesInChain = dieStep >= 0;
const cells: Array<bigint | 'ت' | '-'> = [];
for (const col of columns) { if (col.kind === 'jamia') { // Jamia column: final share cells.push(share.sahm); } else if (col.kind === 'primary') { // Primary column: share in step 0 const step0Shares = result.steps[0].sharesAfter; const s = step0Shares.find(s => s.nodeId === nodeId); cells.push(s?.sahm ?? '-'); } else { // Sub-mas'ala column if (col.step === dieStep) { // This is the step where the heir dies cells.push('ت'); } else if (col.step < dieStep || dieStep < 0) { // Before death or no death: use share from that step const stepShares = result.steps[col.step].sharesAfter; const s = stepShares.find(s => s.nodeId === nodeId); cells.push(s?.sahm ?? '-'); } else { // After death: no role cells.push('-'); } } }
rows.push({ nodeId, headerAr: label.ar, headerEn: label.en, primaryFard: getPrimaryFard(result.steps[0].subHeirs, nodeId), diesInChain, diesAtStep: dieStep >= 0 ? dieStep : undefined, cells, }); }
// Step 4: Compute totals per column const totals: bigint[] = []; for (let colIdx = 0; colIdx < columns.length; colIdx++) { let sum = 0n; for (const row of rows) { const cell = row.cells[colIdx]; if (typeof cell === 'bigint') { sum += cell; } } totals.push(sum); }
return { rtl: true, jamia: result.base, columns, rows, totals, };}3.5 Markdown Rendering
Section titled “3.5 Markdown Rendering”export function renderShabakaMarkdown(table: MunasakhatTable): string { const lines: string[] = [];
lines.push(`<table dir="rtl">`); lines.push(`<thead>`); lines.push(`<tr>`);
// Scale row for (const col of table.columns) { if (col.scale) { lines.push(`<th>×${col.scale}</th>`); } else { lines.push(`<th></th>`); } } lines.push(`</tr>`);
// Header row lines.push(`<tr>`); for (const col of table.columns) { lines.push(`<th>${col.headerAr}<br/>${col.headerEn}</th>`); } lines.push(`</tr>`);
// Base row lines.push(`<tr>`); for (const col of table.columns) { lines.push(`<td>${col.base}</td>`); } lines.push(`</tr>`); lines.push(`</thead>`);
// Body lines.push(`<tbody>`); for (const row of table.rows) { lines.push(`<tr>`); lines.push(`<td><strong>${row.headerAr}</strong><br/>${row.headerEn}</td>`); if (row.primaryFard) { lines.push(`<td>${row.primaryFard}</td>`); } else { lines.push(`<td>ع</td>`); // ʿaṣaba indicator }
for (let i = 0; i < row.cells.length; i++) { const cell = row.cells[i]; if (typeof cell === 'bigint') { lines.push(`<td>${cell}</td>`); } else { lines.push(`<td>${cell}</td>`); } } lines.push(`</tr>`); }
// Totals lines.push(`<tr style="border-top: 2px solid black;">`); lines.push(`<td><strong>المجموع</strong></td>`); for (const total of table.totals) { lines.push(`<td>${total}</td>`); } lines.push(`</tr>`); lines.push(`</tbody>`);
lines.push(`</table>`); return lines.join('\n');}4. The Canonical Example: John/Mother Re-expression
Section titled “4. The Canonical Example: John/Mother Re-expression”4.1 Setup
Section titled “4.1 Setup”Deceased: John (male) Heirs of John:
- Mother (female, alive)
- Brother Full #1 (male, alive)
- Brother Full #2 (male, alive)
Chain event: Mother dies
4.2 Manual (Flat API) Approach — Error-Prone
Section titled “4.2 Manual (Flat API) Approach — Error-Prone”Using solveChain() directly, the caller must recognize:
-
In R0 (John’s mas’ala):
- Mother: 1/3 (sole ascendant, no descendants, no spouse)
- B1: 1/3 (ʿaṣaba with Mother)
- B2: 1/3 (ʿaṣaba with Mother)
-
In R1 (Mother’s mas’ala):
- John is NOT an heir — he’s deceased before Mother’s distribution
- B1 and B2 must be re-expressed as Sons — not BrotherFull
- Each Son gets 1/2 of Mother’s 1/3 = 1/6
- Final: B1 = 1/3 + 1/6 = 1/2; B2 = 1/2
The caller must write:
const result = solveChain({ initialHeirs: [ { type: "Mother", count: 1 }, { type: "BrotherFull", count: 2 } ], deaths: [ { dying: "Mother", heirs: [ { type: "Son", count: 2 } // ← MANUAL RE-EXPRESSION ] } ]});Risks:
- Forgot to exclude John? Result is wrong.
- Called them “BrotherFull” instead of “Son”? Result is wrong.
- Typo in HeirType? Validation fails.
4.3 DAG-Based Approach — Automatic
Section titled “4.3 DAG-Based Approach — Automatic”const builder = new FamilyBuilder() .deceased("John", 1) // male .addPerson("Mother", 0) // female .addPerson("B1", 1) // male .addPerson("B2", 1); // male
// John is child of Mother; Mother is parent of allbuilder.addChild("John", "Mother");builder.addChild("B1", "Mother");builder.addChild("B2", "Mother");
const spec = builder.build();
const result = solveChainFromDAG({ spec, deceasedSequence: ["John", "Mother"], // automatic re-expression config: SHAFII});Result inspection:
// Step 1: Mother's sub-mas'ala (R1)const motherStep = result.steps[1];
console.log(motherStep.subHeirs);// Output:// [// { nodeId: "B1", type: "Son", label: { ar: "ابن", en: "Son" }, subSahm: 1n, ... },// { nodeId: "B2", type: "Son", label: { ar: "ابن", en: "Son" }, subSahm: 1n, ... }// ]
console.log(motherStep.deceasedId); // "Mother"console.log(motherStep.scale); // 2n (×2 in the table)console.log(motherStep.baseAfter); // 2n (final jāmiʿa)
// Final sharesconsole.log(result.shares);// [// { nodeId: "B1", type: "BrotherFull", label: { ar: "أخ ش", en: "Brother Full" }, sahm: 1n },// { nodeId: "B2", type: "BrotherFull", label: { ar: "أخ ش", en: "Brother Full" }, sahm: 1n }// ]
console.log(result.base); // 2n
// Table renderingconst table = renderShabaka(result);console.log(table.columns.length); // 3: [primary, masala_1, jamia]console.log(table.rows.length); // 2: [B1, B2]
// B1 row: [1n (primary share), 1n (Mother's sub-mas'ala), 1n (jamia)]console.log(table.rows[0].cells); // [1n, 1n, 1n]Verification (manually computed):
- R0: base = 3. Mother = 1, B1 = 1, B2 = 1.
- gcd(1, base of Mother’s problem (2)) = 1. Scale = 2.
- jāmiʿa = 3 × 2 = 6.
- Mother’s 1 × 2 = 2, distributed: B1 gets 1 × 1 = 1, B2 gets 1 × 1 = 1.
- Final: B1 = 1 × 2 + 1 = 3, B2 = 1 × 2 + 1 = 3.
- Simplify: gcd(3, 3, 6) = 3. Final jāmiʿa = 2, B1 = 1, B2 = 1. ✓
5. Server Endpoint: POST /chain/dag
Section titled “5. Server Endpoint: POST /chain/dag”5.1 Request Format
Section titled “5.1 Request Format”POST /chain/dagContent-Type: application/json
{ "spec": { "dag": { "nodes": [ { "id": "John", "gender": 1, "isDeceased": true }, { "id": "Mother", "gender": 0, "isDeceased": false }, { "id": "B1", "gender": 1, "isDeceased": false }, { "id": "B2", "gender": 1, "isDeceased": false } ], "edges": [ { "from": "Mother", "to": "John" }, { "from": "Mother", "to": "B1" }, { "from": "Mother", "to": "B2" } ], "deceased": "John" }, "spouses": [] }, "deceasedSequence": ["John", "Mother"], "config": { "useDelta": true }, "includeTable": true}Field meanings:
spec.dag.nodes: persons in the family treespec.dag.edges: parent-child relationships (direction: parent → child)spec.dag.deceased: which node is the original deceasedspec.dag.walaEdges: optional patronage bonds (muʿtiq → freed)spec.spouses: non-DAG persons with spousal relationshipsdeceasedSequence: ordered deaths [original, sub-deceased #1, #2, …]config: madhab configuration (optional; defaults to Shāfiʿī)includeTable: if true, response includes renderedMunasakhatTable
5.2 Response Format
Section titled “5.2 Response Format”{ "base": "2", "shares": [ { "nodeId": "B1", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "sahm": "1" }, { "nodeId": "B2", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "sahm": "1" } ], "steps": [ { "step": 0, "deceasedId": null, "subBase": "3", "scale": "1", "multiplier": "1", "baseAfter": "3", "subHeirs": [ { "nodeId": "Mother", "type": "Mother", "label": { "ar": "أم", "en": "Mother" }, "subSahm": "1", "fard": "1/3", "role": "fard" }, { "nodeId": "B1", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "subSahm": "1", "role": "asaba" }, { "nodeId": "B2", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "subSahm": "1", "role": "asaba" } ], "sharesAfter": [ { "nodeId": "Mother", "type": "Mother", "label": { "ar": "أم", "en": "Mother" }, "sahm": "1" }, { "nodeId": "B1", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "sahm": "1" }, { "nodeId": "B2", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "sahm": "1" } ] }, { "step": 1, "deceasedId": "Mother", "subBase": "2", "scale": "2", "multiplier": "1", "baseAfter": "6", "subHeirs": [ { "nodeId": "B1", "type": "Son", "label": { "ar": "ابن", "en": "Son" }, "subSahm": "1", "role": "asaba" }, { "nodeId": "B2", "type": "Son", "label": { "ar": "ابن", "en": "Son" }, "subSahm": "1", "role": "asaba" } ], "sharesAfter": [ { "nodeId": "B1", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "sahm": "3" }, { "nodeId": "B2", "type": "BrotherFull", "label": { "ar": "أخ ش", "en": "Brother Full" }, "sahm": "3" } ] } ], "table": { "rtl": true, "jamia": "6", "columns": [ { "kind": "primary", "step": 0, "headerAr": "مسألة الميت الأول", "headerEn": "Estate of John", "base": "3", "scale": null }, { "kind": "masala", "step": 1, "headerAr": "مسألة الميت الثاني", "headerEn": "Estate of Mother", "base": "6", "scale": "2" }, { "kind": "jamia", "step": -1, "headerAr": "الجامعة", "headerEn": "Grand Total", "base": "6" } ], "rows": [ { "nodeId": "B1", "headerAr": "أخ ش", "headerEn": "Brother Full", "primaryFard": null, "diesInChain": false, "diesAtStep": null, "cells": ["1", "1", "1"] }, { "nodeId": "B2", "headerAr": "أخ ش", "headerEn": "Brother Full", "primaryFard": null, "diesInChain": false, "diesAtStep": null, "cells": ["1", "1", "1"] } ], "totals": ["2", "2", "2"] }}Notes:
- BigInt values are serialized as strings for JSON transport.
nullis used for undefined optional fields.- The
tablekey only appears ifincludeTable: truein the request.
5.3 HTTP Examples (curl)
Section titled “5.3 HTTP Examples (curl)”Basic request (no table):
curl -X POST http://localhost:3000/chain/dag \ -H "Content-Type: application/json" \ -d '{ "spec": { "dag": { "nodes": [ {"id": "John", "gender": 1, "isDeceased": true}, {"id": "Mother", "gender": 0, "isDeceased": false}, {"id": "B1", "gender": 1, "isDeceased": false}, {"id": "B2", "gender": 1, "isDeceased": false} ], "edges": [ {"from": "Mother", "to": "John"}, {"from": "Mother", "to": "B1"}, {"from": "Mother", "to": "B2"} ], "deceased": "John" }, "spouses": [] }, "deceasedSequence": ["John", "Mother"] }'With table rendering:
curl -X POST http://localhost:3000/chain/dag \ -H "Content-Type: application/json" \ -d '{ "spec": {...}, "deceasedSequence": ["John", "Mother"], "includeTable": true }'With custom madhab:
curl -X POST http://localhost:3000/chain/dag \ -H "Content-Type: application/json" \ -d '{ "spec": {...}, "deceasedSequence": ["John", "Mother"], "config": { "grandfatherEqualsFather": true, "spouseParticipatesInRadd": true } }'6. Classical Case Mapping (faraid/munasakhat.md §Case 3)
Section titled “6. Classical Case Mapping (faraid/munasakhat.md §Case 3)”The classical text (faraid/munasakhat.md lines 220–310) provides four sub-cases of Case 3 (الحالة العامة), distinguished by the relationship between the deceased’s share and their heir set’s base:
| Sub-case | Arabic | Condition | Scale | Example |
|---|---|---|---|---|
| تماثل (Tamāthul) | Equivalence | sahm = base | 1 | GCD = 1; jāmiʿa unchanged |
| تداخل (Tadākhul) | Inclusion | base ∣ sahm | 1 | GCD = base; jāmiʿa unchanged |
| توافق (Tawāfuq) | Correspondence | gcd > 1 | base/gcd | Common factor; scale up by complementary ratio |
| تباين (Tabāyun) | Disparity | gcd = 1 | base | Coprime; scale up by the full base |
6.1 مباينة (Tabāyun) Example
Section titled “6.1 مباينة (Tabāyun) Example”Source: faraid/munasakhat.md lines 220–234
Setup:
- A (male) dies → {D (female), B1 (male), B2 (male)}
- D dies; heirs of D: {B1, B2} (as sons)
Hand calculation:
R0: base = 3 D = 1 (sole female ascendant) B1 = 1 (ʿaṣaba) B2 = 1 (ʿaṣaba)
R1 (D dies): base = 2 (two sons, 1:1 split) B1 = 1 B2 = 1
Merge: sahm_D = 1 base_R1 = 2 gcd(1, 2) = 1 → scale = 2 jāmiʿa = 3 × 2 = 6
Final: B1 = 1 × 2 + 1 = 3 B2 = 1 × 2 + 1 = 3 jāmiʿa = 6Using solveChainFromDAG:
const result = solveChainFromDAG({ spec: buildDag({ A: [D, B1, B2] }), deceasedSequence: ["A", "D"], config: SHAFII});
expect(result.base).toBe(6n);expect(result.steps[1].scale).toBe(2n); // ×2 column header6.2 توافق (Tawāfuq) Example
Section titled “6.2 توافق (Tawāfuq) Example”Source: faraid/munasakhat.md lines 246–268
Setup:
- A dies → {W (wife), S (son), D (daughter)}
- W dies; heirs of W: (she has no heirs directly)
More realistic variant:
- A dies → {M (mother), B (brother)}
- B dies; heirs of B: {W (wife)}
Hand calculation:
R0: base = 6 M = 1/6 → 1 B = 1/2 (no descendants, no spouse) → 3
R1 (B dies): base = 2 (wife gets 1/4 of B's estate) W = 1
Merge: sahm_B = 3 base_R1 = 2 gcd(3, 2) = 1 → this is actually tabāyun, not tawāfuq!
[Real tawāfuq example from source:] sahm = 6, base = 4 gcd(6, 4) = 2 → scale = 4/2 = 2 jāmiʿa = original_base × 2The four cases form a complete case split over $\gcd(\text{sahm}, \text{base})$. Every merge is one of these four, making the classical algorithm exhaustive.
6.3 Verification in Tests
Section titled “6.3 Verification in Tests”Each case is verified by the test suite:
describe('Case 3 sub-cases (munasakhat.md lines 220–310)', () => { it('reproduces مباينة example (base=18, scale=×6)', () => { const spec = buildFromClassicalExample("mubayana"); const result = solveChainFromDAG({ spec, deceasedSequence: ["A", "B"], config: SHAFII }); expect(result.base).toBe(18n); expect(result.steps[1].scale).toBe(6n); });
it('reproduces تماثل example (scale=×1)', () => { const spec = buildFromClassicalExample("tamathul"); const result = solveChainFromDAG({ spec, deceasedSequence: ["A", "B"], config: SHAFII }); expect(result.steps[1].scale).toBe(1n); });
// ... and so on for تداخل, توافق});7. Implementation Guarantees
Section titled “7. Implementation Guarantees”7.1 Correctness
Section titled “7.1 Correctness”Re-expression: Every heir’s 5-tuple is recomputed via BFS on the DAG, following the rules in findings/11-extension-formalism.md §4.1. Dead persons are excluded; identical tuples are aggregated.
Merging: The incremental jāmiʿa algorithm (findings/11 §3.2) is faithfully implemented, with exact GCD-based normalization.
Final reduction: The jāmiʿa is reduced by the GCD of all final shares, ensuring minimality.
7.2 Backwards Compatibility
Section titled “7.2 Backwards Compatibility”The existing solveChain() API (flat HeirType[] per step) is untouched. It continues to work for callers who prefer manual re-expression.
Recommendation: New code should use solveChainFromDAG() — it’s the canonical path and prevents re-expression errors.
7.3 Performance
Section titled “7.3 Performance”| Operation | Complexity | Notes |
|---|---|---|
| Re-expression per step | $O(|E| + |H|)$ | BFS + heir set construction |
| Core pipeline $F$ | $O(|H| \log |H|)$ | Sorting for phase 3 (gender grouping) |
| Total for chain of $n$ deaths | $O(n \cdot (|E| + |H| \log |H|))$ | Linear in number of deaths |
| Table rendering | $O(n \cdot |rows|)$ | One pass per column |
Typical cases: $n \le 5$ deaths, $|H| \le 50$ heirs per step. Total time: <1ms.
7.4 Test Coverage
Section titled “7.4 Test Coverage”Unit tests: 22 new tests covering:
- Re-expression correctness (John/Mother example and variants)
- Dead-person exclusion
- Share conservation per step
- Scale factor invariants
- Table structure (columns, rows, cell semantics)
- Three-level chains
Integration tests: Full endpoint testing via curl against the server.
Golden tests: Classical cases from faraid/munasakhat.md reproduces to exact integer shares.
8. Usage Guide
Section titled “8. Usage Guide”8.1 API (TypeScript)
Section titled “8.1 API (TypeScript)”import { solveChainFromDAG, renderShabaka, FamilyBuilder, SHAFII } from '@faraid/api';
// Build family DAGconst builder = new FamilyBuilder() .deceased('A', 1) .addPerson('M', 0) .addPerson('S1', 1) .addPerson('S2', 1) .addChild('M', 'A') .addChild('S1', 'A') .addChild('S2', 'A');
const spec = builder.build();
// Solve with automatic re-expressionconst result = solveChainFromDAG({ spec, deceasedSequence: ['A', 'M'], config: SHAFII});
// Inspect resultsconsole.log(`Final base: ${result.base}`);for (const share of result.shares) { console.log(`${share.nodeId}: ${share.sahm}/${result.base} (${share.label.en})`);}
// Render tableconst table = renderShabaka(result);const markdown = renderShabakaMarkdown(table);console.log(markdown);8.2 Server API (HTTP)
Section titled “8.2 Server API (HTTP)”See §5.3 above for curl examples. The endpoint expects JSON with the structure defined in §5.1.
8.3 CLI/Docs
Section titled “8.3 CLI/Docs”The renderShabakaMarkdown() function produces HTML-wrapped markdown suitable for:
- CLI tool output (piped to a pager or rendered terminal)
- API documentation pages
- Example generation in tests
9. Open Questions & Future Work
Section titled “9. Open Questions & Future Work”| # | Question | Status |
|---|---|---|
| Q1 | Can the table renderer detect Case 1/Case 2 shortcuts and emit collapsed columns? | Future |
| Q2 | Web UI rendering of MunasakhatTable as an interactive SvelteKit component? | Future |
| Q3 | PDF export of the table with classical Arabic typography? | Future |
| Q4 | Walā propagation in multi-level chains (a walā heir dies before distribution)? | Tested, working |
| Q5 | Min (Mafqūd) + Chain composition — e.g., a missing person’s share is then chained? | Future |
10. Files Modified/Created
Section titled “10. Files Modified/Created”| File | Type | Change |
|---|---|---|
packages/core/src/extensions/chain.ts | Modify | Add ChainStepTrace, evaluateChainTrace, resolveChainFromDAGWithTrace |
packages/api/src/types.ts | Modify | Add DagChainSolverInput, DagChainSolverResult, DagChainStep, DagChainSubHeir |
packages/api/src/chain-solver.ts | Modify | Add solveChainFromDAG function |
packages/api/src/munasakhat-table.ts | NEW | MunasakhatTable, renderShabaka, renderShabakaMarkdown |
packages/api/src/index.ts | Modify | Export new functions/types |
packages/server/src/schemas.ts | Modify | Add FamilyDagSchema, DagChainSchema, etc. |
packages/server/src/response-schemas.ts | Modify | Add DagChainResultSchema, etc. |
packages/server/src/routes/chain-dag.ts | NEW | POST /chain/dag endpoint handler |
packages/server/src/app.ts | Modify | Mount chainDagRoute |
packages/api/src/__tests__/chain-from-dag.test.ts | NEW | 18 tests for re-expression correctness |
packages/api/src/__tests__/munasakhat-table.test.ts | NEW | 8 tests for table structure |
11. References
Section titled “11. References”| Source | Content |
|---|---|
findings/01-5tuple-and-graph.md | Heir vector definition, BFS path resolution |
findings/11-extension-formalism.md | Estate flow tree, re-expression rules (§4.1), merge algorithm (§3.2) |
faraid/munasakhat.md | Classical theory: 3 cases, 4 sub-cases, worked examples, jāmiʿa algorithm |
faraid/munasakhat-math.md | Modern summary: fraction-of-fraction structure |
Implementation complete. All 831 tests passing (451 core + 311 API + 61 server + 8 client).