@trazum/core 1.50.10 → 1.51.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/dist/annual.d.ts +122 -0
- package/dist/annual.d.ts.map +1 -0
- package/dist/annual.js +101 -0
- package/dist/annual.js.map +1 -0
- package/dist/commitment.d.ts +144 -0
- package/dist/commitment.d.ts.map +1 -0
- package/dist/commitment.js +110 -0
- package/dist/commitment.js.map +1 -0
- package/dist/conform.d.ts +1 -1
- package/dist/conform.d.ts.map +1 -1
- package/dist/conform.js +72 -0
- package/dist/conform.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/savings.d.ts.map +1 -1
- package/dist/savings.js +14 -1
- package/dist/savings.js.map +1 -1
- package/package.json +1 -1
- package/src/annual.ts +191 -0
- package/src/commitment.ts +208 -0
- package/src/conform.ts +112 -1
- package/src/index.ts +10 -0
- package/src/savings.ts +14 -1
package/dist/annual.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The year, assembled from what was already written down.
|
|
3
|
+
*
|
|
4
|
+
* The last chapter of the arc, and the one that turns this product's argument
|
|
5
|
+
* into something a stranger can audit. Everything below comes from the store
|
|
6
|
+
* and the plans a team already keeps: **no new data, and nothing computed that
|
|
7
|
+
* could not be checked against a document that already exists.**
|
|
8
|
+
*
|
|
9
|
+
* That constraint is the whole design. An annual report is the document most
|
|
10
|
+
* likely to be quoted out of the room it was written in, and the one nobody
|
|
11
|
+
* goes back to verify. So it may not contain a single figure that this tool
|
|
12
|
+
* would refuse to print anywhere else — which means it is mostly a summing
|
|
13
|
+
* exercise with a great many refusals attached.
|
|
14
|
+
*
|
|
15
|
+
* ## Four questions, and the fourth is the one that matters
|
|
16
|
+
*
|
|
17
|
+
* What was spent. What was planned. What arrived. **And what could not be
|
|
18
|
+
* told** — which in a normal annual report is silently folded into one of the
|
|
19
|
+
* other three, almost always into the flattering one.
|
|
20
|
+
*
|
|
21
|
+
* `verify` has kept those three outcomes apart since 1.39. A year is where the
|
|
22
|
+
* temptation to collapse them is strongest, because "eleven of fourteen
|
|
23
|
+
* actions arrived" reads better than "eleven arrived, one did not, and two
|
|
24
|
+
* could not be judged" — and the second sentence is the one that tells
|
|
25
|
+
* somebody their measurement has a hole in it.
|
|
26
|
+
*
|
|
27
|
+
* ## It reports the record, not the team
|
|
28
|
+
*
|
|
29
|
+
* No per-person anything, no velocity, no ranking of who planned well. The
|
|
30
|
+
* doctrine rule from 1.44, and it matters most here: an annual document is
|
|
31
|
+
* exactly where a cost tool starts being used for performance review, and the
|
|
32
|
+
* way to not be is to hold no data that could be.
|
|
33
|
+
*/
|
|
34
|
+
import type { PlanDocument } from './plan.js';
|
|
35
|
+
import type { PlanVerification } from './verify.js';
|
|
36
|
+
import type { OutcomeReport } from './outcome.js';
|
|
37
|
+
/** One period the year is built from, as the caller sliced it. */
|
|
38
|
+
export interface AnnualPeriod {
|
|
39
|
+
/** `YYYY-MM`. */
|
|
40
|
+
month: string;
|
|
41
|
+
usd: number;
|
|
42
|
+
calls: number;
|
|
43
|
+
/** The plan made for this period, when one was. */
|
|
44
|
+
plan?: PlanDocument;
|
|
45
|
+
/** The verification of the previous plan, when one was run. */
|
|
46
|
+
verification?: PlanVerification;
|
|
47
|
+
/** Outcomes recorded in this period, when any were. */
|
|
48
|
+
outcomes?: OutcomeReport;
|
|
49
|
+
}
|
|
50
|
+
export interface AnnualRecord {
|
|
51
|
+
schemaVersion: 1;
|
|
52
|
+
year: string;
|
|
53
|
+
/** Months present, oldest first. Gaps are named rather than interpolated. */
|
|
54
|
+
months: Array<{
|
|
55
|
+
month: string;
|
|
56
|
+
usd: number;
|
|
57
|
+
calls: number;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Months of the year with no record at all.
|
|
61
|
+
*
|
|
62
|
+
* Named, never filled. A year report that quietly covers nine months and
|
|
63
|
+
* prints an annual total is wrong by a quarter and says nothing about it.
|
|
64
|
+
*/
|
|
65
|
+
missingMonths: string[];
|
|
66
|
+
totalUsd: number;
|
|
67
|
+
totalCalls: number;
|
|
68
|
+
/**
|
|
69
|
+
* What was planned and what became of it — three outcomes, never two.
|
|
70
|
+
*
|
|
71
|
+
* `cannotTell` is the field an ordinary annual report does not have, and its
|
|
72
|
+
* absence is how a year of unmeasurable promises turns into a year of kept
|
|
73
|
+
* ones.
|
|
74
|
+
*/
|
|
75
|
+
promises: {
|
|
76
|
+
planned: number;
|
|
77
|
+
arrived: number;
|
|
78
|
+
notArrived: number;
|
|
79
|
+
cannotTell: number;
|
|
80
|
+
/**
|
|
81
|
+
* Dollars the plans projected, summed.
|
|
82
|
+
*
|
|
83
|
+
* **There is deliberately no `arrivedUsd` beside it**, and the reason is
|
|
84
|
+
* worth the paragraph: a verification says whether each action *arrived*,
|
|
85
|
+
* and its `observed` map carries where the money sits now — but the
|
|
86
|
+
* document has never carried a per-action figure for the saving that
|
|
87
|
+
* actually landed. Summing one out of the observations would mean deciding
|
|
88
|
+
* which of several numbers per action is "the saving", which is a
|
|
89
|
+
* judgement the verification refused to make and this module has no
|
|
90
|
+
* standing to make on its behalf.
|
|
91
|
+
*
|
|
92
|
+
* So the year says what was promised and how many promises were kept, and
|
|
93
|
+
* says plainly that it cannot put a dollar figure on the kept ones. The
|
|
94
|
+
* alternative — a plausible number assembled here — is precisely the
|
|
95
|
+
* annual-report arithmetic this document exists to replace.
|
|
96
|
+
*/
|
|
97
|
+
projectedUsd: number;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The year's outcome coverage, or null when nothing recorded one.
|
|
101
|
+
*
|
|
102
|
+
* Null rather than a rate of zero, for the reason it has been null
|
|
103
|
+
* everywhere since 1.50.4: an uninstrumented year and a failing year are
|
|
104
|
+
* different sentences.
|
|
105
|
+
*/
|
|
106
|
+
outcomes: {
|
|
107
|
+
recorded: number;
|
|
108
|
+
parsed: number;
|
|
109
|
+
/** Share of the year's spend that carried no outcome. */
|
|
110
|
+
unrecordedUsd: number;
|
|
111
|
+
} | null;
|
|
112
|
+
/**
|
|
113
|
+
* Everything this record cannot say, named.
|
|
114
|
+
*
|
|
115
|
+
* The section an annual report is usually missing, and the reason this one
|
|
116
|
+
* is worth trusting: a document that lists its own blind spots is a document
|
|
117
|
+
* somebody can act on the rest of.
|
|
118
|
+
*/
|
|
119
|
+
cannotSay: string[];
|
|
120
|
+
}
|
|
121
|
+
export declare function annualRecord(year: string, periods: readonly AnnualPeriod[]): AnnualRecord;
|
|
122
|
+
//# sourceMappingURL=annual.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annual.d.ts","sourceRoot":"","sources":["../src/annual.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,uDAAuD;IACvD,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D;;;;;OAKG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,QAAQ,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB;;;;;;;;;;;;;;;;WAgBG;QACH,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF;;;;;;OAMG;IACH,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,yDAAyD;QACzD,aAAa,EAAE,MAAM,CAAC;KACvB,GAAG,IAAI,CAAC;IACT;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AASD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,CA+DzF"}
|
package/dist/annual.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The year, assembled from what was already written down.
|
|
3
|
+
*
|
|
4
|
+
* The last chapter of the arc, and the one that turns this product's argument
|
|
5
|
+
* into something a stranger can audit. Everything below comes from the store
|
|
6
|
+
* and the plans a team already keeps: **no new data, and nothing computed that
|
|
7
|
+
* could not be checked against a document that already exists.**
|
|
8
|
+
*
|
|
9
|
+
* That constraint is the whole design. An annual report is the document most
|
|
10
|
+
* likely to be quoted out of the room it was written in, and the one nobody
|
|
11
|
+
* goes back to verify. So it may not contain a single figure that this tool
|
|
12
|
+
* would refuse to print anywhere else — which means it is mostly a summing
|
|
13
|
+
* exercise with a great many refusals attached.
|
|
14
|
+
*
|
|
15
|
+
* ## Four questions, and the fourth is the one that matters
|
|
16
|
+
*
|
|
17
|
+
* What was spent. What was planned. What arrived. **And what could not be
|
|
18
|
+
* told** — which in a normal annual report is silently folded into one of the
|
|
19
|
+
* other three, almost always into the flattering one.
|
|
20
|
+
*
|
|
21
|
+
* `verify` has kept those three outcomes apart since 1.39. A year is where the
|
|
22
|
+
* temptation to collapse them is strongest, because "eleven of fourteen
|
|
23
|
+
* actions arrived" reads better than "eleven arrived, one did not, and two
|
|
24
|
+
* could not be judged" — and the second sentence is the one that tells
|
|
25
|
+
* somebody their measurement has a hole in it.
|
|
26
|
+
*
|
|
27
|
+
* ## It reports the record, not the team
|
|
28
|
+
*
|
|
29
|
+
* No per-person anything, no velocity, no ranking of who planned well. The
|
|
30
|
+
* doctrine rule from 1.44, and it matters most here: an annual document is
|
|
31
|
+
* exactly where a cost tool starts being used for performance review, and the
|
|
32
|
+
* way to not be is to hold no data that could be.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The twelve months of a year, so a gap is a fact rather than an absence.
|
|
36
|
+
*/
|
|
37
|
+
function monthsOf(year) {
|
|
38
|
+
return Array.from({ length: 12 }, (_, i) => `${year}-${String(i + 1).padStart(2, '0')}`);
|
|
39
|
+
}
|
|
40
|
+
export function annualRecord(year, periods) {
|
|
41
|
+
const inYear = [...periods]
|
|
42
|
+
.filter((p) => p.month.startsWith(`${year}-`))
|
|
43
|
+
.sort((a, b) => a.month.localeCompare(b.month));
|
|
44
|
+
const present = new Set(inYear.map((p) => p.month));
|
|
45
|
+
const missingMonths = monthsOf(year).filter((m) => !present.has(m));
|
|
46
|
+
let planned = 0;
|
|
47
|
+
let arrived = 0;
|
|
48
|
+
let notArrived = 0;
|
|
49
|
+
let cannotTell = 0;
|
|
50
|
+
let projectedUsd = 0;
|
|
51
|
+
for (const period of inYear) {
|
|
52
|
+
if (period.plan !== undefined) {
|
|
53
|
+
planned += period.plan.actions.length;
|
|
54
|
+
projectedUsd += period.plan.projectedSavingUsd;
|
|
55
|
+
}
|
|
56
|
+
if (period.verification !== undefined) {
|
|
57
|
+
arrived += period.verification.arrived;
|
|
58
|
+
notArrived += period.verification.notArrived;
|
|
59
|
+
cannotTell += period.verification.cannotTell;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const withOutcomes = inYear.filter((p) => p.outcomes !== undefined);
|
|
63
|
+
const outcomes = withOutcomes.length === 0
|
|
64
|
+
? null
|
|
65
|
+
: {
|
|
66
|
+
recorded: withOutcomes.reduce((sum, p) => sum + (p.outcomes?.coverage.recorded ?? 0), 0),
|
|
67
|
+
parsed: withOutcomes.reduce((sum, p) => sum + (p.outcomes?.coverage.parsed ?? 0), 0),
|
|
68
|
+
unrecordedUsd: withOutcomes.reduce((sum, p) => sum + (p.outcomes?.coverage.unrecordedUsd ?? 0), 0),
|
|
69
|
+
};
|
|
70
|
+
const cannotSay = [];
|
|
71
|
+
if (missingMonths.length > 0)
|
|
72
|
+
cannotSay.push('months-missing');
|
|
73
|
+
if (planned === 0)
|
|
74
|
+
cannotSay.push('nothing-was-planned');
|
|
75
|
+
else if (arrived + notArrived + cannotTell === 0)
|
|
76
|
+
cannotSay.push('nothing-was-verified');
|
|
77
|
+
if (cannotTell > 0)
|
|
78
|
+
cannotSay.push('some-promises-unjudgeable');
|
|
79
|
+
/**
|
|
80
|
+
* Named every time there is anything to verify, because it is a permanent
|
|
81
|
+
* limit of the record rather than a gap in this particular year.
|
|
82
|
+
*/
|
|
83
|
+
if (arrived > 0)
|
|
84
|
+
cannotSay.push('arrived-savings-not-quantified');
|
|
85
|
+
if (outcomes === null)
|
|
86
|
+
cannotSay.push('no-outcomes-recorded');
|
|
87
|
+
else if (outcomes.recorded < outcomes.parsed)
|
|
88
|
+
cannotSay.push('outcome-coverage-partial');
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
year,
|
|
92
|
+
months: inYear.map((p) => ({ month: p.month, usd: p.usd, calls: p.calls })),
|
|
93
|
+
missingMonths,
|
|
94
|
+
totalUsd: inYear.reduce((sum, p) => sum + p.usd, 0),
|
|
95
|
+
totalCalls: inYear.reduce((sum, p) => sum + p.calls, 0),
|
|
96
|
+
promises: { planned, arrived, notArrived, cannotTell, projectedUsd },
|
|
97
|
+
outcomes,
|
|
98
|
+
cannotSay,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=annual.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"annual.js","sourceRoot":"","sources":["../src/annual.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a committed-use deal would have been worth **on the traffic you
|
|
3
|
+
* actually had**.
|
|
4
|
+
*
|
|
5
|
+
* Providers sell committed-use and reserved-capacity contracts, and every team
|
|
6
|
+
* that signs one is doing arithmetic in a spreadsheet against a number they
|
|
7
|
+
* guessed. That is exactly the failure this product exists to end — and it is
|
|
8
|
+
* the highest-stakes instance of it, because the guess is annual and signed.
|
|
9
|
+
*
|
|
10
|
+
* ## An as-if calculation, and the wording never blurs it
|
|
11
|
+
*
|
|
12
|
+
* "On the traffic you actually had, this commitment would have saved $X" is a
|
|
13
|
+
* measurement of the past.
|
|
14
|
+
*
|
|
15
|
+
* "You will save $X" is a claim about the future, and this product has refused
|
|
16
|
+
* that at every scale since 1.27. Nothing here projects, extrapolates, fits a
|
|
17
|
+
* trend or annualises a partial month. Every figure describes months that
|
|
18
|
+
* happened, and the type says so in a field a machine reader can check.
|
|
19
|
+
*
|
|
20
|
+
* ## Both directions, because one direction is the sales pitch
|
|
21
|
+
*
|
|
22
|
+
* A commitment is a **floor** as well as a discount. Below the floor you pay
|
|
23
|
+
* for capacity you did not use, and a saving quoted without that half is not
|
|
24
|
+
* an analysis — it is the vendor's slide.
|
|
25
|
+
*
|
|
26
|
+
* So every month is priced both ways, the months that would have fallen short
|
|
27
|
+
* are counted and named, and what the unused floor would have cost is its own
|
|
28
|
+
* figure rather than netted quietly against the good months.
|
|
29
|
+
*
|
|
30
|
+
* ## The shortfall risk is a count, not a probability
|
|
31
|
+
*
|
|
32
|
+
* "Three of your last twelve months would have fallen short, by $400, $150 and
|
|
33
|
+
* $2,900" is a measurement. "There is a 25% chance of shortfall" is a model of
|
|
34
|
+
* a distribution nobody fitted, presented with the authority of arithmetic.
|
|
35
|
+
* Only the first is available from a log, so only the first is printed.
|
|
36
|
+
*/
|
|
37
|
+
/** The deal, as the provider states it. */
|
|
38
|
+
export interface CommitmentTerms {
|
|
39
|
+
/**
|
|
40
|
+
* What you commit to spending each month, **after** the discount — which is
|
|
41
|
+
* how these contracts are almost always written.
|
|
42
|
+
*
|
|
43
|
+
* If your discounted usage comes in under this, you pay this anyway.
|
|
44
|
+
*/
|
|
45
|
+
monthlyFloorUsd: number;
|
|
46
|
+
/** The discount, 0-1. 0.2 is twenty per cent off. */
|
|
47
|
+
discount: number;
|
|
48
|
+
/** How many months the commitment runs. */
|
|
49
|
+
months: number;
|
|
50
|
+
}
|
|
51
|
+
/** One measured month, as the caller sliced it. */
|
|
52
|
+
export interface MeasuredMonth {
|
|
53
|
+
/** `YYYY-MM`. */
|
|
54
|
+
month: string;
|
|
55
|
+
usd: number;
|
|
56
|
+
}
|
|
57
|
+
export interface MonthReplay {
|
|
58
|
+
month: string;
|
|
59
|
+
/** What was actually paid, with no commitment. */
|
|
60
|
+
listUsd: number;
|
|
61
|
+
/** What the discounted usage would have come to. */
|
|
62
|
+
discountedUsd: number;
|
|
63
|
+
/** What would actually have been paid: the floor, or the discounted usage. */
|
|
64
|
+
paidUsd: number;
|
|
65
|
+
/** Positive means the commitment saved money that month. */
|
|
66
|
+
savingUsd: number;
|
|
67
|
+
/** Whether the floor was the binding number — a month that fell short. */
|
|
68
|
+
shortfall: boolean;
|
|
69
|
+
/** What the unused floor cost, in a month that fell short. Zero otherwise. */
|
|
70
|
+
unusedFloorUsd: number;
|
|
71
|
+
}
|
|
72
|
+
export type CommitmentUnknown = 'no-history' | 'too-few-months' | 'partial-months-excluded-everything';
|
|
73
|
+
export interface CommitmentReplay {
|
|
74
|
+
/**
|
|
75
|
+
* Always `measured-past`. There is no other value, and the field exists so a
|
|
76
|
+
* machine reader cannot mistake this for a projection — the same reason
|
|
77
|
+
* every other document in this product carries its provenance.
|
|
78
|
+
*/
|
|
79
|
+
provenance: 'measured-past';
|
|
80
|
+
months: MonthReplay[];
|
|
81
|
+
/** Summed over the months replayed, positive meaning the deal won. */
|
|
82
|
+
netUsd: number;
|
|
83
|
+
/** What the good months saved, before the shortfalls are taken off. */
|
|
84
|
+
savedInGoodMonthsUsd: number;
|
|
85
|
+
/**
|
|
86
|
+
* What the shortfall months cost, kept as its own figure.
|
|
87
|
+
*
|
|
88
|
+
* Netted against the savings it disappears, and the disappearing is the
|
|
89
|
+
* whole trick a vendor's slide relies on.
|
|
90
|
+
*/
|
|
91
|
+
lostToUnusedFloorUsd: number;
|
|
92
|
+
/** How many of the measured months would have fallen short. A count, never a rate. */
|
|
93
|
+
shortfallMonths: number;
|
|
94
|
+
/**
|
|
95
|
+
* The monthly spend at which the commitment stops losing money.
|
|
96
|
+
*
|
|
97
|
+
* Equal to the floor: below it you pay the floor for less usage. Above it
|
|
98
|
+
* the saving grows, first as (spend − floor) and then, once discounted usage
|
|
99
|
+
* clears the floor, as spend × discount.
|
|
100
|
+
*/
|
|
101
|
+
breakEvenMonthlyUsd: number;
|
|
102
|
+
/**
|
|
103
|
+
* Measured spread across the months replayed — lowest and highest, and how
|
|
104
|
+
* far the range spans as a share of the median.
|
|
105
|
+
*
|
|
106
|
+
* The honest form of "shortfall risk": a reader looking at a range wider than
|
|
107
|
+
* the floor can see the deal is a bet, without anybody modelling a
|
|
108
|
+
* distribution nobody fitted.
|
|
109
|
+
*/
|
|
110
|
+
spread: {
|
|
111
|
+
lowUsd: number;
|
|
112
|
+
highUsd: number;
|
|
113
|
+
medianUsd: number;
|
|
114
|
+
} | null;
|
|
115
|
+
/** Set when nothing could be replayed. A refusal never arrives bare. */
|
|
116
|
+
unknown: CommitmentUnknown | null;
|
|
117
|
+
/** How many whole months would settle it, when there are too few. */
|
|
118
|
+
monthsNeeded: number | null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The fewest whole months worth replaying a commitment against.
|
|
122
|
+
*
|
|
123
|
+
* Three. Two months cannot show a shortfall pattern and one cannot show
|
|
124
|
+
* anything at all — and a commitment is signed for a year, so an answer from a
|
|
125
|
+
* single month is a year-long decision made on a fortnight of evidence.
|
|
126
|
+
*/
|
|
127
|
+
export declare const MIN_MONTHS_FOR_REPLAY = 3;
|
|
128
|
+
export declare function replayCommitment(history: readonly MeasuredMonth[], terms: CommitmentTerms): CommitmentReplay;
|
|
129
|
+
/**
|
|
130
|
+
* Whether the replay covers as long as the commitment runs.
|
|
131
|
+
*
|
|
132
|
+
* Kept separate from the refusals because it does not stop the arithmetic: six
|
|
133
|
+
* months of history against a twelve-month deal is a real answer about six
|
|
134
|
+
* months, and saying so is more useful than refusing. What it must not do is
|
|
135
|
+
* go unsaid — a twelve-month decision read off half a year of evidence, with
|
|
136
|
+
* nothing on the page marking the gap, is the spreadsheet this module was
|
|
137
|
+
* written to replace.
|
|
138
|
+
*/
|
|
139
|
+
export declare function coversTheTerm(replay: CommitmentReplay, terms: CommitmentTerms): {
|
|
140
|
+
covered: number;
|
|
141
|
+
ofMonths: number;
|
|
142
|
+
short: boolean;
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=commitment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commitment.d.ts","sourceRoot":"","sources":["../src/commitment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ,gBAAgB,GAChB,oCAAoC,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,UAAU,EAAE,eAAe,CAAC;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B,sFAAsF;IACtF,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACtE,wEAAwE;IACxE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAClC,qEAAqE;IACrE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,KAAK,EAAE,eAAe,GACrB,gBAAgB,CAkDlB;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,eAAe,GACrB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAMvD"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a committed-use deal would have been worth **on the traffic you
|
|
3
|
+
* actually had**.
|
|
4
|
+
*
|
|
5
|
+
* Providers sell committed-use and reserved-capacity contracts, and every team
|
|
6
|
+
* that signs one is doing arithmetic in a spreadsheet against a number they
|
|
7
|
+
* guessed. That is exactly the failure this product exists to end — and it is
|
|
8
|
+
* the highest-stakes instance of it, because the guess is annual and signed.
|
|
9
|
+
*
|
|
10
|
+
* ## An as-if calculation, and the wording never blurs it
|
|
11
|
+
*
|
|
12
|
+
* "On the traffic you actually had, this commitment would have saved $X" is a
|
|
13
|
+
* measurement of the past.
|
|
14
|
+
*
|
|
15
|
+
* "You will save $X" is a claim about the future, and this product has refused
|
|
16
|
+
* that at every scale since 1.27. Nothing here projects, extrapolates, fits a
|
|
17
|
+
* trend or annualises a partial month. Every figure describes months that
|
|
18
|
+
* happened, and the type says so in a field a machine reader can check.
|
|
19
|
+
*
|
|
20
|
+
* ## Both directions, because one direction is the sales pitch
|
|
21
|
+
*
|
|
22
|
+
* A commitment is a **floor** as well as a discount. Below the floor you pay
|
|
23
|
+
* for capacity you did not use, and a saving quoted without that half is not
|
|
24
|
+
* an analysis — it is the vendor's slide.
|
|
25
|
+
*
|
|
26
|
+
* So every month is priced both ways, the months that would have fallen short
|
|
27
|
+
* are counted and named, and what the unused floor would have cost is its own
|
|
28
|
+
* figure rather than netted quietly against the good months.
|
|
29
|
+
*
|
|
30
|
+
* ## The shortfall risk is a count, not a probability
|
|
31
|
+
*
|
|
32
|
+
* "Three of your last twelve months would have fallen short, by $400, $150 and
|
|
33
|
+
* $2,900" is a measurement. "There is a 25% chance of shortfall" is a model of
|
|
34
|
+
* a distribution nobody fitted, presented with the authority of arithmetic.
|
|
35
|
+
* Only the first is available from a log, so only the first is printed.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* The fewest whole months worth replaying a commitment against.
|
|
39
|
+
*
|
|
40
|
+
* Three. Two months cannot show a shortfall pattern and one cannot show
|
|
41
|
+
* anything at all — and a commitment is signed for a year, so an answer from a
|
|
42
|
+
* single month is a year-long decision made on a fortnight of evidence.
|
|
43
|
+
*/
|
|
44
|
+
export const MIN_MONTHS_FOR_REPLAY = 3;
|
|
45
|
+
export function replayCommitment(history, terms) {
|
|
46
|
+
const bare = (unknown, have) => ({
|
|
47
|
+
provenance: 'measured-past',
|
|
48
|
+
months: [],
|
|
49
|
+
netUsd: 0,
|
|
50
|
+
savedInGoodMonthsUsd: 0,
|
|
51
|
+
lostToUnusedFloorUsd: 0,
|
|
52
|
+
shortfallMonths: 0,
|
|
53
|
+
breakEvenMonthlyUsd: terms.monthlyFloorUsd,
|
|
54
|
+
spread: null,
|
|
55
|
+
unknown,
|
|
56
|
+
monthsNeeded: Math.max(0, MIN_MONTHS_FOR_REPLAY - have),
|
|
57
|
+
});
|
|
58
|
+
if (history.length === 0)
|
|
59
|
+
return bare('no-history', 0);
|
|
60
|
+
if (history.length < MIN_MONTHS_FOR_REPLAY)
|
|
61
|
+
return bare('too-few-months', history.length);
|
|
62
|
+
const months = history.map((entry) => {
|
|
63
|
+
const discountedUsd = entry.usd * (1 - terms.discount);
|
|
64
|
+
const shortfall = discountedUsd < terms.monthlyFloorUsd;
|
|
65
|
+
const paidUsd = shortfall ? terms.monthlyFloorUsd : discountedUsd;
|
|
66
|
+
return {
|
|
67
|
+
month: entry.month,
|
|
68
|
+
listUsd: entry.usd,
|
|
69
|
+
discountedUsd,
|
|
70
|
+
paidUsd,
|
|
71
|
+
savingUsd: entry.usd - paidUsd,
|
|
72
|
+
shortfall,
|
|
73
|
+
// What the floor bought that nobody used. Its own figure, never netted.
|
|
74
|
+
unusedFloorUsd: shortfall ? terms.monthlyFloorUsd - discountedUsd : 0,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
const sorted = [...history].map((m) => m.usd).sort((a, b) => a - b);
|
|
78
|
+
const mid = Math.floor(sorted.length / 2);
|
|
79
|
+
const medianUsd = sorted.length % 2 === 0 ? ((sorted[mid - 1] ?? 0) + (sorted[mid] ?? 0)) / 2 : (sorted[mid] ?? 0);
|
|
80
|
+
return {
|
|
81
|
+
provenance: 'measured-past',
|
|
82
|
+
months,
|
|
83
|
+
netUsd: months.reduce((sum, m) => sum + m.savingUsd, 0),
|
|
84
|
+
savedInGoodMonthsUsd: months.filter((m) => !m.shortfall).reduce((sum, m) => sum + m.savingUsd, 0),
|
|
85
|
+
lostToUnusedFloorUsd: months.reduce((sum, m) => sum + m.unusedFloorUsd, 0),
|
|
86
|
+
shortfallMonths: months.filter((m) => m.shortfall).length,
|
|
87
|
+
breakEvenMonthlyUsd: terms.monthlyFloorUsd,
|
|
88
|
+
spread: { lowUsd: sorted[0] ?? 0, highUsd: sorted[sorted.length - 1] ?? 0, medianUsd },
|
|
89
|
+
unknown: null,
|
|
90
|
+
monthsNeeded: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether the replay covers as long as the commitment runs.
|
|
95
|
+
*
|
|
96
|
+
* Kept separate from the refusals because it does not stop the arithmetic: six
|
|
97
|
+
* months of history against a twelve-month deal is a real answer about six
|
|
98
|
+
* months, and saying so is more useful than refusing. What it must not do is
|
|
99
|
+
* go unsaid — a twelve-month decision read off half a year of evidence, with
|
|
100
|
+
* nothing on the page marking the gap, is the spreadsheet this module was
|
|
101
|
+
* written to replace.
|
|
102
|
+
*/
|
|
103
|
+
export function coversTheTerm(replay, terms) {
|
|
104
|
+
return {
|
|
105
|
+
covered: replay.months.length,
|
|
106
|
+
ofMonths: terms.months,
|
|
107
|
+
short: replay.months.length < terms.months,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=commitment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commitment.js","sourceRoot":"","sources":["../src/commitment.ts"],"names":[],"mappings":"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"}
|
package/dist/conform.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* nobody upgrades. Only *absent* required fields and *wrong* types fail.
|
|
26
26
|
*/
|
|
27
27
|
/** The documents this project emits, and their names. */
|
|
28
|
-
export type ContractName = 'usage-log' | 'profile' | 'plan' | 'verification' | 'history' | 'connected' | 'cost-answer';
|
|
28
|
+
export type ContractName = 'usage-log' | 'profile' | 'plan' | 'verification' | 'history' | 'connected' | 'cost-answer' | 'outcome-report' | 'annual-record';
|
|
29
29
|
export interface ConformanceProblem {
|
|
30
30
|
/** Where: `line 12` for a log, or a dotted path inside a document. */
|
|
31
31
|
at: string;
|
package/dist/conform.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conform.d.ts","sourceRoot":"","sources":["../src/conform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,yDAAyD;AACzD,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,MAAM,GACN,cAAc,GACd,SAAS,GACT,WAAW,GACX,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"conform.d.ts","sourceRoot":"","sources":["../src/conform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,yDAAyD;AACzD,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,SAAS,GACT,MAAM,GACN,cAAc,GACd,SAAS,GACT,WAAW,GACX,aAAa,GACb,gBAAgB,GAChB,eAAe,CAAC;AAEpB,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI;IACF,kCAAkC;IAChC,SAAS;IACX,qDAAqD;OACnD,YAAY;IACd,qEAAqE;OACnE,iBAAiB;IACnB,2DAA2D;OACzD,YAAY,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,CAAC,CAAC;IACjB,gEAAgE;IAChE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,+DAA+D;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;CACnB;AA+SD,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CAwIrF"}
|
package/dist/conform.js
CHANGED
|
@@ -182,12 +182,73 @@ const DOCUMENT_RULES = {
|
|
|
182
182
|
rule('total', 'an object', isObject),
|
|
183
183
|
rule('unavailable', 'an array of findings this source cannot support', isArray),
|
|
184
184
|
],
|
|
185
|
+
/**
|
|
186
|
+
* The outcome chapter — the standard is only worth something if its
|
|
187
|
+
* refusals travel with it.
|
|
188
|
+
*
|
|
189
|
+
* Another tool emitting this format has to handle a **missing numerator**
|
|
190
|
+
* the same way this one does: a rate that is `null` rather than `0` when
|
|
191
|
+
* nothing was recorded, a `noRate` beside it saying which of the two reasons
|
|
192
|
+
* applies, and undeclared values kept in their own list rather than folded
|
|
193
|
+
* into the failures. A format that carried the fields and lost the refusals
|
|
194
|
+
* would be worse than no format, because it would look interoperable.
|
|
195
|
+
*/
|
|
196
|
+
'outcome-report': [
|
|
197
|
+
rule('slices', 'an array of declared outcome values, dearest first', isArray),
|
|
198
|
+
rule('undeclared', 'an array — named, never counted as failures', isArray),
|
|
199
|
+
rule('coverage', 'an object with recorded, parsed and unrecordedUsd', isObject),
|
|
200
|
+
rule('successShareOfRecordedUsd', 'a number, or **null** when nothing was recorded — never 0, which is a real and terrible measurement rather than an absence',
|
|
201
|
+
// `absence-as-zero` is detected from the word "null" in the expected
|
|
202
|
+
// text, so a tool emitting 0 here is told it emitted an absence as a
|
|
203
|
+
// measurement rather than merely getting a type wrong.
|
|
204
|
+
(v) => v === null || isNumber(v)),
|
|
205
|
+
rule('noRate', 'a string saying why there is no rate, or null when there is one — a refusal never arrives bare', (v) => v === null || typeof v === 'string'),
|
|
206
|
+
],
|
|
207
|
+
'annual-record': [
|
|
208
|
+
rule('months', 'an array, oldest first', isArray),
|
|
209
|
+
rule('missingMonths', 'an array — named, never interpolated', isArray),
|
|
210
|
+
rule('promises', 'an object with planned, arrived, notArrived and cannotTell', isObject),
|
|
211
|
+
rule('outcomes', 'an object, or null when nothing recorded one', (v) => v === null || isObject(v)),
|
|
212
|
+
rule('cannotSay', 'an array of what this record cannot answer', isArray),
|
|
213
|
+
],
|
|
185
214
|
'cost-answer': [
|
|
186
215
|
rule('verdict', 'one of within, over, cannot-tell', (v) => v === 'within' || v === 'over' || v === 'cannot-tell'),
|
|
187
216
|
rule('call', 'an object, or null when nothing was described', (v) => v === null || isObject(v)),
|
|
188
217
|
rule('budget', 'an object, or null when there is no budget', (v) => v === null || isObject(v)),
|
|
189
218
|
],
|
|
190
219
|
};
|
|
220
|
+
const CROSS_RULES = {
|
|
221
|
+
'outcome-report': [
|
|
222
|
+
{
|
|
223
|
+
at: 'successShareOfRecordedUsd',
|
|
224
|
+
kind: 'absence-as-zero',
|
|
225
|
+
ok: (doc) => {
|
|
226
|
+
const coverage = doc.coverage;
|
|
227
|
+
const recorded = typeof coverage?.recorded === 'number' ? coverage.recorded : null;
|
|
228
|
+
if (recorded !== 0)
|
|
229
|
+
return true;
|
|
230
|
+
return doc.successShareOfRecordedUsd === null;
|
|
231
|
+
},
|
|
232
|
+
detail: 'nothing was recorded, so the rate must be null — 0 is a real and terrible measurement and this is an absence',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
at: 'noRate',
|
|
236
|
+
kind: 'missing',
|
|
237
|
+
ok: (doc) => (doc.successShareOfRecordedUsd === null ? doc.noRate !== null : doc.noRate === null),
|
|
238
|
+
detail: 'a null rate needs a reason beside it and a stated rate must not carry one — a refusal never arrives bare, and a reason attached to an answer is two answers',
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
'annual-record': [
|
|
242
|
+
{
|
|
243
|
+
at: 'cannotSay',
|
|
244
|
+
kind: 'missing',
|
|
245
|
+
ok: (doc) => !Array.isArray(doc.missingMonths) ||
|
|
246
|
+
doc.missingMonths.length === 0 ||
|
|
247
|
+
(Array.isArray(doc.cannotSay) && doc.cannotSay.includes('months-missing')),
|
|
248
|
+
detail: 'months are missing and cannotSay does not say so — a year that quietly covers nine months and prints an annual total is wrong by a quarter',
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
191
252
|
/**
|
|
192
253
|
* Which contract a document is claiming to be.
|
|
193
254
|
*
|
|
@@ -198,6 +259,10 @@ const DOCUMENT_RULES = {
|
|
|
198
259
|
function contractOf(doc) {
|
|
199
260
|
if (Array.isArray(doc.byLabelAndModel))
|
|
200
261
|
return 'profile';
|
|
262
|
+
if (Array.isArray(doc.missingMonths) && isObject(doc.promises))
|
|
263
|
+
return 'annual-record';
|
|
264
|
+
if (Array.isArray(doc.undeclared) && isObject(doc.coverage))
|
|
265
|
+
return 'outcome-report';
|
|
201
266
|
if (Array.isArray(doc.periods) && Array.isArray(doc.runs))
|
|
202
267
|
return 'history';
|
|
203
268
|
if (Array.isArray(doc.actions) && typeof doc.arrived === 'number')
|
|
@@ -329,6 +394,13 @@ export function conform(text, options = {}) {
|
|
|
329
394
|
});
|
|
330
395
|
}
|
|
331
396
|
}
|
|
397
|
+
// Relational rules last, so a document with a missing field is told about the
|
|
398
|
+
// field before it is told about a relationship that field is half of.
|
|
399
|
+
for (const cross of CROSS_RULES[contract] ?? []) {
|
|
400
|
+
if (!cross.ok(doc)) {
|
|
401
|
+
problems.push({ at: cross.at, kind: cross.kind, detail: cross.detail });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
332
404
|
return {
|
|
333
405
|
schemaVersion: 1,
|
|
334
406
|
contract,
|