@tomismeta/paperclip-aperture 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/manifest.js +1 -1
- package/dist/ui/index.js +145 -18
- package/dist/worker.js +20 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ and agents attention now? actually sees to th
|
|
|
64
64
|
|
|
65
65
|
- a Focus surface inside Paperclip
|
|
66
66
|
- ranked `now`, `next`, and `ambient` attention lanes
|
|
67
|
+
- embedded explainability in the Focus UI, including `Why now`, `Why next`, confidence, signals, thread context, and related activity
|
|
67
68
|
- approval handling, including budget-specific approval semantics
|
|
68
69
|
- issue-aware operator language such as `review required`, `blocked`, and targeted recommended moves
|
|
69
70
|
- agent-aware routing that distinguishes known company agents from human/operator roles when issue text references them
|
|
@@ -75,13 +76,31 @@ and agents attention now? actually sees to th
|
|
|
75
76
|
- durable acknowledge/suppression behavior backed by plugin state and ledger replay
|
|
76
77
|
- a sidebar entry, page, and dashboard widget
|
|
77
78
|
|
|
79
|
+
## Explainability
|
|
80
|
+
|
|
81
|
+
Focus is meant to be more legible than a smart inbox.
|
|
82
|
+
|
|
83
|
+
The first `0.3.0` explainability slice keeps reasoning attached to the cards you are already acting on:
|
|
84
|
+
|
|
85
|
+
- `Why now` on the active card explains why the current item outranks the rest of the queue
|
|
86
|
+
- `Why next` on queued rows explains why something is staged behind the current top item
|
|
87
|
+
- `Confidence` shows how strong the semantic signal is when the plugin has one
|
|
88
|
+
- `Signals` surfaces the specific factors that pushed the item into attention
|
|
89
|
+
- `Thread context` and `Related activity` help explain whether an item is part of a broader episode or continuation
|
|
90
|
+
|
|
91
|
+
The intent is not to expose every internal scoring detail. It is to help an operator answer:
|
|
92
|
+
|
|
93
|
+
- why did this surface?
|
|
94
|
+
- why is it in this lane?
|
|
95
|
+
- how much should I trust this judgment?
|
|
96
|
+
|
|
78
97
|
## Package Boundary
|
|
79
98
|
|
|
80
99
|
This plugin treats Paperclip as the host runtime and UI shell, while embedding [Aperture Core](https://github.com/tomismeta/aperture/tree/main/packages/core) through the npm package [`@tomismeta/aperture-core`](https://www.npmjs.com/package/@tomismeta/aperture-core).
|
|
81
100
|
|
|
82
101
|
It is a pure SDK integration: Aperture Core is used as-is inside a self-contained Paperclip plugin, with no changes to Aperture Core or Paperclip core.
|
|
83
102
|
|
|
84
|
-
For `0.
|
|
103
|
+
For `0.3.x`, the boundary works like this:
|
|
85
104
|
|
|
86
105
|
- the plugin worker owns Aperture ingestion, replay, review state, and display composition
|
|
87
106
|
- Paperclip remains the system of record for issue and approval writes
|
|
@@ -129,6 +148,8 @@ Then open `http://127.0.0.1:3100/APE/aperture` and verify:
|
|
|
129
148
|
- approval actions update Focus correctly
|
|
130
149
|
- resolved blocker comments downgrade stale `Now` items
|
|
131
150
|
- attached issue documents downgrade stale `share the memo/spec` review blockers into monitor-only follow-up
|
|
151
|
+
- the active card exposes `Why now`
|
|
152
|
+
- expanded queued rows expose `Why next`
|
|
132
153
|
|
|
133
154
|
## Links
|
|
134
155
|
|
package/dist/manifest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const manifest = {
|
|
2
2
|
id: "tomismeta.paperclip-aperture",
|
|
3
3
|
apiVersion: 1,
|
|
4
|
-
version: "0.
|
|
4
|
+
version: "0.3.0",
|
|
5
5
|
displayName: "Paperclip Aperture",
|
|
6
6
|
description: "The live attention layer for Paperclip, powered by Aperture's deterministic attention engine.",
|
|
7
7
|
author: "@tomismeta",
|
package/dist/ui/index.js
CHANGED
|
@@ -123,6 +123,82 @@ function mergeStoredFrames(snapshot, companyId, candidates, review, nowIso = (/*
|
|
|
123
123
|
}, review);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// src/aperture/explainability.ts
|
|
127
|
+
function asRecord(value) {
|
|
128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
129
|
+
}
|
|
130
|
+
function readStringArray(value) {
|
|
131
|
+
if (!Array.isArray(value)) return [];
|
|
132
|
+
return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
133
|
+
}
|
|
134
|
+
function readSemanticConfidence(frame) {
|
|
135
|
+
const semantic = asRecord(frame.metadata?.semantic);
|
|
136
|
+
const confidence = semantic?.confidence;
|
|
137
|
+
return confidence === "low" || confidence === "medium" || confidence === "high" ? confidence : null;
|
|
138
|
+
}
|
|
139
|
+
function readSemanticRelationHints(frame) {
|
|
140
|
+
const semantic = asRecord(frame.metadata?.semantic);
|
|
141
|
+
const relationHints = semantic?.relationHints;
|
|
142
|
+
if (!Array.isArray(relationHints)) return [];
|
|
143
|
+
return relationHints.filter((entry) => {
|
|
144
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
145
|
+
const hint = entry;
|
|
146
|
+
return hint.kind === "same_issue" || hint.kind === "resolves" || hint.kind === "supersedes" || hint.kind === "repeats" || hint.kind === "escalates";
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function relationHintLabel(hint) {
|
|
150
|
+
switch (hint.kind) {
|
|
151
|
+
case "same_issue":
|
|
152
|
+
return "Part of the same thread";
|
|
153
|
+
case "resolves":
|
|
154
|
+
return "Resolves an earlier blocker";
|
|
155
|
+
case "supersedes":
|
|
156
|
+
return "Moves the request forward";
|
|
157
|
+
case "repeats":
|
|
158
|
+
return "Repeats an earlier ask";
|
|
159
|
+
case "escalates":
|
|
160
|
+
return "Raises the urgency";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function laneReason(lane) {
|
|
164
|
+
switch (lane) {
|
|
165
|
+
case "active":
|
|
166
|
+
return "This is the most urgent item in the queue right now.";
|
|
167
|
+
case "queued":
|
|
168
|
+
return "This is queued behind the current top item.";
|
|
169
|
+
case "ambient":
|
|
170
|
+
return "This is visible for awareness without needing action yet.";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function continuitySummary(frame) {
|
|
174
|
+
const episode = asRecord(frame.metadata?.episode);
|
|
175
|
+
const size = typeof episode?.size === "number" ? episode.size : null;
|
|
176
|
+
const state = typeof episode?.state === "string" ? episode.state.replace(/_/g, " ") : null;
|
|
177
|
+
if (!size || size <= 1) return null;
|
|
178
|
+
if (state) return `Part of a ${state} thread with ${size} related interactions.`;
|
|
179
|
+
return `Part of a thread with ${size} related interactions.`;
|
|
180
|
+
}
|
|
181
|
+
function frameSignals(frame) {
|
|
182
|
+
const attention = asRecord(frame.metadata?.attention);
|
|
183
|
+
const rationale = readStringArray(attention?.rationale);
|
|
184
|
+
const factors = readStringArray(frame.provenance?.factors);
|
|
185
|
+
return [.../* @__PURE__ */ new Set([...rationale.length > 0 ? rationale : factors, ...factors])];
|
|
186
|
+
}
|
|
187
|
+
function signalStrengthLabel(confidence) {
|
|
188
|
+
return `${confidence} confidence`;
|
|
189
|
+
}
|
|
190
|
+
function explainFrame(frame, lane) {
|
|
191
|
+
const relationLabels = [...new Set(readSemanticRelationHints(frame).map(relationHintLabel))];
|
|
192
|
+
return {
|
|
193
|
+
whyNow: frame.provenance?.whyNow ?? frame.summary ?? null,
|
|
194
|
+
laneReason: laneReason(lane),
|
|
195
|
+
signalStrength: readSemanticConfidence(frame),
|
|
196
|
+
signals: frameSignals(frame),
|
|
197
|
+
relationLabels,
|
|
198
|
+
continuity: continuitySummary(frame)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
126
202
|
// src/aperture/attention-context.ts
|
|
127
203
|
var ATTENTION_CONTEXT_IDS = {
|
|
128
204
|
approvalType: "approval-type",
|
|
@@ -223,6 +299,10 @@ function approvalRecordToFrame(record) {
|
|
|
223
299
|
const decisionContext = typeof payload.decisionContext === "string" ? payload.decisionContext : void 0;
|
|
224
300
|
const summary = typeof payload.summary === "string" ? payload.summary : approvalBlockingSummary(budgetOverride);
|
|
225
301
|
const updatedAt = record.updatedAt ?? record.createdAt;
|
|
302
|
+
const provenance = {
|
|
303
|
+
whyNow: approvalBlockingWhyNow(budgetOverride),
|
|
304
|
+
factors: budgetOverride ? ["budget stop", "approval", "operator decision"] : ["approval", "operator decision"]
|
|
305
|
+
};
|
|
226
306
|
return {
|
|
227
307
|
id: `approval-bootstrap:${record.id}`,
|
|
228
308
|
taskId: `approval:${record.id}`,
|
|
@@ -256,17 +336,20 @@ function approvalRecordToFrame(record) {
|
|
|
256
336
|
...budgetOverride ? [{ id: "request-revision", label: "Request revision", kind: "cancel", emphasis: "secondary" }] : []
|
|
257
337
|
]
|
|
258
338
|
},
|
|
259
|
-
provenance
|
|
260
|
-
whyNow: approvalBlockingWhyNow(budgetOverride),
|
|
261
|
-
factors: budgetOverride ? ["budget stop", "approval", "operator decision"] : ["approval", "operator decision"]
|
|
262
|
-
},
|
|
339
|
+
provenance,
|
|
263
340
|
timing: {
|
|
264
341
|
createdAt: record.createdAt,
|
|
265
342
|
updatedAt
|
|
266
343
|
},
|
|
267
344
|
metadata: {
|
|
268
345
|
approvalStatus: record.status,
|
|
269
|
-
approvalType: record.type
|
|
346
|
+
approvalType: record.type,
|
|
347
|
+
attention: {
|
|
348
|
+
rationale: provenance.factors
|
|
349
|
+
},
|
|
350
|
+
semantic: {
|
|
351
|
+
confidence: "high"
|
|
352
|
+
}
|
|
270
353
|
}
|
|
271
354
|
};
|
|
272
355
|
}
|
|
@@ -933,6 +1016,52 @@ function ContextItems({ frame }) {
|
|
|
933
1016
|
item2.id
|
|
934
1017
|
)) });
|
|
935
1018
|
}
|
|
1019
|
+
function DetailCard(props) {
|
|
1020
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("border border-border bg-secondary/50 px-3 py-2", props.className), children: [
|
|
1021
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground", children: props.label }),
|
|
1022
|
+
/* @__PURE__ */ jsx("div", { className: "mt-1", children: props.children })
|
|
1023
|
+
] });
|
|
1024
|
+
}
|
|
1025
|
+
function ExplainabilityBadges(props) {
|
|
1026
|
+
if (props.values.length === 0) return null;
|
|
1027
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: props.values.map((value) => /* @__PURE__ */ jsx(
|
|
1028
|
+
Badge,
|
|
1029
|
+
{
|
|
1030
|
+
className: props.accent ? "border-transparent" : "border-border bg-background/70 text-foreground/80",
|
|
1031
|
+
style: props.accent ? { color: ACCENT_COLOR, backgroundColor: `${ACCENT_COLOR}14`, borderColor: `${ACCENT_COLOR}33` } : void 0,
|
|
1032
|
+
children: value
|
|
1033
|
+
},
|
|
1034
|
+
value
|
|
1035
|
+
)) });
|
|
1036
|
+
}
|
|
1037
|
+
function ExplainabilityPanel(props) {
|
|
1038
|
+
const explanation = explainFrame(props.frame, props.lane);
|
|
1039
|
+
const strength = explanation.signalStrength ? signalStrengthLabel(explanation.signalStrength) : null;
|
|
1040
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-3 lg:grid-cols-2", children: [
|
|
1041
|
+
/* @__PURE__ */ jsxs(DetailCard, { label: "Why now", className: "lg:col-span-2", children: [
|
|
1042
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm leading-relaxed text-foreground/90", children: explanation.whyNow ?? judgmentLine(props.frame, props.lane) }),
|
|
1043
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-xs leading-relaxed text-muted-foreground", children: explanation.laneReason })
|
|
1044
|
+
] }),
|
|
1045
|
+
strength ? /* @__PURE__ */ jsx(DetailCard, { label: "Confidence", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-foreground/90", children: strength }) }) : null,
|
|
1046
|
+
explanation.continuity ? /* @__PURE__ */ jsx(DetailCard, { label: "Related activity", children: /* @__PURE__ */ jsx("p", { className: "text-sm leading-relaxed text-foreground/90", children: explanation.continuity }) }) : null,
|
|
1047
|
+
explanation.signals.length > 0 ? /* @__PURE__ */ jsx(DetailCard, { label: "Signals", className: explanation.relationLabels.length > 0 ? "lg:col-span-1" : "lg:col-span-2", children: /* @__PURE__ */ jsx(ExplainabilityBadges, { values: explanation.signals }) }) : null,
|
|
1048
|
+
explanation.relationLabels.length > 0 ? /* @__PURE__ */ jsx(DetailCard, { label: "Thread context", className: explanation.signals.length > 0 ? "lg:col-span-1" : "lg:col-span-2", children: /* @__PURE__ */ jsx(ExplainabilityBadges, { values: explanation.relationLabels, accent: true }) }) : null
|
|
1049
|
+
] });
|
|
1050
|
+
}
|
|
1051
|
+
function ExplainabilityStrip(props) {
|
|
1052
|
+
const explanation = explainFrame(props.frame, props.lane);
|
|
1053
|
+
const chips = [
|
|
1054
|
+
...explanation.signalStrength ? [signalStrengthLabel(explanation.signalStrength)] : [],
|
|
1055
|
+
...explanation.signals.slice(0, 2),
|
|
1056
|
+
...explanation.relationLabels.slice(0, 1)
|
|
1057
|
+
];
|
|
1058
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-md border border-border/70 bg-secondary/30 px-3 py-2", children: [
|
|
1059
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium text-muted-foreground", children: props.lane === "queued" ? "Why next" : props.lane === "ambient" ? "Why ambient" : "Why now" }),
|
|
1060
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs leading-relaxed text-muted-foreground", children: explanation.whyNow ?? judgmentLine(props.frame, props.lane) }),
|
|
1061
|
+
/* @__PURE__ */ jsx("div", { className: "text-[11px] leading-relaxed text-muted-foreground", children: explanation.laneReason }),
|
|
1062
|
+
chips.length > 0 ? /* @__PURE__ */ jsx(ExplainabilityBadges, { values: chips, accent: true }) : null
|
|
1063
|
+
] });
|
|
1064
|
+
}
|
|
936
1065
|
function isIssueFrame(frame) {
|
|
937
1066
|
return entityTypeFromFrame(frame) === "issue";
|
|
938
1067
|
}
|
|
@@ -1017,19 +1146,16 @@ function NowDetails(props) {
|
|
|
1017
1146
|
const budgetHref = isBudgetOverride(frame) ? costsHref(props.companyPrefix) : null;
|
|
1018
1147
|
const activityLink = activityHref(frame, props.companyPrefix);
|
|
1019
1148
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-t border-border pt-4", children: [
|
|
1020
|
-
/* @__PURE__ */
|
|
1021
|
-
|
|
1022
|
-
/* @__PURE__ */ jsx("
|
|
1023
|
-
/* @__PURE__ */
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
requestedAmount(frame)
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
] }) : null,
|
|
1031
|
-
budgetReason(frame) ? /* @__PURE__ */ jsx("span", { children: budgetReason(frame) }) : null
|
|
1032
|
-
] })
|
|
1149
|
+
/* @__PURE__ */ jsx(ExplainabilityPanel, { frame, lane: "active" }),
|
|
1150
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
|
|
1151
|
+
/* @__PURE__ */ jsx("span", { children: requestDescriptor(frame) }),
|
|
1152
|
+
/* @__PURE__ */ jsx("span", { children: impactLabel(frame) }),
|
|
1153
|
+
/* @__PURE__ */ jsx("span", { children: formatRelativeTime(frameUpdatedAt(frame, snapshotUpdatedAt)) }),
|
|
1154
|
+
requestedAmount(frame) ? /* @__PURE__ */ jsxs("span", { children: [
|
|
1155
|
+
"Amount requested: ",
|
|
1156
|
+
requestedAmount(frame)
|
|
1157
|
+
] }) : null,
|
|
1158
|
+
budgetReason(frame) ? /* @__PURE__ */ jsx("span", { children: budgetReason(frame) }) : null
|
|
1033
1159
|
] }),
|
|
1034
1160
|
/* @__PURE__ */ jsx(ContextItems, { frame }),
|
|
1035
1161
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
@@ -1311,6 +1437,7 @@ function NextRow(props) {
|
|
|
1311
1437
|
),
|
|
1312
1438
|
expanded ? /* @__PURE__ */ jsxs("div", { className: "space-y-3 px-4 pb-3 pl-12 sm:px-6 sm:pl-14", children: [
|
|
1313
1439
|
showDetailText ? /* @__PURE__ */ jsx("p", { className: "text-sm leading-relaxed text-muted-foreground", children: detailText }) : null,
|
|
1440
|
+
/* @__PURE__ */ jsx(ExplainabilityStrip, { frame, lane: "queued" }),
|
|
1314
1441
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
|
|
1315
1442
|
/* @__PURE__ */ jsx("span", { children: impactLabel(frame) }),
|
|
1316
1443
|
driver ? /* @__PURE__ */ jsx(Accent, { children: driver }) : null,
|
package/dist/worker.js
CHANGED
|
@@ -12556,6 +12556,13 @@ function issueWhyNow(issue, analysis, documentSignal) {
|
|
|
12556
12556
|
|
|
12557
12557
|
// src/aperture/reconciliation.ts
|
|
12558
12558
|
var COMMENT_LOOKUP_CONCURRENCY = 6;
|
|
12559
|
+
function semanticMetadata(confidence, relationHints) {
|
|
12560
|
+
const semantic = {
|
|
12561
|
+
...confidence ? { confidence } : {},
|
|
12562
|
+
...relationHints.length > 0 ? { relationHints } : {}
|
|
12563
|
+
};
|
|
12564
|
+
return Object.keys(semantic).length > 0 ? semantic : void 0;
|
|
12565
|
+
}
|
|
12559
12566
|
function toIsoString2(value) {
|
|
12560
12567
|
if (!value) return void 0;
|
|
12561
12568
|
if (typeof value === "string") return value;
|
|
@@ -12623,6 +12630,7 @@ function issueFrame(issue, comment, documents, directory) {
|
|
|
12623
12630
|
const owner = hasIntent(analysis, "resolution") || documentSignal.resolvesArtifactRequest ? null : analysis.owner;
|
|
12624
12631
|
const move = issueRecommendedMove(issue, analysis, documentSignal);
|
|
12625
12632
|
const target = analysis.blockingTarget;
|
|
12633
|
+
const semantic = semanticMetadata(analysis.semanticConfidence, analysis.relationHints);
|
|
12626
12634
|
return {
|
|
12627
12635
|
lane,
|
|
12628
12636
|
frame: {
|
|
@@ -12661,7 +12669,11 @@ function issueFrame(issue, comment, documents, directory) {
|
|
|
12661
12669
|
issueStatus: issue.status,
|
|
12662
12670
|
issuePriority: issue.priority,
|
|
12663
12671
|
liveReconciled: true,
|
|
12664
|
-
activityPath: comment ? "activity" : void 0
|
|
12672
|
+
activityPath: comment ? "activity" : void 0,
|
|
12673
|
+
attention: {
|
|
12674
|
+
rationale: provenance.factors ?? []
|
|
12675
|
+
},
|
|
12676
|
+
...semantic ? { semantic } : {}
|
|
12665
12677
|
}
|
|
12666
12678
|
}
|
|
12667
12679
|
};
|
|
@@ -12719,7 +12731,13 @@ function agentFrame(agent) {
|
|
|
12719
12731
|
agentStatus: agent.status,
|
|
12720
12732
|
pauseReason: agent.pauseReason,
|
|
12721
12733
|
liveReconciled: true,
|
|
12722
|
-
activityPath: agent.status === "error" ? "activity" : void 0
|
|
12734
|
+
activityPath: agent.status === "error" ? "activity" : void 0,
|
|
12735
|
+
attention: {
|
|
12736
|
+
rationale: provenance.factors ?? []
|
|
12737
|
+
},
|
|
12738
|
+
semantic: {
|
|
12739
|
+
confidence: "high"
|
|
12740
|
+
}
|
|
12723
12741
|
}
|
|
12724
12742
|
}
|
|
12725
12743
|
};
|
package/package.json
CHANGED