Skip to content

Commit d6c0bf0

Browse files
committed
fix: update add measurements to new lexicons
1 parent dfa40bf commit d6c0bf0

File tree

17 files changed

+533
-256
lines changed

17 files changed

+533
-256
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@hypercerts-org/sdk-core": minor
3+
"@hypercerts-org/sdk-react": minor
4+
---
5+
6+
Update measurement API to align with lexicon beta.12+ schema
7+
8+
**Breaking Changes (sdk-core):**
9+
10+
- `addMeasurement()` now accepts `CreateMeasurementParams` instead of individual parameters
11+
- `subject` field replaces `hypercertUri` and accepts both string AT-URIs and StrongRefs
12+
- `unit` is now a required field (e.g., "tons CO2e", "hectares", "%")
13+
- `measurers` is now optional instead of required
14+
- Field name changes: `methodUri``methodURI`, `evidenceUris``evidenceURI`
15+
16+
**Breaking Changes (sdk-react):**
17+
18+
- Removed `OrgHypercertsDefs` export (WorkScope types removed from lexicon)
19+
- `UpdateHypercertParams.workScope` now accepts `string | { uri: string; cid: string } | { $type: string }`
20+
21+
**New Features:**
22+
23+
- Support for `locations` array to specify where measurements were taken
24+
- Added `startDate` and `endDate` for measurement timeframes
25+
- Added `methodType` for short methodology identifiers
26+
- Rich text support via `comment` and `commentFacets` fields
27+
28+
**Internal Improvements:**
29+
30+
- Added `resolveToStrongRef` utility for handling string/StrongRef conversions
31+
- Updated evidence handling to use `HypercertAttachment` schema (beta.13 compatibility)
32+
- Improved error messages in URI resolution functions

packages/sdk-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"@atproto/lexicon": "^0.5.1",
8989
"@atproto/oauth-client": "^0.5.10",
9090
"@atproto/oauth-client-node": "^0.3.12",
91-
"@hypercerts-org/lexicon": "0.10.0-beta.11",
91+
"@hypercerts-org/lexicon": "0.10.0-beta.13",
9292
"eventemitter3": "^5.0.1",
9393
"type-fest": "^5.4.1",
9494
"zod": "^3.24.4"

packages/sdk-core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export {
114114
OrgHypercertsClaimContributorInformation,
115115
OrgHypercertsClaimMeasurement,
116116
OrgHypercertsClaimEvaluation,
117-
OrgHypercertsClaimEvidence,
117+
OrgHypercertsClaimAttachment,
118118
OrgHypercertsClaimCollection,
119119
OrgHypercertsHelperWorkScopeTag,
120120
AppCertifiedLocation,

packages/sdk-core/src/lexicons.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import {
7070
CONTRIBUTION_DETAILS_LEXICON_JSON,
7171
CONTRIBUTOR_INFORMATION_LEXICON_JSON,
7272
EVALUATION_LEXICON_JSON,
73-
EVIDENCE_LEXICON_JSON,
73+
ATTACHMENT_LEXICON_JSON,
7474
MEASUREMENT_LEXICON_JSON,
7575
RIGHTS_LEXICON_JSON,
7676
BADGE_AWARD_LEXICON_JSON,
@@ -86,7 +86,7 @@ import {
8686
CONTRIBUTOR_INFORMATION_NSID,
8787
MEASUREMENT_NSID,
8888
EVALUATION_NSID,
89-
EVIDENCE_NSID,
89+
ATTACHMENT_NSID,
9090
COLLECTION_NSID,
9191
BADGE_AWARD_NSID,
9292
BADGE_DEFINITION_NSID,
@@ -113,7 +113,7 @@ export const HYPERCERT_LEXICONS: LexiconDoc[] = [
113113
CONTRIBUTION_DETAILS_LEXICON_JSON as LexiconDoc,
114114
CONTRIBUTOR_INFORMATION_LEXICON_JSON as LexiconDoc,
115115
EVALUATION_LEXICON_JSON as LexiconDoc,
116-
EVIDENCE_LEXICON_JSON as LexiconDoc,
116+
ATTACHMENT_LEXICON_JSON as LexiconDoc,
117117
MEASUREMENT_LEXICON_JSON as LexiconDoc,
118118
RIGHTS_LEXICON_JSON as LexiconDoc,
119119
BADGE_AWARD_LEXICON_JSON as LexiconDoc,
@@ -168,9 +168,15 @@ export const HYPERCERT_COLLECTIONS = {
168168
EVALUATION: EVALUATION_NSID,
169169

170170
/**
171-
* Evidence record collection.
171+
* Attachment record collection (formerly evidence).
172+
* @remarks Renamed from EVIDENCE in beta.13
172173
*/
173-
EVIDENCE: EVIDENCE_NSID,
174+
ATTACHMENT: ATTACHMENT_NSID,
175+
176+
/**
177+
* @deprecated Use ATTACHMENT instead. Renamed in beta.13.
178+
*/
179+
EVIDENCE: ATTACHMENT_NSID,
174180

175181
/**
176182
* Collection record collection (groups of hypercerts).

packages/sdk-core/src/repository/HypercertOperationsImpl.ts

Lines changed: 97 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
type HypercertContributionDetails,
2525
type HypercertContributorInformation,
2626
type HypercertEvaluation,
27-
type HypercertEvidence,
27+
type HypercertAttachment,
2828
type HypercertLocation,
2929
type CreateLocationParams,
3030
type HypercertMeasurement,
@@ -34,6 +34,7 @@ import {
3434
type StrongRef,
3535
type UpdateCollectionParams,
3636
type UpdateProjectParams,
37+
type CreateMeasurementParams,
3738
} from "../services/hypercerts/types.js";
3839
import type {
3940
CreateHypercertEvidenceParams,
@@ -1033,52 +1034,73 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
10331034

10341035
/**
10351036
* Helper to resolve a location reference to a StrongRef.
1037+
* Uses resolveToStrongRef for string and StrongRef inputs.
10361038
*
10371039
* @param location - Location parameter (StrongRef, string URI, or location object)
10381040
* @returns Promise resolving to a StrongRef
1039-
* @throws {ValidationError} When string input doesn't match AT-URI pattern
1041+
* @throws {ValidationError} When string input doesn't match AT-URI pattern or input is invalid
10401042
* @throws {NetworkError} When getRecord fails or returns no CID
10411043
* @internal
10421044
*/
10431045
private async resolveLocation(location: LocationParams): Promise<StrongRef> {
1044-
if (typeof location === "string") {
1045-
return this.resolveStrongRefFromUri(location);
1046-
}
1047-
1046+
// If it's an object with location data, create new record
10481047
if (this.isLocationObject(location)) {
10491048
return this.createLocationRecord(location);
10501049
}
10511050

1052-
if ("uri" in location && "cid" in location) {
1053-
return { $type: "com.atproto.repo.strongRef" as const, uri: location.uri, cid: location.cid };
1054-
}
1055-
1056-
throw new ValidationError("resolveLocation: Unsupported location input.");
1051+
// Otherwise it's string | StrongRef, resolve to StrongRef
1052+
return this.resolveToStrongRef(location);
10571053
}
10581054

10591055
private async resolveStrongRefFromUri(uri: string): Promise<StrongRef> {
10601056
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
10611057
if (!uriMatch) {
1062-
throw new ValidationError(`resolveLocation: Invalid location AT-URI: "${uri}"`);
1058+
throw new ValidationError(`Invalid AT-URI format: "${uri}"`);
10631059
}
10641060

10651061
const [, repo, collection, rkey] = uriMatch;
10661062
const record = await this.agent.com.atproto.repo.getRecord({ repo, collection, rkey });
10671063
if (!record.success) {
1068-
throw new NetworkError(
1069-
`resolveLocation: getRecord failed for repo=${repo}, collection=${collection}, rkey=${rkey}`,
1070-
);
1064+
throw new NetworkError(`Failed to fetch record for repo=${repo}, collection=${collection}, rkey=${rkey}`);
10711065
}
10721066
if (!record.data.cid) {
1073-
throw new NetworkError(
1074-
`resolveLocation: getRecord returned no CID for repo=${repo}, collection=${collection}, rkey=${rkey}`,
1075-
);
1067+
throw new NetworkError(`Record missing CID for repo=${repo}, collection=${collection}, rkey=${rkey}`);
10761068
}
10771069

10781070
return { $type: "com.atproto.repo.strongRef" as const, uri, cid: record.data.cid };
10791071
}
10801072

10811073
/**
1074+
* Resolves a string URI or StrongRef to a StrongRef.
1075+
* If input is already a StrongRef, returns it as-is.
1076+
* If input is a string URI, fetches the record to get the CID.
1077+
*
1078+
* @param input - String AT-URI or existing StrongRef
1079+
* @returns Promise resolving to a StrongRef with $type, uri, and cid
1080+
* @throws {@link ValidationError} When input is invalid or URI format is incorrect
1081+
* @throws {@link NetworkError} When getRecord fails
1082+
* @internal
1083+
*/
1084+
private async resolveToStrongRef(input: string | StrongRef): Promise<StrongRef> {
1085+
// Check if already a StrongRef
1086+
if (typeof input === "object" && "uri" in input && "cid" in input) {
1087+
return {
1088+
$type: "com.atproto.repo.strongRef" as const,
1089+
uri: input.uri,
1090+
cid: input.cid,
1091+
};
1092+
}
1093+
1094+
// Must be a string URI
1095+
if (typeof input === "string") {
1096+
return this.resolveStrongRefFromUri(input);
1097+
}
1098+
1099+
throw new ValidationError("Invalid input: expected string URI or StrongRef");
1100+
}
1101+
1102+
/**
1103+
* TODO: Match Attachment Lexicon
10821104
* Adds evidence to any subject via the subject ref.
10831105
*
10841106
* @param evidence - HypercertEvidenceInput
@@ -1105,14 +1127,19 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11051127
const createdAt = new Date().toISOString();
11061128

11071129
const evidenceContent = await this.resolveUriOrBlob(content, "application/octet-stream");
1108-
const evidenceRecord: HypercertEvidence = {
1130+
// Note: In beta.13, evidence was renamed to attachment with schema changes
1131+
// - subject -> subjects (array)
1132+
// - content is now an array
1133+
// This is a temporary fix to maintain backward compatibility
1134+
// and since evidence is no longer available in the lexicons
1135+
const evidenceRecord: HypercertAttachment = {
11091136
...rest,
1110-
$type: HYPERCERT_COLLECTIONS.EVIDENCE,
1137+
$type: HYPERCERT_COLLECTIONS.ATTACHMENT,
11111138
createdAt,
1112-
content: evidenceContent,
1113-
subject: { uri: subject.uri, cid: subject.cid },
1139+
content: [evidenceContent], // content is now an array
1140+
subjects: [{ uri: subject.uri, cid: subject.cid }], // subject -> subjects array
11141141
};
1115-
const validation = validate(evidenceRecord, HYPERCERT_COLLECTIONS.EVIDENCE, "main", false);
1142+
const validation = validate(evidenceRecord, HYPERCERT_COLLECTIONS.ATTACHMENT, "main", false);
11161143
if (!validation.success) {
11171144
throw new ValidationError(`Invalid evidence record: ${validation.error?.message}`);
11181145
}
@@ -1490,55 +1517,72 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
14901517
}
14911518

14921519
/**
1493-
* Creates a measurement record for a hypercert.
1494-
*
1495-
* Measurements quantify the impact claimed in a hypercert with
1496-
* specific metrics and values.
1497-
*
1498-
* @param params - Measurement parameters
1499-
* @param params.hypercertUri - AT-URI of the hypercert being measured
1500-
* @param params.measurers - DIDs of entities who performed the measurement
1501-
* @param params.metric - Name of the metric (e.g., "CO2 Reduced", "Trees Planted")
1502-
* @param params.value - Measured value with units (e.g., "100 tons", "10000")
1503-
* @param params.methodUri - Optional URI describing the measurement methodology
1504-
* @param params.evidenceUris - Optional URIs to supporting evidence
1520+
* Creates a measurement record for a hypercert or other subject.
1521+
*
1522+
* Measurements quantify the impact claimed with specific metrics,
1523+
* values, and units.
1524+
*
1525+
* @param params - Measurement parameters (see {@link CreateMeasurementParams})
15051526
* @returns Promise resolving to measurement record URI and CID
15061527
* @throws {@link ValidationError} if validation fails
15071528
* @throws {@link NetworkError} if the operation fails
15081529
*
1509-
* @example
1530+
* @example Basic measurement
15101531
* ```typescript
15111532
* await repo.hypercerts.addMeasurement({
1512-
* hypercertUri: hypercertUri,
1513-
* measurers: ["did:plc:auditor"],
1533+
* subjectUri: hypercertUri,
15141534
* metric: "Carbon Offset",
1515-
* value: "150 tons CO2e",
1535+
* unit: "tons CO2e",
1536+
* value: "150",
1537+
* });
1538+
* ```
1539+
*
1540+
* @example Full measurement with all options
1541+
* ```typescript
1542+
* await repo.hypercerts.addMeasurement({
1543+
* subject: "at://...",
1544+
* metric: "Forest Area",
1545+
* unit: "hectares",
1546+
* value: "500",
1547+
* startDate: "2024-01-01T00:00:00Z",
1548+
* endDate: "2024-12-31T23:59:59Z",
1549+
* locations: [{ uri: "at://...", cid: "..." }],
1550+
* measurers: ["did:plc:auditor"],
1551+
* methodType: "satellite-imagery",
15161552
* methodUri: "https://example.com/methodology",
15171553
* evidenceUris: ["https://example.com/audit-report"],
1554+
* comment: "Verified via satellite imagery",
15181555
* });
15191556
* ```
15201557
*/
1521-
async addMeasurement(params: {
1522-
hypercertUri: string;
1523-
measurers: string[];
1524-
metric: string;
1525-
value: string;
1526-
methodUri?: string;
1527-
evidenceUris?: string[];
1528-
}): Promise<CreateResult> {
1558+
async addMeasurement(params: CreateMeasurementParams): Promise<CreateResult> {
15291559
try {
1530-
const hypercert = await this.get(params.hypercertUri);
1560+
// Resolve subject to get CID
1561+
const subject = await this.resolveToStrongRef(params.subject);
15311562
const createdAt = new Date().toISOString();
15321563

1564+
// Resolve locations if provided (reuse existing processLocations helper)
1565+
const locationRefs = await this.processLocations(params.locations);
1566+
1567+
// Cast to Record<string, unknown> to avoid type mismatches between
1568+
// different Bluesky client packages (@atproto/api vs @atcute/bluesky)
15331569
const measurementRecord: HypercertMeasurement = {
15341570
$type: HYPERCERT_COLLECTIONS.MEASUREMENT,
1535-
hypercert: { uri: hypercert.uri, cid: hypercert.cid },
1536-
measurers: params.measurers,
1571+
subject: { uri: subject.uri, cid: subject.cid },
15371572
metric: params.metric,
1573+
unit: params.unit,
15381574
value: params.value,
15391575
createdAt,
1540-
measurementMethodURI: params.methodUri,
1541-
evidenceURI: params.evidenceUris,
1576+
// Optional fields
1577+
startDate: params.startDate,
1578+
endDate: params.endDate,
1579+
locations: locationRefs,
1580+
methodType: params.methodType,
1581+
methodURI: params.methodURI,
1582+
evidenceURI: params.evidenceURI,
1583+
measurers: params.measurers,
1584+
comment: params.comment,
1585+
commentFacets: params.commentFacets,
15421586
};
15431587

15441588
const validation = validate(measurementRecord, HYPERCERT_COLLECTIONS.MEASUREMENT, "main", false);
@@ -1549,7 +1593,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
15491593
const result = await this.agent.com.atproto.repo.createRecord({
15501594
repo: this.repoDid,
15511595
collection: HYPERCERT_COLLECTIONS.MEASUREMENT,
1552-
record: measurementRecord as Record<string, unknown>,
1596+
record: measurementRecord,
15531597
});
15541598

15551599
if (!result.success) {

0 commit comments

Comments
 (0)