返回源码地图

packages/fs/fs-observation-policy/src/index.ts

main snapshot · da00f7f5358f · 正文引用章节 10 / 10;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/**
2 * Event-only filesystem observation policy; it registers no service. A weak owner/target map
3 * records every authoritative presence/absence observation, single-slot intent listeners derive
4 * guards from that state, and the provider performs the atomic freshness/no-clobber check. Without
5 * this plugin, tools retain the bare provider's unconditional mutation behavior. See the package
6 * README for composition rules.
7 * @module @deepseek-ai/dsh-fs-observation-policy
8 */
9
10import type { Context } from '@deepseek-ai/cordis'
11import { FsError } from '@deepseek-ai/dsh-fs'
12import type { FsObservation, FsTarget, FsVersion, FsWriteIntent } from '@deepseek-ai/dsh-fs'
13import type { FsObservationActor } from './types.ts'
14
15export type { FsObservationActor } from './types.ts'
16
17/**
18 * Per-context observed-file state and the three `fs/*` decisions over it. One
19 * instance is created per `apply()` so disposal can drop all state for HMR.
20 */
21class ObservedStateGate {
22 /**
23 * Observed-file state, keyed first by the owner object (weakly held, so a
24 * collected session frees its state), then by {@link FsTarget.targetKey}. An
25 * entry's presence is the prior-observation record; its discriminant keeps
26 * confirmed absence distinct from an unseen target.
27 */
28 private observed = new WeakMap<object, Map<string, FsObservation>>()
29
30 /**
31 * Derive the observed-state owner from the opaque event actor — normally the
32 * active agent session. `undefined` when no owner can be derived (e.g. a
33 * direct tool call with no agent); such calls read freely but cannot satisfy
34 * the write/edit prior-observation policy.
35 */
36 private owner(actor: object | undefined): object | undefined {
37 // tsgolint treats object as assignable to weak FsObservationActor, while tsc still requires the structural cast for property access.
38 // See the analyzer-divergence consequence in .agents/notes/archived/process/2026-07-29-oxlint-linter.md.
39 // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- The analyzers disagree on this weak type.
40 return (actor as FsObservationActor | undefined)?.agent?.session
41 }
42
43 private get(owner: object, targetKey: string): FsObservation | undefined {
44 return this.observed.get(owner)?.get(targetKey)
45 }
46
47 private set(owner: object, targetKey: string, observation: FsObservation): void {
48 let byTarget = this.observed.get(owner)
49 if (!byTarget) {
50 byTarget = new Map()
51 this.observed.set(owner, byTarget)
52 }
53 byTarget.set(targetKey, observation)
54 }
55
56 /** Drop all recorded state (HMR safety / disposal). */
57 clear(): void {
58 this.observed = new WeakMap()
59 }
60
61 /**
62 * Decide the write intent: unseen or confirmed absent ⇒ `createIfAbsent`;
63 * confirmed present ⇒ `replaceIfVersion` at the observed version.
64 */
65 writeIntent(target: FsTarget, actor: object | undefined): FsWriteIntent {
66 const owner = this.owner(actor)
67 const prior = owner ? this.get(owner, target.targetKey) : undefined
68 return prior?.kind === 'present'
69 ? { kind: 'replaceIfVersion', version: prior.version }
70 : { kind: 'createIfAbsent' }
71 }
72
73 /**
74 * Decide the edit version guard: unseen rejects with `FS_NOT_OBSERVED`,
75 * confirmed absence rejects with `FS_NOT_FOUND`, and presence supplies the
76 * observed version as the CAS basis.
77 */
78 editIntent(target: FsTarget, actor: object | undefined): { version: FsVersion } {
79 const owner = this.owner(actor)
80 const prior = owner ? this.get(owner, target.targetKey) : undefined
81 if (!owner || prior === undefined) {
82 throw new FsError(`edit requires reading "${target.displayPath}" first`, 'FS_NOT_OBSERVED')
83 }
84 if (prior.kind === 'absent') {
85 throw new FsError(`cannot edit "${target.displayPath}": not found`, 'FS_NOT_FOUND')
86 }
87 return { version: prior.version }
88 }
89
90 /** Record an authoritative present or absent observation for this owner and target. */
91 observe(target: FsTarget, observation: FsObservation, actor: object | undefined): void {
92 const owner = this.owner(actor)
93 if (owner) this.set(owner, target.targetKey, observation)
94 }
95}
96
97/** Cordis plugin name used by loader diagnostics. */
98export const name = 'fs-observation-policy'
99
100/**
101 * Register the three `fs/*` listeners. No `inject` — this plugin reads no
102 * services; it operates only on its own `WeakMap`. The waterfalls are unbound
103 * (the tool dispatches them with no `this`), so the listeners take the raw
104 * `(target, actor, next)` arguments.
105 */
106export function apply(ctx: Context): void {
107 const gate = new ObservedStateGate()
108
109 ctx.effect(() => () => {
110 // Drop all recorded state on disposal so a reloaded plugin starts clean
111 // (HMR safety). The WeakMap itself would be GC'd, but replacing it makes the
112 // release observable and immediate for tests.
113 gate.clear()
114 }, 'fs-observation-policy observed-state teardown')
115
116 // fs/write-intent: occupy the single decision slot — do NOT call next().
117 // Deferred through Promise.resolve().then so the declared Promise return type
118 // holds (a throw rejects, never escapes synchronously through the waterfall).
119 ctx.on('fs/write-intent', (target, actor) => Promise.resolve().then(() => gate.writeIntent(target, actor)))
120
121 // fs/edit-intent: occupy the single decision slot — do not call next().
122 ctx.on('fs/edit-intent', (target, actor) => Promise.resolve().then(() => gate.editIntent(target, actor)))
123
124 // fs/observed must remain synchronous and non-throwing: emit does not await
125 // promises, and successful mutations have already committed. WeakMap.set
126 // satisfies that contract for both presence and absence.
127 ctx.on('fs/observed', (target, observation, actor) => {
128 gate.observe(target, observation, actor)
129 })
130}