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
2 changes: 1 addition & 1 deletion packages/core/src/application/BatchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ export class BatchService {
])
return {
proposals: new Set(proposals.map(proposal => proposal.id)),
clarifications: new Set(clarifications.map(ticket => ticket.id)),
clarifications: new Set(clarifications.map(clarification => clarification.id)),
}
}

Expand Down
50 changes: 25 additions & 25 deletions packages/core/src/application/HITLService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class HITLService {
// Candidates are only validated at answer time, since each picks a different op set.
async submitClarification(draft: ClarificationCreate & { submitterId?: UserId }): Promise<Clarification> {
const submitter = draft.submitterId ? await this.userDirectory.resolve(draft.submitterId) : null
const ticket = new Clarification({
const clarification = new Clarification({
id: newClarificationId(),
workspaceId: draft.workspaceId,
question: draft.question,
Expand All @@ -119,20 +119,20 @@ export class HITLService {
...(submitter?.kind ? { ownerKind: submitter.kind } : {}),
})
return this.withLockedWorkspace(draft.workspaceId, async (workspace) => {
await this.deps.clarificationRepository.save(ticket)
await this.deps.clarificationRepository.save(clarification)
await this.commitWorkspaceChange(workspace, {
kind: 'clarification-submit',
subject: `submitted ${ticket.id}`,
subject: `submitted ${clarification.id}`,
userId: SUBMIT_USER_ID,
clarificationId: ticket.id,
clarificationId: clarification.id,
})
this.deps.eventBus?.publish({
type: 'clarification.created',
workspaceId: ticket.workspaceId,
ticketId: ticket.id,
workspaceId: clarification.workspaceId,
clarificationId: clarification.id,
at: this.deps.clock.now(),
})
return ticket
return clarification
})
}

Expand Down Expand Up @@ -191,7 +191,7 @@ export class HITLService {
note?: string
}): Promise<Clarification> {
const { clarificationId, selection, userId, note } = options
let ticket = await this.deps.clarificationRepository.load(clarificationId)
let clarification = await this.deps.clarificationRepository.load(clarificationId)
let candidateId: ClarificationCandidateId
if (selection.kind === 'existing') {
candidateId = selection.candidateId
Expand All @@ -203,14 +203,14 @@ export class HITLService {
sourceReferences: [],
proposedOperations: [],
}
ticket = ticket.appendCandidate(newCandidate)
clarification = clarification.appendCandidate(newCandidate)
candidateId = newCandidate.id
}
const operations = [...ticket.resolveCandidate(candidateId)]
await this.assertOperationsValid(ticket.workspaceId, operations)
const operations = [...clarification.resolveCandidate(candidateId)]
await this.assertOperationsValid(clarification.workspaceId, operations)

return this.withLockedWorkspace(ticket.workspaceId, async (workspace) => {
const answered = ticket.markAnswered(candidateId, userId)
return this.withLockedWorkspace(clarification.workspaceId, async (workspace) => {
const answered = clarification.markAnswered(candidateId, userId)
await this.deps.clarificationRepository.save(answered)
await this.commitWorkspaceChange(workspace, {
kind: 'clarification-answer',
Expand All @@ -220,8 +220,8 @@ export class HITLService {
})
this.deps.eventBus?.publish({
type: 'clarification.answered',
workspaceId: ticket.workspaceId,
ticketId: ticket.id,
workspaceId: clarification.workspaceId,
clarificationId: clarification.id,
at: this.deps.clock.now(),
})
return answered
Expand All @@ -234,9 +234,9 @@ export class HITLService {
userId: UserId,
proposalId?: ProposalId,
): Promise<Clarification> {
const ticket = await this.deps.clarificationRepository.load(clarificationId)
return this.withLockedWorkspace(ticket.workspaceId, async (workspace) => {
const applied = ticket.markApplied(proposalId)
const clarification = await this.deps.clarificationRepository.load(clarificationId)
return this.withLockedWorkspace(clarification.workspaceId, async (workspace) => {
const applied = clarification.markApplied(proposalId)
await this.deps.clarificationRepository.save(applied)
await this.commitWorkspaceChange(workspace, {
kind: 'clarification-apply',
Expand All @@ -247,8 +247,8 @@ export class HITLService {
})
this.deps.eventBus?.publish({
type: 'clarification.applied',
workspaceId: ticket.workspaceId,
ticketId: ticket.id,
workspaceId: clarification.workspaceId,
clarificationId: clarification.id,
...(proposalId ? { proposalId } : {}),
at: this.deps.clock.now(),
})
Expand All @@ -261,9 +261,9 @@ export class HITLService {
reason: string,
userId: UserId,
): Promise<Clarification> {
const ticket = await this.deps.clarificationRepository.load(clarificationId)
return this.withLockedWorkspace(ticket.workspaceId, async (workspace) => {
const skipped = ticket.markSkipped(userId)
const clarification = await this.deps.clarificationRepository.load(clarificationId)
return this.withLockedWorkspace(clarification.workspaceId, async (workspace) => {
const skipped = clarification.markSkipped(userId)
await this.deps.clarificationRepository.save(skipped)
await this.commitWorkspaceChange(workspace, {
kind: 'clarification-skip',
Expand All @@ -273,8 +273,8 @@ export class HITLService {
})
this.deps.eventBus?.publish({
type: 'clarification.skipped',
workspaceId: ticket.workspaceId,
ticketId: ticket.id,
workspaceId: clarification.workspaceId,
clarificationId: clarification.id,
at: this.deps.clock.now(),
})
return skipped
Expand Down
16 changes: 8 additions & 8 deletions packages/core/src/domain/hitl/Clarification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import type {
import { ConflictError, NotFoundError } from '../errors.js'

/**
* Lifecycle. From pending a ticket is either answered then applied, or skipped.
* Lifecycle. From pending a clarification is either answered then applied, or skipped.
*
* `markAnswered` records the user's choice but does NOT mutate the graph,
* the resolution is snapshotted onto the ticket and the ddd:clarify skill is expected to wrap it into a Proposal.
* Once that Proposal lands the ticket transitions to `applied` via `markApplied`,
* the resolution is snapshotted onto the clarification and the ddd:clarify skill is expected to wrap it into a Proposal.
* Once that Proposal lands the clarification transitions to `applied` via `markApplied`,
* which only stamps the (optional) proposalId, the actual graph mutation happens inside `HITLService.applyProposal`.
* proposalId is omitted when the chosen candidate's resolution had no graph impact, so no Proposal was produced.
*
Expand All @@ -44,22 +44,22 @@ export class Clarification {
resolveCandidate(candidateId: ClarificationCandidateId): readonly GraphOperation[] {
const match = this.data.candidates.find(candidate => candidate.id === candidateId)
if (!match) {
throw new NotFoundError(`Candidate "${candidateId}" not in ticket "${this.data.id}"`)
throw new NotFoundError(`Candidate "${candidateId}" not in clarification "${this.data.id}"`)
}
return match.proposedOperations
}

/**
* Append a candidate to a pending ticket.
* Append a candidate to a pending clarification.
* Used when the reviewer's actual answer doesn't match any of the skill-supplied options and they author one inline.
* Refuses on non-pending tickets so an already-answered ticket can't grow new options retroactively,
* Refuses on non-pending clarifications so an already-answered clarification can't grow new options retroactively,
* and rejects duplicate ids to keep `resolveCandidate` deterministic.
*/
appendCandidate(candidate: ClarificationCandidate): Clarification {
this.requireStatus('pending')
if (this.data.candidates.some(existingCandidate => existingCandidate.id === candidate.id)) {
throw new ConflictError(
`Candidate "${candidate.id}" already exists on ticket "${this.data.id}"`,
`Candidate "${candidate.id}" already exists on clarification "${this.data.id}"`,
)
}
return new Clarification({
Expand Down Expand Up @@ -104,7 +104,7 @@ export class Clarification {

private requireStatus(expectedStatus: ClarificationStatus): void {
if (this.data.status !== expectedStatus) {
throw new ConflictError(`Clarification ticket "${this.data.id}" is ${this.data.status}, not ${expectedStatus}`)
throw new ConflictError(`Clarification "${this.data.id}" is ${this.data.status}, not ${expectedStatus}`)
}
}
}
8 changes: 4 additions & 4 deletions packages/core/src/domain/plugin/OntologyPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BatchUnit, EdgeTypeId, ModelSnapshot, NodeStatus, NodeTypeId, OntologyId, SkillId, SourceRole, ValidationIssue } from '@braidhq/schema'
import type { BatchUnit, EdgeTypeId, LocalizedText, ModelSnapshot, NodeStatus, NodeTypeId, OntologyId, SkillId, SourceRole, ValidationIssue } from '@braidhq/schema'
import type { Plugin } from './Plugin.js'

/**
Expand All @@ -8,7 +8,7 @@ import type { Plugin } from './Plugin.js'
*/
export interface SourceRoleDescriptor {
readonly id: SourceRole
readonly label: string
readonly label: LocalizedText
/** Sources of this role must be present for the ontology to run. */
readonly required?: boolean
/** Sources of this role enumerate into batch units, and their sync drives the Reactor. */
Expand All @@ -19,7 +19,7 @@ export interface SourceRoleDescriptor {

export interface NodeTypeDescriptor {
readonly id: NodeTypeId
readonly label: string
readonly label: LocalizedText
readonly description?: string
readonly allowedStatuses?: readonly NodeStatus[]
/**
Expand Down Expand Up @@ -57,7 +57,7 @@ export interface NodeTypeDescriptor {

export interface EdgeTypeDescriptor {
readonly id: EdgeTypeId
readonly label?: string
readonly label?: LocalizedText
/**
* Short prose explaining what this edge means and when to emit it.
* Surfaced to LLMs through the `/ontology` API,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/domain/users/UserDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface UserDirectory {
export interface UserAuthor {
readonly displayName: string
readonly email?: string
// Snapshotted onto proposals and tickets at submit time,
// Snapshotted onto proposals and clarifications at submit time,
// so the HITL views classify an autonomous owner without a read-time lookup.
readonly kind?: UserKind
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,30 @@ export class InMemoryClarificationRepository implements ClarificationRepository
private readonly store = new InMemoryKeyedStore<ClarificationId, Clarification>('Clarification')

async list(filter?: ClarificationFilter): Promise<Clarification[]> {
let tickets = this.store.listAll()
let clarifications = this.store.listAll()
if (filter?.workspaceId !== undefined) {
const wsId = filter.workspaceId
tickets = tickets.filter(ticket => ticket.workspaceId === wsId)
clarifications = clarifications.filter(clarification => clarification.workspaceId === wsId)
}
if (filter?.statuses && filter.statuses.length > 0) {
const statuses = filter.statuses
tickets = tickets.filter(ticket => statuses.includes(ticket.status))
clarifications = clarifications.filter(clarification => statuses.includes(clarification.status))
}
if (filter?.viewerId !== undefined) {
const viewerId = filter.viewerId
const includeServiceOwned = filter.includeServiceOwned ?? false
tickets = tickets.filter(ticket =>
ticket.status !== 'pending' || ticket.owner === viewerId || (includeServiceOwned && ticket.ownerKind === 'service'),
clarifications = clarifications.filter(clarification =>
clarification.status !== 'pending' || clarification.owner === viewerId || (includeServiceOwned && clarification.ownerKind === 'service'),
)
}
return paginate(tickets, filter?.limit, filter?.offset)
return paginate(clarifications, filter?.limit, filter?.offset)
}

async load(clarificationId: ClarificationId): Promise<Clarification> {
return this.store.get(clarificationId)
}

async save(ticket: Clarification): Promise<void> {
this.store.set(ticket.id, ticket)
async save(clarification: Clarification): Promise<void> {
this.store.set(clarification.id, clarification)
}
}
Loading
Loading