UI completion pass + accountability & benchmarking
UI (daily-drivable now): - Board: dnd-kit drag-and-drop between columns; click a card → task detail drawer (Sheet) with status, member assignee picker, send-to-AI-seat dispatch, description/artifact, parent/children navigation; seat-triad assignee chips (AI indigo monogram / human slate). - Cartable page (the personal pending slice), Members & invitations page (invite + copy join token; V1 sends no email), Review inbox now shows a word-level diff of your edits vs the proposal (lib/diff.ts, LCS), Org chart page (React Flow: org → teams → seats in the human/open/AI triad). Nav reordered; nothing left "soon". Accountability & benchmarking: - Identity: GET /members (directory + org role) and GET /invitations (with join token, inviter-only) — the directory also resolves names client-side everywhere. - OrgBoard: work_item_transitions recorded on every status change (AddWorkItemTransitions migration); GET /performance — per assignee (human and AI on the same scale): pending by column, done, worked hours (time in InProgress), avg cycle time (start of work → done), plus the unassigned-pending count. Owner-level capability. - Performance page: benchmark table merging board metrics with AI trust metrics (approval rate + edit distance from analytics); flags work with no one accountable. Verified: build green; ArchitectureTests 8/8; IntegrationTests 43/43 (new: directory, invitations list + Member 403s, transition-derived worked-hours/cycle-time, unassigned count); client npm build green (TS strict). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
export interface DiffSegment {
|
||||
kind: 'same' | 'removed' | 'added'
|
||||
text: string
|
||||
}
|
||||
|
||||
const MAX_TOKENS = 1500
|
||||
|
||||
/**
|
||||
* Word-level diff (LCS) between two texts — used by the review inbox to show what the reviewer
|
||||
* changed vs the agent's proposal. Inputs are capped so the O(n·m) table stays cheap.
|
||||
*/
|
||||
export function diffWords(before: string, after: string): DiffSegment[] {
|
||||
const a = tokenize(before).slice(0, MAX_TOKENS)
|
||||
const b = tokenize(after).slice(0, MAX_TOKENS)
|
||||
|
||||
// LCS length table.
|
||||
const dp: number[][] = Array.from({ length: a.length + 1 }, () => new Array<number>(b.length + 1).fill(0))
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
for (let j = b.length - 1; j >= 0; j--) {
|
||||
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the table, merging consecutive segments of the same kind.
|
||||
const segments: DiffSegment[] = []
|
||||
const push = (kind: DiffSegment['kind'], text: string) => {
|
||||
const last = segments[segments.length - 1]
|
||||
if (last && last.kind === kind) {
|
||||
last.text += text
|
||||
} else {
|
||||
segments.push({ kind, text })
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0
|
||||
let j = 0
|
||||
while (i < a.length && j < b.length) {
|
||||
if (a[i] === b[j]) {
|
||||
push('same', a[i])
|
||||
i++
|
||||
j++
|
||||
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
||||
push('removed', a[i])
|
||||
i++
|
||||
} else {
|
||||
push('added', b[j])
|
||||
j++
|
||||
}
|
||||
}
|
||||
while (i < a.length) push('removed', a[i++])
|
||||
while (j < b.length) push('added', b[j++])
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
/** Splits text into words + whitespace separators (kept, so the diff re-renders faithfully). */
|
||||
function tokenize(text: string): string[] {
|
||||
return text.split(/(\s+)/).filter((t) => t.length > 0)
|
||||
}
|
||||
Reference in New Issue
Block a user