WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// Closure ledger for terminal wiki recovery attempts.
|
||||
//
|
||||
// A recovery attempt that reached a terminal budget state can never continue, so
|
||||
// it must not block unrelated first attempts forever. Closing it is a reviewed
|
||||
// decision: it needs an explicit record naming the exact attempt, its terminal
|
||||
// status and error, a written reason and a timestamp. Anything that does not
|
||||
// match the row we are looking at fails closed, and a pending attempt can never
|
||||
// be closed while it is still running.
|
||||
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const assert = require('node:assert/strict')
|
||||
const { terminalRecoveryStatuses } = require('./wiki_recovery_policy.cjs')
|
||||
|
||||
const closureFile = 'closures.json'
|
||||
const closureKey = (tenant, deadLetterId, attempt) => `${tenant}:${deadLetterId}:${attempt}`
|
||||
|
||||
function parseClosures(parsed, tenant) {
|
||||
assert.equal(parsed.tenant_id, tenant, 'Closure ledger belongs to another tenant')
|
||||
assert.ok(Array.isArray(parsed.closures), 'Closure ledger must list closures')
|
||||
const closures = new Map()
|
||||
for (const closure of parsed.closures) {
|
||||
assert.ok(Number.isSafeInteger(closure.dead_letter_id) && closure.dead_letter_id >= 1 && closure.dead_letter_id <= 33,
|
||||
'Only original dead letters 1..33 can be closed')
|
||||
assert.ok(Number.isSafeInteger(closure.attempt_number) && closure.attempt_number >= 1, 'A closure needs the exact attempt number')
|
||||
assert.ok(terminalRecoveryStatuses.includes(closure.terminal_status), 'Only terminal attempts can be closed')
|
||||
assert.match(closure.error_code || '', /^[A-Z0-9_]{3,64}$/, 'A closure needs the recorded terminal error code')
|
||||
assert.ok(typeof closure.reason === 'string' && closure.reason.trim().length >= 12, 'A closure needs a written reason')
|
||||
assert.ok(Number.isFinite(Date.parse(closure.closed_at)), 'A closure needs a reviewable timestamp')
|
||||
const key = closureKey(tenant, closure.dead_letter_id, closure.attempt_number)
|
||||
assert.ok(!closures.has(key), 'Closure ledger cannot close the same attempt twice')
|
||||
closures.set(key, closure)
|
||||
}
|
||||
return closures
|
||||
}
|
||||
|
||||
function loadClosures(directory, tenant) {
|
||||
if (!directory) return new Map()
|
||||
let raw
|
||||
try {
|
||||
raw = fs.readFileSync(path.join(directory, closureFile), 'utf8')
|
||||
} catch (error) {
|
||||
// A missing ledger simply means nothing has been closed yet.
|
||||
if (error.code === 'ENOENT') return new Map()
|
||||
throw error
|
||||
}
|
||||
return parseClosures(JSON.parse(raw), tenant)
|
||||
}
|
||||
|
||||
function closureFor(row, closures, tenant) {
|
||||
const closure = closures?.get(closureKey(row.tenant_id ?? tenant, row.dead_letter_id, Number(row.attempt_number || 1)))
|
||||
if (!closure) return null
|
||||
// The closure names one terminal attempt. A row that is still running (or that
|
||||
// resolved without a terminal budget state) is simply not covered by it yet.
|
||||
if (!terminalRecoveryStatuses.includes(row.recovery_status)) return null
|
||||
assert.ok(!row.pending && !(row.pending_finalize > 0), 'A pending attempt cannot be closed')
|
||||
assert.equal(closure.terminal_status, row.recovery_status, 'Closure does not match the recorded terminal status')
|
||||
assert.equal(closure.error_code, row.error_code, 'Closure does not match the recorded terminal error')
|
||||
return closure
|
||||
}
|
||||
|
||||
function annotateClosed(rows, closures, tenant) {
|
||||
return rows.map(row => ({ ...row, closed: closureFor(row, closures, tenant) !== null }))
|
||||
}
|
||||
|
||||
module.exports = { loadClosures, parseClosures, closureFor, annotateClosed, closureKey, closureFile }
|
||||
Reference in New Issue
Block a user