pi-proactive 0.2.0-beta.2 → 0.2.0-beta.3
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/CHANGELOG.md +29 -0
- package/README.md +32 -28
- package/build/cli.mjs +121 -0
- package/build/extensions/proactive.js +47 -6
- package/build/src/commands.js +78 -2
- package/build/src/config.js +511 -4
- package/build/src/daemon-config.js +221 -0
- package/build/src/daemon-contact.js +103 -0
- package/build/src/daemon-host.js +84 -0
- package/build/src/daemon.js +193 -3
- package/build/src/health.js +84 -0
- package/build/src/lineage.js +41 -2
- package/build/src/notify.js +183 -0
- package/build/src/progress-eta.js +134 -0
- package/build/src/trigger.js +43 -0
- package/build/src/trust-loop.js +321 -0
- package/build/src/trust-metrics.js +236 -0
- package/build/src/trust-notify.js +73 -0
- package/build/src/trust-policy.js +58 -0
- package/build/src/trust-registry.js +323 -0
- package/daemon/launchd.plist +48 -0
- package/daemon/pi-proactive-daemon.service +29 -0
- package/daemon/run-daemon.sh +84 -0
- package/node_modules/typebox/build/compile/code.mjs +2 -1
- package/node_modules/typebox/build/compile/validator.mjs +1 -5
- package/node_modules/typebox/build/format/date_time.d.mts +1 -2
- package/node_modules/typebox/build/format/date_time.mjs +3 -12
- package/node_modules/typebox/build/format/duration.d.mts +2 -2
- package/node_modules/typebox/build/format/duration.mjs +2 -2
- package/node_modules/typebox/build/format/idn_email.d.mts +0 -1
- package/node_modules/typebox/build/format/idn_email.mjs +0 -1
- package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -1
- package/node_modules/typebox/build/format/idn_hostname.mjs +1 -1
- package/node_modules/typebox/build/format/ipv4.d.mts +2 -2
- package/node_modules/typebox/build/format/ipv4.mjs +2 -2
- package/node_modules/typebox/build/format/ipv6.d.mts +1 -1
- package/node_modules/typebox/build/format/ipv6.mjs +1 -1
- package/node_modules/typebox/build/format/iri.mjs +9 -16
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/guard/emit.mjs +2 -2
- package/node_modules/typebox/build/guard/guard.d.mts +5 -7
- package/node_modules/typebox/build/guard/guard.mjs +9 -14
- package/node_modules/typebox/build/guard/index.mjs +0 -6
- package/node_modules/typebox/build/guard/{unicode/unicode_segment.mjs → string.mjs} +19 -16
- package/node_modules/typebox/build/schema/build.mjs +5 -5
- package/node_modules/typebox/build/schema/check.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/_context.d.mts +1 -5
- package/node_modules/typebox/build/schema/engine/_context.mjs +3 -16
- package/node_modules/typebox/build/schema/engine/_functions.d.mts +2 -2
- package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/_reducer.d.mts +3 -3
- package/node_modules/typebox/build/schema/engine/_refine.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/_refine.mjs +9 -14
- package/node_modules/typebox/build/schema/engine/_stack.d.mts +20 -34
- package/node_modules/typebox/build/schema/engine/_stack.mjs +202 -143
- package/node_modules/typebox/build/schema/engine/_unique.d.mts +2 -4
- package/node_modules/typebox/build/schema/engine/_unique.mjs +3 -7
- package/node_modules/typebox/build/schema/engine/additionalItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/additionalItems.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +8 -5
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +32 -27
- package/node_modules/typebox/build/schema/engine/allOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/allOf.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/anyOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/anyOf.mjs +9 -4
- package/node_modules/typebox/build/schema/engine/boolean.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/boolean.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/const.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/const.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/contains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/contains.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/dependencies.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +7 -5
- package/node_modules/typebox/build/schema/engine/dependentRequired.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependentRequired.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/dependentSchemas.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +1 -2
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +8 -12
- package/node_modules/typebox/build/schema/engine/enum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/enum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/format.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/format.mjs +8 -11
- package/node_modules/typebox/build/schema/engine/if.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/if.mjs +13 -5
- package/node_modules/typebox/build/schema/engine/index.d.mts +0 -1
- package/node_modules/typebox/build/schema/engine/index.mjs +0 -1
- package/node_modules/typebox/build/schema/engine/items.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/items.mjs +5 -8
- package/node_modules/typebox/build/schema/engine/maxContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +8 -4
- package/node_modules/typebox/build/schema/engine/maxItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxItems.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/maxLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxLength.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/maxProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxProperties.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/maximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maximum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minContains.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/minItems.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/minItems.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minLength.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minProperties.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minimum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/multipleOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/multipleOf.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/not.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/not.mjs +8 -4
- package/node_modules/typebox/build/schema/engine/oneOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +11 -6
- package/node_modules/typebox/build/schema/engine/pattern.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/pattern.mjs +7 -3
- package/node_modules/typebox/build/schema/engine/patternProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +5 -6
- package/node_modules/typebox/build/schema/engine/prefixItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/prefixItems.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/properties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/properties.mjs +3 -4
- package/node_modules/typebox/build/schema/engine/propertyNames.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +10 -7
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +8 -12
- package/node_modules/typebox/build/schema/engine/ref.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/ref.mjs +14 -18
- package/node_modules/typebox/build/schema/engine/required.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/required.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/schema.d.mts +7 -7
- package/node_modules/typebox/build/schema/engine/schema.mjs +138 -136
- package/node_modules/typebox/build/schema/engine/type.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/type.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/uniqueItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/uniqueItems.mjs +6 -2
- package/node_modules/typebox/build/schema/errors.mjs +1 -1
- package/node_modules/typebox/build/schema/intern/intern.d.mts +9 -6
- package/node_modules/typebox/build/schema/intern/intern.mjs +56 -90
- package/node_modules/typebox/build/schema/parse.mjs +0 -3
- package/node_modules/typebox/build/schema/pointer/pointer.d.mts +6 -1
- package/node_modules/typebox/build/schema/pointer/pointer.mjs +64 -47
- package/node_modules/typebox/build/schema/resolve/resolve.d.mts +32 -6
- package/node_modules/typebox/build/schema/resolve/resolve.mjs +227 -231
- package/node_modules/typebox/build/schema/schema.d.mts +0 -1
- package/node_modules/typebox/build/schema/schema.mjs +0 -1
- package/node_modules/typebox/build/system/hashing/hash.mjs +34 -63
- package/node_modules/typebox/build/system/locale/en_US.mjs +1 -2
- package/node_modules/typebox/build/system/memory/assign.d.mts +8 -2
- package/node_modules/typebox/build/system/memory/assign.mjs +6 -13
- package/node_modules/typebox/build/system/memory/clone.d.mts +5 -4
- package/node_modules/typebox/build/system/memory/clone.mjs +96 -8
- package/node_modules/typebox/build/system/memory/create.d.mts +5 -1
- package/node_modules/typebox/build/system/memory/create.mjs +13 -18
- package/node_modules/typebox/build/system/memory/discard.d.mts +1 -1
- package/node_modules/typebox/build/system/memory/discard.mjs +8 -9
- package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
- package/node_modules/typebox/build/system/memory/update.d.mts +4 -1
- package/node_modules/typebox/build/system/memory/update.mjs +16 -43
- package/node_modules/typebox/build/system/settings/settings.d.mts +0 -11
- package/node_modules/typebox/build/system/settings/settings.mjs +0 -14
- package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +24 -2
- package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +29 -2
- package/node_modules/typebox/build/type/engine/evaluate/index.d.mts +0 -1
- package/node_modules/typebox/build/type/engine/evaluate/index.mjs +0 -1
- package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +1 -5
- package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +1 -6
- package/node_modules/typebox/build/type/engine/immutable/instantiate_add.d.mts +3 -1
- package/node_modules/typebox/build/type/engine/immutable/instantiate_add.mjs +4 -1
- package/node_modules/typebox/build/type/engine/object/from_intersect.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/object/from_intersect.mjs +2 -2
- package/node_modules/typebox/build/type/engine/object/from_union.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/object/from_union.mjs +2 -2
- package/node_modules/typebox/build/type/engine/optional/instantiate_add.d.mts +3 -1
- package/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs +4 -1
- package/node_modules/typebox/build/type/engine/readonly/instantiate_add.d.mts +3 -1
- package/node_modules/typebox/build/type/engine/readonly/instantiate_add.mjs +4 -1
- package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
- package/node_modules/typebox/build/type/extends/inference.mjs +2 -15
- package/node_modules/typebox/build/type/extends/tuple.d.mts +1 -2
- package/node_modules/typebox/build/type/extends/tuple.mjs +1 -21
- package/node_modules/typebox/build/value/clean/from_object.mjs +1 -1
- package/node_modules/typebox/build/value/clone/clone.d.mts +3 -4
- package/node_modules/typebox/build/value/clone/clone.mjs +7 -5
- package/node_modules/typebox/build/value/parse/parse.mjs +4 -10
- package/node_modules/typebox/build/value/shared/union_priority_sort.mjs +14 -25
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +84 -52
- package/package.json +11 -3
- package/skills/pi-proactive/SKILL.md +27 -24
- package/tools/cli.mjs +121 -0
- package/tools/daemon-install.sh +397 -0
- package/tools/daemon-uninstall.sh +226 -0
- package/node_modules/typebox/build/guard/unicode/unicode.d.mts +0 -6
- package/node_modules/typebox/build/guard/unicode/unicode.mjs +0 -35
- package/node_modules/typebox/build/schema/engine/_pathing.d.mts +0 -1
- package/node_modules/typebox/build/schema/engine/_pathing.mjs +0 -11
- package/node_modules/typebox/build/schema/meta/draft_2019_09.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_2019_09.mjs +0 -288
- package/node_modules/typebox/build/schema/meta/draft_2020_12.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_2020_12.mjs +0 -300
- package/node_modules/typebox/build/schema/meta/draft_3.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_3.mjs +0 -143
- package/node_modules/typebox/build/schema/meta/draft_4.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_4.mjs +0 -149
- package/node_modules/typebox/build/schema/meta/draft_6.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_6.mjs +0 -155
- package/node_modules/typebox/build/schema/meta/draft_7.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_7.mjs +0 -172
- package/node_modules/typebox/build/schema/meta/index.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/index.mjs +0 -2
- package/node_modules/typebox/build/schema/meta/meta.d.mts +0 -9
- package/node_modules/typebox/build/schema/meta/meta.mjs +0 -15
- package/node_modules/typebox/build/schema/meta/schema.d.mts +0 -6
- package/node_modules/typebox/build/schema/meta/schema.mjs +0 -1
- package/node_modules/typebox/build/system/memory/deep.d.mts +0 -2
- package/node_modules/typebox/build/system/memory/deep.mjs +0 -108
- package/node_modules/typebox/build/system/memory/finalize.d.mts +0 -4
- package/node_modules/typebox/build/system/memory/finalize.mjs +0 -30
- package/node_modules/typebox/build/system/memory/keys.d.mts +0 -2
- package/node_modules/typebox/build/system/memory/keys.mjs +0 -8
- package/node_modules/typebox/build/system/settings/internal.d.mts +0 -2
- package/node_modules/typebox/build/system/settings/internal.mjs +0 -20
- package/node_modules/typebox/build/type/engine/call/index.d.mts +0 -1
- package/node_modules/typebox/build/type/engine/call/index.mjs +0 -1
- package/node_modules/typebox/build/type/engine/evaluate/property.d.mts +0 -30
- package/node_modules/typebox/build/type/engine/evaluate/property.mjs +0 -29
- package/node_modules/typebox/build/type/engine/this/index.d.mts +0 -1
- package/node_modules/typebox/build/type/engine/this/index.mjs +0 -1
- package/node_modules/typebox/build/value/repair/from_cyclic.d.mts +0 -2
- package/node_modules/typebox/build/value/repair/from_cyclic.mjs +0 -6
- /package/node_modules/typebox/build/guard/{unicode/unicode_segment.d.mts → string.d.mts} +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 信任层的**指标与守恒等式**(卡 D1 的四段闭环 + 卡 D4 的三项指标)。
|
|
3
|
+
*
|
|
4
|
+
* 口径唯一源:**只读事件流**(`src/lineage.ts` 的 JSONL)——
|
|
5
|
+
* 不读登记册、不读内存状态 → 指标可由第三方**独立复算**(同一份账本 → 同一组数)。
|
|
6
|
+
*
|
|
7
|
+
* 三项指标(承 `docs/product/assistant-trust-model.md` §4 的口径):
|
|
8
|
+
* ```
|
|
9
|
+
* 回应率 = answered / delivered (它主动的 → 我回应的)
|
|
10
|
+
* 托付闭环率 = settled / registered (托付 → 到达 → 知会 → 结账)
|
|
11
|
+
* 守候时长 = 平均(知会时刻 − 登记时刻) (我不用守着的时间)
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* 守恒等式(每条托付一条链,**不得跳段、不得重复**):
|
|
15
|
+
* ```
|
|
16
|
+
* registered → arrived → notified → settled
|
|
17
|
+
* 每条链缺一段 → 记一条违规(少一段 = 红)
|
|
18
|
+
* 聚合等式:登记数 = 结账数 + 撤销数 + 在途数(重复登记 / 悬空都会破坏它)
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @capability: trust-metrics
|
|
22
|
+
* @observable: pi-proactive/src/trust-metrics.ts:computeTrustMetrics
|
|
23
|
+
* @attribution: pi-proactive/src/trust-metrics.ts:computeTrustMetrics
|
|
24
|
+
* @outcome: pi-proactive/src/trust-metrics.ts:ConservationReport
|
|
25
|
+
* @checked: test:pi-proactive/tests/trust-metrics.test.ts#D4-1 三项指标可从事件流算出
|
|
26
|
+
*/
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
/** 托付链的阶段顺序(**不得跳段**)。 */
|
|
30
|
+
const CHAIN = ["registered", "arrived", "notified", "settled"];
|
|
31
|
+
/** 只取信任层事件(`type === "trust"`)。 */
|
|
32
|
+
function trustEvents(events) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const e of events) {
|
|
35
|
+
if (e.type !== "trust" || e.trust === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
const id = e.trust.entrustmentId;
|
|
38
|
+
if (id === undefined || id === "")
|
|
39
|
+
continue;
|
|
40
|
+
out.push({ id, stage: e.trust.stage, ts: e.ts, event: e });
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 托付四段的守恒等式。
|
|
46
|
+
*
|
|
47
|
+
* 判据(可机检、能红):
|
|
48
|
+
* - 每条托付的阶段链必须是 `registered → arrived → notified → settled` 的**子序列**;
|
|
49
|
+
* - **跳段即违规**(例如有 `settled` 却没有 `notified` → 「少一段」→ 红);
|
|
50
|
+
* - **重复即违规**(同一阶段两条 → 记一条;聚合等式也会因此失衡);
|
|
51
|
+
* - 聚合等式:`登记数 == 结账数 + 撤销数 + 在途数`。
|
|
52
|
+
*/
|
|
53
|
+
export function evaluateEntrustmentConservation(events) {
|
|
54
|
+
const rows = trustEvents(events);
|
|
55
|
+
const byEntrustment = new Map();
|
|
56
|
+
for (const r of rows) {
|
|
57
|
+
const list = byEntrustment.get(r.id) ?? [];
|
|
58
|
+
list.push({ stage: r.stage, ts: r.ts });
|
|
59
|
+
byEntrustment.set(r.id, list);
|
|
60
|
+
}
|
|
61
|
+
const violations = [];
|
|
62
|
+
let registered = 0;
|
|
63
|
+
let arrived = 0;
|
|
64
|
+
let notified = 0;
|
|
65
|
+
let settled = 0;
|
|
66
|
+
let revoked = 0;
|
|
67
|
+
let humanGated = 0;
|
|
68
|
+
let inFlight = 0;
|
|
69
|
+
for (const [id, list] of byEntrustment) {
|
|
70
|
+
list.sort((a, b) => a.ts - b.ts);
|
|
71
|
+
const stages = list.map((l) => l.stage);
|
|
72
|
+
const countOf = (s) => stages.filter((x) => x === s).length;
|
|
73
|
+
registered += countOf("registered");
|
|
74
|
+
arrived += countOf("arrived");
|
|
75
|
+
notified += countOf("notified");
|
|
76
|
+
settled += countOf("settled");
|
|
77
|
+
revoked += countOf("revoked");
|
|
78
|
+
humanGated += countOf("human-gated");
|
|
79
|
+
// 重复阶段(去重失效 → 清单里同一件事出现两次)。
|
|
80
|
+
for (const s of CHAIN) {
|
|
81
|
+
if (countOf(s) > 1)
|
|
82
|
+
violations.push(`${id}: 重复阶段 ${s} ×${countOf(s)}`);
|
|
83
|
+
}
|
|
84
|
+
// 起始阶段必须是登记。
|
|
85
|
+
if (countOf("registered") === 0)
|
|
86
|
+
violations.push(`${id}: 缺阶段 registered`);
|
|
87
|
+
// 跳段检查:出现了靠后的阶段,就必须有它前面的阶段。
|
|
88
|
+
for (let i = 1; i < CHAIN.length; i += 1) {
|
|
89
|
+
const s = CHAIN[i];
|
|
90
|
+
if (countOf(s) === 0)
|
|
91
|
+
continue;
|
|
92
|
+
for (let j = 0; j < i; j += 1) {
|
|
93
|
+
const prev = CHAIN[j];
|
|
94
|
+
if (countOf(prev) === 0)
|
|
95
|
+
violations.push(`${id}: 缺阶段 ${prev}(但已有 ${s})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 顺序检查:阶段必须按链的顺序出现(时间序)。
|
|
99
|
+
const orderIdx = new Map(CHAIN.map((s, i) => [s, i]));
|
|
100
|
+
let last = -1;
|
|
101
|
+
for (const s of stages) {
|
|
102
|
+
const idx = orderIdx.get(s);
|
|
103
|
+
if (idx === undefined)
|
|
104
|
+
continue; // revoked / human-gated 不在主链上
|
|
105
|
+
if (idx < last)
|
|
106
|
+
violations.push(`${id}: 阶段乱序 ${s}`);
|
|
107
|
+
last = Math.max(last, idx);
|
|
108
|
+
}
|
|
109
|
+
// 撤销与结账互斥。
|
|
110
|
+
if (countOf("settled") > 0 && countOf("revoked") > 0)
|
|
111
|
+
violations.push(`${id}: 同时结账与撤销`);
|
|
112
|
+
// 终态判定:在途 = 既没结账也没撤销。
|
|
113
|
+
if (countOf("settled") === 0 && countOf("revoked") === 0)
|
|
114
|
+
inFlight += 1;
|
|
115
|
+
}
|
|
116
|
+
const equationHolds = registered === settled + revoked + inFlight;
|
|
117
|
+
if (!equationHolds) {
|
|
118
|
+
violations.push(`聚合等式不成立:登记 ${registered} ≠ 结账 ${settled} + 撤销 ${revoked} + 在途 ${inFlight}`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
registered,
|
|
122
|
+
arrived,
|
|
123
|
+
notified,
|
|
124
|
+
settled,
|
|
125
|
+
revoked,
|
|
126
|
+
humanGated,
|
|
127
|
+
inFlight,
|
|
128
|
+
equationHolds,
|
|
129
|
+
violations,
|
|
130
|
+
balanced: violations.length === 0 && equationHolds,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 三项指标(回应率 / 托付闭环率 / 守候时长)+ 守恒等式。
|
|
135
|
+
*
|
|
136
|
+
* 全部从**事件流**算 → 可由第三方用同一份账本复算。
|
|
137
|
+
*/
|
|
138
|
+
export function computeTrustMetrics(events) {
|
|
139
|
+
let delivered = 0;
|
|
140
|
+
let answered = 0;
|
|
141
|
+
for (const e of events) {
|
|
142
|
+
if (e.type === "delivered" && e.result === "delivered")
|
|
143
|
+
delivered += 1;
|
|
144
|
+
if (e.type === "answered")
|
|
145
|
+
answered += 1;
|
|
146
|
+
}
|
|
147
|
+
const conservation = evaluateEntrustmentConservation(events);
|
|
148
|
+
// 守候时长:登记 → 知会(同一托付的第一条 registered 与 notified)。
|
|
149
|
+
const firstRegistered = new Map();
|
|
150
|
+
const firstNotified = new Map();
|
|
151
|
+
for (const e of events) {
|
|
152
|
+
if (e.type !== "trust" || e.trust === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
const id = e.trust.entrustmentId;
|
|
155
|
+
if (id === undefined)
|
|
156
|
+
continue;
|
|
157
|
+
if (e.trust.stage === "registered" && !firstRegistered.has(id))
|
|
158
|
+
firstRegistered.set(id, e.ts);
|
|
159
|
+
if (e.trust.stage === "notified" && !firstNotified.has(id))
|
|
160
|
+
firstNotified.set(id, e.ts);
|
|
161
|
+
}
|
|
162
|
+
let totalMs = 0;
|
|
163
|
+
let count = 0;
|
|
164
|
+
for (const [id, at] of firstNotified) {
|
|
165
|
+
const from = firstRegistered.get(id);
|
|
166
|
+
if (from === undefined)
|
|
167
|
+
continue;
|
|
168
|
+
totalMs += Math.max(0, at - from);
|
|
169
|
+
count += 1;
|
|
170
|
+
}
|
|
171
|
+
const rate = (n, d) => (d === 0 ? 0 : n / d);
|
|
172
|
+
return {
|
|
173
|
+
responseRate: { numerator: answered, denominator: delivered, value: rate(answered, delivered) },
|
|
174
|
+
entrustmentClosureRate: { numerator: conservation.settled, denominator: conservation.registered, value: rate(conservation.settled, conservation.registered) },
|
|
175
|
+
watchDuration: { averageMs: count === 0 ? 0 : totalMs / count, totalMs, count },
|
|
176
|
+
conservation,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 读一个目录下的全部事件流文件(`*.jsonl`)→ 事件列表。
|
|
181
|
+
*
|
|
182
|
+
* 坏行跳过(不抛错)——与 `src/lineage.ts` 的读方口径一致。
|
|
183
|
+
*/
|
|
184
|
+
export function readLineageEvents(dir) {
|
|
185
|
+
let files;
|
|
186
|
+
try {
|
|
187
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const events = [];
|
|
193
|
+
for (const f of files) {
|
|
194
|
+
let raw;
|
|
195
|
+
try {
|
|
196
|
+
raw = fs.readFileSync(path.join(dir, f), "utf8");
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const line of raw.split("\n")) {
|
|
202
|
+
if (line.trim().length === 0)
|
|
203
|
+
continue;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(line);
|
|
206
|
+
if (typeof parsed?.ts === "number" && typeof parsed?.type === "string")
|
|
207
|
+
events.push(parsed);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// 坏行跳过
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return events;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* CLI:打印指标 JSON(供 `scripts/team/progress-snapshot.sh` 调用 → **同一份实现算指标**,
|
|
218
|
+
* 避免「脚本里另写一套口径」导致漂移)。
|
|
219
|
+
*
|
|
220
|
+
* 用法:`node --experimental-strip-types pi-proactive/src/trust-metrics.ts --dir <lineageDir>`
|
|
221
|
+
*/
|
|
222
|
+
export function main(argv) {
|
|
223
|
+
const idx = argv.indexOf("--dir");
|
|
224
|
+
const dir = idx >= 0 ? argv[idx + 1] : undefined;
|
|
225
|
+
if (dir === undefined) {
|
|
226
|
+
console.error("用法:trust-metrics.ts --dir <lineageDir>");
|
|
227
|
+
return 2;
|
|
228
|
+
}
|
|
229
|
+
const metrics = computeTrustMetrics(readLineageEvents(dir));
|
|
230
|
+
console.log(JSON.stringify(metrics));
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
const invokedDirectly = process.argv[1] !== undefined && process.argv[1].endsWith("trust-metrics.ts");
|
|
234
|
+
if (invokedDirectly) {
|
|
235
|
+
process.exitCode = main(process.argv.slice(2));
|
|
236
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道判定(纯函数)。
|
|
3
|
+
*
|
|
4
|
+
* - 空闲 → `wake`(用户不在 → 直接起一轮);
|
|
5
|
+
* - 忙 → `queue`(排队,不打断正在做的事);
|
|
6
|
+
* - 需人立刻决定(urgent)→ `interrupt`(担责的事不能等)。
|
|
7
|
+
*/
|
|
8
|
+
export function resolveTrustChannels(input) {
|
|
9
|
+
const session = input.urgent ? "interrupt" : input.idle ? "wake" : "queue";
|
|
10
|
+
// 设备级通知**一律发**(托付达成 = 用户明确交代过的事 → 值得一条设备通知;
|
|
11
|
+
// 会话通道决定「什么时候起一轮」,设备通道决定「用户不在电脑前也能收到」)。
|
|
12
|
+
const channels = [`session-${session}`, "device"];
|
|
13
|
+
return { session, device: true, channels };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 托付知会器。
|
|
17
|
+
*
|
|
18
|
+
* 幂等:同一条托付**只**知会一次(第二次 → `duplicate`,不再投递、不再落事件)。
|
|
19
|
+
*/
|
|
20
|
+
export class TrustNotifier {
|
|
21
|
+
deps;
|
|
22
|
+
constructor(deps) {
|
|
23
|
+
this.deps = deps;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 知会一条**已到达**的托付。
|
|
27
|
+
*
|
|
28
|
+
* 顺序:查状态(已知会 → 重复,返回)→ 投递(会话 + 设备)→ 标记知会 → 落 `notified` 事件。
|
|
29
|
+
* 需要人决定的托付 → **不投递**(只产出 human-gated 记录):担责的事不代劳。
|
|
30
|
+
*/
|
|
31
|
+
async notifyEntrustment(entrustmentId, opts = {}) {
|
|
32
|
+
const e = this.deps.registry.getEntrustment(entrustmentId);
|
|
33
|
+
if (e === undefined)
|
|
34
|
+
return { entrustmentId, delivered: false, reason: "not-found", channels: [] };
|
|
35
|
+
if (e.status === "notified" || e.status === "settled") {
|
|
36
|
+
return { entrustmentId, delivered: false, reason: "duplicate", channels: [] };
|
|
37
|
+
}
|
|
38
|
+
if (e.status !== "arrived") {
|
|
39
|
+
return { entrustmentId, delivered: false, reason: "not-arrived", channels: [] };
|
|
40
|
+
}
|
|
41
|
+
if (e.needsHuman === true) {
|
|
42
|
+
const reason = e.humanReason ?? "需要人决定";
|
|
43
|
+
this.deps.registry.markNeedsHuman(entrustmentId, reason);
|
|
44
|
+
this.deps.lineage?.noteTrust({ stage: "human-gated", entrustmentId, goalId: e.goalId, reason });
|
|
45
|
+
return { entrustmentId, delivered: false, reason: "needs-human", channels: [] };
|
|
46
|
+
}
|
|
47
|
+
const plan = resolveTrustChannels({ idle: opts.idle ?? true, urgent: opts.urgent ?? false });
|
|
48
|
+
const title = "托付达成";
|
|
49
|
+
const body = `${e.target} —— 条件已满足。`;
|
|
50
|
+
const channels = [];
|
|
51
|
+
if (this.deps.deliverSession !== undefined) {
|
|
52
|
+
await this.deps.deliverSession({ title, body, intent: plan.session });
|
|
53
|
+
channels.push(`session-${plan.session}`);
|
|
54
|
+
}
|
|
55
|
+
if (plan.device && this.deps.notifyDevice !== undefined) {
|
|
56
|
+
this.deps.notifyDevice(title, body);
|
|
57
|
+
channels.push("device");
|
|
58
|
+
}
|
|
59
|
+
const marked = this.deps.registry.markNotified(entrustmentId);
|
|
60
|
+
if (marked === undefined) {
|
|
61
|
+
return { entrustmentId, delivered: false, reason: "duplicate", channels };
|
|
62
|
+
}
|
|
63
|
+
const waitedMs = marked.notifiedAt !== undefined && marked.createdAt !== undefined ? marked.notifiedAt - marked.createdAt : undefined;
|
|
64
|
+
this.deps.lineage?.noteTrust({
|
|
65
|
+
stage: "notified",
|
|
66
|
+
entrustmentId,
|
|
67
|
+
goalId: e.goalId,
|
|
68
|
+
channel: channels.join("+"),
|
|
69
|
+
...(waitedMs !== undefined ? { waitedMs } : {}),
|
|
70
|
+
});
|
|
71
|
+
return { entrustmentId, delivered: true, channels };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主动性策略:**空闲触发降级为默认浅层**(卡 D2)。
|
|
3
|
+
*
|
|
4
|
+
* 依据 `docs/product/assistant-trust-model.md` §3:
|
|
5
|
+
* > 空闲触发只是**默认浅层用法**(应默认静默、能等就等);主动性的真正来源是**用户的托付**。
|
|
6
|
+
*
|
|
7
|
+
* 判据(两条闸门,缺一即不开口):
|
|
8
|
+
* 1. **有用户交代**:没有任何托付 / 未竟目标支撑 → 不开口(默认静默);
|
|
9
|
+
* 2. **确有价值**:DECIDE1 分数低于阈值 → 不开口(不产生无价值打扰)。
|
|
10
|
+
*
|
|
11
|
+
* 分数**复用 DECIDE1**(`src/idle.ts:scoreDecision` —— 同一把尺子,不另立公式),
|
|
12
|
+
* 输入的三个分量各自带**来源**(可复核「这个数从哪来」)。
|
|
13
|
+
*
|
|
14
|
+
* @capability: trust-shallow-idle
|
|
15
|
+
* @observable: pi-proactive/src/trust-policy.ts:evaluateShallowIdle
|
|
16
|
+
* @attribution: pi-proactive/src/trust-policy.ts:evaluateShallowIdle
|
|
17
|
+
* @outcome: pi-proactive/src/trigger.ts:Trigger
|
|
18
|
+
* @checked: test:pi-proactive/tests/trust-policy.test.ts#D2-4 接线实证
|
|
19
|
+
*/
|
|
20
|
+
import { scoreDecision } from "./idle.js";
|
|
21
|
+
/** 浅层策略的缺省值(**默认静默**)。 */
|
|
22
|
+
export const DEFAULT_SHALLOW_IDLE_POLICY = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
requireUserBacking: true,
|
|
25
|
+
threshold: 1,
|
|
26
|
+
observeRatio: 0.5,
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* 浅层空闲判定(纯函数)。
|
|
30
|
+
*
|
|
31
|
+
* - **默认静默**:没有用户交代 → `speak=false`(reason = `no-backing`);
|
|
32
|
+
* - **不产生无价值打扰**:分数 < 阈值 → `speak=false`(reason = `below-threshold`);
|
|
33
|
+
* - 两条都过 → `speak=true`(reason = `speak`)。
|
|
34
|
+
*/
|
|
35
|
+
export function evaluateShallowIdle(input) {
|
|
36
|
+
const scoring = scoreDecision({
|
|
37
|
+
value: { score: input.value, source: input.valueSource },
|
|
38
|
+
urgency: { score: input.urgency, source: input.urgencySource },
|
|
39
|
+
interruptionCost: { score: input.interruptionCost, source: input.interruptionCostSource },
|
|
40
|
+
}, input.policy.threshold, input.policy.observeRatio);
|
|
41
|
+
if (input.policy.requireUserBacking && !input.backedByUserRequest) {
|
|
42
|
+
return { speak: false, reason: "no-backing", score: scoring.score };
|
|
43
|
+
}
|
|
44
|
+
if (scoring.band !== "trigger") {
|
|
45
|
+
return { speak: false, reason: "below-threshold", score: scoring.score };
|
|
46
|
+
}
|
|
47
|
+
return { speak: true, reason: "speak", score: scoring.score };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 这条信封是不是**空闲触发**(`src/idle.ts:buildIdleEnvelope` 造出来的)。
|
|
51
|
+
*
|
|
52
|
+
* 为什么用信封标记而不是 `cause`:既有空闲路径**没有**把 `cause:"idle"` 传给投递层
|
|
53
|
+
* (`src/idle.ts` 的 `fireIdle` → `deliverToModel` 不带 cause)→ 投递层只能靠信封自身
|
|
54
|
+
* 的确定性标记识别(`id` 前缀 + `subject`),两者都由 `buildIdleEnvelope` 固定生成。
|
|
55
|
+
*/
|
|
56
|
+
export function isIdleTriggerEnvelope(msg) {
|
|
57
|
+
return msg.id.startsWith("idle-") || msg.subject === "空闲触发";
|
|
58
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 托付与目标的**登记册**(信任层的事实源 · 卡 D1/D6)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有登记册:托付("盯着 X")与目标("把发布流程跑通")若只存在于对话里,
|
|
5
|
+
* 就**无法查、无法撤、无法结账** → 主动性的价值无从证明。登记册把二者变成
|
|
6
|
+
* **有状态、可查、可撤销**的记录,每次状态变化都落成**只追加**的事件(`src/lineage.ts`)。
|
|
7
|
+
*
|
|
8
|
+
* 设计要点(对齐 `docs/product/agentic-loop.md` 与 `docs/product/assistant-trust-model.md`):
|
|
9
|
+
* - **托付 = 目标 + 条件**(L-B 是 L-C 的一个实例):托付带一个**可判定**的到达条件;
|
|
10
|
+
* - **目标必须可判定完成**:`doneWhen` 缺失或不可判定 → 目标**不得**进入自主推进(护栏①);
|
|
11
|
+
* - **状态机是显式的**:托付 `active → arrived → notified → settled`(或 `revoked`);
|
|
12
|
+
* 目标 `open → achieved | abandoned | needs-human` —— 「需人决策」是**一等状态**,
|
|
13
|
+
* 不允许被静默折叠成「完成」;
|
|
14
|
+
* - **落盘是原子的**:临时文件 + rename(避免半截 JSON 读坏整个登记册);
|
|
15
|
+
* - **不越界**:目标带 `scope`(范围标签)—— 自主推进只能改变范围内的状态。
|
|
16
|
+
*
|
|
17
|
+
* 落盘位置:`<agentDir>/proactive/trust/registry.json`(与邮箱 / 线程 / 事件流同族目录)。
|
|
18
|
+
*
|
|
19
|
+
* @capability: trust-entrust
|
|
20
|
+
* @observable: pi-proactive/src/trust-registry.ts:TrustRegistry
|
|
21
|
+
* @attribution: pi-proactive/src/trust-registry.ts:TrustRegistry.registerEntrustment
|
|
22
|
+
* @outcome: pi-proactive/src/trust-metrics.ts:ConservationReport
|
|
23
|
+
* @checked: test:pi-proactive/tests/trust-registry.test.ts#D1-2 四段闭环成立
|
|
24
|
+
*/
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { ensureDir, writeAtomicFile } from "./config.js";
|
|
29
|
+
/** 登记册文件格式版本。 */
|
|
30
|
+
export const REGISTRY_SCHEMA_VERSION = 1;
|
|
31
|
+
/** 目标是否**可判定完成**(护栏①的判据)。 */
|
|
32
|
+
export function isDecidable(goal) {
|
|
33
|
+
const d = goal.doneWhen;
|
|
34
|
+
if (d === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
if (d.type === "checklist")
|
|
37
|
+
return d.items.length > 0;
|
|
38
|
+
return Number.isFinite(d.target);
|
|
39
|
+
}
|
|
40
|
+
/** 目标是否**已达成**(按完成定义判定,缺省定义 → 未达成)。 */
|
|
41
|
+
export function goalSatisfied(goal) {
|
|
42
|
+
const d = goal.doneWhen;
|
|
43
|
+
if (d === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
if (d.type === "checklist")
|
|
46
|
+
return d.items.length > 0 && d.items.every((i) => i.done);
|
|
47
|
+
if (d.op === ">=")
|
|
48
|
+
return d.current >= d.target;
|
|
49
|
+
if (d.op === "<=")
|
|
50
|
+
return d.current <= d.target;
|
|
51
|
+
return d.current === d.target;
|
|
52
|
+
}
|
|
53
|
+
/** 目标差距(还差多少 / 哪些没做完)—— 候选行动的来源。 */
|
|
54
|
+
export function goalGap(goal) {
|
|
55
|
+
const d = goal.doneWhen;
|
|
56
|
+
if (d === undefined)
|
|
57
|
+
return { remaining: 1, total: 1, ratio: 1, items: [] };
|
|
58
|
+
if (d.type === "checklist") {
|
|
59
|
+
const undone = d.items.filter((i) => !i.done);
|
|
60
|
+
return {
|
|
61
|
+
remaining: undone.length,
|
|
62
|
+
total: d.items.length,
|
|
63
|
+
ratio: d.items.length === 0 ? 1 : undone.length / d.items.length,
|
|
64
|
+
items: undone.map((i) => i.text),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const span = Math.abs(d.target - d.current);
|
|
68
|
+
const step = span === 0 ? 1 : Math.max(1, Math.abs(d.target));
|
|
69
|
+
return { remaining: span, total: step, ratio: Math.min(1, span / step), items: [`${d.name} → ${d.op} ${d.target}`] };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 托付与目标的登记册(单一事实源)。
|
|
73
|
+
*
|
|
74
|
+
* 状态迁移全部走**显式方法**(`markArrived` / `markNotified` / `settle` / `revoke`),
|
|
75
|
+
* 不允许调用方直接改状态 → 状态机只有一处实现,事件流与登记册不会漂移。
|
|
76
|
+
*/
|
|
77
|
+
export class TrustRegistry {
|
|
78
|
+
agentDir;
|
|
79
|
+
ownerAgentId;
|
|
80
|
+
now;
|
|
81
|
+
goals = [];
|
|
82
|
+
entrustments = [];
|
|
83
|
+
/** 载入时的坏数据计数(读不动的记录被跳过,不抛错)。 */
|
|
84
|
+
stale = 0;
|
|
85
|
+
constructor(opts) {
|
|
86
|
+
this.agentDir = opts.agentDir;
|
|
87
|
+
this.ownerAgentId = opts.agentId;
|
|
88
|
+
this.now = opts.now ?? Date.now;
|
|
89
|
+
this.load();
|
|
90
|
+
}
|
|
91
|
+
/** 登记册文件绝对路径。 */
|
|
92
|
+
path() {
|
|
93
|
+
return path.join(this.agentDir, "proactive", "trust", "registry.json");
|
|
94
|
+
}
|
|
95
|
+
/** 载入登记册(文件缺失 → 空册;坏记录 → 跳过并计数,绝不抛错)。 */
|
|
96
|
+
load() {
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = fs.readFileSync(this.path(), "utf8");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
this.stale += 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const file = parsed;
|
|
113
|
+
this.goals = Array.isArray(file.goals) ? file.goals.filter((g) => g && typeof g.id === "string") : [];
|
|
114
|
+
this.entrustments = Array.isArray(file.entrustments)
|
|
115
|
+
? file.entrustments.filter((e) => e && typeof e.id === "string")
|
|
116
|
+
: [];
|
|
117
|
+
}
|
|
118
|
+
/** 落盘(临时文件 + rename → 原子;失败不抛错,只记日志)。 */
|
|
119
|
+
save() {
|
|
120
|
+
const file = this.path();
|
|
121
|
+
const payload = { schemaVersion: REGISTRY_SCHEMA_VERSION, goals: this.goals, entrustments: this.entrustments };
|
|
122
|
+
try {
|
|
123
|
+
ensureDir(path.dirname(file));
|
|
124
|
+
writeAtomicFile(file, `${JSON.stringify(payload, null, 2)}\n`);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error("[proactive] 托付登记册落盘失败", err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** 载入时跳过的坏记录数(审计用)。 */
|
|
131
|
+
staleRecords() {
|
|
132
|
+
return this.stale;
|
|
133
|
+
}
|
|
134
|
+
// ---- 目标 ----
|
|
135
|
+
/** 登记一个目标(**完成定义缺省 = 不可判定** → 由护栏拦住自主推进)。 */
|
|
136
|
+
registerGoal(input) {
|
|
137
|
+
const at = this.now();
|
|
138
|
+
const goal = {
|
|
139
|
+
id: input.id ?? randomUUID(),
|
|
140
|
+
title: input.title,
|
|
141
|
+
...(input.doneWhen !== undefined ? { doneWhen: input.doneWhen } : {}),
|
|
142
|
+
status: "open",
|
|
143
|
+
scope: input.scope ?? [],
|
|
144
|
+
budget: { maxActions: input.maxActions ?? 8, usedActions: 0 },
|
|
145
|
+
permission: input.permission ?? "auto",
|
|
146
|
+
progressSteps: 0,
|
|
147
|
+
createdAt: at,
|
|
148
|
+
updatedAt: at,
|
|
149
|
+
};
|
|
150
|
+
this.goals = [...this.goals.filter((g) => g.id !== goal.id), goal];
|
|
151
|
+
this.save();
|
|
152
|
+
return goal;
|
|
153
|
+
}
|
|
154
|
+
/** 取目标(不存在 → undefined)。 */
|
|
155
|
+
getGoal(id) {
|
|
156
|
+
return this.goals.find((g) => g.id === id);
|
|
157
|
+
}
|
|
158
|
+
/** 全部目标(按登记顺序)。 */
|
|
159
|
+
listGoals() {
|
|
160
|
+
return [...this.goals];
|
|
161
|
+
}
|
|
162
|
+
/** 未竟目标(`open`;自主推进的发现来源之一)。 */
|
|
163
|
+
listOpenGoals() {
|
|
164
|
+
return this.goals.filter((g) => g.status === "open");
|
|
165
|
+
}
|
|
166
|
+
/** 勾选清单项 / 更新指标当前值(完成定义的推进)。 */
|
|
167
|
+
updateDoneWhen(goalId, update) {
|
|
168
|
+
const goal = this.getGoal(goalId);
|
|
169
|
+
if (goal === undefined || goal.doneWhen === undefined)
|
|
170
|
+
return undefined;
|
|
171
|
+
if (goal.doneWhen.type === "checklist" && update.itemId !== undefined) {
|
|
172
|
+
goal.doneWhen.items = goal.doneWhen.items.map((i) => i.id === update.itemId ? { ...i, done: update.itemDone === true } : i);
|
|
173
|
+
}
|
|
174
|
+
if (goal.doneWhen.type === "metric" && update.metricCurrent !== undefined) {
|
|
175
|
+
goal.doneWhen.current = update.metricCurrent;
|
|
176
|
+
}
|
|
177
|
+
goal.updatedAt = this.now();
|
|
178
|
+
this.save();
|
|
179
|
+
return goal;
|
|
180
|
+
}
|
|
181
|
+
/** 归因推进:这次行动让目标前进了 `delta` 步(可追溯⑤的载体)。 */
|
|
182
|
+
recordProgress(goalId, delta) {
|
|
183
|
+
const goal = this.getGoal(goalId);
|
|
184
|
+
if (goal === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
goal.progressSteps += delta;
|
|
187
|
+
goal.updatedAt = this.now();
|
|
188
|
+
this.save();
|
|
189
|
+
return goal;
|
|
190
|
+
}
|
|
191
|
+
/** 消耗预算(护栏②):超出上限 → 返回 false(调用方必须停)。 */
|
|
192
|
+
consumeBudget(goalId, amount = 1) {
|
|
193
|
+
const goal = this.getGoal(goalId);
|
|
194
|
+
if (goal === undefined)
|
|
195
|
+
return false;
|
|
196
|
+
if (goal.budget.usedActions + amount > goal.budget.maxActions)
|
|
197
|
+
return false;
|
|
198
|
+
goal.budget.usedActions += amount;
|
|
199
|
+
goal.updatedAt = this.now();
|
|
200
|
+
this.save();
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 设置目标状态。
|
|
205
|
+
*
|
|
206
|
+
* `needs-human` **必须**带原因(否则就是「静默结账」的变体 → 违反红线)。
|
|
207
|
+
*/
|
|
208
|
+
setGoalStatus(goalId, status, humanReason) {
|
|
209
|
+
const goal = this.getGoal(goalId);
|
|
210
|
+
if (goal === undefined)
|
|
211
|
+
return undefined;
|
|
212
|
+
if (status === "needs-human" && (humanReason === undefined || humanReason.trim() === "")) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
goal.status = status;
|
|
216
|
+
if (status === "needs-human")
|
|
217
|
+
goal.humanReason = humanReason;
|
|
218
|
+
goal.updatedAt = this.now();
|
|
219
|
+
this.save();
|
|
220
|
+
return goal;
|
|
221
|
+
}
|
|
222
|
+
// ---- 托付 ----
|
|
223
|
+
/** 登记一条托付("盯着 X")。 */
|
|
224
|
+
registerEntrustment(input) {
|
|
225
|
+
const at = this.now();
|
|
226
|
+
const e = {
|
|
227
|
+
id: input.id ?? randomUUID(),
|
|
228
|
+
target: input.target,
|
|
229
|
+
condition: input.condition,
|
|
230
|
+
status: "active",
|
|
231
|
+
...(input.goalId !== undefined ? { goalId: input.goalId } : {}),
|
|
232
|
+
...(input.needsHuman !== undefined ? { needsHuman: input.needsHuman } : {}),
|
|
233
|
+
...(input.humanReason !== undefined ? { humanReason: input.humanReason } : {}),
|
|
234
|
+
createdAt: at,
|
|
235
|
+
};
|
|
236
|
+
this.entrustments = [...this.entrustments.filter((x) => x.id !== e.id), e];
|
|
237
|
+
this.save();
|
|
238
|
+
return e;
|
|
239
|
+
}
|
|
240
|
+
/** 取托付(不存在 → undefined)。 */
|
|
241
|
+
getEntrustment(id) {
|
|
242
|
+
return this.entrustments.find((e) => e.id === id);
|
|
243
|
+
}
|
|
244
|
+
/** 全部托付。 */
|
|
245
|
+
listEntrustments() {
|
|
246
|
+
return [...this.entrustments];
|
|
247
|
+
}
|
|
248
|
+
/** 未闭合托付(`active` / `arrived` / `notified` —— 自主推进的发现来源之二)。 */
|
|
249
|
+
listUnclosedEntrustments() {
|
|
250
|
+
return this.entrustments.filter((e) => e.status === "active" || e.status === "arrived" || e.status === "notified");
|
|
251
|
+
}
|
|
252
|
+
/** 到达(第二段):条件已满足。 */
|
|
253
|
+
markArrived(id) {
|
|
254
|
+
const e = this.getEntrustment(id);
|
|
255
|
+
if (e === undefined || e.status !== "active")
|
|
256
|
+
return undefined;
|
|
257
|
+
e.status = "arrived";
|
|
258
|
+
e.arrivedAt = this.now();
|
|
259
|
+
this.save();
|
|
260
|
+
return e;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 知会(第三段)。
|
|
264
|
+
*
|
|
265
|
+
* **幂等**:已 `notified` 的托付再调用 → 返回 undefined(不重复知会、不重复落事件)。
|
|
266
|
+
* (双通道去重的账本侧:通知器与登记册任一方被重复调用都不会产生第二次知会。)
|
|
267
|
+
*/
|
|
268
|
+
markNotified(id) {
|
|
269
|
+
const e = this.getEntrustment(id);
|
|
270
|
+
if (e === undefined || e.status !== "arrived")
|
|
271
|
+
return undefined;
|
|
272
|
+
e.status = "notified";
|
|
273
|
+
e.notifiedAt = this.now();
|
|
274
|
+
this.save();
|
|
275
|
+
return e;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 结账(第四段)。
|
|
279
|
+
*
|
|
280
|
+
* **需要人决定的托付不得自动结账**(D3)→ `needsHuman` 的托付调用本方法 → 返回 undefined。
|
|
281
|
+
*/
|
|
282
|
+
settle(id) {
|
|
283
|
+
const e = this.getEntrustment(id);
|
|
284
|
+
if (e === undefined)
|
|
285
|
+
return undefined;
|
|
286
|
+
if (e.needsHuman === true)
|
|
287
|
+
return undefined;
|
|
288
|
+
if (e.status !== "notified" && e.status !== "arrived")
|
|
289
|
+
return undefined;
|
|
290
|
+
e.status = "settled";
|
|
291
|
+
e.settledAt = this.now();
|
|
292
|
+
this.save();
|
|
293
|
+
return e;
|
|
294
|
+
}
|
|
295
|
+
/** 撤销(任何时候可撤 —— 护栏④)。 */
|
|
296
|
+
revoke(id) {
|
|
297
|
+
const e = this.getEntrustment(id);
|
|
298
|
+
if (e === undefined || e.status === "settled" || e.status === "revoked")
|
|
299
|
+
return undefined;
|
|
300
|
+
e.status = "revoked";
|
|
301
|
+
e.revokedAt = this.now();
|
|
302
|
+
this.save();
|
|
303
|
+
return e;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 标记「需人决策」(human-gated)。
|
|
307
|
+
*
|
|
308
|
+
* 记录**原因**;托付**不**被结账(保持 `arrived`/`notified`)→ 可查、可继续。
|
|
309
|
+
*/
|
|
310
|
+
markNeedsHuman(id, reason) {
|
|
311
|
+
const e = this.getEntrustment(id);
|
|
312
|
+
if (e === undefined)
|
|
313
|
+
return undefined;
|
|
314
|
+
e.needsHuman = true;
|
|
315
|
+
e.humanReason = reason;
|
|
316
|
+
this.save();
|
|
317
|
+
return e;
|
|
318
|
+
}
|
|
319
|
+
/** 本 agent id(事件流用)。 */
|
|
320
|
+
get agentId() {
|
|
321
|
+
return this.ownerAgentId;
|
|
322
|
+
}
|
|
323
|
+
}
|