@twinsearth/fang-dsh-plugin 0.0.0-stage → 3.3.4
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/CLA.md +79 -0
- package/LICENSE-COMMERCIAL +78 -0
- package/LICENSE-CORE +661 -0
- package/LICENSE-SDK +21 -0
- package/LICENSES/cordis-MIT.txt +39 -0
- package/NOTICE +95 -0
- package/README.md +158 -2
- package/client.js +7287 -0
- package/cordis.patch.yml +163 -0
- package/defaults/umbrella-rules.yaml +113 -0
- package/docs/CONTRACT-INTEROP.md +77 -0
- package/docs/CRASH-SAFETY.md +69 -0
- package/docs/DECISION-RELAY.md +46 -0
- package/docs/DSH-ARCHITECTURE-MAP.md +73 -0
- package/docs/FORK-SEED.md +64 -0
- package/docs/MAIN-CITY-GATE.md +58 -0
- package/docs/OVERLAY-MASTER-SWITCH.md +35 -0
- package/docs/PACKAGE-RENAME-v3.3.3.md +40 -0
- package/docs/PM-DECISION-POLICY.md +61 -0
- package/docs/WORKFLOW-UI.md +86 -0
- package/icon.svg +8 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +71 -4
- package/sdk/README.md +65 -0
- package/sdk/examples/status-badge/README.md +16 -0
- package/sdk/examples/status-badge/client.js +60 -0
- package/sdk/examples/status-badge/cordis.patch.yml +7 -0
- package/sdk/examples/status-badge/index.js +12 -0
- package/sdk/examples/status-badge/package.json +24 -0
- package/sdk/templates/plugin/README.md +36 -0
- package/sdk/templates/plugin/client.js +35 -0
- package/sdk/templates/plugin/cordis.patch.yml +8 -0
- package/sdk/templates/plugin/icon.svg +5 -0
- package/sdk/templates/plugin/index.js +23 -0
- package/sdk/templates/plugin/locale/en.json +6 -0
- package/sdk/templates/plugin/package.json +36 -0
- package/src/compose.mjs +67 -0
- package/src/contract/VENDOR.json +16 -0
- package/src/contract/client.js +93 -0
- package/src/contract/constants.js +94 -0
- package/src/contract/envelope.js +83 -0
- package/src/contract/index.js +20 -0
- package/src/contract/negotiate.js +106 -0
- package/src/contract/relay.js +223 -0
- package/src/contract/server.js +77 -0
- package/src/contract/transports.js +319 -0
- package/src/index.mjs +11 -0
- package/src/kernel/core.mjs +100 -0
- package/src/kernel/extensions.mjs +61 -0
- package/src/kernel/fsx.mjs +349 -0
- package/src/kernel/index.mjs +21 -0
- package/src/kernel/ledger.mjs +94 -0
- package/src/kernel/paths.mjs +182 -0
- package/src/kernel/role.mjs +68 -0
- package/src/kernel/rounds.mjs +172 -0
- package/src/kernel/schema.mjs +5 -0
- package/src/kernel/types.mjs +266 -0
- package/src/kernel/vendor/cosmokit.mjs +421 -0
- package/src/kernel/vendor/schemastery.mjs +638 -0
- package/src/lib/adapter-client.mjs +383 -0
- package/src/lib/adapter-protocol.mjs +243 -0
- package/src/lib/ai-secret-store.mjs +202 -0
- package/src/lib/boot-guard.mjs +186 -0
- package/src/lib/branch-store.mjs +84 -0
- package/src/lib/branch.mjs +74 -0
- package/src/lib/bridge-client.mjs +133 -0
- package/src/lib/broker-adapter.mjs +67 -0
- package/src/lib/broker.mjs +45 -0
- package/src/lib/business-producers.mjs +109 -0
- package/src/lib/command-cache.mjs +209 -0
- package/src/lib/compat.mjs +170 -0
- package/src/lib/composer-journal.mjs +128 -0
- package/src/lib/container/index.mjs +806 -0
- package/src/lib/decision-events.mjs +129 -0
- package/src/lib/decision-ingest-client.mjs +41 -0
- package/src/lib/decision-outbox.mjs +129 -0
- package/src/lib/decision-policy.mjs +52 -0
- package/src/lib/default-essence.mjs +61 -0
- package/src/lib/desktop-launch.mjs +138 -0
- package/src/lib/essence.mjs +1211 -0
- package/src/lib/fork-client.mjs +410 -0
- package/src/lib/fork-config-lock.mjs +166 -0
- package/src/lib/fork-seed.mjs +304 -0
- package/src/lib/fork-sessions.mjs +183 -0
- package/src/lib/fork-supervision-settings.mjs +58 -0
- package/src/lib/fork-supervisor.mjs +122 -0
- package/src/lib/fs-guard.mjs +28 -0
- package/src/lib/http-auth.mjs +162 -0
- package/src/lib/idle-audit.mjs +178 -0
- package/src/lib/interaction-poke.mjs +61 -0
- package/src/lib/main-gate.mjs +579 -0
- package/src/lib/mirror-etag.mjs +92 -0
- package/src/lib/mirror-official-source.mjs +93 -0
- package/src/lib/mirror.mjs +478 -0
- package/src/lib/mother-link.mjs +156 -0
- package/src/lib/mutex.mjs +31 -0
- package/src/lib/permissions.mjs +115 -0
- package/src/lib/pnpm-store.mjs +189 -0
- package/src/lib/preferences.mjs +212 -0
- package/src/lib/producer-contract.mjs +162 -0
- package/src/lib/reflow.mjs +41 -0
- package/src/lib/remote.mjs +408 -0
- package/src/lib/restart-contract.mjs +752 -0
- package/src/lib/roots.mjs +30 -0
- package/src/lib/rule-registry.mjs +1127 -0
- package/src/lib/runtime-capture.mjs +307 -0
- package/src/lib/semaphore.mjs +34 -0
- package/src/lib/session-bundle.mjs +138 -0
- package/src/lib/session-format.mjs +672 -0
- package/src/lib/sse-parser.mjs +102 -0
- package/src/lib/ssrf-guard.mjs +170 -0
- package/src/lib/status-store.mjs +257 -0
- package/src/lib/task-handle.mjs +622 -0
- package/src/lib/ui-actions.mjs +55 -0
- package/src/lib/umbrella.mjs +1661 -0
- package/src/lib/upgrade-coordinator.mjs +107 -0
- package/src/lib/upgrade-status.mjs +43 -0
- package/src/plugins/adapter.mjs +1468 -0
- package/src/plugins/approver.mjs +1210 -0
- package/src/plugins/auditor.mjs +330 -0
- package/src/plugins/buffer-keeper.mjs +522 -0
- package/src/plugins/decision-center.mjs +2581 -0
- package/src/plugins/forker.mjs +1610 -0
- package/src/plugins/healer.mjs +702 -0
- package/src/plugins/hot-reloader.mjs +274 -0
- package/src/plugins/imports.mjs +247 -0
- package/src/plugins/orchestrator.mjs +3938 -0
- package/src/plugins/restart-guard.mjs +1022 -0
- package/src/plugins/router.mjs +714 -0
- package/src/plugins/scorer.mjs +661 -0
- package/src/plugins/session-bridge.mjs +166 -0
- package/src/plugins/session-catalog.mjs +771 -0
- package/src/plugins/session-router.mjs +125 -0
- package/src/plugins/session-sync.mjs +171 -0
- package/src/plugins/umbrella.mjs +419 -0
- package/tools/boot-guard.mjs +95 -0
- package/tools/decision-ingest.mjs +53 -0
- package/tools/desktop-launch.mjs +71 -0
- package/tools/fangctl.mjs +408 -0
- package/tools/gate/main-city.mjs +204 -0
- package/tools/mcp/server.mjs +121 -0
- package/tools/mcp/tools.mjs +65 -0
- package/tools/recover-queue.mjs +133 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 dsh-Fang contributors — see LICENSE-CORE.
|
|
3
|
+
// dsh-Fang 2.8 — fang-auditor: the append-only chained ledger of the whole system.
|
|
4
|
+
//
|
|
5
|
+
// Every cross-plugin fact (fork started, promote committed, restart resolved, ...) is
|
|
6
|
+
// appended through this service, so the chain can be verified later:
|
|
7
|
+
// hash_i = sha256( recursive-canonical(record_without_hash) + prevHash )
|
|
8
|
+
// A single edited/deleted/swapped line makes verify() fail at that index.
|
|
9
|
+
//
|
|
10
|
+
// bootCheck() is the startup admission gate (CONTRACTS.md §5.2): it looks at the tail
|
|
11
|
+
// left by the PREVIOUS run and reports
|
|
12
|
+
// * unclean-boot — the previous run's last record is not shutdown-graceful
|
|
13
|
+
// * interrupted-in-guard— a restart was opened but never resolved
|
|
14
|
+
// This instance marks its own records with `inst`, so records written by this run can
|
|
15
|
+
// never be mistaken for the previous run's tail (and no timestamp-window heuristics are
|
|
16
|
+
// needed — those race when two runs happen within the same millisecond).
|
|
17
|
+
//
|
|
18
|
+
// v2.0.0 (CONTRACTS-V2.md §1/§5.7): the plugin is role-gated (matrix allows mother AND
|
|
19
|
+
// fork) and exposes `scope`. The mother does not share the fork's process/bus, so it
|
|
20
|
+
// pulls each fork's ledger over HTTP and merges it here:
|
|
21
|
+
// aggregate({limit, sources}) -> {ok,total,records,sources}
|
|
22
|
+
// local records get taskId:null, each fork's records get that source's taskId,
|
|
23
|
+
// merged ascending by at (tie-break seq) and the newest `limit` are returned.
|
|
24
|
+
// bootAudit() — alias of bootCheck(), kept for the v2 vocabulary.
|
|
25
|
+
// cloudSink() — commercial backup/cloud hook placeholder ({ok:false,reason:'not-configured'}).
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import {
|
|
28
|
+
AUDIT,
|
|
29
|
+
SERVICE,
|
|
30
|
+
appendChained,
|
|
31
|
+
ensureDir,
|
|
32
|
+
iso,
|
|
33
|
+
newId,
|
|
34
|
+
nowMs,
|
|
35
|
+
readTextSafe,
|
|
36
|
+
resolveFangLayout,
|
|
37
|
+
roleGate,
|
|
38
|
+
verifyChain,
|
|
39
|
+
z,
|
|
40
|
+
} from '../kernel/index.mjs'
|
|
41
|
+
|
|
42
|
+
export const name = 'fang-auditor'
|
|
43
|
+
export const inject = []
|
|
44
|
+
|
|
45
|
+
const DEFAULTS = {
|
|
46
|
+
dataRoot: '',
|
|
47
|
+
bootSelfCheck: true,
|
|
48
|
+
ledgerFile: '',
|
|
49
|
+
role: 'mother',
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Config = z
|
|
53
|
+
.object({
|
|
54
|
+
dataRoot: z.string().default('').description('审计数据目录(空 = 默认 $DSH_HOME/fang/<profile>)'),
|
|
55
|
+
bootSelfCheck: z.boolean().default(true).description('apply 时做一次只读启动自检(不写盘)'),
|
|
56
|
+
ledgerFile: z.string().default('').description('显式账本路径(空 = dataDir/audit.jsonl)'),
|
|
57
|
+
role: z
|
|
58
|
+
.union([z.const('mother'), z.const('fork')])
|
|
59
|
+
.default('mother')
|
|
60
|
+
.description('部署角色(v2 §1);DSH_FANG_ROLE 环境变量优先级更高(forker 启动分身时注入)'),
|
|
61
|
+
})
|
|
62
|
+
.description('fang-auditor 配置')
|
|
63
|
+
|
|
64
|
+
const msgOf = (e) => String(e && e.message ? e.message : e)
|
|
65
|
+
|
|
66
|
+
/** A restart was opened by the guard (requested or already committed). */
|
|
67
|
+
function isRestartOpen(rec) {
|
|
68
|
+
if (!rec || typeof rec !== 'object') return false
|
|
69
|
+
if (rec.kind === AUDIT.RESTART_REQUESTED || rec.kind === 'restart-committed') return true
|
|
70
|
+
return rec.action === 'restart' && (rec.phase === 'requested' || rec.phase === 'committed')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A restart was closed (approved/denied/timeout all resolve to restart-resolved). */
|
|
74
|
+
function isRestartResolved(rec) {
|
|
75
|
+
if (!rec || typeof rec !== 'object') return false
|
|
76
|
+
return rec.kind === AUDIT.RESTART_RESOLVED || rec.action === 'restart-resolved'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function apply(ctx, config) {
|
|
80
|
+
// v2 §1:角色门禁必须是 apply 的第一件事;未部署到该角色时不注册服务/不自检。
|
|
81
|
+
const gate = roleGate('auditor', {
|
|
82
|
+
cfgRole: config && typeof config === 'object' ? config.role : '',
|
|
83
|
+
logger: ctx.logger,
|
|
84
|
+
})
|
|
85
|
+
if (!gate.active) return null
|
|
86
|
+
const scope = gate.role
|
|
87
|
+
const cfg = { ...DEFAULTS, ...(config && typeof config === 'object' ? config : {}) }
|
|
88
|
+
let layout
|
|
89
|
+
try {
|
|
90
|
+
layout = resolveFangLayout({ dataRoot: cfg.dataRoot })
|
|
91
|
+
} catch {
|
|
92
|
+
layout = { dataDir: '', ledgerFile: path.resolve('audit.jsonl') }
|
|
93
|
+
}
|
|
94
|
+
const ledgerFile = cfg.ledgerFile ? path.resolve(cfg.ledgerFile) : layout.ledgerFile
|
|
95
|
+
// Identity of THIS plugin instance: records carrying it are this run's own writes.
|
|
96
|
+
const instanceId = newId('inst')
|
|
97
|
+
let lastError = null
|
|
98
|
+
let lastBoot = null
|
|
99
|
+
// Fail-closed after a chain check failed: appending on top of a broken chain would
|
|
100
|
+
// launder the tampering (the kernel appendChained only refuses an unparsable TAIL).
|
|
101
|
+
let integrityBroken = false
|
|
102
|
+
|
|
103
|
+
/** Tolerant JSONL read: bad lines are skipped here and reported by verify(). */
|
|
104
|
+
const readAll = () => {
|
|
105
|
+
const raw = readTextSafe(ledgerFile, '')
|
|
106
|
+
const out = []
|
|
107
|
+
for (const line of raw.split('\n')) {
|
|
108
|
+
if (!line.trim()) continue
|
|
109
|
+
try {
|
|
110
|
+
const rec = JSON.parse(line)
|
|
111
|
+
if (rec && typeof rec === 'object') out.push(rec)
|
|
112
|
+
} catch {
|
|
113
|
+
/* verify() reports the index; reading must not throw */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Append one chained record. `at` and `seq` are filled automatically; `data` wins on
|
|
121
|
+
* collision (so callers can record a fork's own pid via data.pid).
|
|
122
|
+
* @returns {object|null} the appended record, or null when the ledger tail is unusable.
|
|
123
|
+
*/
|
|
124
|
+
const append = (kind, data) => {
|
|
125
|
+
if (typeof kind !== 'string' || !kind.trim()) return null
|
|
126
|
+
if (integrityBroken) {
|
|
127
|
+
lastError = 'ledger-integrity-broken: refusing to append on top of a broken chain'
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
ensureDir(path.dirname(ledgerFile))
|
|
132
|
+
const body = {
|
|
133
|
+
kind: kind.trim(),
|
|
134
|
+
pid: process.pid,
|
|
135
|
+
at: iso(nowMs()),
|
|
136
|
+
...(data ?? {}),
|
|
137
|
+
inst: (data && data.inst) || instanceId,
|
|
138
|
+
}
|
|
139
|
+
const chain = appendChained(ledgerFile, body)
|
|
140
|
+
lastError = null
|
|
141
|
+
return { ...body, ...chain }
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// An unparsable tail is deliberately NOT overwritten (that would hide tampering).
|
|
144
|
+
lastError = msgOf(e)
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @returns {object[]} the newest `limit` records (default 100, 0 = none). */
|
|
150
|
+
const read = (opts = {}) => {
|
|
151
|
+
const recs = readAll()
|
|
152
|
+
const rawLimit = opts == null ? 100 : opts.limit
|
|
153
|
+
if (rawLimit === undefined || rawLimit === null) return recs.slice(-100)
|
|
154
|
+
const limit = Math.max(0, Math.floor(Number(rawLimit) || 0))
|
|
155
|
+
return limit === 0 ? [] : recs.slice(-limit)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whole-chain verification; names the first bad record by its seq. */
|
|
159
|
+
const verify = () => {
|
|
160
|
+
const result = verifyChain(ledgerFile)
|
|
161
|
+
integrityBroken = !result.ok
|
|
162
|
+
const recs = readAll()
|
|
163
|
+
const badIndex = Number.isInteger(result.badIndex) ? result.badIndex : null
|
|
164
|
+
const badRec = badIndex !== null ? recs[badIndex] : null
|
|
165
|
+
return {
|
|
166
|
+
ok: !!result.ok,
|
|
167
|
+
records: Number.isFinite(result.count) ? result.count : recs.length,
|
|
168
|
+
firstBadSeq: result.ok ? null : (badRec && Number.isFinite(badRec.seq) ? badRec.seq : badIndex),
|
|
169
|
+
badIndex,
|
|
170
|
+
reason: result.reason ?? null,
|
|
171
|
+
expected: result.expected,
|
|
172
|
+
actual: result.actual,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Startup admission gate — read-only (CONTRACTS.md §5.2).
|
|
178
|
+
* Only records not written by this instance AND carrying a parsable timestamp are
|
|
179
|
+
* treated as "the previous run"; kernel boot-verify rows have no `at` and are skipped.
|
|
180
|
+
* @returns {{ok:boolean, findings:{id:string, level:'warn'|'error', message:string, detail?:object}[], records:number}}
|
|
181
|
+
*/
|
|
182
|
+
const bootCheck = () => {
|
|
183
|
+
// Integrity is a precondition for appending; cache the verdict so a corrupt ledger
|
|
184
|
+
// cannot be extended later in this run (bootCheck itself stays read-only).
|
|
185
|
+
try {
|
|
186
|
+
const chain = verifyChain(ledgerFile)
|
|
187
|
+
integrityBroken = !chain.ok
|
|
188
|
+
} catch {
|
|
189
|
+
/* a read-only integrity probe must never throw out of bootCheck */
|
|
190
|
+
}
|
|
191
|
+
const recs = readAll()
|
|
192
|
+
const previous = []
|
|
193
|
+
for (const rec of recs) {
|
|
194
|
+
if (!rec || typeof rec !== 'object') continue
|
|
195
|
+
if (rec.inst !== undefined && rec.inst === instanceId) continue // this run's own writes
|
|
196
|
+
if (!Number.isFinite(Date.parse(String(rec.at ?? '')))) continue // unstructured row (e.g. boot-verify)
|
|
197
|
+
previous.push(rec)
|
|
198
|
+
}
|
|
199
|
+
const findings = []
|
|
200
|
+
|
|
201
|
+
const last = previous.length ? previous[previous.length - 1] : null
|
|
202
|
+
if (last) {
|
|
203
|
+
const lastKind = last.kind ?? last.action ?? null
|
|
204
|
+
if (lastKind !== AUDIT.SHUTDOWN_GRACEFUL) {
|
|
205
|
+
findings.push({
|
|
206
|
+
id: 'unclean-boot',
|
|
207
|
+
level: 'warn',
|
|
208
|
+
message: `上次运行未干净收尾:最后一条记录是 ${lastKind ?? 'unknown'}(缺少 shutdown-graceful)`,
|
|
209
|
+
detail: { lastKind, seq: last.seq ?? null, at: last.at ?? null, pid: last.pid ?? null },
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let lastOpen = -1
|
|
215
|
+
for (let i = 0; i < previous.length; i++) if (isRestartOpen(previous[i])) lastOpen = i
|
|
216
|
+
if (lastOpen >= 0 && !previous.slice(lastOpen + 1).some(isRestartResolved)) {
|
|
217
|
+
const open = previous[lastOpen]
|
|
218
|
+
findings.push({
|
|
219
|
+
id: 'interrupted-in-guard',
|
|
220
|
+
level: 'warn',
|
|
221
|
+
message: '上次运行死在守卫中途:restart 已发起但始终没有 restart-resolved',
|
|
222
|
+
detail: {
|
|
223
|
+
requestId: open.requestId ?? null,
|
|
224
|
+
kind: open.kind ?? null,
|
|
225
|
+
at: open.at ?? null,
|
|
226
|
+
seq: open.seq ?? null,
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { ok: findings.length === 0, findings, records: recs.length, previous: previous.length, pid: process.pid }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ───────── v2 §5.7 汇总 / 商用钩子 ─────────
|
|
235
|
+
|
|
236
|
+
const atMsOf = (rec) => {
|
|
237
|
+
const ms = Date.parse(String(rec?.at ?? ''))
|
|
238
|
+
return Number.isFinite(ms) ? ms : null
|
|
239
|
+
}
|
|
240
|
+
const seqOf = (rec) => {
|
|
241
|
+
const n = Number(rec?.seq)
|
|
242
|
+
return Number.isFinite(n) ? n : null
|
|
243
|
+
}
|
|
244
|
+
/** 合并多进程时间线:优先 at(不同进程的 seq 各自从 0 起,不可跨源比较),同刻退回 seq。 */
|
|
245
|
+
const compareRecords = (a, b) => {
|
|
246
|
+
const aAt = atMsOf(a)
|
|
247
|
+
const bAt = atMsOf(b)
|
|
248
|
+
if (aAt !== null && bAt !== null && aAt !== bAt) return aAt - bAt
|
|
249
|
+
const aSeq = seqOf(a)
|
|
250
|
+
const bSeq = seqOf(b)
|
|
251
|
+
if (aSeq !== null && bSeq !== null && aSeq !== bSeq) return aSeq - bSeq
|
|
252
|
+
return 0
|
|
253
|
+
}
|
|
254
|
+
const sourceTaskId = (source) => {
|
|
255
|
+
const raw = source && typeof source === 'object' ? source.taskId : null
|
|
256
|
+
return raw === undefined || raw === null ? null : String(raw)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* v2 §5.7:纯汇总(只读本进程账本 + 调用方拉来的分身记录;不写盘、不改链)。
|
|
261
|
+
* 本地记录 taskId=null;每个 source 的记录强制打上该 source 的 taskId;
|
|
262
|
+
* 按 at/seq 升序合并后返回最新 limit 条(与 read() 的"最新 N 条"语义一致)。
|
|
263
|
+
*/
|
|
264
|
+
const aggregate = (options = {}) => {
|
|
265
|
+
const rawLimit = options?.limit === undefined || options?.limit === null ? 200 : Number(options.limit)
|
|
266
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : 200
|
|
267
|
+
const sources = Array.isArray(options?.sources) ? options.sources : []
|
|
268
|
+
const merged = []
|
|
269
|
+
for (const rec of readAll()) {
|
|
270
|
+
if (rec && typeof rec === 'object') merged.push({ ...rec, taskId: null })
|
|
271
|
+
}
|
|
272
|
+
for (const source of sources) {
|
|
273
|
+
const taskId = sourceTaskId(source)
|
|
274
|
+
const records = Array.isArray(source?.records) ? source.records : []
|
|
275
|
+
for (const rec of records) {
|
|
276
|
+
if (rec && typeof rec === 'object') merged.push({ ...rec, taskId })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
merged.sort(compareRecords)
|
|
280
|
+
const total = merged.length
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
scope,
|
|
284
|
+
total,
|
|
285
|
+
records: limit === 0 ? [] : merged.slice(-limit),
|
|
286
|
+
sources: sources.map(sourceTaskId),
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** 商用备份/云上传钩子占位(CONTRACTS-V2 §6:本期只留接入点,不实现传输)。 */
|
|
291
|
+
const cloudSink = () => ({ ok: false, reason: 'not-configured' })
|
|
292
|
+
|
|
293
|
+
const service = {
|
|
294
|
+
scope,
|
|
295
|
+
append,
|
|
296
|
+
read,
|
|
297
|
+
verify,
|
|
298
|
+
bootCheck,
|
|
299
|
+
/** v2 别名:bootCheck 的等价物(保留 v1 名称)。 */
|
|
300
|
+
bootAudit: bootCheck,
|
|
301
|
+
aggregate,
|
|
302
|
+
cloudSink,
|
|
303
|
+
get ledgerFile() {
|
|
304
|
+
return ledgerFile
|
|
305
|
+
},
|
|
306
|
+
get instanceId() {
|
|
307
|
+
return instanceId
|
|
308
|
+
},
|
|
309
|
+
get lastError() {
|
|
310
|
+
return lastError
|
|
311
|
+
},
|
|
312
|
+
get lastBoot() {
|
|
313
|
+
return lastBoot
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
ctx.effect(() => ctx.provide(SERVICE.auditor, service))
|
|
318
|
+
if (cfg.bootSelfCheck) {
|
|
319
|
+
// Read-only: apply never writes the ledger. Findings stay on the service for the
|
|
320
|
+
// orchestrator/UI to pick up; recording them is an explicit append() by the caller.
|
|
321
|
+
try {
|
|
322
|
+
lastBoot = bootCheck()
|
|
323
|
+
} catch (e) {
|
|
324
|
+
lastBoot = { ok: false, findings: [], records: 0, error: msgOf(e) }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return service
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const _internals = { isRestartOpen, isRestartResolved, DEFAULTS }
|