segreant 0.1.0 → 0.2.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/LICENSING.md +5 -3
- package/README.md +57 -19
- package/bin/segreant-statusline.mjs +20 -0
- package/bin/segreant.mjs +19 -8
- package/dist/alerts/detect.js +13 -0
- package/dist/billing/epistemic.js +4 -0
- package/dist/billing/importer.js +29 -10
- package/dist/billing/reconcile.js +122 -32
- package/dist/billing/scope.js +7 -0
- package/dist/budget/guard.js +33 -4
- package/dist/cli/allocCmd.js +73 -4
- package/dist/cli/askModelPrices.js +59 -0
- package/dist/cli/backupCmd.js +2 -1
- package/dist/cli/basisLines.js +45 -0
- package/dist/cli/billingCmd.js +68 -9
- package/dist/cli/capitalCmd.js +2 -1
- package/dist/cli/connectCmd.js +50 -9
- package/dist/cli/egressCmd.js +15 -3
- package/dist/cli/evidenceCmd.js +6 -3
- package/dist/cli/flags.js +68 -0
- package/dist/cli/homeCmd.js +117 -0
- package/dist/cli/importCmd.js +210 -54
- package/dist/cli/launchCmd.js +2 -1
- package/dist/cli/opsCmd.js +49 -2
- package/dist/cli/planCmd.js +181 -0
- package/dist/cli/pricingDeclareCmd.js +178 -0
- package/dist/cli/quotaCmd.js +190 -0
- package/dist/cli/runCmd.js +203 -51
- package/dist/cli/showCmd.js +37 -11
- package/dist/cli/teamCmd.js +5 -5
- package/dist/cli/ui.js +24 -8
- package/dist/cli/valueCmd.js +189 -14
- package/dist/cli.js +102 -21
- package/dist/config.js +95 -6
- package/dist/connect/antigravity.js +310 -0
- package/dist/connect/claudeCode.js +43 -3
- package/dist/connect/codex.js +38 -1
- package/dist/connect/importShared.js +58 -5
- package/dist/connect/opencode.js +37 -9
- package/dist/connect/protobuf.js +126 -0
- package/dist/connect/registry.js +46 -0
- package/dist/connect/repoRoot.js +112 -0
- package/dist/cost/basis.js +168 -0
- package/dist/cost/declarations.js +37 -0
- package/dist/cost/exactPricing.js +4 -1
- package/dist/cost/firstSight.js +98 -0
- package/dist/cost/overrides.js +230 -0
- package/dist/cost/pricing.js +228 -15
- package/dist/cost/reprice.js +130 -0
- package/dist/dashboard/claim-support.js +11 -8
- package/dist/dashboard/contracts.js +23 -3
- package/dist/dashboard/routes.js +980 -112
- package/dist/dashboard/sessions.js +345 -0
- package/dist/dashboard/settings.js +2 -1
- package/dist/dashboard/value-cache.js +105 -0
- package/dist/dashboard/web/app/components/claimInspector.js +6 -5
- package/dist/dashboard/web/app/components/drawer.js +105 -35
- package/dist/dashboard/web/app/components/ruleFields.js +144 -0
- package/dist/dashboard/web/app/components/spine.js +54 -24
- package/dist/dashboard/web/app/components/workflow.js +82 -0
- package/dist/dashboard/web/app/core/actions.js +734 -23
- package/dist/dashboard/web/app/core/api.js +37 -1
- package/dist/dashboard/web/app/core/chain.js +11 -4
- package/dist/dashboard/web/app/core/claimLayers.js +179 -51
- package/dist/dashboard/web/app/core/claimTypes.js +30 -0
- package/dist/dashboard/web/app/core/fmt.js +19 -2
- package/dist/dashboard/web/app/core/generated-contract.js +23 -3
- package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
- package/dist/dashboard/web/app/core/generated-types.js +1 -1
- package/dist/dashboard/web/app/core/griffin.js +10 -0
- package/dist/dashboard/web/app/core/jobs.js +12 -0
- package/dist/dashboard/web/app/core/period.js +35 -0
- package/dist/dashboard/web/app/core/periodWords.js +10 -0
- package/dist/dashboard/web/app/core/periods.js +32 -0
- package/dist/dashboard/web/app/core/pricingActions.js +264 -0
- package/dist/dashboard/web/app/core/privacyText.js +78 -0
- package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
- package/dist/dashboard/web/app/core/registry.js +79 -46
- package/dist/dashboard/web/app/core/slug.js +9 -0
- package/dist/dashboard/web/app/core/theme.js +48 -0
- package/dist/dashboard/web/app/core/view.js +69 -0
- package/dist/dashboard/web/app/main.js +197 -15
- package/dist/dashboard/web/app/views/allocation.js +249 -61
- package/dist/dashboard/web/app/views/control.js +57 -16
- package/dist/dashboard/web/app/views/data.js +4 -4
- package/dist/dashboard/web/app/views/evidence.js +336 -87
- package/dist/dashboard/web/app/views/privacy.js +13 -0
- package/dist/dashboard/web/app/views/spend.js +30 -15
- package/dist/dashboard/web/app/views/statement.js +189 -0
- package/dist/dashboard/web/app/views/system.js +44 -4
- package/dist/dashboard/web/app/views/value.js +105 -59
- package/dist/dashboard/web/app/views/workbench.js +312 -0
- package/dist/dashboard/web/classic.html +16 -4
- package/dist/dashboard/web/index.html +1 -1
- package/dist/dashboard/web/styles/app.css +536 -23
- package/dist/demo/seed.js +153 -14
- package/dist/economics/ledger.js +1 -1
- package/dist/egress/policy.js +129 -5
- package/dist/export/allocationCsv.js +70 -0
- package/dist/export/billingCsv.js +6 -2
- package/dist/export/csv.js +10 -2
- package/dist/export/economic.js +6 -2
- package/dist/export/statementCsv.js +104 -0
- package/dist/git/correlate.js +86 -9
- package/dist/git/quality.js +107 -24
- package/dist/git/repoScope.js +207 -0
- package/dist/git/survivalStore.js +186 -0
- package/dist/guide.js +9 -4
- package/dist/plans/detect.js +147 -0
- package/dist/proxy/server.js +134 -14
- package/dist/proxy/usage.js +9 -4
- package/dist/quota/limits.js +138 -0
- package/dist/quota/statusline.js +68 -0
- package/dist/quota/view.js +122 -0
- package/dist/scan/knownApps.js +6 -2
- package/dist/scan/scan.js +9 -0
- package/dist/store/allocation.js +14 -0
- package/dist/store/db.js +259 -34
- package/dist/store/realization.js +78 -14
- package/dist/store/schema.js +59 -1
- package/dist/store/scopeEvidence.js +67 -0
- package/dist/util/pool.js +19 -0
- package/dist/util/time.js +7 -0
- package/dist/value/realization.js +321 -30
- package/dist/value/report.js +28 -8
- package/dist/value/selfReported.js +10 -9
- package/docs/ARCHITECTURE.md +8 -4
- package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
- package/docs/CLI-JSON-CONTRACT.md +38 -0
- package/docs/DATA-BOUNDARIES.md +66 -6
- package/docs/FAQ.md +43 -9
- package/docs/GETTING-STARTED.md +19 -12
- package/docs/GUIDE.md +81 -18
- package/docs/INTEGRATIONS.md +9 -9
- package/docs/METHODOLOGY.md +3 -2
- package/docs/NEUTRALITY.md +1 -1
- package/docs/PRIVACY.md +51 -0
- package/docs/README.md +1 -0
- package/docs/RELEASE-GATE.md +52 -7
- package/docs/THREAT-MODEL.md +2 -1
- package/package.json +3 -2
- package/pricing/models.json +1366 -60
|
@@ -49,7 +49,24 @@ async function request(path, init) {
|
|
|
49
49
|
}
|
|
50
50
|
if (!response.ok) {
|
|
51
51
|
const detail = await response.text().catch(() => '');
|
|
52
|
-
|
|
52
|
+
// Show the human message, never the raw JSON envelope. The dashboard's own
|
|
53
|
+
// routes answer `{ error: "sentence" }`; some answer `{ error: { message } }`.
|
|
54
|
+
// Only the second was read, so every refusal from a dashboard route, an
|
|
55
|
+
// allocation or billing write included, reached the screen as raw JSON.
|
|
56
|
+
let human = null;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(detail);
|
|
59
|
+
const error = typeof parsed === 'object' && parsed !== null && 'error' in parsed ? parsed.error : undefined;
|
|
60
|
+
if (typeof error === 'string' && error.trim())
|
|
61
|
+
human = error;
|
|
62
|
+
else if (typeof error === 'object' && error !== null && typeof error.message === 'string') {
|
|
63
|
+
human = error.message;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Not JSON: keep the text fallback below.
|
|
68
|
+
}
|
|
69
|
+
throw new ApiError(human ?? (detail.slice(0, 400) || `${response.status} ${response.statusText}`), response.status, path);
|
|
53
70
|
}
|
|
54
71
|
const payload = await response.json();
|
|
55
72
|
// The generic type gives the compiler a view of what the caller consumes;
|
|
@@ -86,6 +103,13 @@ export const api = {
|
|
|
86
103
|
overview: (range, signal) => request(`${routePath('overview')}?range=${encodeURIComponent(range)}`, signal ? { signal } : undefined),
|
|
87
104
|
billing: () => request(routePath('billing')),
|
|
88
105
|
allocation: () => request(routePath('allocation')),
|
|
106
|
+
/** One calendar month, `YYYY-MM`; the four claims side by side, never summed. */
|
|
107
|
+
statement: (month) => request(month ? `${routePath('statement')}?month=${encodeURIComponent(month)}` : routePath('statement')),
|
|
108
|
+
/** The Workbench: the range's sessions, buckets by model, and what their commits came to. */
|
|
109
|
+
sessions: (range, signal) => request(`${routePath('sessions')}?range=${encodeURIComponent(range)}`, signal ? { signal } : undefined),
|
|
110
|
+
/** A download link, not a fetch: the browser saves the month's CSV. */
|
|
111
|
+
statementCsvHref: (month) => `${routePath('statement-csv')}?month=${encodeURIComponent(month)}`,
|
|
112
|
+
allocationRunCsvHref: (allocationRunId) => `${routePath('allocation-run-csv')}?id=${encodeURIComponent(allocationRunId)}`,
|
|
89
113
|
economic: (range = '30d', options = {}) => {
|
|
90
114
|
const params = new URLSearchParams(range === 'all' ? { all: '1' } : { days: '30' });
|
|
91
115
|
if (options.targetCurrency !== undefined)
|
|
@@ -107,8 +131,20 @@ export const api = {
|
|
|
107
131
|
importers: () => request(routePath('importers')),
|
|
108
132
|
/** GET /api/scan is the dry run: it detects and reports, and imports nothing. */
|
|
109
133
|
scan: () => request(routePath('scan')),
|
|
134
|
+
pricingUnknown: () => request(routePath('pricing-unknown')),
|
|
110
135
|
/** Mutating calls are grouped so every write in the GUI is greppable in one place. */
|
|
111
136
|
write: {
|
|
137
|
+
pricingDeclare: (body) => request(routePath('pricing-declare'), { method: 'POST', body: JSON.stringify(body) }),
|
|
138
|
+
pricingUnset: (model) => request(routePath('pricing-unset'), { method: 'POST', body: JSON.stringify({ model }) }),
|
|
139
|
+
reprice: (apply) => request(routePath('reprice'), { method: 'POST', body: JSON.stringify({ apply }) }),
|
|
140
|
+
/**
|
|
141
|
+
* Month close: `scope`, `import`, `adopt`, `reconcile`. Without `apply: true`
|
|
142
|
+
* every op is a preview and writes nothing. An import carries the file's raw
|
|
143
|
+
* bytes in base64, so its digest is the one the CLI computes for the file.
|
|
144
|
+
*/
|
|
145
|
+
billing: (body) => request(routePath('billing-write'), { method: 'POST', body: JSON.stringify(body) }),
|
|
146
|
+
/** Cost centres, rules and runs. A run with `apply: false` is a preview and records nothing. */
|
|
147
|
+
allocation: (body) => request(routePath('allocation-write'), { method: 'POST', body: JSON.stringify(body) }),
|
|
112
148
|
outcome: (input, apply) => request(routePath('outcome-record'), { method: 'POST', body: JSON.stringify({ ...input, apply }) }),
|
|
113
149
|
settings: (patch) => request(routePath('settings-update'), { method: 'POST', body: JSON.stringify(patch) }),
|
|
114
150
|
clearProposals: () => request(routePath('clear-proposals'), { method: 'POST' }),
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* zero.
|
|
16
16
|
*/
|
|
17
17
|
import { api } from './api.js';
|
|
18
|
-
import { buildClaimLayers } from './claimLayers.js';
|
|
19
|
-
export async function
|
|
18
|
+
import { buildClaimLayers, chainIsSampleData } from './claimLayers.js';
|
|
19
|
+
export async function loadChainState(range) {
|
|
20
20
|
const [overview, billing, allocation, value] = await Promise.allSettled([
|
|
21
21
|
api.overview(range),
|
|
22
22
|
api.billing(),
|
|
@@ -24,10 +24,17 @@ export async function loadChain(range) {
|
|
|
24
24
|
api.value(),
|
|
25
25
|
]);
|
|
26
26
|
const ok = (r) => (r.status === 'fulfilled' ? r.value : null);
|
|
27
|
-
|
|
27
|
+
const inputs = {
|
|
28
28
|
overview: ok(overview),
|
|
29
29
|
billing: ok(billing),
|
|
30
30
|
allocation: ok(allocation),
|
|
31
31
|
value: ok(value),
|
|
32
|
-
}
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
layers: buildClaimLayers(inputs, range),
|
|
35
|
+
demo: chainIsSampleData(inputs),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function loadChain(range) {
|
|
39
|
+
return (await loadChainState(range)).layers;
|
|
33
40
|
}
|
|
@@ -25,19 +25,120 @@
|
|
|
25
25
|
* server, no sockets, and no ledger. Every `null` input here means "that
|
|
26
26
|
* endpoint did not answer", and each layer degrades on its own.
|
|
27
27
|
*/
|
|
28
|
+
import { periodWords } from './periodWords.js';
|
|
28
29
|
import { projectRenderedAxes, unreachableSupport } from './claimTypes.js';
|
|
29
30
|
/**
|
|
30
31
|
* Freshness is either a real recorded instant or the words "not established".
|
|
31
32
|
* It is never `new Date()` — a timestamp invented at render time would report
|
|
32
33
|
* the age of the screen as the age of the evidence.
|
|
33
34
|
*/
|
|
34
|
-
const iso = (ms) => typeof ms === 'number'
|
|
35
|
+
const iso = (ms) => typeof ms === 'number' && Number.isFinite(ms) && ms > 0 && ms <= 8.64e15
|
|
36
|
+
? new Date(ms).toISOString() : 'not established';
|
|
37
|
+
/**
|
|
38
|
+
* Whether any payload behind the spine is seeded sample data. The spine shows
|
|
39
|
+
* four money figures before any view's own demo banner; on a phone that banner
|
|
40
|
+
* sat a full screen below them, so sample dollars filled the first screen with
|
|
41
|
+
* nothing saying they were samples. A payload that failed to load says nothing.
|
|
42
|
+
*/
|
|
43
|
+
export function chainIsSampleData(input) {
|
|
44
|
+
return Object.values(input).some((p) => p?.demo === true);
|
|
45
|
+
}
|
|
46
|
+
const money = (n) => `$${n.toFixed(2)}`;
|
|
47
|
+
/** "gpt-6.1-sol, codex-auto-review and 1 more". */
|
|
48
|
+
function modelsOf(c) {
|
|
49
|
+
const names = c.models.map((m) => m.model);
|
|
50
|
+
return names.length <= 2 ? names.join(', ') : `${names.slice(0, 2).join(', ')} and ${names.length - 2} more`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The Metered band's basis line. It used to read "priced from a rate card" for
|
|
54
|
+
* every total, including amounts a tool reported and models priced at the
|
|
55
|
+
* generic fallback rate; the inspector then called matched rows "not
|
|
56
|
+
* estimated" (H003). All local cost is an estimate of a bill; what varies is
|
|
57
|
+
* how each part was priced, and that is what this says.
|
|
58
|
+
*/
|
|
59
|
+
export function meteredBasisLine(basis) {
|
|
60
|
+
if (!basis || basis.cohorts.length === 0)
|
|
61
|
+
return 'no recorded requests in this period';
|
|
62
|
+
switch (basis.headline) {
|
|
63
|
+
case 'sample_cost': return 'sample data, priced from the price list';
|
|
64
|
+
case 'tool_reported_cost': return 'amounts the tools reported, not verified';
|
|
65
|
+
case 'list_cost': {
|
|
66
|
+
const line = basis.cohorts.some((c) => c.id === 'list_family')
|
|
67
|
+
? "list prices (some models at the nearest model's price)"
|
|
68
|
+
: "list prices: each model's own published price";
|
|
69
|
+
// A model marked free is priced by its operator's declaration, not by a
|
|
70
|
+
// published price, so a mixed total names it (PR #146 review).
|
|
71
|
+
const free = basis.cohorts.find((c) => c.id === 'user_free');
|
|
72
|
+
return free ? `${line}; no charge on models you marked free (${modelsOf(free)})` : line;
|
|
73
|
+
}
|
|
74
|
+
default: {
|
|
75
|
+
const weak = basis.cohorts.filter((c) => (c.id === 'fallback' || c.id === 'tool_reported' || c.id === 'unrecorded') && c.costUsd >= 0.005);
|
|
76
|
+
// A declared price IS the model's own rate, the one its operator set, so
|
|
77
|
+
// it is named on its own rather than counted as "priced without" a rate
|
|
78
|
+
// (PR #146 review). It still makes the total an estimate: it is not a
|
|
79
|
+
// published list price.
|
|
80
|
+
const declared = basis.cohorts.find((c) => c.id === 'user_declared');
|
|
81
|
+
// With no weak or declared part, the "estimated" headline comes from the
|
|
82
|
+
// mix itself (sample rows beside priced ones, or unpriced requests). It
|
|
83
|
+
// used to print "0% priced without the model's own rate ()".
|
|
84
|
+
if (weak.length === 0 && !declared) {
|
|
85
|
+
if (basis.cohorts.some((c) => c.id === 'demo'))
|
|
86
|
+
return 'mostly sample data, priced from the price list';
|
|
87
|
+
// No rate card priced any of it: every charged request is on a model the
|
|
88
|
+
// user marked free (PR #146 review). It used to read "API list price".
|
|
89
|
+
if (basis.cohorts.some((c) => c.id === 'user_free') && !basis.cohorts.some((c) => c.id === 'list_exact' || c.id === 'list_family')) {
|
|
90
|
+
return basis.cohorts.some((c) => c.id === 'unpriced')
|
|
91
|
+
? 'no charge on models you marked free; some requests have no price'
|
|
92
|
+
: 'no charge: every request is on a model you marked free';
|
|
93
|
+
}
|
|
94
|
+
if (basis.cohorts.some((c) => c.id === 'unpriced'))
|
|
95
|
+
return 'list prices; some requests have no price';
|
|
96
|
+
return "API list price from the rate card";
|
|
97
|
+
}
|
|
98
|
+
const pct = (usd) => (basis.totalUsd > 0 ? Math.round((usd / basis.totalUsd) * 100) : 0);
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (weak.length > 0)
|
|
101
|
+
parts.push(`${pct(weak.reduce((s, c) => s + c.costUsd, 0))}% priced without the model's own rate (${weak.map((c) => c.label).join(', ')})`);
|
|
102
|
+
if (declared)
|
|
103
|
+
parts.push(`${pct(declared.costUsd)}% at prices you set`);
|
|
104
|
+
return `estimated: ${parts.join('; ')}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Share of the dollar total priced by a guess (nearest model or generic rate), 0..1. */
|
|
109
|
+
export function guessedShare(basis) {
|
|
110
|
+
if (!basis || basis.totalUsd <= 0)
|
|
111
|
+
return 0;
|
|
112
|
+
const guessed = basis.cohorts.filter((c) => c.id === 'fallback' || c.id === 'list_family').reduce((s, c) => s + c.costUsd, 0);
|
|
113
|
+
return guessed / basis.totalUsd;
|
|
114
|
+
}
|
|
115
|
+
/** Below this share a guess cannot change what the total says; above it, say where to fix it. */
|
|
116
|
+
export const GUESSED_SHARE_NEXT_STEP = 0.05;
|
|
117
|
+
/** Every part of the total, one clause each, for the inspector. */
|
|
118
|
+
export function meteredBasisParts(basis) {
|
|
119
|
+
if (!basis || basis.cohorts.length === 0)
|
|
120
|
+
return 'no recorded requests in this period';
|
|
121
|
+
return basis.cohorts.map((c) => {
|
|
122
|
+
if (c.id === 'unpriced')
|
|
123
|
+
return `${c.requests} request(s) not priced (${modelsOf(c)})`;
|
|
124
|
+
if (c.id === 'not_sent')
|
|
125
|
+
return `${c.requests} request(s) refused before reaching the provider (no charge)`;
|
|
126
|
+
if (c.id === 'user_free')
|
|
127
|
+
return `${c.requests} request(s) on models you marked free (${modelsOf(c)})`;
|
|
128
|
+
if (c.id === 'tool_reported' && c.costUsd < 0.005)
|
|
129
|
+
return `${c.requests} request(s) the tool reported at no charge (${modelsOf(c)})`;
|
|
130
|
+
const which = c.id === 'list_exact' || c.id === 'demo' || c.id === 'tool_reported' ? '' : ` (${modelsOf(c)})`;
|
|
131
|
+
return `${money(c.costUsd)} ${c.label}${which}`;
|
|
132
|
+
}).join(' · ');
|
|
133
|
+
}
|
|
35
134
|
export function buildClaimLayers(input, range) {
|
|
135
|
+
// Each claim names its own window; only Metered follows the chosen period.
|
|
36
136
|
const { overview: o, billing: b, allocation: a, value: v } = input;
|
|
37
137
|
const estimatedShare = o?.pricing.estimatedSpendShare ?? null;
|
|
38
138
|
const metered = {
|
|
39
139
|
id: 'metered',
|
|
40
|
-
label: '
|
|
140
|
+
label: 'List cost (estimate)',
|
|
141
|
+
period: periodWords(range),
|
|
41
142
|
claim: 'what we observed',
|
|
42
143
|
valueUsd: o?.summary.costUsd ?? null,
|
|
43
144
|
// Metered is the one layer whose figure IS the claim: if the ledger read,
|
|
@@ -47,26 +148,36 @@ export function buildClaimLayers(input, range) {
|
|
|
47
148
|
support: o?.claimSupport ?? unreachableSupport('withheld_unsupported'),
|
|
48
149
|
basis: o === null
|
|
49
150
|
? 'could not read the ledger'
|
|
50
|
-
:
|
|
51
|
-
nextStep: o === null
|
|
151
|
+
: meteredBasisLine(o.pricing.basis),
|
|
152
|
+
nextStep: o === null
|
|
153
|
+
? 'Check that Segreant is running.'
|
|
154
|
+
: guessedShare(o.pricing.basis) >= GUESSED_SHARE_NEXT_STEP
|
|
155
|
+
? `${Math.round(guessedShare(o.pricing.basis) * 100)}% is priced by a guess. Give those models a price under Data, then re-price.`
|
|
156
|
+
: undefined,
|
|
52
157
|
inspection: {
|
|
53
158
|
provenance: 'local request ledger + recorded pricing basis',
|
|
54
159
|
scope: o ? `${range}; ${o.summary.requests} recorded request(s)` : range,
|
|
55
160
|
freshness: o?.generatedAt ?? 'not established',
|
|
56
161
|
coverage: o?.claimSupport?.note
|
|
57
|
-
?? (
|
|
58
|
-
?
|
|
59
|
-
:
|
|
162
|
+
?? (o?.pricing.basis
|
|
163
|
+
? meteredBasisParts(o.pricing.basis)
|
|
164
|
+
: estimatedShare === null
|
|
165
|
+
? 'pricing coverage unavailable'
|
|
166
|
+
: `${Math.round((1 - estimatedShare) * 100)}% of spend priced at the model's own card rate; all of it is a list-price estimate`),
|
|
60
167
|
// The distinction the whole product is built on, stated where someone is
|
|
61
168
|
// most likely to reach for the metered figure as if it were the bill.
|
|
62
169
|
enforceability: 'observation claim; local caps can govern future in-path requests, but metered cost does not become billed cost',
|
|
63
170
|
evidenceSource: 'local request ledger',
|
|
64
|
-
assumptions: ['
|
|
171
|
+
assumptions: ['Every local amount is an estimate of a bill: list price is what the use would cost at published API rates, with no plan, discount, credit or tax.'],
|
|
65
172
|
missingEvidence: o === null
|
|
66
173
|
? ['a readable local ledger']
|
|
67
|
-
:
|
|
68
|
-
|
|
69
|
-
|
|
174
|
+
: [
|
|
175
|
+
...(o.pricing.basis?.cohorts ?? []).flatMap((c) => c.id === 'fallback' ? [`a model rate for ${modelsOf(c)} (if the card has learned it since, segreant reprice previews the new price)`]
|
|
176
|
+
: c.id === 'tool_reported' ? ['verification of the tool-reported amounts']
|
|
177
|
+
: c.id === 'unrecorded' ? ['the pricing basis of rows recorded before it was kept']
|
|
178
|
+
: []),
|
|
179
|
+
...(o.pricing.basis ? [] : estimatedShare !== null && estimatedShare > 0 ? ['exact price-list matches for the estimated rows'] : []),
|
|
180
|
+
],
|
|
70
181
|
},
|
|
71
182
|
};
|
|
72
183
|
// Billed is established only by a recorded reconciliation run. Holding
|
|
@@ -81,7 +192,8 @@ export function buildClaimLayers(input, range) {
|
|
|
81
192
|
const billedNote = b?.claimSupport?.note ?? null;
|
|
82
193
|
const billed = {
|
|
83
194
|
id: 'billed',
|
|
84
|
-
|
|
195
|
+
period: 'latest invoice comparison',
|
|
196
|
+
label: 'Billed cost',
|
|
85
197
|
claim: 'what the provider charged',
|
|
86
198
|
valueUsd: null,
|
|
87
199
|
// Holding provider records and having reconciled them are different states,
|
|
@@ -92,25 +204,29 @@ export function buildClaimLayers(input, range) {
|
|
|
92
204
|
// The band deliberately carries no dollar in any branch: this is an evidence
|
|
93
205
|
// claim about whether a comparison happened, not a second cost figure.
|
|
94
206
|
support: b?.claimSupport ?? unreachableSupport('not_a_money_claim'),
|
|
95
|
-
basis:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
?
|
|
103
|
-
:
|
|
104
|
-
?
|
|
105
|
-
:
|
|
207
|
+
basis: b === null
|
|
208
|
+
? 'could not read the billing report'
|
|
209
|
+
: billedNote
|
|
210
|
+
// A reconciliation whose provider snapshots contradicted each other is not
|
|
211
|
+
// a reconciliation that established anything, and saying "reconciled" here
|
|
212
|
+
// while the axis says `conflicted` is the disagreement this line exists to
|
|
213
|
+
// avoid.
|
|
214
|
+
? `compared with your invoice, but ${billedNote}`
|
|
215
|
+
: runs > 0
|
|
216
|
+
? 'compared with your invoice; see Close for the difference'
|
|
217
|
+
: b && b.summary.recordCount > 0
|
|
218
|
+
? `${b.summary.recordCount} billing records from your provider, not compared yet`
|
|
219
|
+
: 'no invoice imported',
|
|
106
220
|
// A contradiction with no next action leaves an operator looking at a
|
|
107
221
|
// problem they are not told how to work on. Resolving a disagreement is a
|
|
108
222
|
// different task from collecting more evidence, so it gets its own sentence.
|
|
109
|
-
nextStep: b
|
|
110
|
-
? '
|
|
111
|
-
:
|
|
112
|
-
?
|
|
113
|
-
:
|
|
223
|
+
nextStep: b === null
|
|
224
|
+
? 'Check that Segreant is running, then retry the billing report.'
|
|
225
|
+
: b?.claimSupport && projectRenderedAxes(b.claimSupport.profile).epistemic === 'conflicted'
|
|
226
|
+
? 'Re-observe the disagreeing days before relying on this reconciliation.'
|
|
227
|
+
: runs > 0
|
|
228
|
+
? undefined
|
|
229
|
+
: 'Import one under Close to see billed cost beside list cost.',
|
|
114
230
|
inspection: {
|
|
115
231
|
provenance: latestRun?.result.providerSourceKind
|
|
116
232
|
?? (b && b.summary.recordCount > 0 ? 'operator-supplied provider evidence, unreconciled' : 'none'),
|
|
@@ -134,7 +250,8 @@ export function buildClaimLayers(input, range) {
|
|
|
134
250
|
const centres = Array.isArray(a?.costCentres) ? a.costCentres.length : 0;
|
|
135
251
|
const allocated = {
|
|
136
252
|
id: 'allocated',
|
|
137
|
-
|
|
253
|
+
period: 'latest cost split',
|
|
254
|
+
label: 'Allocated cost',
|
|
138
255
|
claim: 'whose cost it is',
|
|
139
256
|
valueUsd: null,
|
|
140
257
|
// Cost centres defined with no run recorded is partial coverage of a claim
|
|
@@ -143,12 +260,16 @@ export function buildClaimLayers(input, range) {
|
|
|
143
260
|
// Showback: the claim is whose cost it is, not how much, so the band never
|
|
144
261
|
// carries a dollar.
|
|
145
262
|
support: a?.claimSupport ?? unreachableSupport('not_a_money_claim'),
|
|
146
|
-
basis:
|
|
147
|
-
? '
|
|
148
|
-
:
|
|
149
|
-
?
|
|
150
|
-
:
|
|
151
|
-
|
|
263
|
+
basis: a === null
|
|
264
|
+
? 'could not read the allocation report'
|
|
265
|
+
: allocRuns > 0
|
|
266
|
+
? 'split by your rules, for reporting (not a bill)'
|
|
267
|
+
: centres > 0
|
|
268
|
+
? `${centres} cost centre${centres === 1 ? '' : 's'} set up, nothing split yet`
|
|
269
|
+
: 'no cost split set up yet',
|
|
270
|
+
nextStep: a === null
|
|
271
|
+
? 'Check that Segreant is running, then retry the allocation report.'
|
|
272
|
+
: allocRuns > 0 ? undefined : 'Set up a cost centre, then split the spend under Allocate.',
|
|
152
273
|
inspection: {
|
|
153
274
|
provenance: a?.basis ?? 'no allocation basis recorded',
|
|
154
275
|
scope: a
|
|
@@ -203,7 +324,8 @@ export function buildClaimLayers(input, range) {
|
|
|
203
324
|
const valued = ret?.basis === 'usd' && typeof ret.manualEquivalentValueUsd === 'number';
|
|
204
325
|
const realized = {
|
|
205
326
|
id: 'realized',
|
|
206
|
-
|
|
327
|
+
period: 'outcomes on record',
|
|
328
|
+
label: 'Value (kept work)',
|
|
207
329
|
claim: 'what it produced',
|
|
208
330
|
valueUsd: valued ? (ret?.manualEquivalentValueUsd ?? null) : null,
|
|
209
331
|
// THE CASE THE OLD BOOLEAN COLLAPSED (AII-014). `realizedUnits > 0 && valued`
|
|
@@ -219,16 +341,20 @@ export function buildClaimLayers(input, range) {
|
|
|
219
341
|
// produced nothing" — an inference from a missing input, in the band whose
|
|
220
342
|
// entire job is to keep realized value distinct from the three cost claims.
|
|
221
343
|
support: v?.claimSupport ?? unreachableSupport('withheld_unsupported'),
|
|
222
|
-
basis:
|
|
223
|
-
? '
|
|
224
|
-
:
|
|
225
|
-
?
|
|
226
|
-
:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
344
|
+
basis: v === null
|
|
345
|
+
? 'could not read the outcome report'
|
|
346
|
+
: realizedUnits === 0
|
|
347
|
+
? 'no finished work old enough to judge yet'
|
|
348
|
+
: valued
|
|
349
|
+
? `${realizedUnits} of ${matured?.units ?? 0} finished pieces of work kept; valued at what the work would cost by hand, less rework`
|
|
350
|
+
: `${realizedUnits} of ${matured?.units ?? 0} finished pieces of work kept, but no hourly rate is set to value them`,
|
|
351
|
+
nextStep: v === null
|
|
352
|
+
? 'Check that Segreant is running, then retry the outcome report.'
|
|
353
|
+
: realizedUnits === 0
|
|
354
|
+
? 'Connect a repository so Segreant can see which work you kept.'
|
|
355
|
+
: valued
|
|
356
|
+
? undefined
|
|
357
|
+
: 'Set an hourly rate to put a dollar value on kept work.',
|
|
232
358
|
inspection: {
|
|
233
359
|
provenance: v?.valueSource ?? 'no outcome source established',
|
|
234
360
|
scope: v?.projectScoped === true
|
|
@@ -253,11 +379,13 @@ export function buildClaimLayers(input, range) {
|
|
|
253
379
|
? 'repository history + recorded outcome signals'
|
|
254
380
|
: (v?.valueSource ?? 'none'),
|
|
255
381
|
assumptions: v?.roi?.notes ? [...v.roi.notes] : [],
|
|
256
|
-
missingEvidence:
|
|
257
|
-
? ['
|
|
258
|
-
:
|
|
259
|
-
? []
|
|
260
|
-
:
|
|
382
|
+
missingEvidence: v === null
|
|
383
|
+
? ['a readable outcome report']
|
|
384
|
+
: realizedUnits === 0
|
|
385
|
+
? ['matured outcome evidence']
|
|
386
|
+
: valued
|
|
387
|
+
? []
|
|
388
|
+
: ['a labour rate, so realized work can be priced'],
|
|
261
389
|
},
|
|
262
390
|
};
|
|
263
391
|
return [metered, billed, allocated, realized];
|
|
@@ -182,6 +182,36 @@ export function claimIsUnevidenced(layer) {
|
|
|
182
182
|
export function claimShowsFigure(layer) {
|
|
183
183
|
return layer.support.figure === 'shown';
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* What is complete when a claim names no missing evidence. Scoped to THIS
|
|
187
|
+
* claim's own standard. An empty `missingEvidence` list means the claim is
|
|
188
|
+
* complete as stated, not that the claim family is complete or that the figure
|
|
189
|
+
* is a measured outcome: "Nothing is missing" read as blanket completeness on
|
|
190
|
+
* a Realized claim that also says "impact uninstrumented" (H018).
|
|
191
|
+
*/
|
|
192
|
+
export function claimCompletionLine(layer, precise) {
|
|
193
|
+
if (layer.id === 'realized') {
|
|
194
|
+
return precise
|
|
195
|
+
? 'No further evidence is required for this claim as stated: an estimated manual-equivalent value of work that shipped and survived.'
|
|
196
|
+
: 'Nothing more is needed for this claim as stated: an estimated manual-equivalent value of work that shipped and survived.';
|
|
197
|
+
}
|
|
198
|
+
return precise
|
|
199
|
+
? 'No further evidence is required for this claim at its stated basis.'
|
|
200
|
+
: 'Nothing more is needed for this claim at its stated basis.';
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The Realized claim is shown with its basis, and that basis is a value
|
|
204
|
+
* estimate. A causal or business-value claim needs evidence this layer does
|
|
205
|
+
* not carry, so the inspector names that gap whether or not `missingEvidence`
|
|
206
|
+
* is empty. The payload has no structured impact-instrumentation flag (impact
|
|
207
|
+
* appears only as free-text `roi.notes`), so the line is shown for every
|
|
208
|
+
* Realized claim rather than conditioned on a flag that does not exist.
|
|
209
|
+
*/
|
|
210
|
+
export function claimStrongerClaimLine(layer) {
|
|
211
|
+
if (layer.id !== 'realized')
|
|
212
|
+
return null;
|
|
213
|
+
return 'A causal or business-value claim would also need independent outcome evidence (impact instrumentation), which this claim does not establish.';
|
|
214
|
+
}
|
|
185
215
|
/**
|
|
186
216
|
* Supported, but no figure because something needed to price it is missing.
|
|
187
217
|
* The case the old boolean reported as "not established", which reads to an
|
|
@@ -86,6 +86,23 @@ export function pct(value, digits = 1) {
|
|
|
86
86
|
return '—';
|
|
87
87
|
return `${(value * 100).toFixed(digits)}%`;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* A part of a whole, as a percentage. A part that is not zero never prints as
|
|
91
|
+
* 0%, and a part short of the whole never prints as 100%: either rounding would
|
|
92
|
+
* state something the figures do not. At the default precision those show as
|
|
93
|
+
* "<1%" and ">99%".
|
|
94
|
+
*/
|
|
95
|
+
export function share(fraction, digits = 0) {
|
|
96
|
+
if (fraction === null || fraction === undefined || Number.isNaN(fraction))
|
|
97
|
+
return '—';
|
|
98
|
+
const step = 10 ** -digits;
|
|
99
|
+
const percent = fraction * 100;
|
|
100
|
+
if (fraction > 0 && percent < step / 2)
|
|
101
|
+
return `<${step.toFixed(digits)}%`;
|
|
102
|
+
if (fraction < 1 && percent >= 100 - step / 2)
|
|
103
|
+
return `>${(100 - step).toFixed(digits)}%`;
|
|
104
|
+
return pct(fraction, digits);
|
|
105
|
+
}
|
|
89
106
|
export function dateTime(ms) {
|
|
90
107
|
if (!ms)
|
|
91
108
|
return '—';
|
|
@@ -93,10 +110,10 @@ export function dateTime(ms) {
|
|
|
93
110
|
year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
94
111
|
});
|
|
95
112
|
}
|
|
96
|
-
export function date(ms) {
|
|
113
|
+
export function date(ms, timeZone) {
|
|
97
114
|
if (!ms)
|
|
98
115
|
return '—';
|
|
99
|
-
return new Date(ms).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
116
|
+
return new Date(ms).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', ...(timeZone ? { timeZone } : {}) });
|
|
100
117
|
}
|
|
101
118
|
export function relative(ms) {
|
|
102
119
|
if (!ms)
|
|
@@ -18,9 +18,19 @@ export const DASHBOARD_API_CONTRACTS = [
|
|
|
18
18
|
{ id: 'scan', path: '/api/scan', methods: ['GET', 'POST'], localOnly: ['POST'], responseType: 'ScanPayload', browserBinding: ['modern-api', 'classic'] },
|
|
19
19
|
{ id: 'overview', path: '/api/overview', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'Overview', browserBinding: ['modern-api', 'classic'] },
|
|
20
20
|
{ id: 'billing', path: '/api/billing', methods: ['GET'], localOnly: [], responseType: 'BillingPayload', browserBinding: ['modern-api', 'classic'] },
|
|
21
|
+
{ id: 'statement', path: '/api/statement', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'StatementPayload', browserBinding: ['modern-api'] },
|
|
22
|
+
{ id: 'sessions', path: '/api/sessions', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'SessionsPayload', browserBinding: ['modern-api'] },
|
|
23
|
+
{ id: 'statement-csv', path: '/api/statement.csv', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'text/csv', browserBinding: ['modern-api'] },
|
|
21
24
|
{ id: 'allocation', path: '/api/allocation', methods: ['GET'], localOnly: [], responseType: 'AllocationPayload', browserBinding: ['modern-api', 'classic'] },
|
|
25
|
+
{ id: 'allocation-write', path: '/api/allocation/write', methods: ['POST'], localOnly: ['POST'], responseType: 'AllocationWritePayload', browserBinding: ['modern-api'] },
|
|
26
|
+
{ id: 'allocation-run-csv', path: '/api/allocation/run.csv', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'text/csv', browserBinding: ['modern-api'] },
|
|
27
|
+
{ id: 'billing-write', path: '/api/billing/write', methods: ['POST'], localOnly: ['POST'], responseType: 'BillingWritePayload', browserBinding: ['modern-api'] },
|
|
22
28
|
{ id: 'economic', path: '/api/economic', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'EconomicPayload', browserBinding: ['modern-api'] },
|
|
23
29
|
{ id: 'pricing', path: '/api/pricing', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'PricingPayload', browserBinding: [] },
|
|
30
|
+
{ id: 'pricing-unknown', path: '/api/pricing/unknown', methods: ['GET'], localOnly: [], responseType: 'UnknownPricingPayload', browserBinding: ['modern-api'] },
|
|
31
|
+
{ id: 'pricing-declare', path: '/api/pricing/declare', methods: ['POST'], localOnly: ['POST'], responseType: 'PricingDeclarePayload', browserBinding: ['modern-api'] },
|
|
32
|
+
{ id: 'pricing-unset', path: '/api/pricing/unset', methods: ['POST'], localOnly: ['POST'], responseType: 'PricingUnsetPayload', browserBinding: ['modern-api'] },
|
|
33
|
+
{ id: 'reprice', path: '/api/reprice', methods: ['POST'], localOnly: ['POST'], responseType: 'RepricePayload', browserBinding: ['modern-api'] },
|
|
24
34
|
{ id: 'export-csv', path: '/api/export.csv', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'text/csv', browserBinding: ['classic', 'actions'] },
|
|
25
35
|
{ id: 'realization', path: '/api/realization', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'RealizationPayload', browserBinding: [] },
|
|
26
36
|
{ id: 'guide', path: '/api/guide', methods: ['GET', 'HEAD'], localOnly: [], responseType: 'GuidePayload', browserBinding: ['modern-api', 'classic'] },
|
|
@@ -59,21 +69,31 @@ export const DASHBOARD_PAYLOAD_CONTRACTS = [
|
|
|
59
69
|
{ routeId: 'scan', method: 'GET', contentType: 'json', responseType: 'ScanPayload', required: [field('ok', 'boolean'), field('tools', 'array'), field('otherApps', 'array'), field('roots', 'array'), field('repoCount', 'number'), field('reposWithSpend', 'number'), field('hitBudget', 'boolean'), field('dirsVisited', 'number'), field('unreadableDirs', 'number'), field('diff', 'object')] },
|
|
60
70
|
{ routeId: 'scan', method: 'POST', contentType: 'json', responseType: 'ScanSetupPayload', required: [field('ok', 'boolean'), field('totalNew', 'number'), field('imported', 'object'), field('correlated', 'number'), field('discovered', 'array')] },
|
|
61
71
|
{ routeId: 'overview', method: 'GET', contentType: 'json', responseType: 'Overview', required: [field('claimSupport', 'object'), field('range', 'string'), field('demo', 'boolean'), field('generatedAt', 'string'), field('budget', 'object'), field('summary', 'object'), field('pricing', 'object'), field('byModel', 'array'), field('byProject', 'array'), field('attributionEvidence', 'array'), field('byUser', 'array'), field('bySource', 'array'), field('characterization', 'object'), field('dimensions', 'array'), field('series', 'array'), field('recent', 'array'), field('alerts', 'array'), field('alertCoverage', 'object')] },
|
|
72
|
+
{ routeId: 'statement', method: 'GET', contentType: 'json', responseType: 'StatementPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('month', 'string'), field('period', 'object'), field('metered', 'object'), field('billed', 'object'), field('allocated', 'object'), field('realized', 'object'), field('close', 'object')] },
|
|
73
|
+
{ routeId: 'sessions', method: 'GET', contentType: 'json', responseType: 'SessionsPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('range', 'string'), field('window', 'object'), field('totals', 'object'), field('models', 'array'), field('buckets', 'array'), field('sessions', 'array'), field('sessionLimit', 'number'), field('sessionsOmitted', 'number'), field('outcomes', 'object')] },
|
|
62
74
|
{ routeId: 'billing', method: 'GET', contentType: 'json', responseType: 'BillingPayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('generatedAt', 'string'), field('evidence', 'object'), field('summary', 'object'), field('imports', 'array'), field('kernel', 'object'), field('readiness', 'object'), field('mapping', 'object'), field('directOpenAiCosts', 'object'), field('reconciliation', 'object')] },
|
|
63
75
|
{ routeId: 'allocation', method: 'GET', contentType: 'json', responseType: 'AllocationPayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('generatedAt', 'string'), field('kind', 'string'), field('trust', 'string'), field('basis', 'string'), field('excludedFrom', 'array'), field('costCentres', 'array'), field('rules', 'array'), field('runs', 'array'), field('reconciliation', 'object')] },
|
|
76
|
+
{ routeId: 'allocation-write', method: 'POST', contentType: 'json', responseType: 'AllocationWritePayload', required: [field('ok', 'boolean'), field('op', 'string'), field('applied', 'boolean')] },
|
|
77
|
+
{ routeId: 'billing-write', method: 'POST', contentType: 'json', responseType: 'BillingWritePayload', required: [field('ok', 'boolean'), field('op', 'string'), field('applied', 'boolean')] },
|
|
64
78
|
{ routeId: 'economic', method: 'GET', contentType: 'json', responseType: 'EconomicPayload', required: [field('kind', 'string'), field('schemaVersion', 'number'), field('demo', 'boolean'), field('window', 'object'), field('translation', 'object', true), field('projection', 'object'), field('periodClose', 'object')] },
|
|
65
79
|
{ routeId: 'pricing', method: 'GET', contentType: 'json', responseType: 'PricingPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('window', 'object'), field('activeRateCard', 'object'), field('total', 'object'), field('provenance', 'array'), field('boundary', 'string')] },
|
|
80
|
+
{ routeId: 'pricing-unknown', method: 'GET', contentType: 'json', responseType: 'UnknownPricingPayload', required: [field('models', 'array'), field('boundary', 'string'), field('demo', 'boolean')] },
|
|
81
|
+
{ routeId: 'pricing-declare', method: 'POST', contentType: 'json', responseType: 'PricingDeclarePayload', required: [field('ok', 'boolean'), field('model', 'string'), field('price', 'object')] },
|
|
82
|
+
{ routeId: 'pricing-unset', method: 'POST', contentType: 'json', responseType: 'PricingUnsetPayload', required: [field('ok', 'boolean'), field('model', 'string'), field('removed', 'boolean')] },
|
|
83
|
+
{ routeId: 'reprice', method: 'POST', contentType: 'json', responseType: 'RepricePayload', required: [field('applied', 'boolean'), field('demo', 'boolean'), field('estimatedRows', 'number'), field('repriceable', 'number'), field('stillEstimated', 'number'), field('totalBeforeUsd', 'number'), field('totalAfterUsd', 'number'), field('byModel', 'array'), field('realizationSync', 'object', true)] },
|
|
66
84
|
{ routeId: 'export-csv', method: 'GET', contentType: 'text', responseType: 'text/csv', required: [] },
|
|
85
|
+
{ routeId: 'statement-csv', method: 'GET', contentType: 'text', responseType: 'text/csv', required: [] },
|
|
86
|
+
{ routeId: 'allocation-run-csv', method: 'GET', contentType: 'text', responseType: 'text/csv', required: [] },
|
|
67
87
|
{ routeId: 'realization', method: 'GET', contentType: 'json', responseType: 'RealizationPayload', required: [field('available', 'boolean'), field('repo', 'string')] },
|
|
68
88
|
{ routeId: 'guide', method: 'GET', contentType: 'json', responseType: 'GuidePayload', required: [field('stage', 'string'), field('headline', 'string'), field('steps', 'array'), field('next', 'object'), field('hint', 'object', true)] },
|
|
69
89
|
{ routeId: 'judge', method: 'POST', contentType: 'json', responseType: 'JudgePayload', required: [] },
|
|
70
90
|
{ routeId: 'kernel', method: 'GET', contentType: 'json', responseType: 'KernelNodePayload', required: [field('found', 'boolean'), field('id', 'string'), field('asOf', 'string', true), field('node', 'object', true)] },
|
|
71
|
-
{ routeId: 'value', method: 'GET', contentType: 'json', responseType: 'ValuePayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('gitRepo', 'boolean'), field('valueSource', 'string', true), field('repo', 'string'), field('generatedAt', 'string'), field('realization', 'object', true), field('roi', 'object', true), field('frontier', 'object', true), field('budget', 'object', true), field('allocation', 'object', true), field('projects', 'array'), field('projectAllocation', 'object', true), field('usage', 'object', true), field('selfReported', 'object'), field('team', 'object', true), field('drift', 'object', true), field('reclaimed', 'object', true)] },
|
|
91
|
+
{ routeId: 'value', method: 'GET', contentType: 'json', responseType: 'ValuePayload', required: [field('claimSupport', 'object'), field('demo', 'boolean'), field('gitRepo', 'boolean'), field('valueSource', 'string', true), field('repo', 'string'), field('generatedAt', 'string'), field('realization', 'object', true), field('roi', 'object', true), field('frontier', 'object', true), field('budget', 'object', true), field('allocation', 'object', true), field('projects', 'array', true), field('projectAllocation', 'object', true), field('usage', 'object', true), field('selfReported', 'object'), field('team', 'object', true), field('drift', 'object', true), field('reclaimed', 'object', true)] },
|
|
72
92
|
{ routeId: 'market', method: 'GET', contentType: 'json', responseType: 'MarketPayload', required: [field('status', 'string'), field('categories', 'array'), field('boundary', 'array'), field('cacheErrors', 'array')] },
|
|
73
93
|
{ routeId: 'outcome-record', method: 'POST', contentType: 'json', responseType: 'OutcomeRecordPayload', required: [field('apply', 'boolean'), field('basis', 'string'), field('event', 'object'), field('matchedRequests', 'number'), field('matchedCostUsd', 'number', true), field('linkStatus', 'string')] },
|
|
74
94
|
{ routeId: 'causal', method: 'GET', contentType: 'json', responseType: 'CausalPayload', required: [field('demo', 'boolean'), field('generatedAt', 'string'), field('studies', 'array'), field('study', 'object', true), field('causalEvidence', 'string'), field('boundary', 'string')] },
|
|
75
|
-
{ routeId: 'settings', method: 'GET', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
76
|
-
{ routeId: 'settings-update', method: 'POST', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
95
|
+
{ routeId: 'settings', method: 'GET', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('demo', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
96
|
+
{ routeId: 'settings-update', method: 'POST', contentType: 'json', responseType: 'SettingsSnapshot', required: [field('version', 'string'), field('home', 'string'), field('configPath', 'string'), field('dbPath', 'string'), field('proxyPort', 'number'), field('dashboardPort', 'number'), field('retentionDays', 'number'), field('proposalRetentionDays', 'number'), field('metadataOnly', 'boolean'), field('demo', 'boolean'), field('features', 'object'), field('budget', 'object'), field('enforcement', 'object'), field('egress', 'object'), field('connections', 'array')] },
|
|
77
97
|
{ routeId: 'clear-proposals', method: 'POST', contentType: 'json', responseType: 'ClearProposalsPayload', required: [field('ok', 'boolean'), field('removed', 'number')] },
|
|
78
98
|
];
|
|
79
99
|
export function dashboardPayloadContract(routeId, method) {
|