Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/claim-lifecycle-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Claim Lifecycle & Reversibility (Design Proposal)

## Overview
This document outlines a robust state machine for Memact Context claims. To respect the fluidity of human identity, every claim's state transition must be entirely reversible.

## Architecture & Lifecycle States

The core object shape now tracks state transitions explicitly using `transitionClaimState()`.

### The Core States
- `pending`: The initial state when a raw signal proposes a context. Visibility is restricted to the Memact UI (`private`).
- `approved`: User explicitly approved the claim. Visibility opens to `shared`.
- `hidden`: A user-triggered temporary revocation. The claim is retained locally but visibility drops to `private` immediately.
- `rejected`: The user explicitly denied the suggestion.
- `deleted` (Soft-delete): Purged from user-view, but a hashed signature is retained locally to prevent the same raw observation from triggering the same annoying suggestion again.

### Action Buffering (The 5-Second Rule)
To prevent accidental misclicks, the SDK layer should implement a standard `5000ms` promise delay before calling `transitionClaimState()`. During this window, a UI toast can allow the user to "Undo" the action, flushing the buffer without mutating the actual local database.

### Revocation Sync
When a claim transitions to `hidden`, `rejected`, or `deleted`, the `revoked_at` timestamp is populated. The API gateway must broadcast a webhook or WebSockets payload to all authorized third-party applications explicitly instructing them to invalidate and drop the cached context.

## Privacy by Design
By segregating `status` and `visibility`, users can curate their active context dynamically (e.g., hiding political context off-season) without permanently destroying their own data history.
17 changes: 15 additions & 2 deletions src/engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,29 @@ export function shapeContextProposal(input = {}, options = {}) {
title: String(submission.title || context.title || `Possible ${category} context`).trim().slice(0, 160),
context,
confidence: round(Number(submission.confidence ?? confidence)),

// NEW: Robust Claim Lifecycle Base State
status: "pending",
visibility: "private",
revoked_at: null,
lifecycle_history: [{
action: "created",
from_status: null,
to_status: "pending",
occurred_at: new Date().toISOString(),
reason: "system_generated"
}],

user_action_required: true,
source_trail: sourceTrail,
guardrails: [
"Activity is not identity.",
"User must be able to accept, edit, reject, or delete this before it becomes memory.",
"Do not expose raw private data by default."
"Do not expose raw private data by default.",
"Every user decision is reversible. Hidden or rejected claims retain local privacy state."
],
created_at: new Date().toISOString()
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
}

Expand Down
84 changes: 84 additions & 0 deletions src/lifecycle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,87 @@ export function schemaLifecycleLabel(state) {
};
return labels[state] || labels[SCHEMA_LIFECYCLE_STATES.EMERGING];
}
// --- Claim Lifecycle Expansion ---

export const CLAIM_LIFECYCLE_STATES = Object.freeze({
PENDING: "pending",
APPROVED: "approved",
REJECTED: "rejected",
HIDDEN: "hidden",
ARCHIVED: "archived",
DELETED: "deleted" // Soft-delete
});

export const CLAIM_VISIBILITY = Object.freeze({
PRIVATE: "private",
SHARED: "shared"
});

export function transitionClaimState(claim = {}, action = "", options = {}) {
const currentStatus = claim.status || CLAIM_LIFECYCLE_STATES.PENDING;
const currentVisibility = claim.visibility || CLAIM_VISIBILITY.PRIVATE;

let nextStatus = currentStatus;
let nextVisibility = currentVisibility;
let revokedAt = claim.revoked_at || null;

switch (action.trim().toLowerCase()) {
case "approve":
nextStatus = CLAIM_LIFECYCLE_STATES.APPROVED;
nextVisibility = CLAIM_VISIBILITY.SHARED;
revokedAt = null;
break;
case "reject":
nextStatus = CLAIM_LIFECYCLE_STATES.REJECTED;
nextVisibility = CLAIM_VISIBILITY.PRIVATE;
revokedAt = new Date().toISOString();
break;
case "hide":
// Allows user to temporarily hide an approved claim from third-party apps
nextStatus = CLAIM_LIFECYCLE_STATES.HIDDEN;
nextVisibility = CLAIM_VISIBILITY.PRIVATE;
revokedAt = new Date().toISOString();
break;
case "unhide":
// Revert a hidden claim back to approved state
if (currentStatus === CLAIM_LIFECYCLE_STATES.HIDDEN) {
nextStatus = CLAIM_LIFECYCLE_STATES.APPROVED;
nextVisibility = CLAIM_VISIBILITY.SHARED;
revokedAt = null;
}
break;
case "edit":
// Edits usually bump status back to approved if it was pending or hidden
nextStatus = CLAIM_LIFECYCLE_STATES.APPROVED;
nextVisibility = CLAIM_VISIBILITY.SHARED;
revokedAt = null;
break;
case "delete":
// Soft-delete to retain a hash of the observation preventing re-suggestion
nextStatus = CLAIM_LIFECYCLE_STATES.DELETED;
nextVisibility = CLAIM_VISIBILITY.PRIVATE;
revokedAt = new Date().toISOString();
break;
default:
break;
}

return {
...claim,
status: nextStatus,
visibility: nextVisibility,
revoked_at: revokedAt,
last_action: action.toLowerCase(),
updated_at: new Date().toISOString(),
lifecycle_history: [
...(Array.isArray(claim.lifecycle_history) ? claim.lifecycle_history : []),
{
action: action.toLowerCase(),
from_status: currentStatus,
to_status: nextStatus,
occurred_at: new Date().toISOString(),
reason: options.reason || "user_action"
}
]
};
}