@@ -35,6 +35,7 @@ import {
3535 type UpdateCollectionParams ,
3636 type UpdateProjectParams ,
3737 type CreateMeasurementParams ,
38+ type UpdateMeasurementParams ,
3839} from "../services/hypercerts/types.js" ;
3940import type {
4041 CreateHypercertEvidenceParams ,
@@ -1190,6 +1191,45 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11901191 }
11911192 }
11921193
1194+ /**
1195+ * Applies updates to an existing measurement record, preserving immutable fields.
1196+ *
1197+ * @param existing - The existing measurement record
1198+ * @param updates - The updates to apply
1199+ * @returns Promise resolving to the updated measurement record
1200+ * @internal
1201+ */
1202+ private async applyMeasurementUpdates (
1203+ existing : HypercertMeasurement ,
1204+ updates : UpdateMeasurementParams ,
1205+ ) : Promise < HypercertMeasurement > {
1206+ const record : HypercertMeasurement = {
1207+ ...existing ,
1208+ // Preserve immutable fields
1209+ $type : existing . $type ,
1210+ subject : existing . subject ,
1211+ createdAt : existing . createdAt ,
1212+ } ;
1213+
1214+ if ( updates . metric !== undefined ) record . metric = updates . metric ;
1215+ if ( updates . unit !== undefined ) record . unit = updates . unit ;
1216+ if ( updates . value !== undefined ) record . value = updates . value ;
1217+ if ( updates . startDate !== undefined ) record . startDate = updates . startDate ;
1218+ if ( updates . endDate !== undefined ) record . endDate = updates . endDate ;
1219+ if ( updates . methodType !== undefined ) record . methodType = updates . methodType ;
1220+ if ( updates . methodURI !== undefined ) record . methodURI = updates . methodURI ;
1221+ if ( updates . evidenceURI !== undefined ) record . evidenceURI = updates . evidenceURI ;
1222+ if ( updates . measurers !== undefined ) record . measurers = updates . measurers ;
1223+ if ( updates . comment !== undefined ) record . comment = updates . comment ;
1224+ if ( updates . commentFacets !== undefined ) record . commentFacets = updates . commentFacets ;
1225+
1226+ if ( updates . locations !== undefined ) {
1227+ record . locations = await this . processLocations ( updates . locations ) ;
1228+ }
1229+
1230+ return record ;
1231+ }
1232+
11931233 /**
11941234 * Processes contribution parameters, creating contributor/contribution records if necessary.
11951235 *
@@ -1530,7 +1570,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
15301570 * @example Basic measurement
15311571 * ```typescript
15321572 * await repo.hypercerts.addMeasurement({
1533- * subjectUri : hypercertUri,
1573+ * subject : hypercertUri,
15341574 * metric: "Carbon Offset",
15351575 * unit: "tons CO2e",
15361576 * value: "150",
@@ -1549,31 +1589,26 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
15491589 * locations: [{ uri: "at://...", cid: "..." }],
15501590 * measurers: ["did:plc:auditor"],
15511591 * methodType: "satellite-imagery",
1552- * methodUri : "https://example.com/methodology",
1553- * evidenceUris : ["https://example.com/audit-report"],
1592+ * methodURI : "https://example.com/methodology",
1593+ * evidenceURI : ["https://example.com/audit-report"],
15541594 * comment: "Verified via satellite imagery",
15551595 * });
15561596 * ```
15571597 */
15581598 async addMeasurement ( params : CreateMeasurementParams ) : Promise < CreateResult > {
15591599 try {
1560- // Resolve subject to get CID
15611600 const subject = await this . resolveToStrongRef ( params . subject ) ;
15621601 const createdAt = new Date ( ) . toISOString ( ) ;
15631602
1564- // Resolve locations if provided (reuse existing processLocations helper)
15651603 const locationRefs = await this . processLocations ( params . locations ) ;
15661604
1567- // Cast to Record<string, unknown> to avoid type mismatches between
1568- // different Bluesky client packages (@atproto/api vs @atcute/bluesky)
15691605 const measurementRecord : HypercertMeasurement = {
15701606 $type : HYPERCERT_COLLECTIONS . MEASUREMENT ,
15711607 subject : { uri : subject . uri , cid : subject . cid } ,
15721608 metric : params . metric ,
15731609 unit : params . unit ,
15741610 value : params . value ,
15751611 createdAt,
1576- // Optional fields
15771612 startDate : params . startDate ,
15781613 endDate : params . endDate ,
15791614 locations : locationRefs ,
@@ -1607,6 +1642,78 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
16071642 }
16081643 }
16091644
1645+ /**
1646+ * Updates a measurement record.
1647+ *
1648+ * Note: The `subject` field is immutable and cannot be changed after creation.
1649+ *
1650+ * @param uri - AT-URI of the measurement to update
1651+ * @param updates - Fields to update (subject is excluded as it's immutable)
1652+ * @returns Promise resolving to updated measurement URI and CID
1653+ * @throws {@link ValidationError } if validation fails or URI format is invalid
1654+ * @throws {@link NetworkError } if the measurement is not found or update fails
1655+ *
1656+ * @example Updating a measurement value
1657+ * ```typescript
1658+ * await repo.hypercerts.updateMeasurement(
1659+ * "at://did:plc:test/org.hypercerts.claim.measurement/xyz",
1660+ * { value: "200", comment: "Re-verified measurement" }
1661+ * );
1662+ * ```
1663+ */
1664+ async updateMeasurement ( uri : string , updates : UpdateMeasurementParams ) : Promise < UpdateResult > {
1665+ try {
1666+ const uriMatch = uri . match ( / ^ a t : \/ \/ ( [ ^ / ] + ) \/ ( [ ^ / ] + ) \/ ( .+ ) $ / ) ;
1667+ if ( ! uriMatch ) {
1668+ throw new ValidationError ( `Invalid URI format: ${ uri } ` ) ;
1669+ }
1670+ const [ , , collection , rkey ] = uriMatch ;
1671+
1672+ if ( collection !== HYPERCERT_COLLECTIONS . MEASUREMENT ) {
1673+ throw new ValidationError (
1674+ `URI must target a measurement collection. Expected '${ HYPERCERT_COLLECTIONS . MEASUREMENT } ', got '${ collection } '` ,
1675+ ) ;
1676+ }
1677+
1678+ const existing = await this . agent . com . atproto . repo . getRecord ( {
1679+ repo : this . repoDid ,
1680+ collection,
1681+ rkey,
1682+ } ) ;
1683+
1684+ if ( ! existing . success ) {
1685+ throw new NetworkError ( `Measurement not found: ${ uri } ` ) ;
1686+ }
1687+
1688+ const recordForUpdate = await this . applyMeasurementUpdates ( existing . data . value as HypercertMeasurement , updates ) ;
1689+
1690+ const validation = validate ( recordForUpdate , HYPERCERT_COLLECTIONS . MEASUREMENT , "main" , false ) ;
1691+ if ( ! validation . success ) {
1692+ throw new ValidationError ( `Invalid measurement record: ${ validation . error ?. message } ` ) ;
1693+ }
1694+
1695+ const result = await this . agent . com . atproto . repo . putRecord ( {
1696+ repo : this . repoDid ,
1697+ collection,
1698+ rkey,
1699+ record : recordForUpdate ,
1700+ } ) ;
1701+
1702+ if ( ! result . success ) {
1703+ throw new NetworkError ( "Failed to update measurement" ) ;
1704+ }
1705+
1706+ this . emit ( "measurementUpdated" , { uri : result . data . uri , cid : result . data . cid } ) ;
1707+ return { uri : result . data . uri , cid : result . data . cid } ;
1708+ } catch ( error ) {
1709+ if ( error instanceof ValidationError || error instanceof NetworkError ) throw error ;
1710+ throw new NetworkError (
1711+ `Failed to update measurement: ${ error instanceof Error ? error . message : "Unknown" } ` ,
1712+ error ,
1713+ ) ;
1714+ }
1715+ }
1716+
16101717 /**
16111718 * Creates an evaluation record for a hypercert or other subject.
16121719 *
0 commit comments