@lmzhen/dsh-evolution-core 0.16.1 → 0.17.0

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.
@@ -0,0 +1,147 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Session } from '@deepseek-ai/dsh-session';
3
+ import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection';
4
+ import { type EvidenceIndex } from './evidence.ts';
5
+ import { type TurnFoldState, type TurnSignals } from './signals.ts';
6
+ /** Time of the newest event folded for one session; 0 for a log with no event. */
7
+ export interface EvolutionActivityState {
8
+ /** Milliseconds of the newest event, or 0 when nothing was folded. */
9
+ readonly lastEventTime: number;
10
+ }
11
+ /**
12
+ * Which session frames carry content, in complement form.
13
+ *
14
+ * `sequenced` counts the frames that carry an integer seq, so 0 is the "this log cannot tell a
15
+ * boundary from a message" answer the evidence reader reports as `undefined` (a stub session, a
16
+ * log whose events carry no seq).
17
+ *
18
+ * `through` is the highest seq folded, so a citation BEYOND the log answers false exactly as the
19
+ * dense set does. Platform logs number their frames densely from 0 (a seq is a frame's index), so
20
+ * complement and enumeration agree; a sparse hand-built log is not a Session and reaches the unit
21
+ * through neither path.
22
+ */
23
+ export interface EvolutionEvidenceState {
24
+ /** Frames folded that carried an integer seq. */
25
+ readonly sequenced: number;
26
+ /** Highest seq folded; -1 before the first sequenced frame. */
27
+ readonly through: number;
28
+ /** Boundary seqs, ascending. Every other seq at or below `through` carries content. */
29
+ readonly bookkeeping: readonly number[];
30
+ }
31
+ declare module '@deepseek-ai/dsh-session-projection/types' {
32
+ interface SessionProjectionStateMap {
33
+ /** The curator's idleness gate input: the newest event time of one session. */
34
+ evolutionActivity: EvolutionActivityState;
35
+ /** The plan path's evidence-class input: the boundary seqs of one session. */
36
+ evolutionEvidence: EvolutionEvidenceState;
37
+ }
38
+ }
39
+ /** The `evolutionActivity` unit (host-only: no client view is registered for it). */
40
+ export declare const evolutionActivityProjection: ProjectionDefinition<'evolutionActivity'>;
41
+ /** The `evolutionEvidence` unit (host-only: no client view is registered for it). */
42
+ export declare const evolutionEvidenceProjection: ProjectionDefinition<'evolutionEvidence'>;
43
+ /**
44
+ * The skill names one session read through a non-failed dispatch, as a fold state.
45
+ *
46
+ * One entry per READ-CAPABLE dispatch (`isSkillReadToolName`), in first-seen order: the ledger
47
+ * key, the skill the arguments name (null when they name none), and whether a settle failed it.
48
+ * A pending dispatch counts as read — the platform settles every started sub-dispatch, so pending
49
+ * is a live-window state, not a failure (`skillReadNameOf` documents the same rule).
50
+ */
51
+ export interface EvolutionReadsState {
52
+ /** Read dispatches seen so far, in first-seen order. */
53
+ readonly reads: readonly {
54
+ readonly key: string;
55
+ readonly skill: string | null;
56
+ readonly failed: boolean;
57
+ }[];
58
+ }
59
+ /**
60
+ * The current turn's window signals plus the facts a turn boundary needs.
61
+ *
62
+ * `turn` is the SAME state `foldTurnEvent` folds for `observeEvent`; it resets at `turn/start`, so
63
+ * the state after the newest event is exactly the window `foldTurn(session, turnStartSeq)` reads.
64
+ * `through`/`lastEventTime` serve readers that only need progress (the curator's idleness gate has
65
+ * its own unit, but a consumer holding this one gets the same answers).
66
+ */
67
+ export interface EvolutionSignalsState {
68
+ /** Seq of the last folded event; -1 before the first. */
69
+ readonly through: number;
70
+ /** Time of the last folded event; 0 before the first. */
71
+ readonly lastEventTime: number;
72
+ /** Seq of the open turn's `turn/start`; -1 when none was seen. */
73
+ readonly turnStartSeq: number;
74
+ /** The open turn's window fold. */
75
+ readonly turn: TurnFoldState;
76
+ }
77
+ declare module '@deepseek-ai/dsh-session-projection/types' {
78
+ interface SessionProjectionStateMap {
79
+ /** The skill names one session read, for the read-before-write gate and the review plan path. */
80
+ evolutionReads: EvolutionReadsState;
81
+ /** The open turn's window signals, for the review cadence. */
82
+ evolutionSignals: EvolutionSignalsState;
83
+ }
84
+ }
85
+ /** The `evolutionReads` unit (host-only: no client view is registered for it). */
86
+ export declare const evolutionReadsProjection: ProjectionDefinition<'evolutionReads'>;
87
+ /** The `evolutionSignals` unit (host-only: no client view is registered for it). */
88
+ export declare const evolutionSignalsProjection: ProjectionDefinition<'evolutionSignals'>;
89
+ /**
90
+ * Register the family's units on the calling context's fiber.
91
+ *
92
+ * Called by the READERS (not by a plugin's mount) and idempotent by the registry's own reference
93
+ * count: the first read of a foldable session installs the definitions, later reads count as
94
+ * co-owners, and the keys survive until the last registrant unloads
95
+ * (`session-projection/src/index.ts:273-292`). Registering at the read is what keeps a session
96
+ * view the registry cannot fold (`isProjectableSession`) from ever reaching its drive — the
97
+ * registry folds the in-memory log on first touch, so a unit that appears mid-session is still
98
+ * correct (`:602-630`).
99
+ *
100
+ * Idempotent by design, so a plugin mounting later does not need its own call.
101
+ * @param ctx - the mounting plugin's context.
102
+ */
103
+ export declare function installEvolutionProjections(ctx: Context): void;
104
+ /**
105
+ * Time of one session's newest event, or `undefined` when the unit is not installed.
106
+ *
107
+ * The curator's idleness gate reads this instead of folding the session log; `undefined` is a
108
+ * measurement that did not happen, which the gate answers with the deployment's fail-open policy.
109
+ * @param ctx - a context of the runtime (the service is read from the global store).
110
+ * @param session - the session to ask about.
111
+ * @returns the newest event's milliseconds, or undefined with no unit or no event.
112
+ */
113
+ export declare function sessionLastEventTime(ctx: Context, session: Session): number | undefined;
114
+ /**
115
+ * The skill names one session read through a non-failed dispatch, or `undefined` when the session
116
+ * cannot be projected (a structural view, or a host without the registry).
117
+ *
118
+ * `undefined` is NOT an empty set: the tool path's read-before-write gate keeps its previous
119
+ * behavior rather than refusing every write in a composition whose session object is a bare stub
120
+ * (`skill-reads.ts` documents that three-state posture).
121
+ * @param ctx - a context of the runtime.
122
+ * @param session - the session whose reads are wanted.
123
+ * @returns the names read, or undefined when the log cannot be folded.
124
+ */
125
+ export declare function sessionReadNames(ctx: Context, session: Session): ReadonlySet<string> | undefined;
126
+ /**
127
+ * The open turn's window signals, or `undefined` when the session cannot be projected.
128
+ *
129
+ * The window is the events since the newest `turn/start`, folded by the same `foldTurnEvent`
130
+ * `observeEvent` uses; `undefined` means the caller has no measurement (a structural view), not
131
+ * that the turn was empty.
132
+ * @param ctx - a context of the runtime.
133
+ * @param session - the session whose open turn is being judged.
134
+ * @returns the window signals, or undefined when the log cannot be folded.
135
+ */
136
+ export declare function sessionTurnSignals(ctx: Context, session: Session): TurnSignals | undefined;
137
+ /**
138
+ * The evidence index of one session, or `undefined` when the log cannot be classified.
139
+ *
140
+ * `undefined` covers both the missing unit and a log whose frames carry no seq: the plan path's
141
+ * `EVIDENCE_CLASS` row is report-only and must stay silent rather than report every op.
142
+ * @param ctx - a context of the runtime.
143
+ * @param session - the session the review plan is authored against.
144
+ * @returns an index answering "does this seq carry content?".
145
+ */
146
+ export declare function sessionEvidenceIndex(ctx: Context, session: Session): EvidenceIndex | undefined;
147
+ //# sourceMappingURL=session-projection.d.ts.map
@@ -5,7 +5,7 @@
5
5
  * is spent. `turn/end` calls `observeEvent`; the returned review kind is
6
6
  * accumulated until a configured interval fires.
7
7
  */
8
- import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
8
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
9
9
  export type ReviewKind = 'memory' | 'skill' | 'combined';
10
10
  export interface SignalConfig {
11
11
  memoryInterval: number;
@@ -33,10 +33,51 @@ export interface ReviewState {
33
33
  turnsSinceSkill: number;
34
34
  lastTurn: number;
35
35
  }
36
- /** Fold one session event into the current turn observation. */
36
+ /**
37
+ * The turn-window fold state: what {@link TurnSignals} carries plus its dispatch ledger.
38
+ *
39
+ * `opened` holds one ledger key per dispatch this window already counted
40
+ * ({@link dispatchStepOf}). It lives in the state — not in a side table — because the state is
41
+ * what a live projection unit and a whole-log fold both carry between events.
42
+ */
43
+ export interface TurnFoldState extends TurnSignals {
44
+ /** Ledger keys this window already counted, in first-seen order. */
45
+ opened: string[];
46
+ }
47
+ /** The empty turn-window fold state. */
48
+ export declare function initialTurnFold(): TurnFoldState;
49
+ /**
50
+ * Fold ONE session event into a turn-window state, purely.
51
+ *
52
+ * The one fold behind all three readers: `observeEvent` (the mutating face the live `turn/end`
53
+ * path uses), `foldTurn` (a whole-log window fold, kept for structural test sessions) and the
54
+ * family's `evolutionSignals` projection unit. P1-1/N4 guards stay: events arrive from disk, so
55
+ * a persisted `data: null` / non-object or a malformed `content` skips instead of throwing.
56
+ * @param state - the window so far.
57
+ * @param event - the next committed session event.
58
+ * @returns the next state; the same reference when the event carries nothing this fold reads.
59
+ */
60
+ export declare function foldTurnEvent(state: TurnFoldState, event: SessionEvent): TurnFoldState;
61
+ /**
62
+ * Fold one session event into the current turn observation (the mutating face of
63
+ * {@link foldTurnEvent}: every reader shares the one fold, this one copies the result back onto
64
+ * the caller's signal object and keeps its ledger for the next event).
65
+ * @param signal - the turn observation to advance.
66
+ * @param event - the next committed session event.
67
+ */
37
68
  export declare function observeEvent(signal: TurnSignals, event: SessionEvent): void;
38
69
  /** Compute review cadence after `turn/end`. */
39
70
  export declare function advanceReview(state: ReviewState, turn: number, signal: TurnSignals, config: SignalConfig): ReviewKind | null;
40
- /** Fold all events between two sequence boundaries into one TurnSignals. */
41
- export declare function foldTurn(session: Session, fromSeq: number): TurnSignals;
71
+ /**
72
+ * Fold a log slice into one turn window, purely.
73
+ *
74
+ * The whole-log reference for {@link foldTurnEvent}: a caller that holds events (a fixture, a
75
+ * structural view, a replay) reads a window without touching a platform Session. Production reads
76
+ * the same window live from the family's `evolutionSignals` projection (`sessionTurnSignals`),
77
+ * which folds `foldTurnEvent` once per committed event.
78
+ * @param events - the log, oldest first. Pass an array, not a live iterator, when it can grow.
79
+ * @param fromSeq - index of the first event of the window (0 for the whole log).
80
+ * @returns the window's signals.
81
+ */
82
+ export declare function foldTurnEvents(events: Iterable<SessionEvent>, fromSeq?: number): TurnSignals;
42
83
  //# sourceMappingURL=signals.d.ts.map
@@ -1,3 +1,28 @@
1
+ /**
2
+ * The read-before-write RULE: which writes need a prior read, and which targets count as read.
3
+ *
4
+ * ONE rule for TWO enforcement points (design `dsh-evolution-write-gate-design.md` §4.2): the
5
+ * tool path refuses an unread mutating write (E-318), and the plan path drops such an op from a
6
+ * background review's plan. Before this module the rule lived in `evolution-review` only, so the
7
+ * tool path could not see it at all.
8
+ *
9
+ * The READER is not here either: `tool-dispatch.collectReadSkillNames(events)` folds a session log
10
+ * into the names read through a non-failed dispatch — the review's old session-shaped copy was a
11
+ * line-for-line duplicate of it. What this module adds over it is the session-shaped ADAPTER
12
+ * (`sessionReadSkillNames`), for the ONE caller that holds a session only STRUCTURALLY (the tool
13
+ * path's gate, whose exec view may be a bare stub): it answers "which accessor, and what does an
14
+ * unreadable session mean" — `undefined`, so the gate keeps its previous behavior instead of
15
+ * refusing every write. The review path reads a real platform `Session` and calls the reader
16
+ * directly; a fallback there would be dead code for an impossible case.
17
+ *
18
+ * v32 REV-06(a) is preserved by the dispatch normalizer: a skill counts as READ only when its read
19
+ * did not fail, so a failed/timeout read cannot pass the gate and let a writer blind-overwrite
20
+ * content the model never saw. Matching `tool/call` directly (the pre-v37 form) meant every PTC
21
+ * session (`tool/ptc-dispatch*`) collected an EMPTY set, so every mutating op the model HAD read
22
+ * was dropped — and nothing reported the loss.
23
+ * @module @lmzhen/dsh-evolution-core
24
+ */
25
+ import type { Context } from '@deepseek-ai/cordis';
1
26
  /** The session-log accessor the platform exposes (0.1.5 on; `session.events` before it). */
2
27
  export interface EvolutionSessionLogView {
3
28
  /** @returns the session's events, oldest first. */
@@ -9,13 +34,19 @@ export interface EvolutionSessionLogView {
9
34
  /**
10
35
  * The skill names ONE session read, or `undefined` when its log is not readable.
11
36
  *
37
+ * Two readers, in this order: the family's `evolutionReads` projection for a platform `Session`
38
+ * (0.2.x's live read — the deprecated synchronous log read is gone from production), then the
39
+ * STRUCTURAL accessor for a view that is not a `Session` at all. The tool path's exec view may be a
40
+ * bare stub, which is what this module exists for; a real Session always takes the first path.
41
+ *
12
42
  * `undefined` is NOT an empty set: a caller that must decide (the tool path's read-before-write
13
43
  * gate) can then keep its previous behavior instead of refusing every write in a composition whose
14
44
  * session objects expose no log at all.
45
+ * @param ctx - a context of the runtime.
15
46
  * @param session - the session whose reads are wanted; `undefined` when the call carries none.
16
47
  * @returns the names read through a non-failed `skill` dispatch, or `undefined` when unreadable.
17
48
  */
18
- export declare function sessionReadSkillNames(session: EvolutionSessionLogView | undefined): ReadonlySet<string> | undefined;
49
+ export declare function sessionReadSkillNames(ctx: Context, session: EvolutionSessionLogView | undefined): ReadonlySet<string> | undefined;
19
50
  /** Actions whose target must have been read first (create is authorship, so it is exempt). */
20
51
  export declare const READ_REQUIRED_ACTIONS: readonly string[];
21
52
  /**
@@ -82,6 +82,55 @@ export interface ToolDispatchSignal {
82
82
  export declare class ToolDispatchPayloadError extends TypeError {
83
83
  name: string;
84
84
  }
85
+ /**
86
+ * The ledger key of one dispatch: a scope-qualified call id.
87
+ *
88
+ * v43 audit (FLOW4-4): a normalizer shared by every session (skill-usage's live listener holds ONE
89
+ * process-wide instance) used the bare call id, so two sessions that produced the same id — PTC
90
+ * sub-call ids are short, and an id-less payload falls back to a type+payload key that is not
91
+ * unique by construction — collided: the second session's read was absorbed as "already seen" and
92
+ * never counted, and a settle in one session flipped the other's `ok`. Callers that span sessions
93
+ * pass the session id as `scope`.
94
+ * @param callId - the dispatch's call id (or its payload-derived fallback).
95
+ * @param scope - the owning session id, or '' for a fold that never leaves one session.
96
+ * @returns the key both the ledger and every projection fold dedupe on.
97
+ */
98
+ export declare function dispatchKeyOf(callId: string, scope?: string): string;
99
+ /**
100
+ * The pure half of the ledger: what one event says about one dispatch.
101
+ *
102
+ * The DECISION (which dispatch, and whether this event is the one that opens it) lives here and
103
+ * only here; carriers differ in what they keep — the class below keeps consumer-visible mutable
104
+ * signals, the family's `evolutionReads`/`evolutionSignals` projection units keep plain-JSON
105
+ * ledgers. `name` is empty on an event that only settles an already-opened dispatch (a native
106
+ * `tool/result`), and `outcome` is present exactly when the event carried one.
107
+ * @param event - one session event; any non-dispatch event answers `null`.
108
+ * @param scope - the owning session id, or '' for a single-session fold.
109
+ * @returns the step, or `null` when the event is not a dispatch.
110
+ */
111
+ export declare function dispatchStepOf(event: {
112
+ type: string;
113
+ data?: unknown;
114
+ }, scope?: string): DispatchStep | null;
115
+ /** One ledger step: what one event tells a fold about one dispatch. */
116
+ export interface DispatchStep {
117
+ /** The scoped ledger key ({@link dispatchKeyOf}). */
118
+ readonly key: string;
119
+ /** How the platform logged this dispatch (native call or PTC program). */
120
+ readonly kind: ToolDispatchKind;
121
+ /** The dispatch's call id, unscoped. */
122
+ readonly callId: string;
123
+ /** The program root this dispatch belongs to (its own id for a native call). */
124
+ readonly rootCallId: string;
125
+ /** The dispatched tool name; empty when the event only settles a dispatch. */
126
+ readonly name: string;
127
+ /** Raw arguments as logged (a JSON string natively, an object in PTC mode). */
128
+ readonly arguments: unknown;
129
+ /** Present exactly when the event carried an outcome. */
130
+ readonly outcome?: {
131
+ readonly ok: boolean;
132
+ };
133
+ }
85
134
  /**
86
135
  * The family's single dispatch ledger: absorbs platform events in log order,
87
136
  * emits one \`ToolDispatchSignal\` per dispatch, and folds later events of the
@@ -105,16 +154,6 @@ export declare class ToolDispatchNormalizer {
105
154
  constructor(options?: {
106
155
  maxTracked?: number;
107
156
  });
108
- /**
109
- * v43 audit (FLOW4-4): the ledger key. A normalizer shared by every session
110
- * (skill-usage's live listener holds ONE process-wide instance) used the bare
111
- * call id, so two sessions that produced the same id — PTC sub-call ids are
112
- * short, and an id-less payload falls back to a type+payload key that is not
113
- * unique by construction — collided: the second session's read was absorbed as
114
- * "already seen" and never counted, and a settle in one session flipped the
115
- * other's `ok`. Callers that span sessions pass the session id as `scope`.
116
- */
117
- private keyOf;
118
157
  /**
119
158
  * Absorb one session event.
120
159
  * @param event - the event to absorb; any non-dispatch event is ignored.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-core",
3
3
  "description": "Shared stores, prompts, signals and lifecycle logic for the dsh-evolution plugin family (community build)",
4
- "version": "0.16.1",
4
+ "version": "0.17.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -30,16 +30,19 @@
30
30
  ],
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "yaml": "^2.4.2"
33
+ "yaml": "^2.4.2",
34
+ "zod": "^4.4.3"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "@deepseek-ai/cordis": "^4.0.1",
37
- "@deepseek-ai/dsh-scope": "^0.1.5-rc.2",
38
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2"
38
+ "@deepseek-ai/dsh-scope": "^0.2.0-rc.1",
39
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
40
+ "@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1"
39
41
  },
40
42
  "devDependencies": {
41
43
  "@deepseek-ai/cordis": "^4.0.1",
42
- "@deepseek-ai/dsh-scope": "^0.1.5-rc.2",
43
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2"
44
+ "@deepseek-ai/dsh-scope": "^0.2.0-rc.1",
45
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
46
+ "@deepseek-ai/dsh-session-projection": "^0.2.0-rc.1"
44
47
  }
45
48
  }