pointbreak 0.10.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::path::{Path, PathBuf};

use crate::error::{Result, ShoreError};
use crate::model::{ObjectId, id_prefix};
use crate::session::body_artifact::{body_artifact_field, note_body_content_hash_from_path};
use crate::session::event::{EventType, ShoreEvent, WorkObjectProposal, WorkObjectProposedPayload};
use crate::session::object_artifact::{
    decode_and_validate_object_artifact, read_bound_object_artifact_bytes,
};
use crate::session::store::content::ContentArtifacts;
use crate::session::store::resolution::{
    prepare_write_landing, resolve_read_store, resolve_write_store,
};
use crate::storage::{CreateOutcome, LocalStorage};

/// The kind of content-addressed artifact an event references.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ArtifactKind {
    /// A captured Revision's content object artifact.
    Object,
    /// A large note-shaped body artifact.
    Body,
}

/// An opaque reference to a content-addressed artifact required by one or more
/// events.
///
/// The stable surface exposes the artifact kind and content hash. Any locator
/// needed to read or write Pointbreak's current on-disk layout stays private and
/// must be passed back to [`export_artifact`] / [`import_artifact`]. Remote
/// consumers derive these refs from forwarded events with
/// [`referenced_artifacts`], fetch bytes by [`ArtifactRef::content_hash`], and
/// pass the same refs to [`import_artifact`].
#[derive(Clone, Eq, PartialEq)]
pub struct ArtifactRef {
    locator: ArtifactLocator,
    content_hash: String,
}

impl ArtifactRef {
    /// The artifact's broad kind.
    pub fn kind(&self) -> ArtifactKind {
        match self.locator {
            ArtifactLocator::Object { .. } => ArtifactKind::Object,
            ArtifactLocator::Body { .. } => ArtifactKind::Body,
        }
    }

    /// The artifact's expected content hash, normalized as `sha256:<hex>`.
    pub fn content_hash(&self) -> &str {
        &self.content_hash
    }
}

impl fmt::Debug for ArtifactRef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ArtifactRef")
            .field("kind", &self.kind())
            .field("content_hash", &self.content_hash)
            .finish()
    }
}

#[derive(Clone, Eq, PartialEq)]
enum ArtifactLocator {
    Object { object_id: ObjectId },
    Body { relative_path: String },
}

/// Options for importing a content-addressed artifact into a repo's `.pointbreak/data`
/// store.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImportArtifactOptions {
    repo: PathBuf,
    artifact: ArtifactRef,
    bytes: Vec<u8>,
}

impl ImportArtifactOptions {
    /// Create artifact-import options from a destination repo, the expected
    /// artifact reference, and the bytes fetched from a source store.
    pub fn new(repo: impl AsRef<Path>, artifact: ArtifactRef, bytes: Vec<u8>) -> Self {
        Self {
            repo: repo.as_ref().to_path_buf(),
            artifact,
            bytes,
        }
    }
}

/// Whether an artifact import created a new blob or found the identical blob
/// already present.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ImportArtifactOutcome {
    /// The artifact bytes were written to the destination store.
    Created,
    /// The destination store already contained the identical artifact.
    Existing,
}

/// The result of importing one artifact.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ImportArtifactResult {
    /// The artifact reference that was imported.
    pub artifact: ArtifactRef,
    /// Whether the import created a new artifact or found an existing one.
    pub outcome: ImportArtifactOutcome,
}

/// Enumerate the artifacts referenced by a set of events.
///
/// The returned references are deduplicated and deterministic. Body artifact
/// hashes are derived from `artifacts/notes/<hex>.json` locators and normalized
/// to `sha256:<hex>` so callers do not need to understand the filename/hash
/// prefix difference.
pub fn referenced_artifacts(events: &[ShoreEvent]) -> Result<Vec<ArtifactRef>> {
    let mut refs = BTreeMap::<String, ArtifactRef>::new();
    for event in events {
        referenced_artifacts_for_event(event, &mut refs)?;
    }
    Ok(refs.into_values().collect())
}

/// Content identities that can pull removal/signature support carriers into a
/// selected product read.
///
/// This set is intentionally broader than [`referenced_artifacts`]. The latter
/// enumerates Pointbreak-owned transferable bytes; this closure also includes
/// external validation-log hashes and the flat content-hash fields carried by
/// historical event families. A removal or detached signature over any of
/// those identities can change a selected revision's diagnostics even when
/// Pointbreak does not own bytes for the referenced content.
pub(crate) fn selected_support_content_hashes(events: &[ShoreEvent]) -> Result<BTreeSet<String>> {
    const HASH_FIELDS: [&str; 5] = [
        "bodyContentHash",
        "summaryContentHash",
        "reasonContentHash",
        "objectArtifactContentHash",
        "contentHash",
    ];
    let mut hashes = referenced_artifacts(events)?
        .into_iter()
        .map(|artifact| artifact.content_hash().to_owned())
        .collect::<BTreeSet<_>>();
    for event in events {
        let Some(payload) = event.payload.as_object() else {
            continue;
        };
        hashes.extend(
            HASH_FIELDS
                .iter()
                .filter_map(|field| payload.get(*field).and_then(serde_json::Value::as_str))
                .map(str::to_owned),
        );
        hashes.extend(
            payload
                .get("logArtifactContentHashes")
                .and_then(serde_json::Value::as_array)
                .into_iter()
                .flatten()
                .filter_map(serde_json::Value::as_str)
                .map(str::to_owned),
        );
    }
    Ok(hashes)
}

/// Export an artifact's validated bytes from a source repo.
///
/// Reads resolve through the linked clone-local store when one is registered
/// for the worktree. Imports stay worktree-local; see [`import_artifact`].
pub fn export_artifact(repo: impl AsRef<Path>, artifact: &ArtifactRef) -> Result<Vec<u8>> {
    match &artifact.locator {
        ArtifactLocator::Object { object_id } => {
            let bytes = read_bound_object_artifact_bytes(repo, object_id, &artifact.content_hash)?;
            let stored = decode_and_validate_object_artifact(&bytes)?;
            if stored.content_hash != artifact.content_hash {
                return Err(ShoreError::Message(format!(
                    "object artifact content hash mismatch for {}",
                    artifact.content_hash
                )));
            }
            Ok(bytes)
        }
        ArtifactLocator::Body { relative_path } => {
            let read_store = resolve_read_store(repo.as_ref())?;
            ContentArtifacts::from_backend(read_store.backend())
                .read_note_body_bytes(relative_path, &artifact.content_hash)
        }
    }
}

/// Import an artifact into a destination repo after validating its content
/// hash.
///
/// The write is idempotent: importing the same valid artifact again returns
/// [`ImportArtifactOutcome::Existing`]. A conflicting existing artifact or
/// bytes that do not match the reference hash are rejected.
pub fn import_artifact(options: ImportArtifactOptions) -> Result<ImportArtifactResult> {
    let write_store = resolve_write_store(&options.repo)?;
    let storage = LocalStorage::new(write_store.store_dir());
    // The dir layout + `.git/info/exclude` are a worktree/file concern with no
    // non-file analogue, so landing prep stays on `LocalStorage`; the content
    // I/O below flows through the resolved backend handle.
    prepare_write_landing(&write_store, &storage)?;

    let content = ContentArtifacts::from_backend(write_store.backend());
    let outcome = match &options.artifact.locator {
        ArtifactLocator::Object { object_id } => {
            content.import_object(object_id, &options.artifact.content_hash, &options.bytes)?
        }
        ArtifactLocator::Body { relative_path } => content.import_body(
            relative_path,
            &options.artifact.content_hash,
            &options.bytes,
        )?,
    };

    Ok(ImportArtifactResult {
        artifact: options.artifact,
        outcome: match outcome {
            CreateOutcome::Created => ImportArtifactOutcome::Created,
            CreateOutcome::AlreadyExists => ImportArtifactOutcome::Existing,
        },
    })
}

fn referenced_artifacts_for_event(
    event: &ShoreEvent,
    refs: &mut BTreeMap<String, ArtifactRef>,
) -> Result<()> {
    // The object family externalizes a captured Revision's content object; it
    // needs the typed `ObjectId`, so it stays a typed, strict arm that errors on
    // a malformed payload.
    if event.event_type == EventType::WorkObjectProposed {
        let payload: WorkObjectProposedPayload = serde_json::from_value(event.payload.clone())?;
        if let WorkObjectProposal::Revision {
            revision,
            object_artifact_content_hash,
            ..
        } = payload.work_object
        {
            insert_artifact_ref(
                refs,
                format!(
                    "{}:{object_artifact_content_hash}",
                    id_prefix::ARTIFACT_OBJECT
                ),
                ArtifactRef {
                    locator: ArtifactLocator::Object {
                        object_id: revision.object_id,
                    },
                    content_hash: object_artifact_content_hash,
                },
            )?;
        }
        // A task-attempt proposal references no content-addressed artifact.
        return Ok(());
    }

    // Every body-bearing family externalizes exactly one path field, named by the
    // shared registry. Read it leniently from raw JSON (aligned with the bundle
    // path); `insert_body_ref` still validates the path shape and hash.
    if let Some(field) = body_artifact_field(event.event_type) {
        let path = event
            .payload
            .get(field.payload_field())
            .and_then(|value| value.as_str());
        return insert_body_ref(refs, path);
    }

    Ok(())
}

fn insert_body_ref(
    refs: &mut BTreeMap<String, ArtifactRef>,
    relative_path: Option<&str>,
) -> Result<()> {
    let Some(relative_path) = relative_path else {
        return Ok(());
    };
    let content_hash = note_body_content_hash_from_path(relative_path)?;
    insert_artifact_ref(
        refs,
        format!("{}:{content_hash}", id_prefix::ARTIFACT_BODY),
        ArtifactRef {
            locator: ArtifactLocator::Body {
                relative_path: relative_path.to_owned(),
            },
            content_hash,
        },
    )
}

fn insert_artifact_ref(
    refs: &mut BTreeMap<String, ArtifactRef>,
    key: String,
    artifact: ArtifactRef,
) -> Result<()> {
    if let Some(existing) = refs.get(&key) {
        if existing == &artifact {
            return Ok(());
        }
        return Err(ShoreError::Message(format!(
            "conflicting artifact reference for {}",
            artifact.content_hash
        )));
    }
    refs.insert(key, artifact);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{
        JournalId, RevisionId, TrackId, ValidationCheckId, ValidationStatus, ValidationTarget,
        ValidationTrigger,
    };
    use crate::session::event::{EventTarget, EventType, ValidationCheckRecordedPayload, Writer};

    #[test]
    fn referenced_artifacts_includes_validation_summary_body() {
        let hash = "a".repeat(64);
        let event = validation_event_with_summary_path(&format!("artifacts/notes/{hash}.json"));

        let refs = referenced_artifacts(&[event]).unwrap();

        assert!(refs.iter().any(|artifact| {
            artifact.kind() == ArtifactKind::Body
                && artifact.content_hash() == format!("sha256:{hash}")
        }));
    }

    #[test]
    fn selected_support_hashes_include_owned_objects_bodies_and_external_logs() {
        let body_hash = "a".repeat(64);
        let mut validation =
            validation_event_with_summary_path(&format!("artifacts/notes/{body_hash}.json"));
        validation.payload["logArtifactContentHashes"] = serde_json::json!(["sha256:external-log"]);

        let hashes = selected_support_content_hashes(&[validation]).unwrap();

        assert!(hashes.contains(&format!("sha256:{body_hash}")));
        assert!(hashes.contains("sha256:external-log"));
    }

    #[test]
    fn every_registry_body_family_yields_a_body_ref() {
        let hash = "b".repeat(64);
        let path = format!("artifacts/notes/{hash}.json");

        for event_type in EventType::ALL {
            let Some(field) = body_artifact_field(event_type) else {
                continue;
            };
            let field_name = field.payload_field();
            let mut event = base_event();
            event.event_type = event_type;
            event.payload = serde_json::json!({ field_name: path });

            let refs = referenced_artifacts(&[event]).unwrap();
            assert!(
                refs.iter().any(|a| a.kind() == ArtifactKind::Body
                    && a.content_hash() == format!("sha256:{hash}")),
                "path 1 dropped the body artifact for {event_type:?}"
            );
        }
    }

    #[test]
    fn non_body_families_yield_no_body_ref() {
        for event_type in EventType::ALL {
            if body_artifact_field(event_type).is_some()
                || event_type == EventType::WorkObjectProposed
            {
                continue; // body families and the object family are covered elsewhere
            }
            let mut event = base_event();
            event.event_type = event_type;
            event.payload = serde_json::json!({ "bodyArtifactPath": "artifacts/notes/x.json" });

            let refs = referenced_artifacts(&[event]).unwrap();
            assert!(
                refs.iter().all(|a| a.kind() != ArtifactKind::Body),
                "path 1 spuriously enumerated a body artifact for non-body {event_type:?}"
            );
        }
    }

    #[test]
    fn malformed_work_object_proposed_payload_still_errors() {
        // The object family stays typed + strict: a malformed payload must error,
        // not be silently skipped.
        let mut event = base_event();
        event.event_type = EventType::WorkObjectProposed;
        event.payload = serde_json::json!({ "workObject": "not-an-object" });

        assert!(referenced_artifacts(&[event]).is_err());
    }

    /// A minimal valid event to clone/overwrite; the enumeration reads only
    /// `event_type` + `payload`, so any well-formed base works.
    fn base_event() -> ShoreEvent {
        validation_event_with_summary_path("artifacts/notes/placeholder.json")
    }

    fn validation_event_with_summary_path(path: &str) -> ShoreEvent {
        let revision_id = RevisionId::new("review-unit:sha256:one");
        let target = EventTarget::for_revision(
            JournalId::new("journal:default"),
            revision_id.clone(),
            Some(TrackId::new("agent:codex")),
        )
        .unwrap();
        ShoreEvent::new(
            EventType::ValidationCheckRecorded,
            "validation_check_recorded:one",
            target,
            Writer::shore_local("0.1.0"),
            ValidationCheckRecordedPayload {
                validation_check_id: ValidationCheckId::new("validation:sha256:one"),
                target: ValidationTarget::Revision { revision_id },
                check_name: "cargo test".to_owned(),
                command: None,
                status: ValidationStatus::Passed,
                exit_code: Some(0),
                trigger: ValidationTrigger::Manual,
                source_fingerprint: None,
                summary: None,
                summary_content_type: Default::default(),
                summary_artifact_path: Some(path.to_owned()),
                summary_byte_size: Some(10),
                summary_content_hash: Some("sha256:summary".to_owned()),
                started_at: None,
                completed_at: None,
                log_artifact_content_hashes: Vec::new(),
            },
            "2026-05-13T10:00:00Z",
        )
        .unwrap()
    }
}