d853609213
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>
60 lines
1.7 KiB
TypeScript
60 lines
1.7 KiB
TypeScript
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)
|
|
}
|