@critical-path/core 0.21.0 → 0.22.0
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/CHANGELOG.md +34 -0
- package/README.md +4 -0
- package/dist/domain/ddd.test.js +12 -0
- package/dist/domain/ddd.test.js.map +1 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +3 -2
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/errors.d.ts +13 -0
- package/dist/domain/errors.d.ts.map +1 -0
- package/dist/domain/errors.js +19 -0
- package/dist/domain/errors.js.map +1 -0
- package/dist/domain/events.d.ts +9 -1
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +1 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +72 -10
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +20 -0
- package/dist/index.test.js.map +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +5 -1
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +20 -0
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/fractional-index.d.ts.map +1 -1
- package/dist/utils/fractional-index.js +22 -26
- package/dist/utils/fractional-index.js.map +1 -1
- package/dist/utils/fractional-index.test.js +31 -0
- package/dist/utils/fractional-index.test.js.map +1 -1
- package/package.json +1 -5
- package/src/domain/ddd.test.ts +22 -0
- package/src/domain/entities.ts +3 -2
- package/src/domain/errors.ts +19 -0
- package/src/domain/events.ts +6 -0
- package/src/domain/index.ts +1 -0
- package/src/engine/index.ts +79 -11
- package/src/index.test.ts +24 -0
- package/src/store/sqlite.test.ts +22 -0
- package/src/store/sqlite.ts +7 -1
- package/src/types/index.ts +1 -0
- package/src/utils/fractional-index.test.ts +37 -0
- package/src/utils/fractional-index.ts +20 -26
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when input fails a domain rule. Route handlers map it to HTTP 400.
|
|
3
|
+
*/
|
|
4
|
+
export class ValidationError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ValidationError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when a referenced entity does not exist. Route handlers map it to HTTP 404.
|
|
13
|
+
*/
|
|
14
|
+
export class NotFoundError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'NotFoundError';
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/domain/events.ts
CHANGED
|
@@ -134,6 +134,11 @@ export interface ProjectUpdatedEvent extends DomainEvent<{ project: Project; pre
|
|
|
134
134
|
readonly aggregateType: 'Project';
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
export interface ProjectDeletedEvent extends DomainEvent<{ projectId: string; name: string; deletedTaskIds: string[] }> {
|
|
138
|
+
readonly name: 'project.deleted';
|
|
139
|
+
readonly aggregateType: 'Project';
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
export interface WorkflowCreatedEvent extends DomainEvent<{ workflow: Workflow }> {
|
|
138
143
|
readonly name: 'workflow.created';
|
|
139
144
|
readonly aggregateType: 'Workflow';
|
|
@@ -207,6 +212,7 @@ export type CriticalPathDomainEvent =
|
|
|
207
212
|
| TaskDependencyAddedEvent
|
|
208
213
|
| ProjectCreatedEvent
|
|
209
214
|
| ProjectUpdatedEvent
|
|
215
|
+
| ProjectDeletedEvent
|
|
210
216
|
| WorkflowCreatedEvent
|
|
211
217
|
| WorkflowUpdatedEvent
|
|
212
218
|
| WorkflowDeletedEvent
|
package/src/domain/index.ts
CHANGED
package/src/engine/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
TaskDeletedEvent,
|
|
57
57
|
ProjectCreatedEvent,
|
|
58
58
|
ProjectUpdatedEvent,
|
|
59
|
+
ProjectDeletedEvent,
|
|
59
60
|
WorkflowCreatedEvent,
|
|
60
61
|
WorkflowUpdatedEvent,
|
|
61
62
|
WorkflowDeletedEvent,
|
|
@@ -80,6 +81,7 @@ import {
|
|
|
80
81
|
import { validateAttachmentUrl, AttachmentValidationError } from '../domain/entities.js';
|
|
81
82
|
import { validateCustomFieldValues } from '../domain/custom-fields.js';
|
|
82
83
|
import { detectDependencyCycle, CircularDependencyError } from '../domain/graph.js';
|
|
84
|
+
import { ValidationError, NotFoundError } from '../domain/errors.js';
|
|
83
85
|
import { calculateCPM, type CPMOptions } from '../domain/cpm.js';
|
|
84
86
|
import { buildTimelineLadder, aggregateConcreteEvidenceForTask } from '../domain/ladder.js';
|
|
85
87
|
import {
|
|
@@ -340,6 +342,46 @@ export class CriticalPathEngine {
|
|
|
340
342
|
return updated;
|
|
341
343
|
}
|
|
342
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Deletes a project and its tasks. Each task goes through `deleteTask`, so plugin
|
|
347
|
+
* hooks run and `task.deleted` events fire before `project.deleted` is published.
|
|
348
|
+
*/
|
|
349
|
+
async deleteProject(id: string): Promise<boolean> {
|
|
350
|
+
const existing = await this.store.getProject(id);
|
|
351
|
+
if (!existing) return false;
|
|
352
|
+
|
|
353
|
+
const tasks = await this.store.getTasks(id);
|
|
354
|
+
const deletedTaskIds: string[] = [];
|
|
355
|
+
for (const task of tasks) {
|
|
356
|
+
if (await this.deleteTask(task.id)) {
|
|
357
|
+
deletedTaskIds.push(task.id);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const deleted = await this.store.deleteProject(id);
|
|
362
|
+
if (deleted) {
|
|
363
|
+
const now = new Date().toISOString();
|
|
364
|
+
const event: ProjectDeletedEvent = {
|
|
365
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
366
|
+
name: 'project.deleted',
|
|
367
|
+
aggregateId: id,
|
|
368
|
+
aggregateType: 'Project',
|
|
369
|
+
occurredAt: now,
|
|
370
|
+
payload: { projectId: id, name: existing.name, deletedTaskIds }
|
|
371
|
+
};
|
|
372
|
+
await this.events.publish(event);
|
|
373
|
+
|
|
374
|
+
await this.store.logActivity({
|
|
375
|
+
projectId: id,
|
|
376
|
+
actorId: 'system',
|
|
377
|
+
action: 'project.deleted',
|
|
378
|
+
details: { name: existing.name, deletedTaskCount: deletedTaskIds.length }
|
|
379
|
+
});
|
|
380
|
+
this.dispatchWebhook('project.deleted', { projectId: id, name: existing.name, deletedTaskIds });
|
|
381
|
+
}
|
|
382
|
+
return deleted;
|
|
383
|
+
}
|
|
384
|
+
|
|
343
385
|
// --- Tasks ---
|
|
344
386
|
async getTasks(projectId?: string): Promise<Task[]> {
|
|
345
387
|
return this.store.getTasks(projectId);
|
|
@@ -916,14 +958,9 @@ export class CriticalPathEngine {
|
|
|
916
958
|
|
|
917
959
|
// --- Task Dependencies & Graph ---
|
|
918
960
|
async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
|
|
919
|
-
const existingDependencies = await this.store.getDependencies(dep.taskId);
|
|
920
|
-
const allProjectDeps = [
|
|
921
|
-
...existingDependencies,
|
|
922
|
-
...(await this.store.getDependencies(dep.dependsOnTaskId))
|
|
923
|
-
];
|
|
924
|
-
|
|
925
961
|
// Enforce Directed Acyclic Graph (DAG) Invariant
|
|
926
|
-
const
|
|
962
|
+
const reachableDeps = await this.collectUpstreamDependencies(dep.dependsOnTaskId);
|
|
963
|
+
const cycleCheck = detectDependencyCycle(reachableDeps, dep);
|
|
927
964
|
if (cycleCheck.hasCycle) {
|
|
928
965
|
throw new CircularDependencyError(dep.taskId, dep.dependsOnTaskId, cycleCheck.cyclePath);
|
|
929
966
|
}
|
|
@@ -944,6 +981,37 @@ export class CriticalPathEngine {
|
|
|
944
981
|
return created;
|
|
945
982
|
}
|
|
946
983
|
|
|
984
|
+
/**
|
|
985
|
+
* Collects every dependency edge reachable upstream from `startTaskId`. A new edge
|
|
986
|
+
* `taskId -> dependsOnTaskId` creates a cycle exactly when `taskId` is reachable from
|
|
987
|
+
* `dependsOnTaskId`, so this is the full set of edges the cycle check needs.
|
|
988
|
+
*/
|
|
989
|
+
private async collectUpstreamDependencies(startTaskId: string): Promise<TaskDependency[]> {
|
|
990
|
+
const edges = new Map<string, TaskDependency>();
|
|
991
|
+
const visited = new Set<string>();
|
|
992
|
+
let frontier = [startTaskId];
|
|
993
|
+
|
|
994
|
+
while (frontier.length > 0) {
|
|
995
|
+
const next: string[] = [];
|
|
996
|
+
const results = await Promise.all(frontier.map((id) => this.store.getDependencies(id)));
|
|
997
|
+
frontier.forEach((id) => visited.add(id));
|
|
998
|
+
|
|
999
|
+
for (const deps of results) {
|
|
1000
|
+
for (const d of deps) {
|
|
1001
|
+
// getDependencies returns edges in both directions; follow only outgoing ones.
|
|
1002
|
+
if (!visited.has(d.taskId)) continue;
|
|
1003
|
+
edges.set(d.id, d);
|
|
1004
|
+
if (!visited.has(d.dependsOnTaskId) && !next.includes(d.dependsOnTaskId)) {
|
|
1005
|
+
next.push(d.dependsOnTaskId);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
frontier = next;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
return Array.from(edges.values());
|
|
1013
|
+
}
|
|
1014
|
+
|
|
947
1015
|
async getTaskDependencyGraph(taskId: string): Promise<TaskDependencyGraph> {
|
|
948
1016
|
const dependencies = await this.store.getDependencies(taskId);
|
|
949
1017
|
const upstreamTaskIds = new Set<string>();
|
|
@@ -996,8 +1064,8 @@ export class CriticalPathEngine {
|
|
|
996
1064
|
|
|
997
1065
|
// --- Time Tracking ---
|
|
998
1066
|
async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt' | 'userId'> & { userId?: string; loggedAt?: string }): Promise<TimeEntry> {
|
|
999
|
-
if (entry.hours <= 0) {
|
|
1000
|
-
throw new
|
|
1067
|
+
if (!Number.isFinite(entry.hours) || entry.hours <= 0) {
|
|
1068
|
+
throw new ValidationError('Logged hours must be a positive number.');
|
|
1001
1069
|
}
|
|
1002
1070
|
|
|
1003
1071
|
const task = await this.store.getTask(entry.taskId);
|
|
@@ -1288,7 +1356,7 @@ export class CriticalPathEngine {
|
|
|
1288
1356
|
async readAttachmentText(id: string): Promise<string> {
|
|
1289
1357
|
const attachment = await this.getAttachment(id);
|
|
1290
1358
|
if (!attachment) {
|
|
1291
|
-
throw new
|
|
1359
|
+
throw new NotFoundError(`Attachment with id "${id}" not found.`);
|
|
1292
1360
|
}
|
|
1293
1361
|
|
|
1294
1362
|
if (attachment.storageKey && this.fileStorage && typeof this.fileStorage.download === 'function') {
|
|
@@ -1661,7 +1729,7 @@ export class CriticalPathEngine {
|
|
|
1661
1729
|
): Promise<TimelineLadder> {
|
|
1662
1730
|
const project = await this.store.getProject(projectId);
|
|
1663
1731
|
if (!project) {
|
|
1664
|
-
throw new
|
|
1732
|
+
throw new NotFoundError(`Project with ID "${projectId}" not found.`);
|
|
1665
1733
|
}
|
|
1666
1734
|
|
|
1667
1735
|
let tasks = await this.store.getTasks(projectId);
|
package/src/index.test.ts
CHANGED
|
@@ -3,6 +3,30 @@ import { CriticalPathEngine, deriveTaskLifecycleState, DEFAULT_VFX_WORKFLOW } fr
|
|
|
3
3
|
import type { CriticalPathPlugin } from './types/index.js';
|
|
4
4
|
|
|
5
5
|
describe('CriticalPathEngine Core Tests', () => {
|
|
6
|
+
it('deletes a project with its tasks and publishes deletion events', async () => {
|
|
7
|
+
const engine = new CriticalPathEngine();
|
|
8
|
+
const proj = await engine.createProject({ key: 'DEL', name: 'Doomed Project' });
|
|
9
|
+
const keep = await engine.createProject({ key: 'KEEP', name: 'Survivor' });
|
|
10
|
+
const t1 = await engine.createTask({ projectId: proj.id, title: 'One' });
|
|
11
|
+
const t2 = await engine.createTask({ projectId: proj.id, title: 'Two' });
|
|
12
|
+
const other = await engine.createTask({ projectId: keep.id, title: 'Other' });
|
|
13
|
+
|
|
14
|
+
const published: string[] = [];
|
|
15
|
+
engine.events.subscribe('*', (event) => {
|
|
16
|
+
published.push(event.name);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(await engine.deleteProject(proj.id)).toBe(true);
|
|
20
|
+
expect(await engine.getProject(proj.id)).toBeNull();
|
|
21
|
+
expect(await engine.getTask(t1.id)).toBeNull();
|
|
22
|
+
expect(await engine.getTask(t2.id)).toBeNull();
|
|
23
|
+
expect(await engine.getTask(other.id)).not.toBeNull();
|
|
24
|
+
expect(published.filter((n) => n === 'task.deleted')).toHaveLength(2);
|
|
25
|
+
expect(published.at(-1)).toBe('project.deleted');
|
|
26
|
+
|
|
27
|
+
expect(await engine.deleteProject(proj.id)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
6
30
|
it('creates projects and tasks with activity logs', async () => {
|
|
7
31
|
const engine = new CriticalPathEngine();
|
|
8
32
|
|
package/src/store/sqlite.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
2
5
|
import { SQLiteStore } from './sqlite.js';
|
|
3
6
|
|
|
4
7
|
describe('SQLiteStore', () => {
|
|
@@ -194,3 +197,22 @@ describe('SQLiteStore', () => {
|
|
|
194
197
|
expect(remaining).toHaveLength(0);
|
|
195
198
|
});
|
|
196
199
|
});
|
|
200
|
+
|
|
201
|
+
describe('SQLiteStore outside the vitest runtime', () => {
|
|
202
|
+
const distEntry = fileURLToPath(new URL('../../dist/index.js', import.meta.url));
|
|
203
|
+
|
|
204
|
+
// vitest injects `require` into ESM modules, which once hid an `eval("require(...)")` failure
|
|
205
|
+
// that broke `new SQLiteStore({ filename })` in plain Node. Run the built package in a real Node process.
|
|
206
|
+
it.skipIf(!existsSync(distEntry))('constructs from a filename in a plain Node ESM process', () => {
|
|
207
|
+
const script = `
|
|
208
|
+
const { SQLiteStore } = await import(${JSON.stringify(pathToFileURL(distEntry).href)});
|
|
209
|
+
const store = new SQLiteStore({ filename: ':memory:' });
|
|
210
|
+
const project = await store.createProject({ key: 'SMOKE', name: 'Smoke' });
|
|
211
|
+
process.stdout.write(project.key);
|
|
212
|
+
`;
|
|
213
|
+
const output = execFileSync(process.execPath, ['--input-type=module', '--no-warnings', '-e', script], {
|
|
214
|
+
encoding: 'utf8'
|
|
215
|
+
});
|
|
216
|
+
expect(output).toBe('SMOKE');
|
|
217
|
+
});
|
|
218
|
+
});
|
package/src/store/sqlite.ts
CHANGED
|
@@ -41,7 +41,13 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
41
41
|
throw new Error('SQLiteStore is only available in Node.js environments.');
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
|
-
|
|
44
|
+
// `require` is undefined in ESM, and a static import would break browser bundles of core.
|
|
45
|
+
const nodeSqlite = (globalThis as any).process?.getBuiltinModule?.('node:sqlite') as
|
|
46
|
+
| typeof import('node:sqlite')
|
|
47
|
+
| undefined;
|
|
48
|
+
if (!nodeSqlite) {
|
|
49
|
+
throw new Error('process.getBuiltinModule is unavailable (requires Node.js 22.3+)');
|
|
50
|
+
}
|
|
45
51
|
this.db = new nodeSqlite.DatabaseSync(config.filename || ':memory:');
|
|
46
52
|
} catch (err: any) {
|
|
47
53
|
throw new Error(`Failed to load node:sqlite module: ${err.message}`);
|
package/src/types/index.ts
CHANGED
|
@@ -120,6 +120,43 @@ describe('fractional-index', () => {
|
|
|
120
120
|
current = next;
|
|
121
121
|
}
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
it('handles repeated insertions directly after the same lower bound', () => {
|
|
125
|
+
const lower = 'a0';
|
|
126
|
+
let upper = 'a1';
|
|
127
|
+
|
|
128
|
+
// Repeatedly drop a card just below the first card
|
|
129
|
+
for (let i = 0; i < 50; i++) {
|
|
130
|
+
const next = generateKeyBetween(lower, upper);
|
|
131
|
+
expect(lower < next).toBe(true);
|
|
132
|
+
expect(next < upper).toBe(true);
|
|
133
|
+
upper = next;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('keeps keys strictly ordered under random insertions', () => {
|
|
138
|
+
const keys = ['a0', 'a1'];
|
|
139
|
+
let seed = 42;
|
|
140
|
+
const random = () => {
|
|
141
|
+
seed = (seed * 1103515245 + 12345) % 2147483648;
|
|
142
|
+
return seed / 2147483648;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < 500; i++) {
|
|
146
|
+
const idx = Math.floor(random() * (keys.length + 1));
|
|
147
|
+
const next = generateKeyBetween(keys[idx - 1] ?? null, keys[idx] ?? null);
|
|
148
|
+
keys.splice(idx, 0, next);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (let i = 1; i < keys.length; i++) {
|
|
152
|
+
expect(keys[i - 1] < keys[i]).toBe(true);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('throws when the lower bound does not sort before the upper bound', () => {
|
|
157
|
+
expect(() => generateKeyBetween('a1', 'a0')).toThrow(RangeError);
|
|
158
|
+
expect(() => generateKeyBetween('a0', 'a0')).toThrow(RangeError);
|
|
159
|
+
});
|
|
123
160
|
});
|
|
124
161
|
|
|
125
162
|
describe('generateNKeysBetween', () => {
|
|
@@ -108,40 +108,35 @@ export function getIntegerPart(key: string): string {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* Computes a fractional
|
|
111
|
+
* Computes a fractional suffix strictly between suffixes `a` and `b` (a < result < b).
|
|
112
|
+
* Missing digits in `a` are treated as '0', so `a` may be shorter than `b`.
|
|
113
|
+
* `b === null` means there is no upper bound.
|
|
112
114
|
*/
|
|
113
115
|
function midpointSuffix(a: string, b: string | null): string {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
if (b !== null) {
|
|
117
|
+
let n = 0;
|
|
118
|
+
while (n < b.length && (a[n] ?? '0') === b[n]) {
|
|
119
|
+
n++;
|
|
120
|
+
}
|
|
121
|
+
if (n > 0) {
|
|
122
|
+
return b.slice(0, n) + midpointSuffix(a.slice(n), b.slice(n));
|
|
123
|
+
}
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const charB = b !== null && i < b.length ? b[i] : null;
|
|
122
|
-
|
|
123
|
-
const digitA = charA !== null ? BASE_62.indexOf(charA) : 0;
|
|
124
|
-
const digitB = charB !== null ? BASE_62.indexOf(charB) : BASE_62.length;
|
|
126
|
+
const digitA = a.length > 0 ? BASE_62.indexOf(a[0]) : 0;
|
|
127
|
+
const digitB = b !== null ? BASE_62.indexOf(b[0]) : BASE_62.length;
|
|
125
128
|
|
|
126
129
|
if (digitB - digitA > 1) {
|
|
127
|
-
|
|
128
|
-
return common + BASE_62[mid];
|
|
130
|
+
return BASE_62[Math.floor((digitA + digitB) / 2)];
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
if
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return common + charA + midpointSuffix(restA, restB);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (b !== null && i < b.length) {
|
|
138
|
-
if (digitB === 0) {
|
|
139
|
-
const restB = b.slice(i + 1);
|
|
140
|
-
return common + '0' + midpointSuffix('', restB);
|
|
141
|
-
}
|
|
133
|
+
// Adjacent digits: if b has more digits, its first digit alone already sits between a and b.
|
|
134
|
+
if (b !== null && b.length > 1) {
|
|
135
|
+
return b[0];
|
|
142
136
|
}
|
|
143
137
|
|
|
144
|
-
|
|
138
|
+
// Otherwise keep a's digit and subdivide the remainder with no upper bound.
|
|
139
|
+
return BASE_62[digitA] + midpointSuffix(a.slice(1), null);
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
/**
|
|
@@ -185,8 +180,7 @@ export function generateKeyBetween(
|
|
|
185
180
|
|
|
186
181
|
// Both a and b are present
|
|
187
182
|
if (a! >= b!) {
|
|
188
|
-
|
|
189
|
-
return a! + 'V';
|
|
183
|
+
throw new RangeError(`generateKeyBetween: lower bound "${a}" must sort before upper bound "${b}"`);
|
|
190
184
|
}
|
|
191
185
|
|
|
192
186
|
const intA = getIntegerPart(a!);
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/calendar.test.ts","./src/domain/calendar.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/task-duration.test.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/fractional-index.test.ts","./src/utils/fractional-index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/calendar.test.ts","./src/domain/calendar.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/errors.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/task-duration.test.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/fractional-index.test.ts","./src/utils/fractional-index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|