deepline 0.3.258 → 0.3.260
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundling-sources/sdk/src/api-error.ts +45 -0
- package/dist/bundling-sources/sdk/src/client.ts +98 -459
- package/dist/bundling-sources/sdk/src/http.ts +1 -34
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
- package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
- package/dist/bundling-sources/sdk/src/types.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
- package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
- package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
- package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-crash-details.ts +95 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +13 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
- package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
- package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
- package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
- package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
- package/dist/cli/index.js +4957 -771
- package/dist/cli/index.mjs +4957 -771
- package/dist/client-B5XbKrrs.d.mts +6564 -0
- package/dist/client-C9Y49yya.d.ts +6564 -0
- package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
- package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
- package/dist/index.d.mts +7 -6275
- package/dist/index.d.ts +7 -6275
- package/dist/index.js +360 -326
- package/dist/index.mjs +360 -326
- package/dist/install-integrity.json +22 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +117 -18
- package/dist/plays/csv-data-input.d.mts +37 -0
- package/dist/plays/csv-data-input.d.ts +37 -0
- package/dist/plays/csv-data-input.js +3135 -0
- package/dist/plays/csv-data-input.mjs +3108 -0
- package/dist/plays/run-request.d.mts +6 -0
- package/dist/plays/run-request.d.ts +6 -0
- package/dist/plays/run-request.js +664 -0
- package/dist/plays/run-request.mjs +614 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +12 -1
|
@@ -1444,34 +1444,116 @@ function sourceLoaderForPath(path: string): Loader {
|
|
|
1444
1444
|
return 'ts';
|
|
1445
1445
|
}
|
|
1446
1446
|
|
|
1447
|
-
function
|
|
1447
|
+
function isFunctionNode(node: AstNode | null | undefined): node is AstNode {
|
|
1448
|
+
return (
|
|
1449
|
+
!!node &&
|
|
1450
|
+
(node.type === 'ArrowFunctionExpression' ||
|
|
1451
|
+
node.type === 'FunctionExpression')
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* The run function a definePlay call declares inline: the positional callback
|
|
1457
|
+
* of `definePlay(name, run, options?)`, or the `run` property (arrow, function
|
|
1458
|
+
* expression, or method shorthand) of the object form `definePlay({ id, run })`.
|
|
1459
|
+
*/
|
|
1460
|
+
function definePlayRunFunction(call: AstNode): AstNode | null {
|
|
1448
1461
|
if (!isDefinePlayCallExpression(call)) return null;
|
|
1449
1462
|
const args = astArray(call.arguments);
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1463
|
+
const positional = args.find((argument) => isFunctionNode(argument));
|
|
1464
|
+
if (positional) return positional;
|
|
1465
|
+
const config = unwrapStaticExpression(args[0] ?? null);
|
|
1466
|
+
if (!config || config.type !== 'ObjectExpression') return null;
|
|
1467
|
+
let run: AstNode | null = null;
|
|
1468
|
+
for (const property of astArray(config.properties)) {
|
|
1469
|
+
if (property.type !== 'Property') continue;
|
|
1470
|
+
if (propertyNameFromKey(property) !== 'run') continue;
|
|
1471
|
+
const value = unwrapStaticExpression(
|
|
1472
|
+
isAstNode(property.value) ? property.value : null,
|
|
1473
|
+
);
|
|
1474
|
+
run = isFunctionNode(value) ? value : null;
|
|
1475
|
+
}
|
|
1476
|
+
return run;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** Context parameter name injected into a run function that declares none. */
|
|
1480
|
+
const DOCFLOW_INJECTED_CONTEXT_NAME = '__deeplineDocflowContext';
|
|
1481
|
+
|
|
1482
|
+
type DocflowContextResolution =
|
|
1483
|
+
/** The run function's first parameter is a plain identifier. */
|
|
1484
|
+
| { kind: 'named'; name: string }
|
|
1485
|
+
/** The run function declares no parameters; one is injected at `at`. */
|
|
1486
|
+
| { kind: 'inject'; name: string; at: number }
|
|
1487
|
+
/** The run function exists but its context cannot be addressed. */
|
|
1488
|
+
| { kind: 'unsupported'; reason: string };
|
|
1489
|
+
|
|
1490
|
+
function docflowContextFromCall(
|
|
1491
|
+
call: AstNode,
|
|
1492
|
+
sourceCode: string,
|
|
1493
|
+
): DocflowContextResolution | null {
|
|
1494
|
+
if (!isDefinePlayCallExpression(call)) return null;
|
|
1495
|
+
const run = definePlayRunFunction(call);
|
|
1496
|
+
if (!run) {
|
|
1497
|
+
return {
|
|
1498
|
+
kind: 'unsupported',
|
|
1499
|
+
reason:
|
|
1500
|
+
'its definePlay call does not declare the run function inline (pass `definePlay(name, async (ctx, input) => …)` or `definePlay({ id, run: async (ctx, input) => … })`)',
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const params = astArray(run.params);
|
|
1504
|
+
const first = params[0];
|
|
1505
|
+
if (first) {
|
|
1506
|
+
const name = getIdentifierName(first);
|
|
1507
|
+
return name
|
|
1508
|
+
? { kind: 'named', name }
|
|
1509
|
+
: {
|
|
1510
|
+
kind: 'unsupported',
|
|
1511
|
+
reason: `its run function's first parameter is not a plain identifier (found ${first.type}; declare the context as \`ctx\` and destructure inside the body)`,
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
const runBounds = astNodeBounds(run);
|
|
1515
|
+
const body = isAstNode(run.body) ? astNodeBounds(run.body) : null;
|
|
1516
|
+
if (!runBounds || !body) {
|
|
1517
|
+
return {
|
|
1518
|
+
kind: 'unsupported',
|
|
1519
|
+
reason: 'its run function has no source span',
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const signature = sourceCode.slice(runBounds.start, body.start);
|
|
1523
|
+
const emptyParams = /\(\s*\)/.exec(signature);
|
|
1524
|
+
if (!emptyParams) {
|
|
1525
|
+
return {
|
|
1526
|
+
kind: 'unsupported',
|
|
1527
|
+
reason: 'its parameterless run function has no `()` parameter list',
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
kind: 'inject',
|
|
1532
|
+
name: DOCFLOW_INJECTED_CONTEXT_NAME,
|
|
1533
|
+
at: runBounds.start + emptyParams.index + 1,
|
|
1534
|
+
};
|
|
1457
1535
|
}
|
|
1458
1536
|
|
|
1459
1537
|
/**
|
|
1460
1538
|
* Finds the context parameter for the definePlay call that owns one binding.
|
|
1461
1539
|
* A file may export several plays with different parameter names, so a single
|
|
1462
1540
|
* file-global context identifier is not safe for instrumentation.
|
|
1541
|
+
*
|
|
1542
|
+
* `null` means no definePlay call owns the binding: it sits in a module-level
|
|
1543
|
+
* helper the run function calls, which has no context in scope.
|
|
1463
1544
|
*/
|
|
1464
|
-
function
|
|
1545
|
+
function findDocflowContext(input: {
|
|
1465
1546
|
ast: AstNode;
|
|
1547
|
+
sourceCode: string;
|
|
1466
1548
|
statement: AstNode | null;
|
|
1467
1549
|
bindingLine: number;
|
|
1468
1550
|
lineStarts: readonly number[];
|
|
1469
1551
|
parents: ReadonlyMap<AstNode, AstNode>;
|
|
1470
|
-
}):
|
|
1552
|
+
}): DocflowContextResolution | null {
|
|
1471
1553
|
let owner: AstNode | null = input.statement;
|
|
1472
1554
|
while (owner) {
|
|
1473
|
-
const
|
|
1474
|
-
if (
|
|
1555
|
+
const context = docflowContextFromCall(owner, input.sourceCode);
|
|
1556
|
+
if (context) return context;
|
|
1475
1557
|
owner = input.parents.get(owner) ?? null;
|
|
1476
1558
|
}
|
|
1477
1559
|
|
|
@@ -1504,7 +1586,7 @@ function findDocflowContextIdentifier(input: {
|
|
|
1504
1586
|
else if (isAstNode(child)) pending.push(child);
|
|
1505
1587
|
}
|
|
1506
1588
|
}
|
|
1507
|
-
return best ?
|
|
1589
|
+
return best ? docflowContextFromCall(best, input.sourceCode) : null;
|
|
1508
1590
|
}
|
|
1509
1591
|
|
|
1510
1592
|
type DocflowInstrumentationReplacement = {
|
|
@@ -1530,6 +1612,14 @@ function docflowObservationTarget(
|
|
|
1530
1612
|
end: number;
|
|
1531
1613
|
awaitsResult: boolean;
|
|
1532
1614
|
expression: AstNode;
|
|
1615
|
+
/**
|
|
1616
|
+
* The statement is a `throw`: its node is observed FAILING, not completing.
|
|
1617
|
+
* The observed span is the thrown value, and the observation itself throws
|
|
1618
|
+
* it — so the node settles as failed with the authored error, and the outer
|
|
1619
|
+
* `throw` it replaces is never reached. A hit marker before the statement
|
|
1620
|
+
* used to record the node completed an instant before the run failed there.
|
|
1621
|
+
*/
|
|
1622
|
+
throws: boolean;
|
|
1533
1623
|
} | null {
|
|
1534
1624
|
if (!statement) return null;
|
|
1535
1625
|
let expression: AstNode | null = null;
|
|
@@ -1550,6 +1640,11 @@ function docflowObservationTarget(
|
|
|
1550
1640
|
expression = statement.expression;
|
|
1551
1641
|
} else if (statement.type === 'IfStatement' && isAstNode(statement.test)) {
|
|
1552
1642
|
expression = statement.test;
|
|
1643
|
+
} else if (
|
|
1644
|
+
statement.type === 'ThrowStatement' &&
|
|
1645
|
+
isAstNode(statement.argument)
|
|
1646
|
+
) {
|
|
1647
|
+
expression = statement.argument;
|
|
1553
1648
|
}
|
|
1554
1649
|
if (!expression) return null;
|
|
1555
1650
|
const bounds = astNodeBounds(expression);
|
|
@@ -1558,6 +1653,7 @@ function docflowObservationTarget(
|
|
|
1558
1653
|
...bounds,
|
|
1559
1654
|
awaitsResult: expressionContainsTopLevelAwait(expression),
|
|
1560
1655
|
expression,
|
|
1656
|
+
throws: statement.type === 'ThrowStatement',
|
|
1561
1657
|
};
|
|
1562
1658
|
}
|
|
1563
1659
|
|
|
@@ -1568,10 +1664,13 @@ function docflowObservedExpression(input: {
|
|
|
1568
1664
|
outputs: readonly string[];
|
|
1569
1665
|
expression: string;
|
|
1570
1666
|
awaitsResult: boolean;
|
|
1667
|
+
/** Observe the expression as the value a `throw` raises (see `throws`). */
|
|
1668
|
+
throws?: boolean;
|
|
1571
1669
|
}): string {
|
|
1572
|
-
const
|
|
1573
|
-
? `
|
|
1574
|
-
: `(
|
|
1670
|
+
const body = input.throws
|
|
1671
|
+
? `{throw (${input.expression})}`
|
|
1672
|
+
: `(${input.expression})`;
|
|
1673
|
+
const callback = input.awaitsResult ? `async()=>${body}` : `()=>${body}`;
|
|
1575
1674
|
const observation = `((runtimeContext,inputCaptures,execute)=>typeof runtimeContext.__deeplineObserveDocflowNode==="function"?runtimeContext.__deeplineObserveDocflowNode(${JSON.stringify(input.nodeId)},inputCaptures,${JSON.stringify(input.outputs)},execute):execute())(${input.contextName},${docflowInputCaptureSource(input.inputs)},${callback})`;
|
|
1576
1675
|
// Parenthesize the awaited form so it is self-delimiting at every source
|
|
1577
1676
|
// boundary, including compact `return{...}` syntax. This avoids relying on
|
|
@@ -1627,10 +1726,13 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1627
1726
|
start: number;
|
|
1628
1727
|
end: number;
|
|
1629
1728
|
awaitsResult: boolean;
|
|
1729
|
+
throws: boolean;
|
|
1630
1730
|
contextName: string;
|
|
1631
1731
|
binding: PlayDocflowBinding;
|
|
1632
1732
|
};
|
|
1633
1733
|
const targets: ObservationTarget[] = [];
|
|
1734
|
+
/** Parameterless run functions that receive an injected context param. */
|
|
1735
|
+
const injectedContextAt = new Set<number>();
|
|
1634
1736
|
|
|
1635
1737
|
// Build the parent index once so every binding's symbol resolution reuses it.
|
|
1636
1738
|
const parents = buildDocflowParentIndex(ast);
|
|
@@ -1639,14 +1741,27 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1639
1741
|
// to positional resolution. Instrumentation and `plays check` share this
|
|
1640
1742
|
// resolver, so the observed statement and the lint agree.
|
|
1641
1743
|
const resolution = resolveDocflowBinding(ast, binding, lineStarts, parents);
|
|
1642
|
-
const
|
|
1744
|
+
const context = findDocflowContext({
|
|
1643
1745
|
ast,
|
|
1746
|
+
sourceCode,
|
|
1644
1747
|
statement: resolution.statement,
|
|
1645
1748
|
bindingLine: binding.line,
|
|
1646
1749
|
lineStarts,
|
|
1647
1750
|
parents,
|
|
1648
1751
|
});
|
|
1649
|
-
|
|
1752
|
+
// No owning definePlay: the binding is in a module-level helper with no
|
|
1753
|
+
// context in scope, so it is observed through its caller's bindings.
|
|
1754
|
+
if (!context) continue;
|
|
1755
|
+
if (context.kind === 'unsupported') {
|
|
1756
|
+
throw new Error(
|
|
1757
|
+
`@mermaid-node ${binding.nodeId} (line ${binding.line}) cannot be observed at runtime: ${context.reason}.`,
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
1760
|
+
if (context.kind === 'inject') injectedContextAt.add(context.at);
|
|
1761
|
+
// An injected parameter is undefined when the run function is invoked
|
|
1762
|
+
// directly (outside the runtime), so its reads stay null-safe.
|
|
1763
|
+
const contextName =
|
|
1764
|
+
context.kind === 'inject' ? `(${context.name}??{})` : context.name;
|
|
1650
1765
|
const target = docflowObservationTarget(resolution.statement);
|
|
1651
1766
|
// A binding naming a `.step('<leg>', …)` leg of a waterfall is NOT
|
|
1652
1767
|
// instrumented, and the omission is deliberate rather than a gap.
|
|
@@ -1706,6 +1821,7 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1706
1821
|
start: number;
|
|
1707
1822
|
end: number;
|
|
1708
1823
|
awaitsResult: boolean;
|
|
1824
|
+
throws: boolean;
|
|
1709
1825
|
contextName: string;
|
|
1710
1826
|
bindings: PlayDocflowBinding[];
|
|
1711
1827
|
};
|
|
@@ -1719,6 +1835,7 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1719
1835
|
start: target.start,
|
|
1720
1836
|
end: target.end,
|
|
1721
1837
|
awaitsResult: target.awaitsResult,
|
|
1838
|
+
throws: target.throws,
|
|
1722
1839
|
contextName: target.contextName,
|
|
1723
1840
|
bindings: [target.binding],
|
|
1724
1841
|
});
|
|
@@ -1728,6 +1845,9 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1728
1845
|
for (const group of groups) {
|
|
1729
1846
|
// Wrap in reverse order so the first-listed annotation observes outermost.
|
|
1730
1847
|
let expression = sourceCode.slice(group.start, group.end);
|
|
1848
|
+
// Only the innermost observation raises a thrown value; every annotation
|
|
1849
|
+
// wrapped around it sees that raise as its own failure.
|
|
1850
|
+
let innermost = true;
|
|
1731
1851
|
for (const binding of [...group.bindings].reverse()) {
|
|
1732
1852
|
expression = docflowObservedExpression({
|
|
1733
1853
|
contextName: group.contextName,
|
|
@@ -1736,7 +1856,9 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1736
1856
|
outputs: binding.outputs ?? [],
|
|
1737
1857
|
expression,
|
|
1738
1858
|
awaitsResult: group.awaitsResult,
|
|
1859
|
+
throws: group.throws && innermost,
|
|
1739
1860
|
});
|
|
1861
|
+
innermost = false;
|
|
1740
1862
|
}
|
|
1741
1863
|
replacements.push({
|
|
1742
1864
|
start: group.start,
|
|
@@ -1745,6 +1867,14 @@ export function instrumentPlayDocflowRuntimeHits(sourceCode: string): string {
|
|
|
1745
1867
|
});
|
|
1746
1868
|
}
|
|
1747
1869
|
|
|
1870
|
+
for (const at of injectedContextAt) {
|
|
1871
|
+
replacements.push({
|
|
1872
|
+
start: at,
|
|
1873
|
+
end: at,
|
|
1874
|
+
value: DOCFLOW_INJECTED_CONTEXT_NAME,
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1748
1878
|
return replacements
|
|
1749
1879
|
.sort((left, right) => right.start - left.start)
|
|
1750
1880
|
.reduce(
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// A cascade's falloff on one run: how many rows reached each attempt, how many
|
|
2
|
+
// it answered, and how many were left for the next — derived from the run
|
|
3
|
+
// summary's leg-hit patterns (`SheetSummary.cascades`) and the cascade's
|
|
4
|
+
// declared attempt order.
|
|
5
|
+
//
|
|
6
|
+
// A row is answered by the FIRST attempt, in declared order, whose leg holds a
|
|
7
|
+
// value. That is the waterfall's meaning even when later attempts also ran
|
|
8
|
+
// (a `steps()` chain without `runIf` runs every leg), so a later attempt never
|
|
9
|
+
// gets credit for a row an earlier one already answered.
|
|
10
|
+
//
|
|
11
|
+
// PURE — shared by the dashboard and the CLI so both state the same numbers.
|
|
12
|
+
|
|
13
|
+
export type CascadeFillPattern = {
|
|
14
|
+
/** The leg columns that held a value on these rows. */
|
|
15
|
+
filled: readonly string[];
|
|
16
|
+
rows: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CascadeFalloffStep = {
|
|
20
|
+
leg: string;
|
|
21
|
+
/** Rows no earlier attempt answered. */
|
|
22
|
+
entering: number;
|
|
23
|
+
/** Rows this attempt answered first. */
|
|
24
|
+
hits: number;
|
|
25
|
+
/** Rows still unanswered after this attempt. */
|
|
26
|
+
remaining: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CascadeFalloff = {
|
|
30
|
+
/** The rows the cascade worked on. */
|
|
31
|
+
total: number;
|
|
32
|
+
steps: CascadeFalloffStep[];
|
|
33
|
+
/** Rows no attempt answered. */
|
|
34
|
+
noResult: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function cascadeFalloff(input: {
|
|
38
|
+
/** Leg columns in the cascade's declared order. */
|
|
39
|
+
legs: readonly string[];
|
|
40
|
+
patterns: readonly CascadeFillPattern[];
|
|
41
|
+
/** Rows the cascade worked on (the run's rows in its table). */
|
|
42
|
+
total: number;
|
|
43
|
+
}): CascadeFalloff {
|
|
44
|
+
const order = new Map(input.legs.map((leg, index) => [leg, index]));
|
|
45
|
+
const firstHits = input.legs.map(() => 0);
|
|
46
|
+
for (const pattern of input.patterns) {
|
|
47
|
+
let first = -1;
|
|
48
|
+
for (const leg of pattern.filled) {
|
|
49
|
+
const index = order.get(leg);
|
|
50
|
+
if (index !== undefined && (first === -1 || index < first)) first = index;
|
|
51
|
+
}
|
|
52
|
+
if (first !== -1) firstHits[first] += pattern.rows;
|
|
53
|
+
}
|
|
54
|
+
const answered = firstHits.reduce((sum, hits) => sum + hits, 0);
|
|
55
|
+
const total = Math.max(input.total, answered);
|
|
56
|
+
let entering = total;
|
|
57
|
+
const steps = input.legs.map((leg, index) => {
|
|
58
|
+
const hits = firstHits[index]!;
|
|
59
|
+
const step = { leg, entering, hits, remaining: entering - hits };
|
|
60
|
+
entering -= hits;
|
|
61
|
+
return step;
|
|
62
|
+
});
|
|
63
|
+
return { total, steps, noResult: total - answered };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `x%` — a count's share of `total`, never rounding a partial share up to a
|
|
67
|
+
* whole one or down to nothing. */
|
|
68
|
+
export function formatHitRate(count: number, total: number): string {
|
|
69
|
+
if (total <= 0) return '—';
|
|
70
|
+
const share = (count / total) * 100;
|
|
71
|
+
const percent =
|
|
72
|
+
share === 0 || share === 100
|
|
73
|
+
? share.toString()
|
|
74
|
+
: share >= 10
|
|
75
|
+
? Math.min(99, Math.round(share)).toString()
|
|
76
|
+
: share < 1
|
|
77
|
+
? '<1'
|
|
78
|
+
: share.toFixed(1).replace(/\.0$/, '');
|
|
79
|
+
return `${percent}%`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `n · x%` — a count and its share of `total`, for a dense surface. */
|
|
83
|
+
export function formatHitShare(count: number, total: number): string {
|
|
84
|
+
if (total <= 0) return count.toLocaleString('en-US');
|
|
85
|
+
return `${count.toLocaleString('en-US')} · ${formatHitRate(count, total)}`;
|
|
86
|
+
}
|