feat: add staging entry source anchors
This commit is contained in:
parent
08c4547a2e
commit
31798fb285
|
|
@ -206,8 +206,8 @@ pub fn render_tidy_hints(tidy: &TidyHints) -> String {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::consolidate::tidy::{SimilarSlugCluster, SourcesOverflow};
|
use crate::consolidate::tidy::{SimilarSlugCluster, SourcesOverflow};
|
||||||
use crate::extract::{ExtractedPayload, write_staging};
|
use crate::extract::{DecisionEntry, ExtractedPayload, write_staging};
|
||||||
use crate::schema::SourceRef;
|
use crate::schema::{EvidenceKind, SourceEvidenceRef, SourceRef};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
@ -277,6 +277,46 @@ mod tests {
|
||||||
assert!(out.contains("no explicit memory usage events"));
|
assert!(out.contains("no explicit memory usage events"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn staging_render_preserves_entry_source_refs() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let layout = WorkspaceLayout::new(dir.path().to_path_buf());
|
||||||
|
let (_id, _) = write_staging(
|
||||||
|
&layout,
|
||||||
|
SourceRef {
|
||||||
|
segment_id: "segment-record".into(),
|
||||||
|
range: [0, 10],
|
||||||
|
},
|
||||||
|
ExtractedPayload {
|
||||||
|
decisions: vec![DecisionEntry {
|
||||||
|
options: vec!["preserve".into()],
|
||||||
|
chosen: "preserve".into(),
|
||||||
|
rationale: "consolidation input is lossless JSON".into(),
|
||||||
|
source_refs: vec![SourceEvidenceRef {
|
||||||
|
session_id: Some("session-1".into()),
|
||||||
|
segment_id: Some("segment-1".into()),
|
||||||
|
entry_range: Some([3, 4]),
|
||||||
|
evidence_id: Some("ev-1".into()),
|
||||||
|
evidence_kind: Some(EvidenceKind::new(EvidenceKind::MESSAGE)),
|
||||||
|
label: Some("user message".into()),
|
||||||
|
summary: Some("bounded summary".into()),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let staging = crate::consolidate::staging::list_staging_entries(&layout);
|
||||||
|
|
||||||
|
let out = render_staging_records(&staging);
|
||||||
|
|
||||||
|
assert!(out.contains("source_refs"));
|
||||||
|
assert!(out.contains("session-1"));
|
||||||
|
assert!(out.contains("entry_range"));
|
||||||
|
assert!(out.contains("ev-1"));
|
||||||
|
assert!(out.contains("message"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_inputs_render_placeholders() {
|
fn empty_inputs_render_placeholders() {
|
||||||
let dir = tempfile::TempDir::new().unwrap();
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -150,10 +150,11 @@ mod tests {
|
||||||
let layout = WorkspaceLayout::new(tmp.path().to_path_buf());
|
let layout = WorkspaceLayout::new(tmp.path().to_path_buf());
|
||||||
let (_id, _) = write_staging(&layout, source("s", [0, 1]), empty_payload()).unwrap();
|
let (_id, _) = write_staging(&layout, source("s", [0, 1]), empty_payload()).unwrap();
|
||||||
|
|
||||||
// Drop a non-UUID json file, an unparsable UUID-named json file, an
|
// Drop a non-UUID json file, an unparsable UUID-named json file, a
|
||||||
// old-schema UUID-named json file, and a bare lock file alongside.
|
// legacy source UUID-named json file, and a bare lock file alongside.
|
||||||
// Lock files are not `.json`; invalid `.json` files are surfaced
|
// Lock files are not `.json`; invalid `.json` files are surfaced
|
||||||
// separately instead of being mistaken for an empty staging directory.
|
// separately instead of being mistaken for an empty staging directory.
|
||||||
|
// Legacy `source.session_id` staging remains readable for compatibility.
|
||||||
std::fs::write(layout.staging_dir().join("not-a-uuid.json"), "{}").unwrap();
|
std::fs::write(layout.staging_dir().join("not-a-uuid.json"), "{}").unwrap();
|
||||||
let bad_id = Uuid::now_v7();
|
let bad_id = Uuid::now_v7();
|
||||||
std::fs::write(layout.staging_dir().join(format!("{bad_id}.json")), "{").unwrap();
|
std::fs::write(layout.staging_dir().join(format!("{bad_id}.json")), "{").unwrap();
|
||||||
|
|
@ -173,11 +174,17 @@ mod tests {
|
||||||
std::fs::write(layout.staging_dir().join(".consolidation.lock"), "{}").unwrap();
|
std::fs::write(layout.staging_dir().join(".consolidation.lock"), "{}").unwrap();
|
||||||
|
|
||||||
let entries = list_staging_entries(&layout);
|
let entries = list_staging_entries(&layout);
|
||||||
assert_eq!(entries.len(), 1);
|
assert_eq!(entries.len(), 2);
|
||||||
|
|
||||||
let snapshot = list_staging_entries_snapshot(&layout);
|
let snapshot = list_staging_entries_snapshot(&layout);
|
||||||
assert_eq!(snapshot.entries.len(), 1);
|
assert_eq!(snapshot.entries.len(), 2);
|
||||||
assert_eq!(snapshot.invalid_count, 3);
|
assert_eq!(snapshot.invalid_count, 2);
|
||||||
|
assert!(
|
||||||
|
snapshot
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.any(|entry| entry.record.source.segment_id == "legacy-session")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
//! extract 抽出の出力 schema。
|
//! extract 抽出の出力 schema。
|
||||||
//!
|
//!
|
||||||
//! LLM は [`ExtractedPayload`] そのもの(source 抜き)を返し、Worker 側
|
//! LLM は [`ExtractedPayload`] そのもの(record-level source 抜き)を返し、Worker 側
|
||||||
//! ラッパーが [`StagingRecord`] に組み立てて staging へ書き出す。
|
//! ラッパーが [`StagingRecord`] に組み立てて staging へ書き出す。
|
||||||
//! source は機械付与する契約 (`docs/plan/memory.md` §Extract)。
|
//! source は機械付与する契約 (`docs/plan/memory.md` §Extract)。
|
||||||
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::schema::SourceRef;
|
use crate::schema::{SourceEvidenceRef, SourceRef};
|
||||||
|
|
||||||
/// LLM が返す活動ログ候補の集合。すべて optional(空配列は許容)。
|
/// LLM が返す活動ログ候補の集合。すべて optional(空配列は許容)。
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
@ -42,6 +42,10 @@ pub struct DecisionEntry {
|
||||||
pub chosen: String,
|
pub chosen: String,
|
||||||
/// 採用理由 / 根拠。
|
/// 採用理由 / 根拠。
|
||||||
pub rationale: String,
|
pub rationale: String,
|
||||||
|
/// Host-resolved anchors backing this individual claim.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
#[schemars(skip)]
|
||||||
|
pub source_refs: Vec<SourceEvidenceRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 議論したこと(トピック + 論点)。結論が出ていなくてもよい。
|
/// 議論したこと(トピック + 論点)。結論が出ていなくてもよい。
|
||||||
|
|
@ -51,6 +55,10 @@ pub struct DiscussionEntry {
|
||||||
pub topic: String,
|
pub topic: String,
|
||||||
/// 主題の中で挙がった論点 / 観点。
|
/// 主題の中で挙がった論点 / 観点。
|
||||||
pub points: Vec<String>,
|
pub points: Vec<String>,
|
||||||
|
/// Host-resolved anchors backing this individual claim.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
#[schemars(skip)]
|
||||||
|
pub source_refs: Vec<SourceEvidenceRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 試したこと(試行 + 結果 + 成否)。
|
/// 試したこと(試行 + 結果 + 成否)。
|
||||||
|
|
@ -62,6 +70,10 @@ pub struct AttemptEntry {
|
||||||
pub result: String,
|
pub result: String,
|
||||||
/// 試行が目的に対して成功したか。失敗 / 部分成功も含めて bool で表現する。
|
/// 試行が目的に対して成功したか。失敗 / 部分成功も含めて bool で表現する。
|
||||||
pub succeeded: bool,
|
pub succeeded: bool,
|
||||||
|
/// Host-resolved anchors backing this individual claim.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
#[schemars(skip)]
|
||||||
|
pub source_refs: Vec<SourceEvidenceRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ユーザー submit の構造化要約。
|
/// ユーザー submit の構造化要約。
|
||||||
|
|
@ -74,6 +86,10 @@ pub struct RequestEntry {
|
||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
/// 一文サマリ。
|
/// 一文サマリ。
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
|
/// Host-resolved anchors backing this individual claim.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
#[schemars(skip)]
|
||||||
|
pub source_refs: Vec<SourceEvidenceRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// staging に書き出される 1 ファイル分のレコード。
|
/// staging に書き出される 1 ファイル分のレコード。
|
||||||
|
|
@ -86,3 +102,153 @@ pub struct StagingRecord {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub payload: ExtractedPayload,
|
pub payload: ExtractedPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::schema::{EvidenceKind, SourceEvidenceRef};
|
||||||
|
|
||||||
|
fn record_source() -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"source": {
|
||||||
|
"segment_id": "seg-old",
|
||||||
|
"range": [1, 5]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn old_staging_json_without_entry_source_refs_deserializes() {
|
||||||
|
let mut raw = record_source();
|
||||||
|
raw["decisions"] = serde_json::json!([
|
||||||
|
{
|
||||||
|
"options": ["keep", "drop"],
|
||||||
|
"chosen": "keep",
|
||||||
|
"rationale": "compatible"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
raw["discussions"] = serde_json::json!([
|
||||||
|
{
|
||||||
|
"topic": "compatibility",
|
||||||
|
"points": ["missing source_refs should default empty"]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
raw["attempts"] = serde_json::json!([
|
||||||
|
{
|
||||||
|
"action": "deserialize old staging",
|
||||||
|
"result": "ok",
|
||||||
|
"succeeded": true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
raw["requests"] = serde_json::json!([
|
||||||
|
{
|
||||||
|
"intent": "preserve old JSON",
|
||||||
|
"summary": "old payload has no entry anchors"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let record: StagingRecord = serde_json::from_value(raw).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(record.source.segment_id, "seg-old");
|
||||||
|
assert!(record.payload.decisions[0].source_refs.is_empty());
|
||||||
|
assert!(record.payload.discussions[0].source_refs.is_empty());
|
||||||
|
assert!(record.payload.attempts[0].source_refs.is_empty());
|
||||||
|
assert!(record.payload.requests[0].source_refs.is_empty());
|
||||||
|
let serialized = serde_json::to_string(&record).unwrap();
|
||||||
|
assert!(!serialized.contains("\"source_refs\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_staging_json_roundtrips_entry_source_refs() {
|
||||||
|
let evidence = SourceEvidenceRef {
|
||||||
|
session_id: Some("session-1".into()),
|
||||||
|
segment_id: Some("segment-1".into()),
|
||||||
|
entry_range: Some([10, 12]),
|
||||||
|
evidence_id: Some("ev-1".into()),
|
||||||
|
evidence_kind: Some(EvidenceKind::new(EvidenceKind::TOOL_RESULT)),
|
||||||
|
label: Some("cargo test result".into()),
|
||||||
|
summary: Some("bounded host summary".into()),
|
||||||
|
};
|
||||||
|
let record = StagingRecord {
|
||||||
|
source: SourceRef {
|
||||||
|
segment_id: "segment-record".into(),
|
||||||
|
range: [0, 20],
|
||||||
|
},
|
||||||
|
payload: ExtractedPayload {
|
||||||
|
decisions: vec![DecisionEntry {
|
||||||
|
options: vec!["a".into(), "b".into()],
|
||||||
|
chosen: "a".into(),
|
||||||
|
rationale: "evidence-backed".into(),
|
||||||
|
source_refs: vec![evidence.clone()],
|
||||||
|
}],
|
||||||
|
discussions: vec![DiscussionEntry {
|
||||||
|
topic: "anchor shape".into(),
|
||||||
|
points: vec!["entry refs roundtrip".into()],
|
||||||
|
source_refs: vec![SourceEvidenceRef {
|
||||||
|
evidence_kind: Some(EvidenceKind::new(EvidenceKind::MESSAGE)),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
attempts: vec![AttemptEntry {
|
||||||
|
action: "serialize".into(),
|
||||||
|
result: "contains source refs".into(),
|
||||||
|
succeeded: true,
|
||||||
|
source_refs: vec![SourceEvidenceRef {
|
||||||
|
evidence_kind: Some(EvidenceKind::new(EvidenceKind::FILE_REF)),
|
||||||
|
evidence_id: Some("file:crates/memory/src/extract/payload.rs".into()),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
requests: vec![RequestEntry {
|
||||||
|
intent: "keep anchors".into(),
|
||||||
|
target: Some("memory staging".into()),
|
||||||
|
summary: "entry-level anchors survive".into(),
|
||||||
|
source_refs: vec![SourceEvidenceRef {
|
||||||
|
evidence_kind: Some(EvidenceKind::new(EvidenceKind::TICKET_REF)),
|
||||||
|
evidence_id: Some("00001KXNYXNM6".into()),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string_pretty(&record).unwrap();
|
||||||
|
assert!(json.contains("source_refs"));
|
||||||
|
assert!(json.contains("tool_result"));
|
||||||
|
assert!(json.contains("entry_range"));
|
||||||
|
let parsed: StagingRecord = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
let source_ref = &parsed.payload.decisions[0].source_refs[0];
|
||||||
|
assert_eq!(source_ref.session_id.as_deref(), Some("session-1"));
|
||||||
|
assert_eq!(source_ref.segment_id.as_deref(), Some("segment-1"));
|
||||||
|
assert_eq!(source_ref.entry_range, Some([10, 12]));
|
||||||
|
assert_eq!(source_ref.evidence_id.as_deref(), Some("ev-1"));
|
||||||
|
assert_eq!(
|
||||||
|
source_ref.evidence_kind.as_ref().map(EvidenceKind::as_str),
|
||||||
|
Some(EvidenceKind::TOOL_RESULT)
|
||||||
|
);
|
||||||
|
assert_eq!(source_ref.label.as_deref(), Some("cargo test result"));
|
||||||
|
assert_eq!(source_ref.summary.as_deref(), Some("bounded host summary"));
|
||||||
|
assert_eq!(
|
||||||
|
parsed.payload.discussions[0].source_refs[0]
|
||||||
|
.evidence_kind
|
||||||
|
.as_ref()
|
||||||
|
.map(EvidenceKind::as_str),
|
||||||
|
Some(EvidenceKind::MESSAGE)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.payload.attempts[0].source_refs[0]
|
||||||
|
.evidence_kind
|
||||||
|
.as_ref()
|
||||||
|
.map(EvidenceKind::as_str),
|
||||||
|
Some(EvidenceKind::FILE_REF)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed.payload.requests[0].source_refs[0]
|
||||||
|
.evidence_kind
|
||||||
|
.as_ref()
|
||||||
|
.map(EvidenceKind::as_str),
|
||||||
|
Some(EvidenceKind::TICKET_REF)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ mod tests {
|
||||||
options: vec!["a".into(), "b".into()],
|
options: vec!["a".into(), "b".into()],
|
||||||
chosen: "a".into(),
|
chosen: "a".into(),
|
||||||
rationale: "shorter".into(),
|
rationale: "shorter".into(),
|
||||||
|
source_refs: Vec::new(),
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
//! Common frontmatter helpers and shared types.
|
//! Common frontmatter helpers and shared types.
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::LintError;
|
use crate::error::LintError;
|
||||||
|
|
@ -8,13 +9,94 @@ pub use lint_common::Frontmatter;
|
||||||
|
|
||||||
/// Reference to a session-store entry range. Stored in `sources` /
|
/// Reference to a session-store entry range. Stored in `sources` /
|
||||||
/// `last_sources` arrays for traceability back to raw session logs.
|
/// `last_sources` arrays for traceability back to raw session logs.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct SourceRef {
|
pub struct SourceRef {
|
||||||
pub segment_id: String,
|
pub segment_id: String,
|
||||||
/// `[start_entry, end_entry]` inclusive range of session-store entry indices.
|
/// `[start_entry, end_entry]` inclusive range of session-store entry indices.
|
||||||
pub range: [u64; 2],
|
pub range: [u64; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for SourceRef {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct RawSourceRef {
|
||||||
|
#[serde(default)]
|
||||||
|
segment_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
session_id: Option<String>,
|
||||||
|
range: [u64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = RawSourceRef::deserialize(deserializer)?;
|
||||||
|
let segment_id = raw
|
||||||
|
.segment_id
|
||||||
|
.or(raw.session_id)
|
||||||
|
.ok_or_else(|| serde::de::Error::missing_field("segment_id"))?;
|
||||||
|
Ok(SourceRef {
|
||||||
|
segment_id,
|
||||||
|
range: raw.range,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extensible evidence kind tag used by staging source anchors.
|
||||||
|
///
|
||||||
|
/// Known values include `message`, `tool_call`, `tool_result`, `file_ref`,
|
||||||
|
/// `ticket_ref`, and `objective_ref`, but callers may use newer bounded tags
|
||||||
|
/// without changing the schema.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct EvidenceKind(pub String);
|
||||||
|
|
||||||
|
impl EvidenceKind {
|
||||||
|
pub const MESSAGE: &'static str = "message";
|
||||||
|
pub const TOOL_CALL: &'static str = "tool_call";
|
||||||
|
pub const TOOL_RESULT: &'static str = "tool_result";
|
||||||
|
pub const FILE_REF: &'static str = "file_ref";
|
||||||
|
pub const TICKET_REF: &'static str = "ticket_ref";
|
||||||
|
pub const OBJECTIVE_REF: &'static str = "objective_ref";
|
||||||
|
|
||||||
|
pub fn new(kind: impl Into<String>) -> Self {
|
||||||
|
Self(kind.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Host-resolved source/evidence metadata for an individual staging claim.
|
||||||
|
///
|
||||||
|
/// This deliberately stores only bounded anchor metadata: stable ids, entry
|
||||||
|
/// ranges, and short labels/summaries. It must not carry raw message bodies or
|
||||||
|
/// full tool result content.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct SourceEvidenceRef {
|
||||||
|
/// Stable session id when the anchor crosses or disambiguates segments.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session_id: Option<String>,
|
||||||
|
/// Session segment id containing the anchored log entries.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub segment_id: Option<String>,
|
||||||
|
/// `[start_entry, end_entry]` inclusive range of session-store entry indices.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub entry_range: Option<[u64; 2]>,
|
||||||
|
/// Host-assigned evidence id within the referenced evidence set.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub evidence_id: Option<String>,
|
||||||
|
/// Extensible evidence kind tag.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub evidence_kind: Option<EvidenceKind>,
|
||||||
|
/// Short host-provided display label, not raw evidence content.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub label: Option<String>,
|
||||||
|
/// Short host-provided summary, not raw evidence content.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub summary: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Split a markdown document into `(yaml_frontmatter, body)`.
|
/// Split a markdown document into `(yaml_frontmatter, body)`.
|
||||||
pub fn split_frontmatter(content: &str) -> Result<(&str, &str), LintError> {
|
pub fn split_frontmatter(content: &str) -> Result<(&str, &str), LintError> {
|
||||||
lint_common::split_frontmatter(content).map_err(Into::into)
|
lint_common::split_frontmatter(content).map_err(Into::into)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ mod decision;
|
||||||
mod request;
|
mod request;
|
||||||
mod summary;
|
mod summary;
|
||||||
|
|
||||||
pub use common::{Frontmatter, SourceRef, split_frontmatter};
|
pub use common::{EvidenceKind, Frontmatter, SourceEvidenceRef, SourceRef, split_frontmatter};
|
||||||
pub use decision::{DecisionFrontmatter, DecisionStatus};
|
pub use decision::{DecisionFrontmatter, DecisionStatus};
|
||||||
pub use request::RequestFrontmatter;
|
pub use request::RequestFrontmatter;
|
||||||
pub use summary::SummaryFrontmatter;
|
pub use summary::SummaryFrontmatter;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user