@@ -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" ;
3839import 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 ( / ^ a t : \/ \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \/ ( .+ ) $ / ) ;
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,79 @@ 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+ * @remarks
1531+ * **New in beta.12:**
1532+ * - `unit` is now required
1533+ * - `measurers` is now optional
1534+ * - `locations` replaces singular `location` (array of StrongRefs)
1535+ * - Added `startDate`, `endDate`, `comment`, `commentFacets`
1536+ *
1537+ * @example Basic measurement
15101538 * ```typescript
15111539 * await repo.hypercerts.addMeasurement({
1512- * hypercertUri: hypercertUri,
1513- * measurers: ["did:plc:auditor"],
1540+ * subjectUri: hypercertUri,
15141541 * metric: "Carbon Offset",
1515- * value: "150 tons CO2e",
1542+ * unit: "tons CO2e",
1543+ * value: "150",
1544+ * });
1545+ * ```
1546+ *
1547+ * @example Full measurement with all options
1548+ * ```typescript
1549+ * await repo.hypercerts.addMeasurement({
1550+ * subject: "at://...",
1551+ * metric: "Forest Area",
1552+ * unit: "hectares",
1553+ * value: "500",
1554+ * startDate: "2024-01-01T00:00:00Z",
1555+ * endDate: "2024-12-31T23:59:59Z",
1556+ * locations: [{ uri: "at://...", cid: "..." }],
1557+ * measurers: ["did:plc:auditor"],
1558+ * methodType: "satellite-imagery",
15161559 * methodUri: "https://example.com/methodology",
15171560 * evidenceUris: ["https://example.com/audit-report"],
1561+ * comment: "Verified via satellite imagery",
15181562 * });
15191563 * ```
15201564 */
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 > {
1565+ async addMeasurement ( params : CreateMeasurementParams ) : Promise < CreateResult > {
15291566 try {
1530- const hypercert = await this . get ( params . hypercertUri ) ;
1567+ // Resolve subject to get CID
1568+ const subject = await this . resolveToStrongRef ( params . subject ) ;
15311569 const createdAt = new Date ( ) . toISOString ( ) ;
15321570
1571+ // Resolve locations if provided (reuse existing processLocations helper)
1572+ const locationRefs = await this . processLocations ( params . locations ) ;
1573+
1574+ // Cast to Record<string, unknown> to avoid type mismatches between
1575+ // different Bluesky client packages (@atproto/api vs @atcute/bluesky)
15331576 const measurementRecord : HypercertMeasurement = {
15341577 $type : HYPERCERT_COLLECTIONS . MEASUREMENT ,
1535- hypercert : { uri : hypercert . uri , cid : hypercert . cid } ,
1536- measurers : params . measurers ,
1578+ subject : { uri : subject . uri , cid : subject . cid } ,
15371579 metric : params . metric ,
1580+ unit : params . unit ,
15381581 value : params . value ,
15391582 createdAt,
1540- measurementMethodURI : params . methodUri ,
1541- evidenceURI : params . evidenceUris ,
1583+ // Optional fields
1584+ startDate : params . startDate ,
1585+ endDate : params . endDate ,
1586+ locations : locationRefs ,
1587+ methodType : params . methodType ,
1588+ methodURI : params . methodURI ,
1589+ evidenceURI : params . evidenceURI ,
1590+ measurers : params . measurers ,
1591+ comment : params . comment ,
1592+ commentFacets : params . commentFacets ,
15421593 } ;
15431594
15441595 const validation = validate ( measurementRecord , HYPERCERT_COLLECTIONS . MEASUREMENT , "main" , false ) ;
@@ -1549,7 +1600,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
15491600 const result = await this . agent . com . atproto . repo . createRecord ( {
15501601 repo : this . repoDid ,
15511602 collection : HYPERCERT_COLLECTIONS . MEASUREMENT ,
1552- record : measurementRecord as Record < string , unknown > ,
1603+ record : measurementRecord ,
15531604 } ) ;
15541605
15551606 if ( ! result . success ) {
0 commit comments