@common-agent/reputations 0.0.0-stage → 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/LICENSE +202 -0
- package/LICENSE-UNICODE +39 -0
- package/NOTICE +9 -0
- package/README.md +38 -2
- package/dist/casefolding-15.1.0.json +1539 -0
- package/dist/heads.d.ts +184 -0
- package/dist/heads.js +358 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +28 -0
- package/dist/ledger.d.ts +92 -0
- package/dist/ledger.js +157 -0
- package/dist/nda-claims.d.ts +141 -0
- package/dist/nda-claims.js +168 -0
- package/dist/nda-record.d.ts +86 -0
- package/dist/nda-record.js +83 -0
- package/dist/pool.d.ts +59 -0
- package/dist/pool.js +77 -0
- package/dist/ports.d.ts +6 -0
- package/dist/ports.js +1 -0
- package/dist/report.d.ts +176 -0
- package/dist/report.js +147 -0
- package/dist/score.d.ts +110 -0
- package/dist/score.js +129 -0
- package/dist/signing.d.ts +150 -0
- package/dist/signing.js +299 -0
- package/package.json +53 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NDA record (Agent Reputations §4.9): the part of a report that shows
|
|
3
|
+
* the claims of breach about a subject, as dated findings and counts, and,
|
|
4
|
+
* for the readers rule 6 names, the claims it filed. Pure: the bureau reads
|
|
5
|
+
* each claim's standing from its ledger (nda-claims.ts says how a claim
|
|
6
|
+
* stands) and this decides what a reader may be shown of it.
|
|
7
|
+
*
|
|
8
|
+
* What a report never shows (rule 4) is never an input here: no agreement
|
|
9
|
+
* identifier or digest, no claimant identity, no note, no answer text, no
|
|
10
|
+
* record a claim cites. `agreement` and `respondent` below only group upheld
|
|
11
|
+
* claims, and nothing of either reaches the output.
|
|
12
|
+
*/
|
|
13
|
+
import { RECORD_PERIOD_DAYS } from "./nda-claims.js";
|
|
14
|
+
const later = (a, b) => (!a ? b : !b ? a : Date.parse(b) > Date.parse(a) ? b : a);
|
|
15
|
+
const count = (vs, s) => vs.filter((v) => v.state === s).length;
|
|
16
|
+
/** How many distinct claimants: those that share an anchor or an operator are one (rule 2). */
|
|
17
|
+
function distinctClaimants(views) {
|
|
18
|
+
const parent = views.map((_, i) => i);
|
|
19
|
+
const find = (i) => (parent[i] === i ? i : (parent[i] = find(parent[i])));
|
|
20
|
+
const seen = new Map();
|
|
21
|
+
views.forEach((v, i) => {
|
|
22
|
+
for (const k of [`a:${v.claimantAnchorHash}`, ...(v.claimantOperatorHash ? [`o:${v.claimantOperatorHash}`] : [])]) {
|
|
23
|
+
const j = seen.get(k);
|
|
24
|
+
if (j === undefined)
|
|
25
|
+
seen.set(k, i);
|
|
26
|
+
else
|
|
27
|
+
parent[find(i)] = find(j);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return new Set(views.map((_, i) => find(i))).size;
|
|
31
|
+
}
|
|
32
|
+
/** The counts of a party's filings in the record period, in each state (rule 6). */
|
|
33
|
+
export function filedCounts(filed) {
|
|
34
|
+
const shown = filed.filter((v) => v.inRecord);
|
|
35
|
+
return {
|
|
36
|
+
open: count(shown, "open"),
|
|
37
|
+
upheld: count(shown, "upheld"),
|
|
38
|
+
dismissed: count(shown, "dismissed"),
|
|
39
|
+
withdrawn: count(shown, "withdrawn"),
|
|
40
|
+
set_aside: count(shown, "set_aside"),
|
|
41
|
+
lapsed: count(shown, "lapsed"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The NDA record of a subject from the claims against it. Rejected claims
|
|
46
|
+
* are never inputs (they appear in the two parties' excerpts and nowhere
|
|
47
|
+
* else), and only claims inside the record period are shown. `filed`, the
|
|
48
|
+
* claims the subject filed, is given only for a reader rule 6 serves it to.
|
|
49
|
+
*/
|
|
50
|
+
export function ndaRecord(about, opts = {}) {
|
|
51
|
+
const shown = about.filter((v) => v.inRecord);
|
|
52
|
+
// 1. Upheld breaches, one entry per agreement and respondent, with the latest date.
|
|
53
|
+
const byAgreement = new Map();
|
|
54
|
+
for (const v of shown.filter((x) => x.state === "upheld")) {
|
|
55
|
+
const k = `${v.agreement}|${v.respondent}`;
|
|
56
|
+
byAgreement.set(k, [...(byAgreement.get(k) ?? []), v]);
|
|
57
|
+
}
|
|
58
|
+
const upheld = [...byAgreement.values()].map((vs) => {
|
|
59
|
+
const latest = vs.reduce((a, b) => (Date.parse(b.resolvedAt ?? b.filedAt) > Date.parse(a.resolvedAt ?? a.filedAt) ? b : a));
|
|
60
|
+
const answeredAt = vs.reduce((a, v) => later(a, v.lastAnsweredAt), undefined);
|
|
61
|
+
return {
|
|
62
|
+
upheld_at: latest.resolvedAt ?? latest.filedAt,
|
|
63
|
+
basis: latest.basis ?? "",
|
|
64
|
+
categories: [...new Set(vs.flatMap((v) => v.categories))].sort(),
|
|
65
|
+
direction: latest.direction,
|
|
66
|
+
claimant_anchor_hash: latest.claimantAnchorHash,
|
|
67
|
+
answered: vs.some((v) => v.answers > 0),
|
|
68
|
+
...(answeredAt ? { answered_at: answeredAt } : {}),
|
|
69
|
+
};
|
|
70
|
+
}).sort((a, b) => Date.parse(b.upheld_at) - Date.parse(a.upheld_at));
|
|
71
|
+
// 2. Open claims, as counts only.
|
|
72
|
+
const open = shown.filter((v) => v.state === "open");
|
|
73
|
+
const earliest = open.reduce((a, v) => (!a || Date.parse(v.filedAt) < Date.parse(a) ? v.filedAt : a), null);
|
|
74
|
+
return {
|
|
75
|
+
record_days: opts.recordDays ?? RECORD_PERIOD_DAYS,
|
|
76
|
+
upheld,
|
|
77
|
+
open: { count: open.length, claimants: distinctClaimants(open), earliest, answered: open.filter((v) => v.answers > 0).length },
|
|
78
|
+
// 3. Closed claims, in the states they ended in.
|
|
79
|
+
closed: { dismissed: count(shown, "dismissed"), withdrawn: count(shown, "withdrawn"), set_aside: count(shown, "set_aside"), lapsed: count(shown, "lapsed") },
|
|
80
|
+
// 6. Only for the readers rule 6 names.
|
|
81
|
+
...(opts.filed ? { filed: filedCounts(opts.filed) } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/pool.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** One ledger event, as §4 writes it. Only the members the pool reads are named. */
|
|
2
|
+
export type LedgerEvent = {
|
|
3
|
+
seq: number;
|
|
4
|
+
action: "fact";
|
|
5
|
+
interaction_id: string;
|
|
6
|
+
ended_at: string;
|
|
7
|
+
derived_outcome: string;
|
|
8
|
+
} | {
|
|
9
|
+
seq: number;
|
|
10
|
+
action: "attested";
|
|
11
|
+
interaction_id: string;
|
|
12
|
+
outcome: string;
|
|
13
|
+
excluded?: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
seq: number;
|
|
16
|
+
action: "probed";
|
|
17
|
+
interaction_id: string;
|
|
18
|
+
ended_at: string;
|
|
19
|
+
result: "PASS" | "FAIL";
|
|
20
|
+
} | {
|
|
21
|
+
seq: number;
|
|
22
|
+
action: string;
|
|
23
|
+
interaction_id?: string;
|
|
24
|
+
[k: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
/** One scored event of the pool: its outcome, where it came from, its value and its weight. */
|
|
27
|
+
export interface PoolEntry {
|
|
28
|
+
interaction_id: string;
|
|
29
|
+
outcome: string;
|
|
30
|
+
from: "fact" | "attested" | "probed";
|
|
31
|
+
v: number;
|
|
32
|
+
w: number;
|
|
33
|
+
value_judgment?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** The codes on which there was no work to judge (§6.2 rule 5): an attestation about one is rejected. */
|
|
36
|
+
export declare const NO_WORK: Set<string>;
|
|
37
|
+
/**
|
|
38
|
+
* The pool of §7.1 for one (subject, capability), from that pair's events.
|
|
39
|
+
*
|
|
40
|
+
* - A fact scores its derived outcome, unless an attestation overrides it.
|
|
41
|
+
* - An attestation is rejected without a fact for its interaction (§6.2
|
|
42
|
+
* rule 1), on an interaction with no work (rule 5), or when it names
|
|
43
|
+
* NDA_BREACH (rule 6). A later attestation for one interaction is an
|
|
44
|
+
* amendment, and only the last one scores (rule 3). An excluded
|
|
45
|
+
* attestation (§6.4) does not score, and the derived outcome stands.
|
|
46
|
+
* - A probe scores PASS as 1 and FAIL as 0.
|
|
47
|
+
* - Every other action (stops, claims of breach and their resolution, and the
|
|
48
|
+
* rest) never enters a pool, and neither does an outcome §5 gives no value.
|
|
49
|
+
*
|
|
50
|
+
* Every event's age runs from the `ended_at` of the interaction it scores to
|
|
51
|
+
* the evaluation instant (§7.2): an attestation takes its interaction's age,
|
|
52
|
+
* however late it was signed. A value judgment's weight is multiplied by
|
|
53
|
+
* K_value (§7.6).
|
|
54
|
+
*/
|
|
55
|
+
export declare function poolFromEvents(events: readonly LedgerEvent[], opts: {
|
|
56
|
+
evaluatedAt: string | number;
|
|
57
|
+
halfLifeDays?: number;
|
|
58
|
+
kValue?: number;
|
|
59
|
+
}): PoolEntry[];
|
package/dist/pool.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quality pool of Agent Reputations §7.1, drawn from ledger events in the
|
|
3
|
+
* standard's shape (§4): which events score, with what value and weight.
|
|
4
|
+
*
|
|
5
|
+
* A ledger that still keeps the earlier shape draws its pool with ledger.ts,
|
|
6
|
+
* and moves to this one when it takes the standard's actions. The published vectors
|
|
7
|
+
* (fdbk-score-1) are written in this shape, and run against it.
|
|
8
|
+
*/
|
|
9
|
+
import { HALF_LIFE_DAYS, K_VALUE, OUTCOME_VALUES, VALUE_OUTCOMES } from "./score.js";
|
|
10
|
+
/** The codes on which there was no work to judge (§6.2 rule 5): an attestation about one is rejected. */
|
|
11
|
+
export const NO_WORK = new Set(["DECLINED", "REFUSED", "REFUSED_UNSTATED", "UNAVAILABLE", "STOPPED"]);
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
/**
|
|
14
|
+
* The pool of §7.1 for one (subject, capability), from that pair's events.
|
|
15
|
+
*
|
|
16
|
+
* - A fact scores its derived outcome, unless an attestation overrides it.
|
|
17
|
+
* - An attestation is rejected without a fact for its interaction (§6.2
|
|
18
|
+
* rule 1), on an interaction with no work (rule 5), or when it names
|
|
19
|
+
* NDA_BREACH (rule 6). A later attestation for one interaction is an
|
|
20
|
+
* amendment, and only the last one scores (rule 3). An excluded
|
|
21
|
+
* attestation (§6.4) does not score, and the derived outcome stands.
|
|
22
|
+
* - A probe scores PASS as 1 and FAIL as 0.
|
|
23
|
+
* - Every other action (stops, claims of breach and their resolution, and the
|
|
24
|
+
* rest) never enters a pool, and neither does an outcome §5 gives no value.
|
|
25
|
+
*
|
|
26
|
+
* Every event's age runs from the `ended_at` of the interaction it scores to
|
|
27
|
+
* the evaluation instant (§7.2): an attestation takes its interaction's age,
|
|
28
|
+
* however late it was signed. A value judgment's weight is multiplied by
|
|
29
|
+
* K_value (§7.6).
|
|
30
|
+
*/
|
|
31
|
+
export function poolFromEvents(events, opts) {
|
|
32
|
+
const now = typeof opts.evaluatedAt === "number" ? opts.evaluatedAt : Date.parse(opts.evaluatedAt);
|
|
33
|
+
const halfLife = opts.halfLifeDays ?? HALF_LIFE_DAYS;
|
|
34
|
+
const kValue = opts.kValue ?? K_VALUE;
|
|
35
|
+
const sorted = [...events].sort((x, y) => x.seq - y.seq);
|
|
36
|
+
const facts = new Map();
|
|
37
|
+
for (const e of sorted)
|
|
38
|
+
if (e.action === "fact")
|
|
39
|
+
facts.set(e.interaction_id, e);
|
|
40
|
+
const attestations = new Map();
|
|
41
|
+
for (const e of sorted) {
|
|
42
|
+
if (e.action !== "attested")
|
|
43
|
+
continue;
|
|
44
|
+
const a = e;
|
|
45
|
+
const f = facts.get(a.interaction_id);
|
|
46
|
+
if (!f || NO_WORK.has(f.derived_outcome) || a.outcome === "NDA_BREACH" || a.excluded)
|
|
47
|
+
continue;
|
|
48
|
+
attestations.set(a.interaction_id, a);
|
|
49
|
+
}
|
|
50
|
+
const weight = (endedAt, code) => {
|
|
51
|
+
const age = (now - Date.parse(endedAt)) / DAY_MS;
|
|
52
|
+
return (VALUE_OUTCOMES.has(code) ? kValue : 1) * 2 ** (-age / halfLife);
|
|
53
|
+
};
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const e of sorted) {
|
|
56
|
+
if (e.action === "fact") {
|
|
57
|
+
const f = e;
|
|
58
|
+
const a = attestations.get(f.interaction_id);
|
|
59
|
+
const outcome = a ? a.outcome : f.derived_outcome;
|
|
60
|
+
if (!(outcome in OUTCOME_VALUES))
|
|
61
|
+
continue;
|
|
62
|
+
out.push({
|
|
63
|
+
interaction_id: f.interaction_id,
|
|
64
|
+
outcome,
|
|
65
|
+
from: a ? "attested" : "fact",
|
|
66
|
+
v: OUTCOME_VALUES[outcome],
|
|
67
|
+
w: weight(f.ended_at, outcome),
|
|
68
|
+
...(VALUE_OUTCOMES.has(outcome) ? { value_judgment: true } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (e.action === "probed") {
|
|
72
|
+
const p = e;
|
|
73
|
+
out.push({ interaction_id: p.interaction_id, outcome: p.result, from: "probed", v: p.result === "PASS" ? 1 : 0, w: weight(p.ended_at, p.result) });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
package/dist/ports.d.ts
ADDED
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bureau's report (Agent Reputations §8), as data: the signing tags, the
|
|
3
|
+
* report's shape, and how a capability's block and the whole report are put
|
|
4
|
+
* together from a pool and a score. Signing the report is the bureau's
|
|
5
|
+
* (signing.ts holds the bytes), because the key lives with it.
|
|
6
|
+
*
|
|
7
|
+
* The shape is the earlier bureau's own `fdbk-report-v1`, from before 0.6.0.
|
|
8
|
+
*/
|
|
9
|
+
import type { NdaRecord } from "./nda-record.js";
|
|
10
|
+
import { type QualityScore } from "./score.js";
|
|
11
|
+
import type { EnrollmentInfo, PoolResult } from "./ledger.js";
|
|
12
|
+
/** The prefix a report is signed under, over its canonical JSON. */
|
|
13
|
+
export declare const REPORT_SIG_PREFIX = "fdbk-report-v1\n";
|
|
14
|
+
/** The colon-joined signed forms of the enrollment acts (§3). */
|
|
15
|
+
export declare const ENROLL_TAG = "fdbk-enroll-v1";
|
|
16
|
+
export declare const WITHDRAW_TAG = "fdbk-withdraw-v1";
|
|
17
|
+
/** §6.6's signed form, colon-joined like the enrollment acts:
|
|
18
|
+
* fdbk-value-v1:<interaction_id>:<subject>:<capability>:<outcome>:<amount>:<currency>:<units>:<at> */
|
|
19
|
+
export declare const VALUE_TAG = "fdbk-value-v1";
|
|
20
|
+
/** What an agent signs to enroll at a bureau or to withdraw from it (§3.2):
|
|
21
|
+
* the tag, the subject's key, the bureau's URL and the instant, joined by
|
|
22
|
+
* colons. The bureau verifies these bytes and the adapter signs them, both
|
|
23
|
+
* from here. */
|
|
24
|
+
export declare function enrollmentActText(tag: typeof ENROLL_TAG | typeof WITHDRAW_TAG, subject: string, bureauUrl: string, at: string): string;
|
|
25
|
+
/** A value judgment's signed form (§6.6), the bytes the client signs and the
|
|
26
|
+
* bureau verifies. A judgment MAY carry no amount or no units, and an
|
|
27
|
+
* absent number signs as an empty position, never as zero. */
|
|
28
|
+
export declare function valueJudgmentText(j: {
|
|
29
|
+
interactionId: string;
|
|
30
|
+
seller: string;
|
|
31
|
+
offering: string;
|
|
32
|
+
outcome: string;
|
|
33
|
+
amount: number | null;
|
|
34
|
+
currency: string;
|
|
35
|
+
units: number | null;
|
|
36
|
+
at: string;
|
|
37
|
+
}): string;
|
|
38
|
+
/** How long a report stays good after it is issued. */
|
|
39
|
+
export declare const REPORT_TTL_MS: number;
|
|
40
|
+
export type CapabilityState = "UNENROLLED" | "WITHDRAWN" | "UNPROVEN" | "RATED";
|
|
41
|
+
export interface CapabilityBlock {
|
|
42
|
+
capability: string;
|
|
43
|
+
enrollment: string;
|
|
44
|
+
quality: {
|
|
45
|
+
lcb?: number;
|
|
46
|
+
mean?: number;
|
|
47
|
+
n_eff: number;
|
|
48
|
+
};
|
|
49
|
+
candor: {
|
|
50
|
+
decline_rate: number;
|
|
51
|
+
silent_failure_rate: number;
|
|
52
|
+
};
|
|
53
|
+
evidence: {
|
|
54
|
+
scored_interactions: number;
|
|
55
|
+
declined: number;
|
|
56
|
+
attester_anchors: number;
|
|
57
|
+
top_anchor_share: number;
|
|
58
|
+
value_judgments?: number;
|
|
59
|
+
value_judgment_share?: number;
|
|
60
|
+
client_not_furnished?: number;
|
|
61
|
+
oldest?: string;
|
|
62
|
+
newest?: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** What an UNENROLLED subject's capability shows: only that interactions were observed (§8.3). */
|
|
66
|
+
export interface FactsBlock {
|
|
67
|
+
capability: string;
|
|
68
|
+
enrollment: "UNENROLLED";
|
|
69
|
+
facts: {
|
|
70
|
+
interactions_recorded: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface Report {
|
|
74
|
+
subject: string;
|
|
75
|
+
enrollment: string;
|
|
76
|
+
enrolled_at?: string;
|
|
77
|
+
withdrew_at?: string;
|
|
78
|
+
withdrawal_effective_at?: string;
|
|
79
|
+
capabilities: Array<CapabilityBlock | FactsBlock>;
|
|
80
|
+
half_life_days: number;
|
|
81
|
+
unproven_threshold: number;
|
|
82
|
+
k_value: number;
|
|
83
|
+
formula_version: string;
|
|
84
|
+
bureau: string;
|
|
85
|
+
issued_at: string;
|
|
86
|
+
expires_at: string;
|
|
87
|
+
nda?: NdaRecord;
|
|
88
|
+
}
|
|
89
|
+
/** A report in its envelope (§8.1): the bureau's key and signature beside it, and the key's id
|
|
90
|
+
* in the 0.6.0 envelope; the envelope from before 0.6.0 carries no id. */
|
|
91
|
+
export interface SignedReport {
|
|
92
|
+
report: Report;
|
|
93
|
+
bureau_key: string;
|
|
94
|
+
bureau_kid?: string;
|
|
95
|
+
bureau_sig: string;
|
|
96
|
+
}
|
|
97
|
+
/** Per-capability enrollment state (§3.1): UNPROVEN vs RATED is a property
|
|
98
|
+
* of the evidence for the capability queried, not of the subject. */
|
|
99
|
+
export declare function capabilityState(info: EnrollmentInfo, nEff: number): CapabilityState;
|
|
100
|
+
/** One capability's block of an enrolled subject's report, from its pool and its score. */
|
|
101
|
+
export declare function capabilityBlock(capability: string, info: EnrollmentInfo, pool: PoolResult, q: QualityScore): CapabilityBlock;
|
|
102
|
+
/** One capability of an UNENROLLED subject: quality is opt-in (§3.1), so only
|
|
103
|
+
* the fact that interactions were observed is served, clearly labeled (§8.3). */
|
|
104
|
+
export declare function factsBlock(capability: string, pool: PoolResult): FactsBlock;
|
|
105
|
+
/** The answer to a resolution query (§8), unsigned. */
|
|
106
|
+
export declare function assembleReport(x: {
|
|
107
|
+
subject: string;
|
|
108
|
+
info: EnrollmentInfo;
|
|
109
|
+
capabilities: Array<CapabilityBlock | FactsBlock>;
|
|
110
|
+
bureau: string;
|
|
111
|
+
nowMs: number;
|
|
112
|
+
/** The NDA record (§4.9), served for any subject, enrolled or not, beside the scores and never in them. */
|
|
113
|
+
nda?: NdaRecord;
|
|
114
|
+
}): Report;
|
|
115
|
+
/**
|
|
116
|
+
* A report as 0.6.0 writes it (§8.1): one subject and one capability, the
|
|
117
|
+
* enrollment state for that capability at the top, and the score, candor and
|
|
118
|
+
* evidence blocks beside it. A facts-only report about an UNENROLLED subject
|
|
119
|
+
* (§8.3) carries no quality or candor block.
|
|
120
|
+
*/
|
|
121
|
+
export interface CapabilityReport {
|
|
122
|
+
subject: string;
|
|
123
|
+
capability: string;
|
|
124
|
+
enrollment: CapabilityState;
|
|
125
|
+
enrolled_at?: string;
|
|
126
|
+
withdrew_at?: string;
|
|
127
|
+
withdrawal_effective_at?: string;
|
|
128
|
+
quality?: {
|
|
129
|
+
lcb?: number;
|
|
130
|
+
mean?: number;
|
|
131
|
+
n_eff: number;
|
|
132
|
+
};
|
|
133
|
+
candor?: {
|
|
134
|
+
decline_rate: number;
|
|
135
|
+
silent_failure_rate: number;
|
|
136
|
+
};
|
|
137
|
+
/** This bureau's one fact for a facts-only report: how many interactions it recorded. */
|
|
138
|
+
facts?: {
|
|
139
|
+
interactions_recorded: number;
|
|
140
|
+
};
|
|
141
|
+
evidence?: {
|
|
142
|
+
half_life_days: number;
|
|
143
|
+
oldest?: string;
|
|
144
|
+
newest?: string;
|
|
145
|
+
attester_anchors: number;
|
|
146
|
+
top_anchor_share: number;
|
|
147
|
+
value_judgments?: number;
|
|
148
|
+
value_judgment_share?: number;
|
|
149
|
+
client_not_furnished?: number;
|
|
150
|
+
/** §7.5: whether a portfolio prior is in effect. This bureau applies none yet. */
|
|
151
|
+
portfolio_prior: boolean;
|
|
152
|
+
};
|
|
153
|
+
formula_version: string;
|
|
154
|
+
k_value: number;
|
|
155
|
+
unproven_threshold: number;
|
|
156
|
+
/** The subject's NDA record (§4.9), the same in every capability's report, and never in its figures. */
|
|
157
|
+
nda?: NdaRecord;
|
|
158
|
+
bureau: string;
|
|
159
|
+
issued_at: string;
|
|
160
|
+
expires_at: string;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The answer to a resolution query about one capability (§8.1), unsigned:
|
|
164
|
+
* the same figures `capabilityBlock` gives, laid out as the standard lays a
|
|
165
|
+
* report out, with the mandatory context of §8.2 beside every score.
|
|
166
|
+
*/
|
|
167
|
+
export declare function assembleCapabilityReport(x: {
|
|
168
|
+
subject: string;
|
|
169
|
+
capability: string;
|
|
170
|
+
info: EnrollmentInfo;
|
|
171
|
+
pool: PoolResult;
|
|
172
|
+
score: QualityScore;
|
|
173
|
+
bureau: string;
|
|
174
|
+
nowMs: number;
|
|
175
|
+
nda?: NdaRecord;
|
|
176
|
+
}): CapabilityReport;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { FORMULA_VERSION, HALF_LIFE_DAYS, K_VALUE, UNPROVEN_THRESHOLD } from "./score.js";
|
|
2
|
+
/** The prefix a report is signed under, over its canonical JSON. */
|
|
3
|
+
export const REPORT_SIG_PREFIX = "fdbk-report-v1\n";
|
|
4
|
+
/** The colon-joined signed forms of the enrollment acts (§3). */
|
|
5
|
+
export const ENROLL_TAG = "fdbk-enroll-v1";
|
|
6
|
+
export const WITHDRAW_TAG = "fdbk-withdraw-v1";
|
|
7
|
+
/** §6.6's signed form, colon-joined like the enrollment acts:
|
|
8
|
+
* fdbk-value-v1:<interaction_id>:<subject>:<capability>:<outcome>:<amount>:<currency>:<units>:<at> */
|
|
9
|
+
export const VALUE_TAG = "fdbk-value-v1";
|
|
10
|
+
/** What an agent signs to enroll at a bureau or to withdraw from it (§3.2):
|
|
11
|
+
* the tag, the subject's key, the bureau's URL and the instant, joined by
|
|
12
|
+
* colons. The bureau verifies these bytes and the adapter signs them, both
|
|
13
|
+
* from here. */
|
|
14
|
+
export function enrollmentActText(tag, subject, bureauUrl, at) {
|
|
15
|
+
return `${tag}:${subject}:${bureauUrl}:${at}`;
|
|
16
|
+
}
|
|
17
|
+
/** A value judgment's signed form (§6.6), the bytes the client signs and the
|
|
18
|
+
* bureau verifies. A judgment MAY carry no amount or no units, and an
|
|
19
|
+
* absent number signs as an empty position, never as zero. */
|
|
20
|
+
export function valueJudgmentText(j) {
|
|
21
|
+
return [
|
|
22
|
+
VALUE_TAG, j.interactionId, j.seller, j.offering, j.outcome,
|
|
23
|
+
j.amount === null ? "" : String(j.amount),
|
|
24
|
+
j.currency,
|
|
25
|
+
j.units === null ? "" : String(j.units),
|
|
26
|
+
j.at,
|
|
27
|
+
].join(":");
|
|
28
|
+
}
|
|
29
|
+
/** How long a report stays good after it is issued. */
|
|
30
|
+
export const REPORT_TTL_MS = 60 * 60 * 1000;
|
|
31
|
+
const round = (x) => Math.round(x * 1000) / 1000;
|
|
32
|
+
/** Per-capability enrollment state (§3.1): UNPROVEN vs RATED is a property
|
|
33
|
+
* of the evidence for the capability queried, not of the subject. */
|
|
34
|
+
export function capabilityState(info, nEff) {
|
|
35
|
+
if (info.state === "UNENROLLED")
|
|
36
|
+
return "UNENROLLED";
|
|
37
|
+
if (info.state === "WITHDRAWN")
|
|
38
|
+
return "WITHDRAWN";
|
|
39
|
+
return nEff < UNPROVEN_THRESHOLD ? "UNPROVEN" : "RATED";
|
|
40
|
+
}
|
|
41
|
+
/** One capability's block of an enrolled subject's report, from its pool and its score. */
|
|
42
|
+
export function capabilityBlock(capability, info, pool, q) {
|
|
43
|
+
const state = capabilityState(info, q.n_eff);
|
|
44
|
+
const anchors = pool.attesterAnchors;
|
|
45
|
+
const totalAttested = [...anchors.values()].reduce((a, b) => a + b, 0);
|
|
46
|
+
const top = anchors.size ? Math.max(...anchors.values()) : 0;
|
|
47
|
+
return {
|
|
48
|
+
capability,
|
|
49
|
+
enrollment: state,
|
|
50
|
+
// The score, only where the evidence has earned the right to be one
|
|
51
|
+
// (§7.4): an UNPROVEN capability serves its sample size, not a number
|
|
52
|
+
// dressed as seasoned.
|
|
53
|
+
...(state === "RATED" || state === "WITHDRAWN"
|
|
54
|
+
? { quality: { lcb: round(q.lcb), mean: round(q.mean), n_eff: round(q.n_eff) } }
|
|
55
|
+
: { quality: { n_eff: round(q.n_eff) } }),
|
|
56
|
+
candor: {
|
|
57
|
+
decline_rate: round(pool.declined / Math.max(1, pool.declined + pool.scoredInteractions)),
|
|
58
|
+
silent_failure_rate: round(pool.silentFailures / Math.max(1, pool.scoredInteractions)),
|
|
59
|
+
},
|
|
60
|
+
evidence: {
|
|
61
|
+
scored_interactions: pool.scoredInteractions,
|
|
62
|
+
declined: pool.declined,
|
|
63
|
+
attester_anchors: anchors.size,
|
|
64
|
+
top_anchor_share: round(totalAttested ? top / totalAttested : 0),
|
|
65
|
+
// §8.2: REQUIRED whenever the pool contains a value judgment, and
|
|
66
|
+
// absent otherwise so its presence means something. It is a share of
|
|
67
|
+
// the pool's WEIGHT after §7.6, not a count of events.
|
|
68
|
+
...(pool.valueJudgments
|
|
69
|
+
? { value_judgments: pool.valueJudgments, value_judgment_share: round(q.value_judgment_share) }
|
|
70
|
+
: {}),
|
|
71
|
+
// SPEC §10.8a: stops the platform attributed to the caller, because it
|
|
72
|
+
// checked the named need against what this agent declared. Reported so
|
|
73
|
+
// that "not scored" does not quietly become "never happened" — a
|
|
74
|
+
// reader can see the ending and see that it cost the provider nothing.
|
|
75
|
+
...(pool.notFurnished ? { client_not_furnished: pool.notFurnished } : {}),
|
|
76
|
+
...(pool.oldest ? { oldest: pool.oldest } : {}),
|
|
77
|
+
...(pool.newest ? { newest: pool.newest } : {}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** One capability of an UNENROLLED subject: quality is opt-in (§3.1), so only
|
|
82
|
+
* the fact that interactions were observed is served, clearly labeled (§8.3). */
|
|
83
|
+
export function factsBlock(capability, pool) {
|
|
84
|
+
return { capability, enrollment: "UNENROLLED", facts: { interactions_recorded: pool.scoredInteractions + pool.declined } };
|
|
85
|
+
}
|
|
86
|
+
/** The answer to a resolution query (§8), unsigned. */
|
|
87
|
+
export function assembleReport(x) {
|
|
88
|
+
const { info } = x;
|
|
89
|
+
return {
|
|
90
|
+
subject: x.subject,
|
|
91
|
+
enrollment: info.state === "ENROLLED" ? "ENROLLED" : info.state,
|
|
92
|
+
...(info.enrolled_at ? { enrolled_at: info.enrolled_at } : {}),
|
|
93
|
+
...(info.withdrew_at ? { withdrew_at: info.withdrew_at, withdrawal_effective_at: info.withdrawal_effective_at } : {}),
|
|
94
|
+
capabilities: x.capabilities,
|
|
95
|
+
half_life_days: HALF_LIFE_DAYS,
|
|
96
|
+
unproven_threshold: UNPROVEN_THRESHOLD,
|
|
97
|
+
// §7.6/§10.3: a bureau MAY choose K_value but MUST publish the value it
|
|
98
|
+
// uses. Published here, inside the signed bytes, so a consumer
|
|
99
|
+
// recomputing §7 knows the multiplier that produced the number.
|
|
100
|
+
k_value: K_VALUE,
|
|
101
|
+
formula_version: FORMULA_VERSION,
|
|
102
|
+
...(x.nda ? { nda: x.nda } : {}),
|
|
103
|
+
bureau: x.bureau,
|
|
104
|
+
issued_at: new Date(x.nowMs).toISOString(),
|
|
105
|
+
expires_at: new Date(x.nowMs + REPORT_TTL_MS).toISOString(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The answer to a resolution query about one capability (§8.1), unsigned:
|
|
110
|
+
* the same figures `capabilityBlock` gives, laid out as the standard lays a
|
|
111
|
+
* report out, with the mandatory context of §8.2 beside every score.
|
|
112
|
+
*/
|
|
113
|
+
export function assembleCapabilityReport(x) {
|
|
114
|
+
const { info, pool } = x;
|
|
115
|
+
const head = {
|
|
116
|
+
subject: x.subject,
|
|
117
|
+
capability: x.capability,
|
|
118
|
+
...(info.enrolled_at ? { enrolled_at: info.enrolled_at } : {}),
|
|
119
|
+
...(info.withdrew_at ? { withdrew_at: info.withdrew_at, withdrawal_effective_at: info.withdrawal_effective_at } : {}),
|
|
120
|
+
};
|
|
121
|
+
const tail = {
|
|
122
|
+
formula_version: FORMULA_VERSION,
|
|
123
|
+
k_value: K_VALUE,
|
|
124
|
+
unproven_threshold: UNPROVEN_THRESHOLD,
|
|
125
|
+
...(x.nda ? { nda: x.nda } : {}),
|
|
126
|
+
bureau: x.bureau,
|
|
127
|
+
issued_at: new Date(x.nowMs).toISOString(),
|
|
128
|
+
expires_at: new Date(x.nowMs + REPORT_TTL_MS).toISOString(),
|
|
129
|
+
};
|
|
130
|
+
if (info.state === "UNENROLLED") {
|
|
131
|
+
// §8.3: quality is opt-in; an unenrolled subject's report states facts only.
|
|
132
|
+
return { ...head, enrollment: "UNENROLLED", facts: factsBlock(x.capability, pool).facts, ...tail };
|
|
133
|
+
}
|
|
134
|
+
const block = capabilityBlock(x.capability, info, pool, x.score);
|
|
135
|
+
return {
|
|
136
|
+
...head,
|
|
137
|
+
enrollment: block.enrollment,
|
|
138
|
+
quality: block.quality,
|
|
139
|
+
candor: block.candor,
|
|
140
|
+
evidence: {
|
|
141
|
+
half_life_days: HALF_LIFE_DAYS,
|
|
142
|
+
...block.evidence,
|
|
143
|
+
portfolio_prior: false,
|
|
144
|
+
},
|
|
145
|
+
...tail,
|
|
146
|
+
};
|
|
147
|
+
}
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fdbk-score-1 — the published scoring formula of the Agent Reputations
|
|
3
|
+
* specification (§5, §7), the arithmetic of the Agent Reputations core. Pure arithmetic over an event pool: no I/O, no
|
|
4
|
+
* randomness, no iteration. Anyone holding the events computes the same
|
|
5
|
+
* numbers, which is the whole point — a score that cannot be recomputed is
|
|
6
|
+
* an opinion with a haircut.
|
|
7
|
+
*
|
|
8
|
+
* The constants are formula constants, not tunables: changing any of them is
|
|
9
|
+
* a new formula version (fdbk-score-2), never a quiet edit to this one.
|
|
10
|
+
*/
|
|
11
|
+
/** Quality value per outcome code (spec §5). DECLINED and UNREACHABLE carry
|
|
12
|
+
* no quality value: declining feeds candor, unreachability feeds facts.
|
|
13
|
+
*
|
|
14
|
+
* VALUE_OK and VALUE_POOR answer a DIFFERENT question — was the spend worth
|
|
15
|
+
* it, on engagements where nothing was named that either party could accept
|
|
16
|
+
* (§6.6) — and enter the pool at the reduced weight of §7.6. They carry the
|
|
17
|
+
* same 1.0/0.0 values as acceptance and rejection; what separates them is
|
|
18
|
+
* the weight, not the value. */
|
|
19
|
+
export declare const OUTCOME_VALUES: Record<string, number>;
|
|
20
|
+
/** The two value-judgment codes (§6.6), by name. A value judgment MUST NOT be
|
|
21
|
+
* recorded through the acceptance path, and an acceptance MUST NOT be
|
|
22
|
+
* recorded through this one; the doors check against this set. */
|
|
23
|
+
export declare const VALUE_OUTCOMES: Set<string>;
|
|
24
|
+
/** Outcomes excluded from the quality pool but still counted elsewhere. */
|
|
25
|
+
export declare const UNSCORED_OUTCOMES: Set<string>;
|
|
26
|
+
export declare const FORMULA_VERSION = "fdbk-score-1";
|
|
27
|
+
export declare const HALF_LIFE_DAYS = 60;
|
|
28
|
+
/** Below this effective sample size a capability is UNPROVEN (§7.4). */
|
|
29
|
+
export declare const UNPROVEN_THRESHOLD = 20;
|
|
30
|
+
/**
|
|
31
|
+
* §7.6: a value judgment's decay weight is multiplied by this.
|
|
32
|
+
*
|
|
33
|
+
* A formula constant of fdbk-score-1, alongside H and the UNPROVEN threshold,
|
|
34
|
+
* not a tunable. With no artifact to point at, a judgment about whether money
|
|
35
|
+
* was well spent is more subjective and more disputable than a judgment about
|
|
36
|
+
* work that was named and delivered, and the honest way to price that is one
|
|
37
|
+
* number a reader can see and argue with. The spec forbids setting it above
|
|
38
|
+
* 1.0 and advises against above 0.5; 0.5 is the published default and what
|
|
39
|
+
* this bureau uses.
|
|
40
|
+
*/
|
|
41
|
+
export declare const K_VALUE = 0.5;
|
|
42
|
+
export interface ScoredEvent {
|
|
43
|
+
/** Quality value from OUTCOME_VALUES. */
|
|
44
|
+
value: number;
|
|
45
|
+
/** ISO timestamp of the interaction's end (decay reference). */
|
|
46
|
+
at: string;
|
|
47
|
+
/** True for a §6.6 value judgment: the decay weight is multiplied by
|
|
48
|
+
* K_VALUE (§7.6). Absent means an ordinary event at full weight. */
|
|
49
|
+
value_judgment?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface QualityScore {
|
|
52
|
+
a: number;
|
|
53
|
+
b: number;
|
|
54
|
+
mean: number;
|
|
55
|
+
sd: number;
|
|
56
|
+
lcb: number;
|
|
57
|
+
n_eff: number;
|
|
58
|
+
unproven: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* §8.2: the share of the pool's weight, AFTER §7.6, that came from value
|
|
61
|
+
* judgments. Required in every report whose pool contains one — a reader
|
|
62
|
+
* who cannot separate acceptances of named work from judgments about
|
|
63
|
+
* whether a spend was worth it is reading two measurements as one number.
|
|
64
|
+
*/
|
|
65
|
+
value_judgment_share: number;
|
|
66
|
+
}
|
|
67
|
+
/** Exponential decay weight (§7.2): w = 2^(-age_days / H). Future-dated
|
|
68
|
+
* events get weight 1 rather than >1 — clock skew is not extra credit. */
|
|
69
|
+
export declare function decayWeight(atIso: string, nowMs: number, halfLifeDays?: number): number;
|
|
70
|
+
/** The weight one event contributes: the §7.2 decay, times §7.6's K_value
|
|
71
|
+
* when the event is a value judgment. Because the multiplier lands on w_i it
|
|
72
|
+
* reduces the event's effect on mean, sd, lcb and n_eff together — which is
|
|
73
|
+
* the correct reading of thinner evidence, and why §7.6 needs no other
|
|
74
|
+
* change to §7.3. */
|
|
75
|
+
export declare function eventWeight(e: ScoredEvent, nowMs: number, halfLifeDays?: number): number;
|
|
76
|
+
/**
|
|
77
|
+
* The prior of §7.3 and §7.5: the pair that takes the place of the two 1s in
|
|
78
|
+
* a and b. The uniform prior is { a0: 1, b0: 1 }. A portfolio prior may be
|
|
79
|
+
* stronger, but never by more than the equivalent of 10 effective events.
|
|
80
|
+
*/
|
|
81
|
+
export interface Prior {
|
|
82
|
+
a0: number;
|
|
83
|
+
b0: number;
|
|
84
|
+
}
|
|
85
|
+
export declare const UNIFORM_PRIOR: Prior;
|
|
86
|
+
/** §7.5: a portfolio prior's weight, a0 + b0 − 2, MUST NOT exceed this. A formula constant. */
|
|
87
|
+
export declare const MAX_PRIOR_WEIGHT = 10;
|
|
88
|
+
/** Throws when a prior is not one §7.5 allows: both terms positive and finite, its weight from 0 to 10. */
|
|
89
|
+
export declare function checkPrior(prior: Prior): void;
|
|
90
|
+
/**
|
|
91
|
+
* A portfolio prior from the anchor's other agents RATED for the same
|
|
92
|
+
* capability (§7.5), as their mean quality spread over `weight` effective
|
|
93
|
+
* events: a0 = 1 + weight · m, b0 = 1 + weight · (1 − m). The uniform prior
|
|
94
|
+
* when there are none. The caller chooses which agents count; §7.5 says only
|
|
95
|
+
* the anchor's own members RATED for this capability may.
|
|
96
|
+
*/
|
|
97
|
+
export declare function portfolioPrior(ratedMeans: readonly number[], weight?: number): Prior;
|
|
98
|
+
/**
|
|
99
|
+
* The Beta posterior summary of §7.3 over weighted values, with the prior of
|
|
100
|
+
* §7.5. n_eff counts direct evidence only: a + b less the two prior terms, so
|
|
101
|
+
* a portfolio prior moves the mean and the bound and never makes a capability
|
|
102
|
+
* RATED (§7.4).
|
|
103
|
+
*/
|
|
104
|
+
export declare function scoreWeighted(entries: ReadonlyArray<{
|
|
105
|
+
v: number;
|
|
106
|
+
w: number;
|
|
107
|
+
value_judgment?: boolean;
|
|
108
|
+
}>, prior?: Prior): QualityScore;
|
|
109
|
+
/** The Beta posterior summary of §7.3, exactly as published, over scored events, with an optional prior (§7.5). */
|
|
110
|
+
export declare function scorePool(events: ScoredEvent[], nowMs: number, prior?: Prior): QualityScore;
|