dsh-bettertaskgraphs 0.0.0-stage → 0.2.2

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/journal.mjs ADDED
@@ -0,0 +1,131 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ import { FileGraphStore } from './store.mjs'
3
+ import { canonical, copy, digest, GraphError, validateDocument } from './core.mjs'
4
+ export const MAX_CLAIMS = 1024
5
+ const fail = code => { throw new GraphError(code) }
6
+ const integer = value => Number.isSafeInteger(value) && value >= 0
7
+ const hex = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
8
+ const fields = (value, keys) => value && typeof value === 'object' && !Array.isArray(value)
9
+ && Object.keys(value).every(key => keys.includes(key)) && keys.every(key => Object.hasOwn(value, key))
10
+ export function seal(value) { const body = copy(value); delete body.checksum; return { ...body, checksum: digest(body) } }
11
+ export function claimDigest(value) { const body = copy(value); delete body.claimDigest; delete body.intentSeq; return digest(body) }
12
+ export function initialJournal(binding) {
13
+ return seal({ schema: 4, revision: 0, binding: copy(binding), document: null, activeProof: null, pending: null, claims: [] })
14
+ }
15
+ export function validateJournal(value, binding) {
16
+ if (!fields(value, ['schema', 'revision', 'binding', 'document', 'activeProof', 'pending', 'claims', 'checksum'])
17
+ || value.schema !== 4 || !integer(value.revision) || canonical(value.binding) !== canonical(binding)
18
+ || !hex(value.checksum) || seal(value).checksum !== value.checksum || !Array.isArray(value.claims) || value.claims.length > MAX_CLAIMS)
19
+ fail('JOURNAL_SCOPE_CHECKSUM_OR_LIMIT')
20
+ if (value.document) validateDocument(value.document)
21
+ const operations = new Set()
22
+ for (const row of value.claims) {
23
+ if (!fields(row, ['operationId', 'payloadHash', 'status', 'graphRevision', 'graphChecksum', 'resultSeq', 'reason', 'evidenceDigests',
24
+ 'claimDigest', 'intentSeq', 'candidateChecksum', 'expectedGraphRevision', 'operation'])
25
+ || typeof row.operationId !== 'string' || operations.has(row.operationId) || !hex(row.payloadHash)
26
+ || !['COMMITTED', 'ABORTED'].includes(row.status) || !integer(row.graphRevision)
27
+ || (row.graphChecksum !== null && !hex(row.graphChecksum)) || !integer(row.resultSeq)
28
+ || (row.reason !== null && (typeof row.reason !== 'string' || row.reason.length > 1200))
29
+ || !hex(row.claimDigest) || !hex(row.candidateChecksum) || !integer(row.intentSeq) || !integer(row.expectedGraphRevision)
30
+ || !['create', 'begin', 'finish', 'skip', 'reconcile'].includes(row.operation)
31
+ || !Array.isArray(row.evidenceDigests) || row.evidenceDigests.length > 32 || row.evidenceDigests.some(x => !hex(x)))
32
+ fail('JOURNAL_CLAIM_INVALID')
33
+ operations.add(row.operationId)
34
+ if ((value.document?.state.operations.includes(row.operationId) ?? false) !== (row.status === 'COMMITTED')) fail('JOURNAL_COMMIT_OPERATION_MISMATCH')
35
+ }
36
+ if (value.activeProof) {
37
+ if (!fields(value.activeProof, ['attemptId', 'beginResultSeq', 'beginTurn', 'workStartedAtBegin', 'workExecutedAtBegin']) || !integer(value.activeProof.beginResultSeq)
38
+ || !integer(value.activeProof.beginTurn) || !integer(value.activeProof.workStartedAtBegin) || !integer(value.activeProof.workExecutedAtBegin)
39
+ || value.document?.state.active?.attemptId !== value.activeProof.attemptId)
40
+ fail('JOURNAL_ACTIVE_PROOF_MISMATCH')
41
+ } else if (value.document?.state.active) fail('JOURNAL_ACTIVE_PROOF_REQUIRED')
42
+ const p = value.pending
43
+ if (p) {
44
+ if (!fields(p, ['operationId', 'payloadHash', 'operation', 'arguments', 'callId', 'rootCallId', 'turn', 'sessionId',
45
+ 'owner', 'bindingDigest', 'expectedGraphRevision', 'candidate', 'resolvedEvidence', 'intentSeq', 'claimDigest', 'clearedWork'])
46
+ || operations.has(p.operationId) || !hex(p.payloadHash) || !hex(p.bindingDigest) || !hex(p.claimDigest)
47
+ || claimDigest(p) !== p.claimDigest || p.bindingDigest !== digest(binding) || p.sessionId !== binding.sessionId
48
+ || !['create', 'begin', 'finish', 'skip', 'reconcile'].includes(p.operation) || !integer(p.turn)
49
+ || p.expectedGraphRevision !== (value.document?.revision ?? 0) || (p.intentSeq !== null && !integer(p.intentSeq))
50
+ || !Array.isArray(p.resolvedEvidence) || p.resolvedEvidence.length > 32 || !Array.isArray(p.clearedWork) || p.clearedWork.length > 1024
51
+ || p.clearedWork.some(row => !fields(row, ['rootCallId', 'callSeq', 'rootResultSeq', 'eventDigest'])
52
+ || typeof row.rootCallId !== 'string' || !row.rootCallId || row.rootCallId.length > 512 || !integer(row.callSeq)
53
+ || row.rootResultSeq !== null && !integer(row.rootResultSeq) || !hex(row.eventDigest))) fail('JOURNAL_PENDING_INVALID')
54
+ validateDocument(p.candidate)
55
+ if (p.candidate.revision !== p.expectedGraphRevision + 1 || p.candidate.events.at(-1).operationId !== p.operationId
56
+ || canonical(p.candidate.events.slice(0, -1)) !== canonical(value.document?.events ?? [])) fail('JOURNAL_CANDIDATE_NOT_SINGLE_APPEND')
57
+ }
58
+ return true
59
+ }
60
+
61
+ export class HostJournal {
62
+ constructor({ projectRoot, directory, binding, store, readOnly = false }) {
63
+ this.binding = copy(binding)
64
+ const validate = value => validateJournal(value, this.binding)
65
+ this.store = store ?? new FileGraphStore({ projectRoot, directory, fileName: 'transaction-host-journal.json', maximumBytes: 12 * 1024 * 1024, readOnly,
66
+ validator: validate, updateValidator: (previous, next) => {
67
+ if (next.revision !== (previous?.revision ?? 0) + 1 || previous && canonical(previous.binding) !== canonical(next.binding)) fail('JOURNAL_CAS_REVISION_OR_SCOPE')
68
+ const oldClaims = previous?.claims ?? []
69
+ if (canonical(next.claims.slice(0, oldClaims.length)) !== canonical(oldClaims)) fail('JOURNAL_CLAIM_HISTORY_IMMUTABLE')
70
+ } })
71
+ }
72
+ read() {
73
+ const value = this.store.read() ?? initialJournal(this.binding)
74
+ validateJournal(value, this.binding)
75
+ return value
76
+ }
77
+ change(expected, update) {
78
+ return this.store.compareAndSwap(expected, previous => {
79
+ const before = previous ?? initialJournal(this.binding)
80
+ validateJournal(before, this.binding)
81
+ const after = seal({ ...update(copy(before)), schema: 4, binding: copy(this.binding), revision: before.revision + 1 })
82
+ validateJournal(after, this.binding)
83
+ return after
84
+ })
85
+ }
86
+ reserve(claim) {
87
+ const before = this.read()
88
+ return this.change(before.revision, state => {
89
+ if (state.pending) fail('PENDING_INTENT_REQUIRES_RECONCILIATION')
90
+ if (state.claims.some(row => row.operationId === claim.operationId)) fail('HOST_TOOL_CALL_REPLAY')
91
+ if (state.claims.length >= MAX_CLAIMS) fail('HOST_CLAIM_LIMIT')
92
+ return { ...state, pending: copy(claim) }
93
+ })
94
+ }
95
+ bindIntent(operationId, intentSeq) {
96
+ const before = this.read()
97
+ return this.change(before.revision, state => {
98
+ if (state.pending?.operationId !== operationId || state.pending.intentSeq !== null && state.pending.intentSeq !== intentSeq) fail('HOST_INTENT_CHANGED')
99
+ return { ...state, pending: { ...state.pending, intentSeq } }
100
+ })
101
+ }
102
+ resolve(projected, { rejectReason = null } = {}) {
103
+ const before = this.read(), pending = before.pending
104
+ if (!pending) return before
105
+ if (projected.data.operationId !== pending.operationId || projected.data.claimDigest !== pending.claimDigest
106
+ || projected.data.bindingDigest !== pending.bindingDigest || projected.intentSeq !== pending.intentSeq
107
+ || projected.data.operation !== pending.operation || projected.data.expectedGraphRevision !== pending.expectedGraphRevision
108
+ || projected.data.candidateChecksum !== pending.candidate.checksum || projected.data.callId !== pending.callId
109
+ || projected.data.rootCallId !== pending.rootCallId || projected.data.turn !== pending.turn || projected.data.sessionId !== pending.sessionId
110
+ || canonical(projected.data.clearedWork) !== canonical(pending.clearedWork))
111
+ fail('HOST_PERSISTED_INTENT_PROOF_MISMATCH')
112
+ if (!['SUCCESS', 'FAILED'].includes(projected.status) || !integer(projected.rootResultSeq)) fail('HOST_RESULT_UNKNOWN_NO_COMMIT')
113
+ return this.change(before.revision, state => {
114
+ if (canonical(state.pending) !== canonical(pending)) fail('HOST_PENDING_CHANGED')
115
+ if (rejectReason !== null && (typeof rejectReason !== 'string' || !rejectReason || rejectReason.length > 1200)) fail('INVALID_CANDIDATE_REJECTION')
116
+ const committed = projected.status === 'SUCCESS' && rejectReason === null, document = committed ? pending.candidate : state.document
117
+ let activeProof = state.activeProof
118
+ if (committed && pending.operation === 'begin') activeProof = { attemptId: document.state.active.attemptId,
119
+ beginResultSeq: projected.rootResultSeq, beginTurn: pending.turn, workStartedAtBegin: projected.workStartedAtRoot,
120
+ workExecutedAtBegin: projected.workExecutedAtRoot }
121
+ if (committed && ['finish', 'reconcile'].includes(pending.operation)) activeProof = null
122
+ const row = { operationId: pending.operationId, payloadHash: pending.payloadHash, status: committed ? 'COMMITTED' : 'ABORTED',
123
+ graphRevision: document?.revision ?? 0, graphChecksum: document?.checksum ?? null, resultSeq: projected.rootResultSeq,
124
+ reason: committed ? null : rejectReason ?? 'Root/ancestor execution or typed-result qualification failed; candidate not applied. Original tool/result unchanged.',
125
+ claimDigest: pending.claimDigest, intentSeq: pending.intentSeq, candidateChecksum: pending.candidate.checksum,
126
+ expectedGraphRevision: pending.expectedGraphRevision, operation: pending.operation,
127
+ evidenceDigests: pending.resolvedEvidence.map(digest) }
128
+ return { ...state, document, activeProof, pending: null, claims: [...state.claims, row] }
129
+ })
130
+ }
131
+ }
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "dsh-bettertaskgraphs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.2",
4
+ "description": "Transactional task graphs with a conversation panel for DeepSeek Harness",
5
+ "license": "GPL-3.0-only",
6
+ "keywords": ["dsh", "deepseek-harness", "cordis", "task-graph"],
7
+ "homepage": "https://github.com/oldcitynight/dsh-bettertaskgraphs#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/oldcitynight/dsh-bettertaskgraphs.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/oldcitynight/dsh-bettertaskgraphs/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./index.mjs",
17
+ "exports": {
18
+ ".": "./index.mjs",
19
+ "./client": "./client.js"
20
+ },
21
+ "files": [
22
+ "client.js",
23
+ "client.source.mjs",
24
+ "cordis.patch.yml",
25
+ "core.mjs",
26
+ "host.mjs",
27
+ "index.mjs",
28
+ "journal.mjs",
29
+ "projection.mjs",
30
+ "remote.mjs",
31
+ "scripts/build-client.mjs",
32
+ "store.mjs",
33
+ "tools.mjs",
34
+ "view-model.mjs",
35
+ "work-policy.mjs"
36
+ ],
37
+ "engines": {
38
+ "node": "^22.19.0 || >=24.0.0",
39
+ "dsh": "0.2.1-rc.5"
40
+ },
41
+ "scripts": {
42
+ "build": "node scripts/build-client.mjs",
43
+ "check": "node scripts/build-client.mjs --check",
44
+ "test": "npm run check && node --test",
45
+ "prepack": "npm run build",
46
+ "prepublishOnly": "npm test",
47
+ "pack:bundle": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true })\" && npm pack --pack-destination dist"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org/"
52
+ },
53
+ "dependencies": {
54
+ "zod": "4.6.7"
55
+ },
56
+ "peerDependencies": {
57
+ "@deepseek-ai/dsh-session": "0.2.1-rc.5",
58
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.5",
59
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.5"
60
+ },
61
+ "devDependencies": {
62
+ "@deepseek-ai/cordis": "4.0.5",
63
+ "@deepseek-ai/dsh-session": "0.2.1-rc.5",
64
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.5",
65
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.5",
66
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.5"
67
+ },
68
+ "dsh": {
69
+ "bundle": {
70
+ "patch": "./cordis.patch.yml"
71
+ },
72
+ "client": {
73
+ "platform": "web",
74
+ "inject": [
75
+ "@deepseek-ai/dsh-client-ui-conversation",
76
+ "@deepseek-ai/dsh-api-remotes"
77
+ ]
78
+ }
79
+ }
80
+ }
package/projection.mjs ADDED
@@ -0,0 +1,205 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ import { z } from 'zod'
3
+ import { TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '@deepseek-ai/dsh-session'
4
+ import { canonical, copy, digest, GraphError } from './core.mjs'
5
+ import { administrative, workKey } from './work-policy.mjs'
6
+ export const KEY = 'projectTransactionGraphV4'
7
+ export const INTENT = 'plugin:project-transaction-graph/intent-v4'
8
+ export const RECEIPT = 'plugin:project-transaction-graph/receipt-v4'
9
+ export const RECOVERY = 'plugin:project-transaction-graph/operator-recovery-v4'
10
+ export const MAX_PROOFS = 1024
11
+ const bounded = n => z.string().min(1).max(n)
12
+ const hex = z.string().regex(/^[a-f0-9]{64}$/)
13
+ const seq = z.number().int().nonnegative()
14
+ const workSchema = z.object({ rootCallId: bounded(512), callSeq: seq, rootResultSeq: seq.nullable(), eventDigest: hex }).strict()
15
+ const factSchema = z.object({ seq, callId: bounded(512), tool: bounded(300), isError: z.boolean(),
16
+ outcome: z.enum(['UNVERIFIED', 'FAIL', 'NOT_RUN', 'UNKNOWN']) }).strict()
17
+ const intentSchema = z.object({ version: z.literal(4), operationId: bounded(80), callId: bounded(512),
18
+ rootCallId: bounded(512), turn: seq, sessionId: bounded(512), bindingDigest: hex, claimDigest: hex,
19
+ operation: z.enum(['create', 'begin', 'finish', 'skip', 'reconcile']), expectedGraphRevision: seq, candidateChecksum: hex,
20
+ clearedWork: z.array(workSchema).max(1024) }).strict()
21
+ const dispatchSchema = z.object({ seq, subCallId: bounded(512), parentCallId: bounded(512), rootCallId: bounded(512),
22
+ tool: bounded(300), isError: z.boolean(), outcome: z.enum(['UNVERIFIED', 'FAIL', 'NOT_RUN', 'UNKNOWN']), eventDigest: hex, turn: seq }).strict()
23
+ const proofSchema = z.object({ seq, callId: bounded(512), rootCallId: bounded(512), rootCallSeq: seq, rootResultSeq: seq,
24
+ tool: bounded(300), successful: z.boolean(), qualification: z.enum(['SUCCESS', 'FAILED', 'UNKNOWN']), isError: z.boolean(),
25
+ outcome: z.enum(['UNVERIFIED', 'FAIL', 'NOT_RUN', 'UNKNOWN']), ownOutcome: z.enum(['UNVERIFIED', 'FAIL', 'NOT_RUN', 'UNKNOWN']),
26
+ ancestorOutcomes: z.array(factSchema).max(33), eventDigest: hex, turn: seq }).strict()
27
+ const pendingSchema = z.object({ intentSeq: seq, data: intentSchema,
28
+ intentDigest: hex, status: z.enum(['PENDING', 'SUCCESS', 'FAILED', 'UNKNOWN']),
29
+ rootResultSeq: seq.nullable(), rootResultDigest: hex.nullable(), workStartedAtRoot: seq.nullable(), workExecutedAtRoot: seq.nullable() }).strict()
30
+ export const stateSchema = z.object({ sessionId: bounded(512), ownFrom: seq, turn: seq, lastSeq: z.number().int().min(-1),
31
+ workStarted: seq, workExecuted: seq, uncertainties: z.array(workSchema).max(1024), acknowledgedWork: z.array(workSchema).max(1024),
32
+ calls: z.record(z.string(), z.object({ tool: bounded(300), turn: seq, seq, eventDigest: hex }).strict()),
33
+ dispatches: z.array(dispatchSchema).max(256), intents: z.array(pendingSchema).max(1024),
34
+ proofs: z.array(proofSchema).max(MAX_PROOFS), receipts: z.array(z.object({ operationId: bounded(80), seq,
35
+ status: z.enum(['COMMITTED', 'ABORTED']), graphRevision: seq, graphChecksum: hex.nullable(), eventDigest: hex }).strict()).max(1024), failure: z.string().nullable() }).strict()
36
+ export function initialState(header, inheritedEventCount = 0) {
37
+ return { sessionId: header.id, ownFrom: inheritedEventCount, turn: 0, lastSeq: -1, workStarted: 0, workExecuted: 0,
38
+ uncertainties: [], acknowledgedWork: [],
39
+ calls: {}, dispatches: [], intents: [], proofs: [], receipts: [], failure: null }
40
+ }
41
+ export function outcomeOfContent(content = []) {
42
+ const outcomes = new Set()
43
+ for (const block of content) {
44
+ if (block.type !== 'text' || typeof block.text !== 'string') continue
45
+ if (block.text.length > 32768) return 'UNKNOWN' // No bounded parser means no positive completion qualification.
46
+ let value
47
+ try { value = JSON.parse(block.text) } catch { continue }
48
+ if (!value || typeof value !== 'object' || Array.isArray(value)) continue
49
+ if (value.passed === false || value.ok === false || value.success === false) outcomes.add('FAIL')
50
+ for (const status of [value.goalVerification?.status, value.verification?.status, value.status, value.state]) if (typeof status === 'string') {
51
+ if (['FAIL', 'FAILED', 'ERROR'].includes(status.toUpperCase())) outcomes.add('FAIL')
52
+ if (['NOT_RUN', 'UNKNOWN'].includes(status.toUpperCase())) outcomes.add(status.toUpperCase())
53
+ }
54
+ }
55
+ // A first FAIL/NOT_RUN block may not hide a later UNKNOWN/contradiction.
56
+ return outcomes.has('UNKNOWN') || outcomes.size > 1 ? 'UNKNOWN' : [...outcomes][0] ?? 'UNVERIFIED'
57
+ }
58
+ export function chainQualification(start, root, dispatches) {
59
+ let child = start
60
+ const visited = new Set()
61
+ let failed = false, unknown = false
62
+ while (child !== root) {
63
+ if (visited.has(child) || visited.size >= 32) return 'UNKNOWN'
64
+ visited.add(child)
65
+ const matches = dispatches.filter(row => row.subCallId === child && row.rootCallId === root)
66
+ if (matches.length !== 1) return 'UNKNOWN'
67
+ const row = matches[0]
68
+ if (row.isError || ['FAIL', 'NOT_RUN'].includes(row.outcome)) failed = true
69
+ if (row.outcome === 'UNKNOWN' || !['UNVERIFIED', 'FAIL', 'NOT_RUN'].includes(row.outcome)) unknown = true
70
+ child = row.parentCallId
71
+ }
72
+ return unknown ? 'UNKNOWN' : failed ? 'FAILED' : 'SUCCESS'
73
+ }
74
+ export const chainSucceeded = (start, root, dispatches) => chainQualification(start, root, dispatches) === 'SUCCESS'
75
+ function ancestryFacts(start, root, dispatches) {
76
+ const facts = [], visited = new Set()
77
+ let cursor = start
78
+ while (cursor !== root && !visited.has(cursor) && facts.length < 32) {
79
+ visited.add(cursor)
80
+ const matches = dispatches.filter(row => row.subCallId === cursor && row.rootCallId === root)
81
+ if (matches.length !== 1) break
82
+ const row = matches[0]
83
+ if (cursor !== start) facts.push({ seq: row.seq, callId: row.subCallId, tool: row.tool, isError: row.isError, outcome: row.outcome })
84
+ cursor = row.parentCallId
85
+ }
86
+ return facts
87
+ }
88
+ const qualifiedStatus = (root, chain) => root === 'UNKNOWN' || chain === 'UNKNOWN' ? 'UNKNOWN'
89
+ : root === 'FAILED' || chain === 'FAILED' ? 'FAILED' : 'SUCCESS'
90
+ const internal = administrative
91
+ export function applyEvent(state, event) {
92
+ if (state.failure) return state
93
+ try {
94
+ if (event.seq < state.ownFrom) return state // Fork parents cannot authorize a child's graph.
95
+ if (event.type.startsWith('plugin:project-transaction-graph/') && ![INTENT, RECEIPT, RECOVERY].includes(event.type))
96
+ throw new GraphError('LEGACY_GRAPH_REQUIRES_EXPLICIT_MIGRATION')
97
+ if (!['turn/start', 'turn/end', 'tool/call', 'tool/ptc-dispatch', 'tool/result', INTENT, RECEIPT, RECOVERY].includes(event.type)) return state
98
+ const d = event.data
99
+ let next = { ...state, lastSeq: event.seq }
100
+ if (event.type === 'turn/start') return { ...next, turn: d.turn }
101
+ if (event.type === 'tool/call') {
102
+ if (Object.hasOwn(state.calls, d.callId) || Object.keys(state.calls).length >= 256) throw new GraphError('CALL_ID_REPLAY_OR_LIMIT')
103
+ return { ...next, workStarted: state.workStarted + (!internal(d.name) && d.name !== 'run_code' ? 1 : 0),
104
+ calls: { ...state.calls, [d.callId]: { tool: d.name, turn: state.turn, seq: event.seq, eventDigest: digest(event) } } }
105
+ }
106
+ if (event.type === INTENT || event.type === RECOVERY) {
107
+ const data = intentSchema.parse(d)
108
+ if (data.sessionId !== state.sessionId || data.turn !== state.turn || (event.type === INTENT && !state.turn)
109
+ || state.intents.some(row => row.data.operationId === data.operationId) || state.intents.length >= 1024) throw new GraphError('INTENT_SCOPE_REPLAY_OR_LIMIT')
110
+ if (event.type === INTENT && data.clearedWork.length) throw new GraphError('MODEL_INTENT_CANNOT_ACKNOWLEDGE_WORK')
111
+ let uncertainties = state.uncertainties, acknowledgedWork = state.acknowledgedWork
112
+ if (event.type === RECOVERY) {
113
+ for (const work of data.clearedWork) {
114
+ const unknown = uncertainties.find(row => canonical(row) === canonical(work))
115
+ const open = state.calls[work.rootCallId]
116
+ if (!unknown && !(work.rootResultSeq === null && open?.seq === work.callSeq && open.eventDigest === work.eventDigest))
117
+ throw new GraphError('OPERATOR_WORK_ACK_NOT_OWN_RECORDED_FACT')
118
+ }
119
+ const keys = new Set(data.clearedWork.map(workKey))
120
+ uncertainties = uncertainties.filter(row => !keys.has(workKey(row)))
121
+ acknowledgedWork = [...acknowledgedWork, ...data.clearedWork.filter(row => !acknowledgedWork.some(old => workKey(old) === workKey(row)))]
122
+ if (acknowledgedWork.length > 1024) throw new GraphError('ACKNOWLEDGED_WORK_LIMIT')
123
+ }
124
+ return { ...next, uncertainties, acknowledgedWork, intents: [...state.intents, { intentSeq: event.seq, data,
125
+ intentDigest: digest(event), status: event.type === RECOVERY ? 'SUCCESS' : 'PENDING',
126
+ rootResultSeq: event.type === RECOVERY ? event.seq : null, rootResultDigest: event.type === RECOVERY ? digest(event) : null,
127
+ workStartedAtRoot: event.type === RECOVERY ? state.workStarted : null, workExecutedAtRoot: event.type === RECOVERY ? state.workExecuted : null }] }
128
+ }
129
+ if (event.type === 'tool/ptc-dispatch') {
130
+ const rootCallId = d.rootCallId ?? d.parentCallId
131
+ if (!Object.hasOwn(state.calls, rootCallId)) {
132
+ // Only a child of an open, owned root can become evidence. Once a
133
+ // graph has history, an unexplained dispatch could hide actual work.
134
+ if (state.intents.length || state.receipts.length) throw new GraphError('UNATTRIBUTED_PTC_DISPATCH_IN_GRAPH_SESSION')
135
+ return next
136
+ }
137
+ const dispatch = dispatchSchema.parse({ seq: event.seq, subCallId: d.subCallId, parentCallId: d.parentCallId,
138
+ rootCallId, tool: d.name, isError: d.isError,
139
+ outcome: outcomeOfContent(d.content), eventDigest: digest(event), turn: state.turn })
140
+ if (state.dispatches.some(row => row.subCallId === dispatch.subCallId && row.rootCallId === dispatch.rootCallId)
141
+ || state.dispatches.length >= 256) throw new GraphError('PTC_DISPATCH_REPLAY_OR_LIMIT')
142
+ return { ...next, workStarted: state.workStarted + (!internal(d.name) && d.name !== 'run_code' ? 1 : 0),
143
+ dispatches: [...state.dispatches, dispatch] }
144
+ }
145
+ if (event.type === 'tool/result') {
146
+ const root = d.message.source.callId, success = d.message.isError !== true // Optional false in the published ToolResultMessage API.
147
+ const syntheticUnknown = [TOOL_OUTCOME_UNKNOWN, TOOL_NOT_STARTED].includes(d.error?.code)
148
+ const rootOutcome = outcomeOfContent(d.message.content)
149
+ const rootQualification = syntheticUnknown || rootOutcome === 'UNKNOWN' ? 'UNKNOWN'
150
+ : !success || ['FAIL', 'NOT_RUN'].includes(rootOutcome) ? 'FAILED' : 'SUCCESS'
151
+ const sources = state.dispatches.filter(row => row.rootCallId === root)
152
+ const call = state.calls[root]
153
+ if (!call) {
154
+ if (state.intents.some(row => row.data.rootCallId === root && ['PENDING', 'UNKNOWN'].includes(row.status)))
155
+ throw new GraphError('OWN_INTENT_RESULT_WITHOUT_OWN_TOOL_CALL')
156
+ // Fork/restore seeds may synthesize child-owned closers for an inherited
157
+ // call. They are not child work, evidence, or authority for a candidate.
158
+ return next
159
+ }
160
+ const standaloneCode = call.tool === 'run_code' && !sources.length
161
+ const leafCode = sources.filter(row => row.tool === 'run_code' && !sources.some(child => child.parentCallId === row.subCallId))
162
+ const workStarted = state.workStarted + (standaloneCode ? 1 : 0) + leafCode.length
163
+ const executed = sources.filter(row => !internal(row.tool) && (row.tool !== 'run_code' || leafCode.includes(row)) && !['NOT_RUN', 'UNKNOWN'].includes(row.outcome)).length
164
+ + (!internal(call.tool) && (call.tool !== 'run_code' || standaloneCode) && !['NOT_RUN', 'UNKNOWN'].includes(rootOutcome) && !syntheticUnknown ? 1 : 0)
165
+ const workExecuted = state.workExecuted + executed
166
+ const intents = state.intents.map(row => row.data.rootCallId !== root ? row : {
167
+ ...row, status: qualifiedStatus(rootQualification, chainQualification(row.data.callId, root, sources)),
168
+ rootResultSeq: event.seq, rootResultDigest: digest(event), workStartedAtRoot: workStarted, workExecutedAtRoot: workExecuted })
169
+ const rootFact = { seq: event.seq, callId: root, tool: call.tool, isError: !success, outcome: syntheticUnknown ? 'UNKNOWN' : rootOutcome }
170
+ const proofs = sources.filter(row => !internal(row.tool) && (row.tool !== 'run_code' || leafCode.includes(row))).map(row => {
171
+ const qualification = qualifiedStatus(rootQualification, chainQualification(row.subCallId, root, sources))
172
+ return { seq: row.seq, callId: row.subCallId, rootCallId: root, rootResultSeq: event.seq, rootCallSeq: call.seq,
173
+ tool: row.tool, successful: qualification === 'SUCCESS', qualification, isError: row.isError,
174
+ outcome: row.outcome, ownOutcome: row.outcome, ancestorOutcomes: [...ancestryFacts(row.subCallId, root, sources), rootFact],
175
+ eventDigest: row.eventDigest, turn: row.turn }
176
+ })
177
+ if (!internal(call.tool) && (call.tool !== 'run_code' || !sources.length)) proofs.push({ seq: event.seq,
178
+ callId: root, rootCallId: root, rootResultSeq: event.seq, tool: call.tool,
179
+ rootCallSeq: call.seq,
180
+ successful: rootQualification === 'SUCCESS', qualification: rootQualification, isError: !success,
181
+ outcome: syntheticUnknown ? 'UNKNOWN' : rootOutcome, ownOutcome: syntheticUnknown ? 'UNKNOWN' : rootOutcome, ancestorOutcomes: [],
182
+ eventDigest: digest(event), turn: call.turn })
183
+ const calls = { ...state.calls }; delete calls[root]
184
+ let uncertainties = state.uncertainties
185
+ if (rootQualification === 'UNKNOWN' || sources.some(row => row.outcome === 'UNKNOWN') || proofs.some(row => row.qualification === 'UNKNOWN')) {
186
+ const work = { rootCallId: root, callSeq: call.seq, rootResultSeq: event.seq, eventDigest: digest(event) }
187
+ if (!state.acknowledgedWork.some(row => workKey(row) === workKey(work)) && !uncertainties.some(row => workKey(row) === workKey(work)))
188
+ uncertainties = [...uncertainties, work]
189
+ if (uncertainties.length > 1024) throw new GraphError('UNRESOLVED_WORK_LIMIT')
190
+ }
191
+ return { ...next, calls, intents, workStarted, workExecuted, uncertainties, dispatches: state.dispatches.filter(row => row.rootCallId !== root),
192
+ proofs: [...state.proofs, ...proofs].slice(-MAX_PROOFS) }
193
+ }
194
+ if (event.type === RECEIPT) {
195
+ const row = z.object({ operationId: bounded(80), status: z.enum(['COMMITTED', 'ABORTED']),
196
+ graphRevision: seq, graphChecksum: hex.nullable() }).strict().parse(d)
197
+ const previous = state.receipts.find(old => old.operationId === row.operationId)
198
+ if (previous && ['status', 'graphRevision', 'graphChecksum'].some(key => previous[key] !== row[key])) throw new GraphError('RECEIPT_CONFLICT')
199
+ return previous ? next : { ...next, receipts: [...state.receipts, { ...row, seq: event.seq, eventDigest: digest(event) }].slice(-1024) }
200
+ }
201
+ if (event.type === 'turn/end') return { ...next, turn: 0, intents: state.intents.map(row => row.status === 'PENDING' ? { ...row, status: 'UNKNOWN' } : row) }
202
+ return next
203
+ } catch (error) { return { ...state, failure: `Transaction projection failed at ${event.seq}: ${error.message}` } }
204
+ }
205
+ export const projection = { key: KEY, stateVersion: 5, stateSchema, init: initialState, apply: applyEvent }
package/remote.mjs ADDED
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ // Shared by the Host registration and the lazy browser module.
3
+ export function createRemoteContribution(z) {
4
+ const packageName = 'dsh-bettertaskgraphs'
5
+ const id = z.string().min(1).max(128)
6
+ const input = z.object({ sessionId: id }).strict()
7
+ const node = z.object({
8
+ id, label: z.string().min(1).max(512), kind: z.enum(['container', 'leaf']),
9
+ parent: id.nullable(),
10
+ status: z.enum(['COMPLETE', 'CONTAINER_INCOMPLETE', 'ACTIVE', 'INACTIVE', 'INCOMPLETE', 'PENDING', 'SKIPPED']),
11
+ blocked: z.literal(true).optional(),
12
+ disposition: z.enum(['COMPLETED', 'SKIPPED_NOT_RUN', 'UNSATISFIED', 'FAILED']).optional(),
13
+ branchState: z.enum(['SELECTED', 'WAITING', 'NOT_SELECTED']).optional(),
14
+ selectedBranch: z.object({
15
+ outcome: z.enum(['complete', 'incomplete']), case: z.string().min(1).max(64).optional(), targetId: id.optional(),
16
+ }).strict().optional(),
17
+ }).strict()
18
+ const edge = z.discriminatedUnion('kind', [
19
+ z.object({ from: id, to: id, kind: z.literal('prerequisite') }).strict(),
20
+ z.object({ from: id, to: id, kind: z.literal('route'), outcome: z.enum(['complete', 'incomplete']) }).strict(),
21
+ z.object({ from: id, to: id, kind: z.literal('case'), case: z.string().min(1).max(64) }).strict(),
22
+ ])
23
+ const choice = z.object({ id, choice: z.number().int().min(0).max(127) }).strict()
24
+ const graph = z.object({
25
+ revision: z.number().int().min(1).max(1024), title: z.string().min(1).max(512),
26
+ nodes: z.array(node).min(2).max(128), edges: z.array(edge).max(512),
27
+ active: z.object({ nodeId: id, recoveryRequired: z.boolean() }).strict().nullable(),
28
+ pending: z.object({ operation: z.string().min(1).max(64), qualification: z.string().min(1).max(128) }).strict().nullable(),
29
+ legalNext: z.array(choice).max(128), skipChoices: z.array(choice).max(128),
30
+ }).strict()
31
+ const output = z.discriminatedUnion('status', [
32
+ z.object({ sessionId: id, status: z.literal('ready'), graph }).strict(),
33
+ z.object({ sessionId: id, status: z.literal('empty'), graph: z.null() }).strict(),
34
+ z.object({ sessionId: id, status: z.literal('unavailable'), graph: z.null() }).strict(),
35
+ ])
36
+ const codec = (key, schema) => ({ mode: 'strict', typeSymbol: `${packageName}#${key}`, create: () => schema })
37
+ return {
38
+ package: packageName,
39
+ descriptors: [{
40
+ id: `${packageName}#transactionGraphView/status`,
41
+ service: 'transactionGraph', namespace: 'transactionGraphView', method: 'status', invocation: { kind: 'direct' },
42
+ parameters: [{
43
+ name: 'request', wire: 'request', source: 'json',
44
+ codec: codec('TransactionGraphStatusRequest', input),
45
+ strictCodec: codec('TransactionGraphStatusStrictRequest', input),
46
+ }],
47
+ cancellation: { parameter: 'signal' },
48
+ result: codec('TransactionGraphStatusResult', output),
49
+ }],
50
+ }
51
+ }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ import { readFileSync, writeFileSync } from 'node:fs'
3
+
4
+ const root = new URL('../', import.meta.url)
5
+ const { name: packageName, version, license, repository } = JSON.parse(readFileSync(new URL('package.json', root), 'utf8'))
6
+ const sourceUrl = repository.url.replace(/^git\+/, '').replace(/\.git$/, '')
7
+ const sources = ['remote.mjs', 'view-model.mjs', 'client.source.mjs'].map(name => {
8
+ const source = readFileSync(new URL(name, root), 'utf8')
9
+ if (/^import\s/m.test(source)) throw new Error(`${name} must be self-contained for the DSH lazy client factory`)
10
+ return source.replace(/^export\s+(?=(?:function|const|class)\b)/gm, '')
11
+ })
12
+ const output = `// SPDX-License-Identifier: ${license}
13
+ // Corresponding source: ${sourceUrl}/tree/v${version}
14
+ // Generated by scripts/build-client.mjs.
15
+ window.__ModuleLoader__.load({id:${JSON.stringify(packageName)},factory(require){
16
+ ${sources.join('\n')}
17
+ return createClientPlugin(require('react'),require('zod').z,createRemoteContribution,graphPresentation,branchLayout);
18
+ }});
19
+ `
20
+ if (process.argv.slice(2).join(' ') === '--check') {
21
+ if (readFileSync(new URL('client.js', root), 'utf8') !== output) throw new Error('Transaction graph client bundle is stale')
22
+ } else if (process.argv.length === 2) {
23
+ writeFileSync(new URL('client.js', root), output)
24
+ } else {
25
+ throw new Error('Use no arguments to build, or --check to verify')
26
+ }