///|
/// CapsuleTrace models data-retention capsules, not repository acceptance proof.
///
/// The library is intentionally deterministic and offline: callers pass a
/// logical day number, retention rules, and in-memory capsule records. The
/// result is a deletion/anonymization/quarantine plan that can be rendered for
/// review, tests, or CI artifacts without scanning a repository or calling a
/// remote service.
pub(all) enum CapsuleSensitivity {
PublicData
InternalData
ConfidentialData
SensitiveData
RestrictedData
} derive(Eq, Debug)
///|
pub(all) enum ConsentState {
ConsentGranted
ConsentDenied
ConsentExpired
ConsentWithdrawn
ConsentNotRequired
} derive(Eq, Debug)
///|
pub(all) enum SharingMode {
InternalOnly
ProcessorShared
PublicShared
} derive(Eq, Debug)
///|
pub(all) enum RetentionAction {
KeepData
AnonymizeData
DeleteData
QuarantineData
BlockCollection
} derive(Eq, Debug)
///|
pub(all) enum RetentionSeverity {
RetentionCritical
RetentionHigh
RetentionMedium
RetentionLow
RetentionInfo
} derive(Eq, Debug)
///|
pub(all) enum RetentionIssueKind {
MissingRule
ConsentInvalid
RetentionExpired
AnonymizationDue
RegionDenied
EncryptionMissing
SharingDenied
CollectionBlocked
} derive(Eq, Debug)
///|
pub(all) struct RetentionRule {
id : String
purpose : String
data_kind : String
max_keep_days : Int
anonymize_after_days : Int
requires_consent : Bool
allowed_regions : Array[String]
allow_external_sharing : Bool
owner : String
note : String
} derive(Eq, Debug)
///|
pub(all) struct DataCapsule {
id : String
subject_kind : String
purpose : String
data_kind : String
collected_day : Int
last_used_day : Int
consent : ConsentState
sensitivity : CapsuleSensitivity
region : String
encrypted : Bool
sharing : SharingMode
note : String
} derive(Eq, Debug)
///|
pub(all) struct RetentionReason {
kind : RetentionIssueKind
severity : RetentionSeverity
message : String
remedy : String
} derive(Eq, Debug)
///|
pub(all) struct CapsuleDecision {
capsule : DataCapsule
rule_id : String
action : RetentionAction
severity : RetentionSeverity
age_days : Int
idle_days : Int
days_until_delete : Int
reasons : Array[RetentionReason]
owner : String
} derive(Eq, Debug)
///|
pub(all) struct PlanSummary {
total : Int
keep_count : Int
anonymize_count : Int
delete_count : Int
quarantine_count : Int
block_count : Int
critical_count : Int
high_count : Int
medium_count : Int
encrypted_count : Int
external_shared_count : Int
risk_score : Int
} derive(Eq, Debug)
///|
pub(all) struct RetentionPlan {
generated_day : Int
decisions : Array[CapsuleDecision]
rules : Array[RetentionRule]
summary : PlanSummary
} derive(Eq, Debug)
///|
pub(all) struct PurposeSummary {
purpose : String
total : Int
keep_count : Int
anonymize_count : Int
delete_count : Int
quarantine_count : Int
} derive(Eq, Debug)
///|
pub(all) struct RegionSummary {
region : String
total : Int
encrypted_count : Int
external_shared_count : Int
high_or_critical_count : Int
} derive(Eq, Debug)
///|
pub(all) struct RetentionWindow {
day : Int
action : RetentionAction
capsule_ids : Array[String]
} derive(Eq, Debug)
///|
pub(all) struct RetentionCalendar {
generated_day : Int
windows : Array[RetentionWindow]
} derive(Eq, Debug)
///|
pub fn CapsuleSensitivity::label(self : CapsuleSensitivity) -> String {
match self {
PublicData => "public"
InternalData => "internal"
ConfidentialData => "confidential"
SensitiveData => "sensitive"
RestrictedData => "restricted"
}
}
///|
pub fn CapsuleSensitivity::risk_weight(self : CapsuleSensitivity) -> Int {
match self {
PublicData => 0
InternalData => 1
ConfidentialData => 4
SensitiveData => 7
RestrictedData => 10
}
}
///|
pub fn CapsuleSensitivity::requires_encryption(
self : CapsuleSensitivity,
) -> Bool {
self == ConfidentialData || self == SensitiveData || self == RestrictedData
}
///|
pub fn ConsentState::label(self : ConsentState) -> String {
match self {
ConsentGranted => "granted"
ConsentDenied => "denied"
ConsentExpired => "expired"
ConsentWithdrawn => "withdrawn"
ConsentNotRequired => "not-required"
}
}
///|
pub fn ConsentState::allows_processing(self : ConsentState) -> Bool {
self == ConsentGranted || self == ConsentNotRequired
}
///|
pub fn SharingMode::label(self : SharingMode) -> String {
match self {
InternalOnly => "internal"
ProcessorShared => "processor"
PublicShared => "public"
}
}
///|
pub fn SharingMode::is_external(self : SharingMode) -> Bool {
self == ProcessorShared || self == PublicShared
}
///|
pub fn RetentionAction::label(self : RetentionAction) -> String {
match self {
KeepData => "keep"
AnonymizeData => "anonymize"
DeleteData => "delete"
QuarantineData => "quarantine"
BlockCollection => "block"
}
}
///|
pub fn RetentionAction::priority(self : RetentionAction) -> Int {
match self {
KeepData => 0
AnonymizeData => 20
QuarantineData => 35
BlockCollection => 45
DeleteData => 50
}
}
///|
pub fn RetentionSeverity::label(self : RetentionSeverity) -> String {
match self {
RetentionCritical => "critical"
RetentionHigh => "high"
RetentionMedium => "medium"
RetentionLow => "low"
RetentionInfo => "info"
}
}
///|
pub fn RetentionSeverity::weight(self : RetentionSeverity) -> Int {
match self {
RetentionCritical => 40
RetentionHigh => 24
RetentionMedium => 10
RetentionLow => 3
RetentionInfo => 0
}
}
///|
pub fn RetentionIssueKind::label(self : RetentionIssueKind) -> String {
match self {
MissingRule => "missing-rule"
ConsentInvalid => "consent-invalid"
RetentionExpired => "retention-expired"
AnonymizationDue => "anonymization-due"
RegionDenied => "region-denied"
EncryptionMissing => "encryption-missing"
SharingDenied => "sharing-denied"
CollectionBlocked => "collection-blocked"
}
}
///|
pub fn retention_rule(
id : String,
purpose : String,
data_kind : String,
max_keep_days : Int,
anonymize_after_days : Int,
requires_consent : Bool,
allowed_regions : Array[String],
allow_external_sharing : Bool,
owner : String,
note : String,
) -> RetentionRule {
{
id,
purpose,
data_kind,
max_keep_days,
anonymize_after_days,
requires_consent,
allowed_regions,
allow_external_sharing,
owner,
note,
}
}
///|
pub fn data_capsule(
id : String,
subject_kind : String,
purpose : String,
data_kind : String,
collected_day : Int,
last_used_day : Int,
consent : ConsentState,
sensitivity : CapsuleSensitivity,
region : String,
encrypted : Bool,
sharing : SharingMode,
note : String,
) -> DataCapsule {
{
id,
subject_kind,
purpose,
data_kind,
collected_day,
last_used_day,
consent,
sensitivity,
region,
encrypted,
sharing,
note,
}
}
///|
pub fn evaluate_capsule(
capsule : DataCapsule,
rules : Array[RetentionRule],
today : Int,
) -> CapsuleDecision {
match find_rule_for(capsule, rules) {
None =>
build_decision(capsule, "", today, 0, "privacy-team", [
{
kind: MissingRule,
severity: RetentionHigh,
message: "no retention rule matches purpose '" +
capsule.purpose +
"' and data kind '" +
capsule.data_kind +
"'",
remedy: "add a retention rule before collecting or processing this capsule",
},
])
Some(rule) => {
let reasons : Array[RetentionReason] = []
let age_days = days_since(today, capsule.collected_day)
let idle_days = days_since(today, capsule.last_used_day)
if rule.requires_consent && capsule.consent == ConsentDenied {
reasons.push({
kind: CollectionBlocked,
severity: RetentionCritical,
message: "consent was denied for a consent-gated data purpose",
remedy: "block collection and delete any already-collected records",
})
}
if rule.requires_consent && capsule.consent == ConsentWithdrawn {
reasons.push({
kind: ConsentInvalid,
severity: RetentionCritical,
message: "consent was withdrawn for this capsule",
remedy: "delete the capsule unless another lawful basis is documented",
})
}
if rule.requires_consent && capsule.consent == ConsentExpired {
reasons.push({
kind: ConsentInvalid,
severity: RetentionHigh,
message: "consent has expired for this capsule",
remedy: "refresh consent or anonymize data before further processing",
})
}
if age_days >= rule.max_keep_days {
reasons.push({
kind: RetentionExpired,
severity: RetentionCritical,
message: "capsule age exceeds the retention limit of " +
rule.max_keep_days.to_string() +
" days",
remedy: "delete the capsule from active storage and scheduled exports",
})
} else if age_days >= rule.anonymize_after_days ||
idle_days >= rule.anonymize_after_days {
reasons.push({
kind: AnonymizationDue,
severity: RetentionMedium,
message: "capsule passed the anonymization window of " +
rule.anonymize_after_days.to_string() +
" days",
remedy: "strip direct identifiers or aggregate the capsule",
})
}
if !region_allowed(rule, capsule.region) {
reasons.push({
kind: RegionDenied,
severity: RetentionHigh,
message: "region '" +
capsule.region +
"' is not allowed by rule " +
rule.id,
remedy: "move the capsule to an allowed region or quarantine it",
})
}
if capsule.sensitivity.requires_encryption() && !capsule.encrypted {
reasons.push({
kind: EncryptionMissing,
severity: RetentionHigh,
message: capsule.sensitivity.label() + " data is not encrypted",
remedy: "encrypt the capsule before processing or export",
})
}
if capsule.sharing.is_external() && !rule.allow_external_sharing {
reasons.push({
kind: SharingDenied,
severity: RetentionHigh,
message: "external sharing is not permitted by rule " + rule.id,
remedy: "remove processors/exports or create a separate approved rule",
})
}
build_decision(
capsule,
rule.id,
today,
rule.max_keep_days,
rule.owner,
reasons,
)
}
}
}
///|
pub fn plan_retention(
capsules : Array[DataCapsule],
rules : Array[RetentionRule],
today : Int,
) -> RetentionPlan {
let decisions : Array[CapsuleDecision] = []
for capsule in capsules {
decisions.push(evaluate_capsule(capsule, rules, today))
}
let decisions = sort_decisions(decisions)
{ generated_day: today, decisions, rules, summary: summarize(decisions) }
}
///|
pub fn RetentionPlan::is_ready(self : RetentionPlan) -> Bool {
self.summary.critical_count == 0 && self.summary.high_count == 0
}
///|
pub fn RetentionPlan::has_action(
self : RetentionPlan,
action : RetentionAction,
) -> Bool {
self.decisions.any(decision => decision.action == action)
}
///|
pub fn RetentionPlan::action_count(
self : RetentionPlan,
action : RetentionAction,
) -> Int {
self.decisions.count_if(decision => decision.action == action)
}
///|
pub fn RetentionPlan::decision(
self : RetentionPlan,
capsule_id : StringView,
) -> CapsuleDecision? {
let expected = capsule_id.trim().to_owned()
self.decisions.iter().find_first(decision => decision.capsule.id == expected)
}
///|
pub fn RetentionPlan::deletion_queue(
self : RetentionPlan,
) -> Array[CapsuleDecision] {
self.decisions.filter(decision => decision.action == DeleteData)
}
///|
pub fn RetentionPlan::anonymization_queue(
self : RetentionPlan,
) -> Array[CapsuleDecision] {
self.decisions.filter(decision => decision.action == AnonymizeData)
}
///|
pub fn RetentionPlan::quarantine_queue(
self : RetentionPlan,
) -> Array[CapsuleDecision] {
self.decisions.filter(decision => decision.action == QuarantineData)
}
///|
pub fn RetentionPlan::blocked_collection(
self : RetentionPlan,
) -> Array[CapsuleDecision] {
self.decisions.filter(decision => decision.action == BlockCollection)
}
///|
pub fn RetentionPlan::keep_queue(
self : RetentionPlan,
) -> Array[CapsuleDecision] {
self.decisions.filter(decision => decision.action == KeepData)
}
///|
pub fn RetentionPlan::purpose_summaries(
self : RetentionPlan,
) -> Array[PurposeSummary] {
let summaries : Array[PurposeSummary] = []
for decision in self.decisions {
if find_purpose_summary(summaries, decision.capsule.purpose) is None {
summaries.push({
purpose: decision.capsule.purpose,
total: 0,
keep_count: 0,
anonymize_count: 0,
delete_count: 0,
quarantine_count: 0,
})
}
}
let result : Array[PurposeSummary] = []
for summary in summaries {
let related = self.decisions.filter(decision => {
decision.capsule.purpose == summary.purpose
})
result.push({
purpose: summary.purpose,
total: related.length(),
keep_count: related.count_if(decision => decision.action == KeepData),
anonymize_count: related.count_if(decision => {
decision.action == AnonymizeData
}),
delete_count: related.count_if(decision => decision.action == DeleteData),
quarantine_count: related.count_if(decision => {
decision.action == QuarantineData || decision.action == BlockCollection
}),
})
}
sort_purpose_summaries(result)
}
///|
pub fn RetentionPlan::region_summaries(
self : RetentionPlan,
) -> Array[RegionSummary] {
let regions : Array[String] = []
for decision in self.decisions {
if !regions.contains(decision.capsule.region) {
regions.push(decision.capsule.region)
}
}
let result : Array[RegionSummary] = []
for region in regions {
let related = self.decisions.filter(decision => {
decision.capsule.region == region
})
result.push({
region,
total: related.length(),
encrypted_count: related.count_if(decision => decision.capsule.encrypted),
external_shared_count: related.count_if(decision => {
decision.capsule.sharing.is_external()
}),
high_or_critical_count: related.count_if(decision => {
decision.severity == RetentionCritical ||
decision.severity == RetentionHigh
}),
})
}
sort_region_summaries(result)
}
///|
pub fn RetentionPlan::build_calendar(self : RetentionPlan) -> RetentionCalendar {
let windows : Array[RetentionWindow] = []
for decision in self.decisions {
let day = action_due_day(decision)
let action = decision.action
match find_window(windows, day, action) {
Some(index) => windows[index].capsule_ids.push(decision.capsule.id)
None => windows.push({ day, action, capsule_ids: [decision.capsule.id] })
}
}
{ generated_day: self.generated_day, windows: sort_windows(windows) }
}
///|
pub fn RetentionPlan::to_markdown(self : RetentionPlan) -> String {
let lines : Array[String] = []
lines.push("# CapsuleTrace Retention Plan")
lines.push("")
lines.push("- Generated day: " + self.generated_day.to_string())
lines.push("- Ready: " + bool_label(self.is_ready()))
lines.push("- Risk score: " + self.summary.risk_score.to_string())
lines.push("- Capsules: " + self.summary.total.to_string())
lines.push(
"- Actions: keep=" +
self.summary.keep_count.to_string() +
", anonymize=" +
self.summary.anonymize_count.to_string() +
", delete=" +
self.summary.delete_count.to_string() +
", quarantine=" +
self.summary.quarantine_count.to_string() +
", block=" +
self.summary.block_count.to_string(),
)
lines.push("")
lines.push("## Decisions")
lines.push("")
lines.push(
"| Capsule | Purpose | Kind | Region | Action | Severity | Rule | Reason |",
)
lines.push("| --- | --- | --- | --- | --- | --- | --- | --- |")
if self.decisions.is_empty() {
lines.push("| - | - | - | - | keep | info | - | no capsules |")
} else {
for decision in self.decisions {
lines.push(
"| " +
markdown_cell(decision.capsule.id) +
" | " +
markdown_cell(decision.capsule.purpose) +
" | " +
markdown_cell(decision.capsule.data_kind) +
" | " +
markdown_cell(decision.capsule.region) +
" | " +
decision.action.label() +
" | " +
decision.severity.label() +
" | " +
markdown_cell(printable(decision.rule_id)) +
" | " +
markdown_cell(decision.reason_summary()) +
" |",
)
}
}
lines.push("")
lines.push("## Purpose Summary")
lines.push("")
lines.push(
"| Purpose | Total | Keep | Anonymize | Delete | Quarantine/Block |",
)
lines.push("| --- | --- | --- | --- | --- | --- |")
for item in self.purpose_summaries() {
lines.push(
"| " +
markdown_cell(item.purpose) +
" | " +
item.total.to_string() +
" | " +
item.keep_count.to_string() +
" | " +
item.anonymize_count.to_string() +
" | " +
item.delete_count.to_string() +
" | " +
item.quarantine_count.to_string() +
" |",
)
}
lines.join("\n")
}
///|
pub fn RetentionPlan::to_json_string(self : RetentionPlan) -> String {
Json::object(
Map([
("generated_day", json_int(self.generated_day)),
("ready", Json::boolean(self.is_ready())),
("risk_score", json_int(self.summary.risk_score)),
("summary", plan_summary_to_json(self.summary)),
("decisions", decisions_to_json(self.decisions)),
("purpose_summaries", purpose_summaries_to_json(self.purpose_summaries())),
("region_summaries", region_summaries_to_json(self.region_summaries())),
]),
).stringify(indent=2)
}
///|
pub fn CapsuleDecision::reason_summary(self : CapsuleDecision) -> String {
if self.reasons.is_empty() {
"within policy"
} else {
let labels : Array[String] = []
for reason in self.reasons {
labels.push(reason.kind.label())
}
labels.join(", ")
}
}
///|
pub fn CapsuleDecision::needs_operator_action(self : CapsuleDecision) -> Bool {
self.action != KeepData
}
///|
pub fn RetentionCalendar::to_markdown(self : RetentionCalendar) -> String {
let lines : Array[String] = []
lines.push("# CapsuleTrace Retention Calendar")
lines.push("")
lines.push("- Generated day: " + self.generated_day.to_string())
lines.push("")
lines.push("| Day | Action | Capsules |")
lines.push("| --- | --- | --- |")
if self.windows.is_empty() {
lines.push("| - | keep | - |")
} else {
for window in self.windows {
lines.push(
"| " +
window.day.to_string() +
" | " +
window.action.label() +
" | " +
markdown_cell(sort_strings(window.capsule_ids).join(", ")) +
" |",
)
}
}
lines.join("\n")
}
///|
pub fn sample_rules() -> Array[RetentionRule] {
[
retention_rule(
"analytics.behavior",
"analytics",
"behavior",
180,
30,
true,
["eu", "cn", "us"],
false,
"privacy-analytics",
"aggregate behavior capsules after short-term product analysis",
),
retention_rule(
"support.ticket",
"support",
"ticket",
365,
120,
false,
["cn", "sg"],
true,
"support-ops",
"support tickets may be processed by approved vendors",
),
retention_rule(
"billing.invoice",
"billing",
"invoice",
2555,
2555,
false,
["cn"],
false,
"finance",
"billing records stay longer but must remain local and encrypted",
),
]
}
///|
pub fn sample_capsules() -> Array[DataCapsule] {
[
data_capsule(
"cap.active-analytics",
"visitor",
"analytics",
"behavior",
200,
216,
ConsentGranted,
SensitiveData,
"cn",
true,
InternalOnly,
"active product telemetry capsule",
),
data_capsule(
"cap.idle-analytics",
"visitor",
"analytics",
"behavior",
120,
150,
ConsentGranted,
SensitiveData,
"cn",
true,
InternalOnly,
"idle enough to anonymize",
),
data_capsule(
"cap.withdrawn",
"visitor",
"analytics",
"behavior",
160,
190,
ConsentWithdrawn,
SensitiveData,
"cn",
true,
InternalOnly,
"consent withdrawal should trigger deletion",
),
data_capsule(
"cap.unencrypted-ticket",
"customer",
"support",
"ticket",
40,
205,
ConsentNotRequired,
ConfidentialData,
"sg",
false,
ProcessorShared,
"vendor support record missing encryption",
),
data_capsule(
"cap.region-mismatch",
"buyer",
"billing",
"invoice",
20,
210,
ConsentNotRequired,
RestrictedData,
"us",
true,
InternalOnly,
"invoice stored outside allowed region",
),
]
}
///|
pub fn sample_plan() -> RetentionPlan {
plan_retention(sample_capsules(), sample_rules(), 220)
}
///|
pub fn compact_sample() -> String {
sample_plan().to_markdown()
}
///|
fn build_decision(
capsule : DataCapsule,
rule_id : String,
today : Int,
max_keep_days : Int,
owner : String,
reasons : Array[RetentionReason],
) -> CapsuleDecision {
let age_days = days_since(today, capsule.collected_day)
let idle_days = days_since(today, capsule.last_used_day)
let action = action_for(reasons)
{
capsule,
rule_id,
action,
severity: severity_for(reasons),
age_days,
idle_days,
days_until_delete: days_remaining(age_days, max_keep_days),
reasons,
owner,
}
}
///|
fn action_for(reasons : Array[RetentionReason]) -> RetentionAction {
let mut action = KeepData
for reason in reasons {
let next = action_for_issue(reason.kind)
if next.priority() > action.priority() {
action = next
}
}
action
}
///|
fn action_for_issue(kind : RetentionIssueKind) -> RetentionAction {
match kind {
MissingRule => QuarantineData
ConsentInvalid => DeleteData
RetentionExpired => DeleteData
AnonymizationDue => AnonymizeData
RegionDenied => QuarantineData
EncryptionMissing => QuarantineData
SharingDenied => QuarantineData
CollectionBlocked => BlockCollection
}
}
///|
fn severity_for(reasons : Array[RetentionReason]) -> RetentionSeverity {
let mut severity = RetentionInfo
for reason in reasons {
severity = max_severity(severity, reason.severity)
}
severity
}
///|
fn max_severity(
a : RetentionSeverity,
b : RetentionSeverity,
) -> RetentionSeverity {
if b.weight() > a.weight() {
b
} else {
a
}
}
///|
fn days_since(today : Int, day : Int) -> Int {
if today > day {
today - day
} else {
0
}
}
///|
fn days_remaining(age_days : Int, max_keep_days : Int) -> Int {
if max_keep_days <= 0 || age_days >= max_keep_days {
0
} else {
max_keep_days - age_days
}
}
///|
fn region_allowed(rule : RetentionRule, region : String) -> Bool {
rule.allowed_regions.any(item => {
normalize_token(item) == normalize_token(region)
})
}
///|
fn find_rule_for(
capsule : DataCapsule,
rules : Array[RetentionRule],
) -> RetentionRule? {
let purpose = normalize_token(capsule.purpose)
let data_kind = normalize_token(capsule.data_kind)
match
rules
.iter()
.find_first(rule => {
normalize_token(rule.purpose) == purpose &&
normalize_token(rule.data_kind) == data_kind
}) {
Some(rule) => Some(rule)
None =>
rules
.iter()
.find_first(rule => {
normalize_token(rule.purpose) == purpose &&
normalize_token(rule.data_kind) == "*"
})
}
}
///|
fn summarize(decisions : Array[CapsuleDecision]) -> PlanSummary {
let total = decisions.length()
let keep_count = decisions.count_if(decision => decision.action == KeepData)
let anonymize_count = decisions.count_if(decision => {
decision.action == AnonymizeData
})
let delete_count = decisions.count_if(decision => {
decision.action == DeleteData
})
let quarantine_count = decisions.count_if(decision => {
decision.action == QuarantineData
})
let block_count = decisions.count_if(decision => {
decision.action == BlockCollection
})
let critical_count = decisions.count_if(decision => {
decision.severity == RetentionCritical
})
let high_count = decisions.count_if(decision => {
decision.severity == RetentionHigh
})
let medium_count = decisions.count_if(decision => {
decision.severity == RetentionMedium
})
let encrypted_count = decisions.count_if(decision => {
decision.capsule.encrypted
})
let external_shared_count = decisions.count_if(decision => {
decision.capsule.sharing.is_external()
})
let mut risk_score = 0
for decision in decisions {
risk_score = risk_score +
decision.severity.weight() +
decision.capsule.sensitivity.risk_weight()
}
{
total,
keep_count,
anonymize_count,
delete_count,
quarantine_count,
block_count,
critical_count,
high_count,
medium_count,
encrypted_count,
external_shared_count,
risk_score,
}
}
///|
fn sort_decisions(values : Array[CapsuleDecision]) -> Array[CapsuleDecision] {
let mut sorted : Array[CapsuleDecision] = []
for value in values {
let next : Array[CapsuleDecision] = []
let mut inserted = false
for existing in sorted {
if !inserted && compare_decision(value, existing) < 0 {
next.push(value)
inserted = true
}
next.push(existing)
}
if !inserted {
next.push(value)
}
sorted = next
}
sorted
}
///|
fn compare_decision(a : CapsuleDecision, b : CapsuleDecision) -> Int {
let severity_delta = b.severity.weight() - a.severity.weight()
if severity_delta != 0 {
severity_delta
} else {
a.capsule.id.lexical_compare(b.capsule.id)
}
}
///|
fn sort_strings(values : Array[String]) -> Array[String] {
let mut sorted : Array[String] = []
for value in values {
let next : Array[String] = []
let mut inserted = false
for existing in sorted {
if !inserted && value.lexical_compare(existing) < 0 {
next.push(value)
inserted = true
}
next.push(existing)
}
if !inserted {
next.push(value)
}
sorted = next
}
sorted
}
///|
fn find_purpose_summary(
summaries : Array[PurposeSummary],
purpose : String,
) -> PurposeSummary? {
summaries.iter().find_first(summary => summary.purpose == purpose)
}
///|
fn sort_purpose_summaries(
values : Array[PurposeSummary],
) -> Array[PurposeSummary] {
let mut sorted : Array[PurposeSummary] = []
for value in values {
let next : Array[PurposeSummary] = []
let mut inserted = false
for existing in sorted {
if !inserted && value.purpose.lexical_compare(existing.purpose) < 0 {
next.push(value)
inserted = true
}
next.push(existing)
}
if !inserted {
next.push(value)
}
sorted = next
}
sorted
}
///|
fn sort_region_summaries(values : Array[RegionSummary]) -> Array[RegionSummary] {
let mut sorted : Array[RegionSummary] = []
for value in values {
let next : Array[RegionSummary] = []
let mut inserted = false
for existing in sorted {
if !inserted && value.region.lexical_compare(existing.region) < 0 {
next.push(value)
inserted = true
}
next.push(existing)
}
if !inserted {
next.push(value)
}
sorted = next
}
sorted
}
///|
fn find_window(
windows : Array[RetentionWindow],
day : Int,
action : RetentionAction,
) -> Int? {
for index, window in windows.iter2() {
if window.day == day && window.action == action {
return Some(index)
}
}
None
}
///|
fn sort_windows(values : Array[RetentionWindow]) -> Array[RetentionWindow] {
let mut sorted : Array[RetentionWindow] = []
for value in values {
let next : Array[RetentionWindow] = []
let mut inserted = false
for existing in sorted {
if !inserted && compare_window(value, existing) < 0 {
next.push(value)
inserted = true
}
next.push(existing)
}
if !inserted {
next.push(value)
}
sorted = next
}
sorted
}
///|
fn compare_window(a : RetentionWindow, b : RetentionWindow) -> Int {
if a.day != b.day {
a.day - b.day
} else {
b.action.priority() - a.action.priority()
}
}
///|
fn action_due_day(decision : CapsuleDecision) -> Int {
if decision.action == KeepData {
decision.generated_due_day()
} else {
decision.capsule.last_used_day
}
}
///|
fn CapsuleDecision::generated_due_day(self : CapsuleDecision) -> Int {
if self.days_until_delete <= 0 {
self.capsule.collected_day + self.age_days
} else {
self.capsule.collected_day + self.age_days + self.days_until_delete
}
}
///|
fn bool_label(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}
///|
fn printable(value : String) -> String {
if value.trim().is_empty() {
"-"
} else {
value
}
}
///|
fn markdown_cell(value : String) -> String {
value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}
///|
fn normalize_token(value : String) -> String {
value.trim().to_lower().to_owned()
}
///|
fn json_int(value : Int) -> Json {
Json::number(value.to_double(), repr=value.to_string())
}
///|
fn plan_summary_to_json(summary : PlanSummary) -> Json {
Json::object(
Map([
("total", json_int(summary.total)),
("keep_count", json_int(summary.keep_count)),
("anonymize_count", json_int(summary.anonymize_count)),
("delete_count", json_int(summary.delete_count)),
("quarantine_count", json_int(summary.quarantine_count)),
("block_count", json_int(summary.block_count)),
("critical_count", json_int(summary.critical_count)),
("high_count", json_int(summary.high_count)),
("medium_count", json_int(summary.medium_count)),
("encrypted_count", json_int(summary.encrypted_count)),
("external_shared_count", json_int(summary.external_shared_count)),
("risk_score", json_int(summary.risk_score)),
]),
)
}
///|
fn decisions_to_json(decisions : Array[CapsuleDecision]) -> Json {
let values : Array[Json] = []
for decision in decisions {
values.push(
Json::object(
Map([
("id", Json::string(decision.capsule.id)),
("purpose", Json::string(decision.capsule.purpose)),
("data_kind", Json::string(decision.capsule.data_kind)),
("region", Json::string(decision.capsule.region)),
("sensitivity", Json::string(decision.capsule.sensitivity.label())),
("consent", Json::string(decision.capsule.consent.label())),
("sharing", Json::string(decision.capsule.sharing.label())),
("encrypted", Json::boolean(decision.capsule.encrypted)),
("rule_id", Json::string(decision.rule_id)),
("action", Json::string(decision.action.label())),
("severity", Json::string(decision.severity.label())),
("age_days", json_int(decision.age_days)),
("idle_days", json_int(decision.idle_days)),
("days_until_delete", json_int(decision.days_until_delete)),
("owner", Json::string(decision.owner)),
("reasons", reasons_to_json(decision.reasons)),
]),
),
)
}
Json::array(values)
}
///|
fn reasons_to_json(reasons : Array[RetentionReason]) -> Json {
let values : Array[Json] = []
for reason in reasons {
values.push(
Json::object(
Map([
("kind", Json::string(reason.kind.label())),
("severity", Json::string(reason.severity.label())),
("message", Json::string(reason.message)),
("remedy", Json::string(reason.remedy)),
]),
),
)
}
Json::array(values)
}
///|
fn purpose_summaries_to_json(items : Array[PurposeSummary]) -> Json {
let values : Array[Json] = []
for item in items {
values.push(
Json::object(
Map([
("purpose", Json::string(item.purpose)),
("total", json_int(item.total)),
("keep_count", json_int(item.keep_count)),
("anonymize_count", json_int(item.anonymize_count)),
("delete_count", json_int(item.delete_count)),
("quarantine_count", json_int(item.quarantine_count)),
]),
),
)
}
Json::array(values)
}
///|
fn region_summaries_to_json(items : Array[RegionSummary]) -> Json {
let values : Array[Json] = []
for item in items {
values.push(
Json::object(
Map([
("region", Json::string(item.region)),
("total", json_int(item.total)),
("encrypted_count", json_int(item.encrypted_count)),
("external_shared_count", json_int(item.external_shared_count)),
("high_or_critical_count", json_int(item.high_or_critical_count)),
]),
),
)
}
Json::array(values)
}