jevkit-drift 0.1.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/README.md +70 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +104 -0
- package/dist/cli.js.map +1 -0
- package/dist/compare.d.ts +94 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +209 -0
- package/dist/compare.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +14 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +52 -0
- package/dist/replay.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# jevkit-drift
|
|
2
|
+
|
|
3
|
+
TypeSafe's docs warn that `jev-latest` moves when a new version ships, and that
|
|
4
|
+
confidence thresholds tuned against one version do not automatically hold on the
|
|
5
|
+
next. `jevkit-drift` answers the question that warning implies: for the requests
|
|
6
|
+
you actually care about, what changed?
|
|
7
|
+
|
|
8
|
+
> Unofficial and unaffiliated with TypeSafe.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install jevkit-drift
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Two kinds of change
|
|
15
|
+
|
|
16
|
+
A **flip** is a changed decision. Your code takes a different branch. This is
|
|
17
|
+
what breaks things.
|
|
18
|
+
|
|
19
|
+
A **shift** is movement in the probability distribution with the same decision
|
|
20
|
+
still on top. Harmless on its own, but it is what walks an answer toward a
|
|
21
|
+
threshold, so a large shift is an early warning before anything has flipped.
|
|
22
|
+
|
|
23
|
+
Both are reported separately, because conflating them is how a drift report ends
|
|
24
|
+
up either too noisy to read or too quiet to help.
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { readRecords, writeRecords } from "jevkit-core";
|
|
30
|
+
import { compareSets, replay } from "jevkit-drift";
|
|
31
|
+
|
|
32
|
+
const baseline = readRecords("golden.jevl");
|
|
33
|
+
const candidate = await replay(baseline, (s, q) =>
|
|
34
|
+
client.systemOne(s, q, { model: "jev-1.14.0" }),
|
|
35
|
+
);
|
|
36
|
+
writeRecords("candidate.jevl", candidate);
|
|
37
|
+
|
|
38
|
+
const report = compareSets(baseline, candidate);
|
|
39
|
+
console.log(report.summary());
|
|
40
|
+
|
|
41
|
+
for (const [requestId, delta] of report.flips) {
|
|
42
|
+
console.log(requestId, delta.describe());
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## CLI
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx jevkit-drift golden.jevl candidate.jevl
|
|
50
|
+
npx jevkit-drift golden.jevl candidate.jevl --max-flips 3
|
|
51
|
+
npx jevkit-drift golden.jevl candidate.jevl --max-shift 0.15
|
|
52
|
+
npx jevkit-drift golden.jevl candidate.jevl --format json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Exit codes: `0` within tolerance, `1` drifted, `2` bad usage.
|
|
56
|
+
|
|
57
|
+
## How records are paired
|
|
58
|
+
|
|
59
|
+
On `request_id`, the digest of state plus questions with the model deliberately
|
|
60
|
+
excluded. That is the whole reason the record format keeps two digests: the full
|
|
61
|
+
`id` changes when the model changes, so it cannot pair a baseline with its
|
|
62
|
+
replay, while `request_id` can.
|
|
63
|
+
|
|
64
|
+
Change the state or a question and the records stop pairing, which is correct.
|
|
65
|
+
They are no longer the same request, and comparing them would be meaningless.
|
|
66
|
+
The CLI says so rather than reporting zero drift.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** `jevkit-drift` command line interface. */
|
|
3
|
+
export declare const EXIT_OK = 0;
|
|
4
|
+
export declare const EXIT_DRIFTED = 1;
|
|
5
|
+
export declare const EXIT_USAGE = 2;
|
|
6
|
+
export declare function main(argv?: string[]): number;
|
|
7
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,6CAA6C;AAM7C,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,UAAU,IAAI,CAAC;AAY5B,wBAAgB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,MAAM,CAoEnE"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** `jevkit-drift` command line interface. */
|
|
3
|
+
import { RecordFormatError, readRecords } from "jevkit-core";
|
|
4
|
+
import { compareSets } from "./compare.js";
|
|
5
|
+
export const EXIT_OK = 0;
|
|
6
|
+
export const EXIT_DRIFTED = 1;
|
|
7
|
+
export const EXIT_USAGE = 2;
|
|
8
|
+
const USAGE = `usage: jevkit-drift <baseline.jevl> <candidate.jevl> [options]
|
|
9
|
+
|
|
10
|
+
Compare two .jevl runs of the same requests and report which decisions flipped.
|
|
11
|
+
Never calls the API.
|
|
12
|
+
|
|
13
|
+
--max-flips N tolerated number of flipped decisions (default 0)
|
|
14
|
+
--max-shift F fail if any distribution moves more than this (0..1)
|
|
15
|
+
--format text|json
|
|
16
|
+
--quiet only print the summary`;
|
|
17
|
+
export function main(argv = process.argv.slice(2)) {
|
|
18
|
+
const files = [];
|
|
19
|
+
let maxFlips = 0;
|
|
20
|
+
let maxShift = null;
|
|
21
|
+
let format = "text";
|
|
22
|
+
let quiet = false;
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const arg = argv[i];
|
|
25
|
+
switch (arg) {
|
|
26
|
+
case "--max-flips":
|
|
27
|
+
maxFlips = Number(argv[++i]);
|
|
28
|
+
break;
|
|
29
|
+
case "--max-shift":
|
|
30
|
+
maxShift = Number(argv[++i]);
|
|
31
|
+
break;
|
|
32
|
+
case "--format": {
|
|
33
|
+
const v = argv[++i];
|
|
34
|
+
if (v !== "text" && v !== "json") {
|
|
35
|
+
console.error("jevkit-drift: --format expects text or json");
|
|
36
|
+
return EXIT_USAGE;
|
|
37
|
+
}
|
|
38
|
+
format = v;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case "--quiet":
|
|
42
|
+
quiet = true;
|
|
43
|
+
break;
|
|
44
|
+
case "-h":
|
|
45
|
+
case "--help":
|
|
46
|
+
console.log(USAGE);
|
|
47
|
+
return EXIT_OK;
|
|
48
|
+
default:
|
|
49
|
+
if (arg.startsWith("--")) {
|
|
50
|
+
console.error(`jevkit-drift: unknown option ${arg}`);
|
|
51
|
+
return EXIT_USAGE;
|
|
52
|
+
}
|
|
53
|
+
files.push(arg);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (files.length !== 2) {
|
|
57
|
+
console.error(USAGE);
|
|
58
|
+
return EXIT_USAGE;
|
|
59
|
+
}
|
|
60
|
+
let before, after;
|
|
61
|
+
try {
|
|
62
|
+
before = readRecords(files[0]);
|
|
63
|
+
after = readRecords(files[1]);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const message = err instanceof RecordFormatError ? err.message : err.message;
|
|
67
|
+
console.error(`jevkit-drift: ${message}`);
|
|
68
|
+
return EXIT_USAGE;
|
|
69
|
+
}
|
|
70
|
+
let report;
|
|
71
|
+
try {
|
|
72
|
+
report = compareSets(before, after);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(`jevkit-drift: ${err.message}`);
|
|
76
|
+
return EXIT_USAGE;
|
|
77
|
+
}
|
|
78
|
+
if (format === "json") {
|
|
79
|
+
console.log(JSON.stringify(report.toJSON(), null, 2));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(report.summary());
|
|
83
|
+
if (report.flips.length && !quiet) {
|
|
84
|
+
console.log("\nflipped decisions:");
|
|
85
|
+
for (const [requestId, delta] of report.flips) {
|
|
86
|
+
console.log(` ${requestId.slice(7, 19)} ${delta.describe()}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!report.deltas.length && (before.length || after.length)) {
|
|
91
|
+
console.error("jevkit-drift: no requests matched between the two files. Records pair on state " +
|
|
92
|
+
"and questions, so a change to either makes them incomparable.");
|
|
93
|
+
return EXIT_USAGE;
|
|
94
|
+
}
|
|
95
|
+
if (report.flips.length > maxFlips)
|
|
96
|
+
return EXIT_DRIFTED;
|
|
97
|
+
if (maxShift !== null && report.maxShift > maxShift)
|
|
98
|
+
return EXIT_DRIFTED;
|
|
99
|
+
return EXIT_OK;
|
|
100
|
+
}
|
|
101
|
+
const invokedDirectly = process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`;
|
|
102
|
+
if (invokedDirectly)
|
|
103
|
+
process.exit(main());
|
|
104
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparing two answers to the same request.
|
|
3
|
+
*
|
|
4
|
+
* TypeSafe's docs warn that `jev-latest` moves when a new version ships, and
|
|
5
|
+
* that confidence thresholds tuned against one version do not automatically
|
|
6
|
+
* hold on the next. This module answers the question that warning implies: for
|
|
7
|
+
* a set of requests you already care about, what actually changed?
|
|
8
|
+
*
|
|
9
|
+
* Two kinds of change are tracked separately, because they mean different
|
|
10
|
+
* things:
|
|
11
|
+
*
|
|
12
|
+
* - A **flip** is a changed decision. Your code takes a different branch. This
|
|
13
|
+
* is what breaks things.
|
|
14
|
+
* - A **shift** is movement in the probability distribution with the same
|
|
15
|
+
* decision on top. Harmless alone, but it is what moves an answer toward a
|
|
16
|
+
* threshold, so a large shift is an early warning before anything flips.
|
|
17
|
+
*/
|
|
18
|
+
import { type Answer, type Record as JevlRecord } from "jevkit-core";
|
|
19
|
+
/**
|
|
20
|
+
* Total variation distance between two distributions, on 0..1.
|
|
21
|
+
*
|
|
22
|
+
* Outcomes present in one distribution and not the other count in full, which
|
|
23
|
+
* is what makes this meaningful when a new model version changes the option
|
|
24
|
+
* set. Distributions are not renormalized: if the API returns something that
|
|
25
|
+
* does not sum to 1, that is reported rather than hidden.
|
|
26
|
+
*/
|
|
27
|
+
export declare function totalVariation(a: Record<string, number>, b: Record<string, number>): number;
|
|
28
|
+
export declare class QuestionDelta {
|
|
29
|
+
readonly questionId: string;
|
|
30
|
+
readonly type: string;
|
|
31
|
+
readonly before: unknown;
|
|
32
|
+
readonly after: unknown;
|
|
33
|
+
readonly distributionShift: number;
|
|
34
|
+
readonly confidenceBefore: number | null;
|
|
35
|
+
readonly confidenceAfter: number | null;
|
|
36
|
+
readonly scoreBefore: number | null;
|
|
37
|
+
readonly scoreAfter: number | null;
|
|
38
|
+
constructor(questionId: string, type: string, before: unknown, after: unknown, distributionShift: number, confidenceBefore: number | null, confidenceAfter: number | null, scoreBefore?: number | null, scoreAfter?: number | null);
|
|
39
|
+
/** The selected outcome changed. */
|
|
40
|
+
get flipped(): boolean;
|
|
41
|
+
get confidenceDelta(): number | null;
|
|
42
|
+
describe(): string;
|
|
43
|
+
}
|
|
44
|
+
export declare class RecordDelta {
|
|
45
|
+
readonly requestId: string;
|
|
46
|
+
readonly modelBefore: string;
|
|
47
|
+
readonly modelAfter: string;
|
|
48
|
+
readonly questions: QuestionDelta[];
|
|
49
|
+
constructor(requestId: string, modelBefore: string, modelAfter: string, questions?: QuestionDelta[]);
|
|
50
|
+
get flips(): QuestionDelta[];
|
|
51
|
+
get maxShift(): number;
|
|
52
|
+
}
|
|
53
|
+
export declare function compareAnswers(qid: string, before: Answer, after: Answer): QuestionDelta;
|
|
54
|
+
export declare function compareRecords(before: JevlRecord, after: JevlRecord): RecordDelta;
|
|
55
|
+
export declare class DriftReport {
|
|
56
|
+
readonly deltas: RecordDelta[];
|
|
57
|
+
readonly unmatchedBefore: string[];
|
|
58
|
+
readonly unmatchedAfter: string[];
|
|
59
|
+
constructor(deltas?: RecordDelta[], unmatchedBefore?: string[], unmatchedAfter?: string[]);
|
|
60
|
+
get totalQuestions(): number;
|
|
61
|
+
get flips(): Array<[string, QuestionDelta]>;
|
|
62
|
+
get flipRate(): number;
|
|
63
|
+
get maxShift(): number;
|
|
64
|
+
get meanShift(): number;
|
|
65
|
+
models(): [Set<string>, Set<string>];
|
|
66
|
+
summary(): string;
|
|
67
|
+
toJSON(): {
|
|
68
|
+
requests: number;
|
|
69
|
+
questions: number;
|
|
70
|
+
flips: number;
|
|
71
|
+
flipRate: number;
|
|
72
|
+
meanShift: number;
|
|
73
|
+
maxShift: number;
|
|
74
|
+
unmatchedBefore: string[];
|
|
75
|
+
unmatchedAfter: string[];
|
|
76
|
+
details: {
|
|
77
|
+
requestId: string;
|
|
78
|
+
questionId: string;
|
|
79
|
+
before: unknown;
|
|
80
|
+
after: unknown;
|
|
81
|
+
distributionShift: number;
|
|
82
|
+
confidenceDelta: number | null;
|
|
83
|
+
}[];
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pair two sets of records by request id and compare each pair.
|
|
88
|
+
*
|
|
89
|
+
* Records pair on `requestId`, the digest of state plus questions with the
|
|
90
|
+
* model excluded, which is exactly what makes a baseline comparable to its
|
|
91
|
+
* replay on a different model version.
|
|
92
|
+
*/
|
|
93
|
+
export declare function compareSets(before: JevlRecord[], after: JevlRecord[]): DriftReport;
|
|
94
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../src/compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,MAAM,IAAI,UAAU,EAAgB,MAAM,aAAa,CAAC;AAEnF;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACxB,MAAM,CAKR;AAED,qBAAa,aAAa;IAEtB,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,MAAM,EAAE,OAAO;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO;IACvB,QAAQ,CAAC,iBAAiB,EAAE,MAAM;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI;IACxC,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;gBARzB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,OAAO,EACd,iBAAiB,EAAE,MAAM,EACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,EAC/B,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,WAAW,GAAE,MAAM,GAAG,IAAW,EACjC,UAAU,GAAE,MAAM,GAAG,IAAW;IAG3C,oCAAoC;IACpC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,eAAe,IAAI,MAAM,GAAG,IAAI,CAGnC;IAED,QAAQ,IAAI,MAAM;CAenB;AAED,qBAAa,WAAW;IAEpB,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,SAAS,EAAE,aAAa,EAAE;gBAH1B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,aAAa,EAAO;IAG1C,IAAI,KAAK,IAAI,aAAa,EAAE,CAE3B;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;CACF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAkBxF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,WAAW,CA2BjF;AAED,qBAAa,WAAW;IAEpB,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,EAAE;IAClC,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE;gBAFxB,MAAM,GAAE,WAAW,EAAO,EAC1B,eAAe,GAAE,MAAM,EAAO,EAC9B,cAAc,GAAE,MAAM,EAAO;IAGxC,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,KAAK,IAAI,KAAK,CAAC,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAE1C;IAED,IAAI,QAAQ,IAAI,MAAM,CAGrB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,SAAS,IAAI,MAAM,CAGtB;IAED,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAOpC,OAAO,IAAI,MAAM;IAwBjB,MAAM;;;;;;;;;;;;;;;;;;CAoBP;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,WAAW,CASlF"}
|
package/dist/compare.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comparing two answers to the same request.
|
|
3
|
+
*
|
|
4
|
+
* TypeSafe's docs warn that `jev-latest` moves when a new version ships, and
|
|
5
|
+
* that confidence thresholds tuned against one version do not automatically
|
|
6
|
+
* hold on the next. This module answers the question that warning implies: for
|
|
7
|
+
* a set of requests you already care about, what actually changed?
|
|
8
|
+
*
|
|
9
|
+
* Two kinds of change are tracked separately, because they mean different
|
|
10
|
+
* things:
|
|
11
|
+
*
|
|
12
|
+
* - A **flip** is a changed decision. Your code takes a different branch. This
|
|
13
|
+
* is what breaks things.
|
|
14
|
+
* - A **shift** is movement in the probability distribution with the same
|
|
15
|
+
* decision on top. Harmless alone, but it is what moves an answer toward a
|
|
16
|
+
* threshold, so a large shift is an early warning before anything flips.
|
|
17
|
+
*/
|
|
18
|
+
import { parseAnswers } from "jevkit-core";
|
|
19
|
+
/**
|
|
20
|
+
* Total variation distance between two distributions, on 0..1.
|
|
21
|
+
*
|
|
22
|
+
* Outcomes present in one distribution and not the other count in full, which
|
|
23
|
+
* is what makes this meaningful when a new model version changes the option
|
|
24
|
+
* set. Distributions are not renormalized: if the API returns something that
|
|
25
|
+
* does not sum to 1, that is reported rather than hidden.
|
|
26
|
+
*/
|
|
27
|
+
export function totalVariation(a, b) {
|
|
28
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
29
|
+
let sum = 0;
|
|
30
|
+
for (const k of keys)
|
|
31
|
+
sum += Math.abs((a[k] ?? 0) - (b[k] ?? 0));
|
|
32
|
+
return sum / 2;
|
|
33
|
+
}
|
|
34
|
+
export class QuestionDelta {
|
|
35
|
+
questionId;
|
|
36
|
+
type;
|
|
37
|
+
before;
|
|
38
|
+
after;
|
|
39
|
+
distributionShift;
|
|
40
|
+
confidenceBefore;
|
|
41
|
+
confidenceAfter;
|
|
42
|
+
scoreBefore;
|
|
43
|
+
scoreAfter;
|
|
44
|
+
constructor(questionId, type, before, after, distributionShift, confidenceBefore, confidenceAfter, scoreBefore = null, scoreAfter = null) {
|
|
45
|
+
this.questionId = questionId;
|
|
46
|
+
this.type = type;
|
|
47
|
+
this.before = before;
|
|
48
|
+
this.after = after;
|
|
49
|
+
this.distributionShift = distributionShift;
|
|
50
|
+
this.confidenceBefore = confidenceBefore;
|
|
51
|
+
this.confidenceAfter = confidenceAfter;
|
|
52
|
+
this.scoreBefore = scoreBefore;
|
|
53
|
+
this.scoreAfter = scoreAfter;
|
|
54
|
+
}
|
|
55
|
+
/** The selected outcome changed. */
|
|
56
|
+
get flipped() {
|
|
57
|
+
return this.before !== this.after;
|
|
58
|
+
}
|
|
59
|
+
get confidenceDelta() {
|
|
60
|
+
if (this.confidenceBefore === null || this.confidenceAfter === null)
|
|
61
|
+
return null;
|
|
62
|
+
return this.confidenceAfter - this.confidenceBefore;
|
|
63
|
+
}
|
|
64
|
+
describe() {
|
|
65
|
+
const head = this.flipped
|
|
66
|
+
? `${this.questionId}: FLIP ${JSON.stringify(this.before)} -> ${JSON.stringify(this.after)}`
|
|
67
|
+
: `${this.questionId}: stable (${JSON.stringify(this.before)})`;
|
|
68
|
+
const parts = [`shift=${this.distributionShift.toFixed(3)}`];
|
|
69
|
+
const delta = this.confidenceDelta;
|
|
70
|
+
if (delta !== null) {
|
|
71
|
+
const sign = delta >= 0 ? "+" : "";
|
|
72
|
+
parts.push(`conf ${this.confidenceBefore.toFixed(3)} -> ${this.confidenceAfter.toFixed(3)} ` +
|
|
73
|
+
`(${sign}${delta.toFixed(3)})`);
|
|
74
|
+
}
|
|
75
|
+
return `${head} [${parts.join(", ")}]`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class RecordDelta {
|
|
79
|
+
requestId;
|
|
80
|
+
modelBefore;
|
|
81
|
+
modelAfter;
|
|
82
|
+
questions;
|
|
83
|
+
constructor(requestId, modelBefore, modelAfter, questions = []) {
|
|
84
|
+
this.requestId = requestId;
|
|
85
|
+
this.modelBefore = modelBefore;
|
|
86
|
+
this.modelAfter = modelAfter;
|
|
87
|
+
this.questions = questions;
|
|
88
|
+
}
|
|
89
|
+
get flips() {
|
|
90
|
+
return this.questions.filter((q) => q.flipped);
|
|
91
|
+
}
|
|
92
|
+
get maxShift() {
|
|
93
|
+
return this.questions.reduce((m, q) => Math.max(m, q.distributionShift), 0);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function compareAnswers(qid, before, after) {
|
|
97
|
+
if (before.type !== after.type) {
|
|
98
|
+
throw new Error(`question "${qid}": type changed from "${before.type}" to "${after.type}". ` +
|
|
99
|
+
`These are not the same question and cannot be compared.`);
|
|
100
|
+
}
|
|
101
|
+
return new QuestionDelta(qid, before.type, before.predicted(), after.predicted(), totalVariation(before.probabilities, after.probabilities), before.confidence, after.confidence, before.score, after.score);
|
|
102
|
+
}
|
|
103
|
+
export function compareRecords(before, after) {
|
|
104
|
+
if (before.requestId !== after.requestId) {
|
|
105
|
+
throw new Error(`records describe different requests and cannot be compared ` +
|
|
106
|
+
`(${before.requestId} vs ${after.requestId})`);
|
|
107
|
+
}
|
|
108
|
+
const old = parseAnswers(before.answers);
|
|
109
|
+
const fresh = parseAnswers(after.answers);
|
|
110
|
+
const missing = Object.keys(old).filter((k) => !(k in fresh)).sort();
|
|
111
|
+
const added = Object.keys(fresh).filter((k) => !(k in old)).sort();
|
|
112
|
+
if (missing.length || added.length) {
|
|
113
|
+
const detail = [];
|
|
114
|
+
if (missing.length)
|
|
115
|
+
detail.push(`missing in the new run: ${missing.join(", ")}`);
|
|
116
|
+
if (added.length)
|
|
117
|
+
detail.push(`only in the new run: ${added.join(", ")}`);
|
|
118
|
+
throw new Error(`answer sets differ for request ${before.requestId}: ${detail.join("; ")}`);
|
|
119
|
+
}
|
|
120
|
+
return new RecordDelta(before.requestId, before.model, after.model, Object.keys(old)
|
|
121
|
+
.sort()
|
|
122
|
+
.map((qid) => compareAnswers(qid, old[qid], fresh[qid])));
|
|
123
|
+
}
|
|
124
|
+
export class DriftReport {
|
|
125
|
+
deltas;
|
|
126
|
+
unmatchedBefore;
|
|
127
|
+
unmatchedAfter;
|
|
128
|
+
constructor(deltas = [], unmatchedBefore = [], unmatchedAfter = []) {
|
|
129
|
+
this.deltas = deltas;
|
|
130
|
+
this.unmatchedBefore = unmatchedBefore;
|
|
131
|
+
this.unmatchedAfter = unmatchedAfter;
|
|
132
|
+
}
|
|
133
|
+
get totalQuestions() {
|
|
134
|
+
return this.deltas.reduce((n, d) => n + d.questions.length, 0);
|
|
135
|
+
}
|
|
136
|
+
get flips() {
|
|
137
|
+
return this.deltas.flatMap((d) => d.flips.map((q) => [d.requestId, q]));
|
|
138
|
+
}
|
|
139
|
+
get flipRate() {
|
|
140
|
+
const total = this.totalQuestions;
|
|
141
|
+
return total ? this.flips.length / total : 0;
|
|
142
|
+
}
|
|
143
|
+
get maxShift() {
|
|
144
|
+
return this.deltas.reduce((m, d) => Math.max(m, d.maxShift), 0);
|
|
145
|
+
}
|
|
146
|
+
get meanShift() {
|
|
147
|
+
const shifts = this.deltas.flatMap((d) => d.questions.map((q) => q.distributionShift));
|
|
148
|
+
return shifts.length ? shifts.reduce((a, b) => a + b, 0) / shifts.length : 0;
|
|
149
|
+
}
|
|
150
|
+
models() {
|
|
151
|
+
return [
|
|
152
|
+
new Set(this.deltas.map((d) => d.modelBefore)),
|
|
153
|
+
new Set(this.deltas.map((d) => d.modelAfter)),
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
summary() {
|
|
157
|
+
const [before, after] = this.models();
|
|
158
|
+
const lines = [
|
|
159
|
+
`compared ${this.deltas.length} request(s), ${this.totalQuestions} question(s)`,
|
|
160
|
+
` ${[...before].sort().join(", ") || "?"} -> ${[...after].sort().join(", ") || "?"}`,
|
|
161
|
+
` flips: ${this.flips.length} (${(this.flipRate * 100).toFixed(1)}%)`,
|
|
162
|
+
` mean shift: ${this.meanShift.toFixed(4)}`,
|
|
163
|
+
` max shift: ${this.maxShift.toFixed(4)}`,
|
|
164
|
+
];
|
|
165
|
+
if (this.unmatchedBefore.length) {
|
|
166
|
+
lines.push(` ${this.unmatchedBefore.length} record(s) in the baseline had no counterpart ` +
|
|
167
|
+
`and were not compared`);
|
|
168
|
+
}
|
|
169
|
+
if (this.unmatchedAfter.length) {
|
|
170
|
+
lines.push(` ${this.unmatchedAfter.length} record(s) in the new run had no counterpart ` +
|
|
171
|
+
`and were not compared`);
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
toJSON() {
|
|
176
|
+
return {
|
|
177
|
+
requests: this.deltas.length,
|
|
178
|
+
questions: this.totalQuestions,
|
|
179
|
+
flips: this.flips.length,
|
|
180
|
+
flipRate: this.flipRate,
|
|
181
|
+
meanShift: this.meanShift,
|
|
182
|
+
maxShift: this.maxShift,
|
|
183
|
+
unmatchedBefore: this.unmatchedBefore,
|
|
184
|
+
unmatchedAfter: this.unmatchedAfter,
|
|
185
|
+
details: this.flips.map(([rid, q]) => ({
|
|
186
|
+
requestId: rid,
|
|
187
|
+
questionId: q.questionId,
|
|
188
|
+
before: q.before,
|
|
189
|
+
after: q.after,
|
|
190
|
+
distributionShift: q.distributionShift,
|
|
191
|
+
confidenceDelta: q.confidenceDelta,
|
|
192
|
+
})),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Pair two sets of records by request id and compare each pair.
|
|
198
|
+
*
|
|
199
|
+
* Records pair on `requestId`, the digest of state plus questions with the
|
|
200
|
+
* model excluded, which is exactly what makes a baseline comparable to its
|
|
201
|
+
* replay on a different model version.
|
|
202
|
+
*/
|
|
203
|
+
export function compareSets(before, after) {
|
|
204
|
+
const old = new Map(before.map((r) => [r.requestId, r]));
|
|
205
|
+
const fresh = new Map(after.map((r) => [r.requestId, r]));
|
|
206
|
+
const shared = [...old.keys()].filter((k) => fresh.has(k)).sort();
|
|
207
|
+
return new DriftReport(shared.map((rid) => compareRecords(old.get(rid), fresh.get(rid))), [...old.keys()].filter((k) => !fresh.has(k)).sort(), [...fresh.keys()].filter((k) => !old.has(k)).sort());
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../src/compare.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect when a new Jev model version changes decisions you depend on.
|
|
3
|
+
*
|
|
4
|
+
* TypeSafe's docs warn that `jev-latest` moves and that tuned confidence
|
|
5
|
+
* thresholds move with it. This package replays a golden set against a new
|
|
6
|
+
* version and reports what actually changed, separating flipped decisions from
|
|
7
|
+
* probability shifts that have not flipped anything yet.
|
|
8
|
+
*/
|
|
9
|
+
export { DriftReport, QuestionDelta, RecordDelta, compareAnswers, compareRecords, compareSets, totalVariation, } from "./compare.js";
|
|
10
|
+
export { replay, type SystemOneCallable } from "./replay.js";
|
|
11
|
+
export declare const VERSION = "0.1.0";
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EACvE,WAAW,EAAE,cAAc,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,MAAM,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE7D,eAAO,MAAM,OAAO,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect when a new Jev model version changes decisions you depend on.
|
|
3
|
+
*
|
|
4
|
+
* TypeSafe's docs warn that `jev-latest` moves and that tuned confidence
|
|
5
|
+
* thresholds move with it. This package replays a golden set against a new
|
|
6
|
+
* version and reports what actually changed, separating flipped decisions from
|
|
7
|
+
* probability shifts that have not flipped anything yet.
|
|
8
|
+
*/
|
|
9
|
+
export { DriftReport, QuestionDelta, RecordDelta, compareAnswers, compareRecords, compareSets, totalVariation, } from "./compare.js";
|
|
10
|
+
export { replay } from "./replay.js";
|
|
11
|
+
export const VERSION = "0.1.0";
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EACvE,WAAW,EAAE,cAAc,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,MAAM,EAA0B,MAAM,aAAa,CAAC;AAE7D,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Replaying a golden set against a model. */
|
|
2
|
+
import { Record as JevlRecord } from "jevkit-core";
|
|
3
|
+
export type SystemOneCallable = (state: unknown, questions: Record<string, unknown>) => Promise<unknown> | unknown;
|
|
4
|
+
/**
|
|
5
|
+
* Re-send each record's request and return the new answers as new records.
|
|
6
|
+
*
|
|
7
|
+
* `systemOne` is any callable taking `(state, questions)`, which is the shape
|
|
8
|
+
* both official SDKs already expose. Labels and tags carry over from the
|
|
9
|
+
* baseline so a replayed set stays scoreable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function replay(records: Iterable<JevlRecord>, systemOne: SystemOneCallable, options?: {
|
|
12
|
+
tags?: string[];
|
|
13
|
+
}): Promise<JevlRecord[]>;
|
|
14
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC/B,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;AAyBhC;;;;;;GAMG;AACH,wBAAsB,MAAM,CAC1B,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,EAC7B,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAChC,OAAO,CAAC,UAAU,EAAE,CAAC,CAoBvB"}
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Replaying a golden set against a model. */
|
|
2
|
+
import { Record as JevlRecord } from "jevkit-core";
|
|
3
|
+
function plain(value) {
|
|
4
|
+
if (value === null || value === undefined)
|
|
5
|
+
return value;
|
|
6
|
+
if (typeof value !== "object")
|
|
7
|
+
return value;
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return value;
|
|
10
|
+
// Class instances from an SDK become plain objects; own enumerable keys only.
|
|
11
|
+
return { ...value };
|
|
12
|
+
}
|
|
13
|
+
function extract(response) {
|
|
14
|
+
const obj = (response ?? {});
|
|
15
|
+
const answers = (obj["answers"] ?? {});
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [qid, answer] of Object.entries(answers))
|
|
18
|
+
out[qid] = plain(answer);
|
|
19
|
+
const usage = obj["usage"];
|
|
20
|
+
return {
|
|
21
|
+
model: String(obj["model"] ?? ""),
|
|
22
|
+
answers: out,
|
|
23
|
+
usage: usage === undefined || usage === null ? null : plain(usage),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Re-send each record's request and return the new answers as new records.
|
|
28
|
+
*
|
|
29
|
+
* `systemOne` is any callable taking `(state, questions)`, which is the shape
|
|
30
|
+
* both official SDKs already expose. Labels and tags carry over from the
|
|
31
|
+
* baseline so a replayed set stays scoreable.
|
|
32
|
+
*/
|
|
33
|
+
export async function replay(records, systemOne, options = {}) {
|
|
34
|
+
const extraTags = options.tags ?? [];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const record of records) {
|
|
37
|
+
const response = await systemOne(record.state, record.questions);
|
|
38
|
+
const { model, answers, usage } = extract(response);
|
|
39
|
+
out.push(new JevlRecord({
|
|
40
|
+
model: model || record.model,
|
|
41
|
+
state: record.state,
|
|
42
|
+
questions: record.questions,
|
|
43
|
+
answers,
|
|
44
|
+
usage,
|
|
45
|
+
label: record.label,
|
|
46
|
+
tags: [...record.tags, ...extraTags],
|
|
47
|
+
meta: { ...record.meta, replayed_from: record.id },
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAE9C,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAC;AASnD,SAAS,KAAK,CAAC,KAAc;IAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,8EAA8E;IAC9E,OAAO,EAAE,GAAI,KAAiC,EAAE,CAAC;AACnD,CAAC;AAED,SAAS,OAAO,CAAC,QAAiB;IAChC,MAAM,GAAG,GAAG,CAAC,QAAQ,IAAI,EAAE,CAA4B,CAAC;IACxD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAA4B,CAAC;IAClE,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QACjC,OAAO,EAAE,GAAG;QACZ,KAAK,EAAE,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,KAAK,CAAC,KAAK,CAA6B;KAChG,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,OAA6B,EAC7B,SAA4B,EAC5B,UAA+B,EAAE;IAEjC,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;IACrC,MAAM,GAAG,GAAiB,EAAE,CAAC;IAC7B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;QACjE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpD,GAAG,CAAC,IAAI,CACN,IAAI,UAAU,CAAC;YACb,KAAK,EAAE,KAAK,IAAI,MAAM,CAAC,KAAK;YAC5B,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,OAAO;YACP,KAAK;YACL,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC;YACpC,IAAI,EAAE,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,EAAE;SACnD,CAAC,CACH,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jevkit-drift",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Detect when a new TypeSafe Jev model version flips decisions your code depends on. Replays a golden set and diffs the answers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jev",
|
|
7
|
+
"typesafe",
|
|
8
|
+
"system-one",
|
|
9
|
+
"drift",
|
|
10
|
+
"regression",
|
|
11
|
+
"testing"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"jevkit-drift": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/pjdurden/jevkit-js.git"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"jevkit-core": "^0.2.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -b"
|
|
42
|
+
}
|
|
43
|
+
}
|