@lmzhen/dsh-evolution-core 0.1.0-rc.70 → 0.1.0-rc.72

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/lib/index.js CHANGED
@@ -294,8 +294,21 @@ async function mutateUsage(root, io, task) {
294
294
  * folds copy this set only.
295
295
  */
296
296
  function applyCuratorFields(disk, curated) {
297
+ applyCuratorMetaFields(disk, curated);
298
+ applyCuratorLifecycleFields(disk, curated);
299
+ }
300
+ /** Copy only the lifecycle pair (state/archived_at) — see the ownership split
301
+ * rationale on {@link applyCuratorMetaFields}. */
302
+ function applyCuratorLifecycleFields(disk, curated) {
297
303
  disk.state = curated.state;
298
304
  disk.archived_at = curated.archived_at;
305
+ }
306
+ /**
307
+ * Copy the recomputed meta pair (quality_score/quality_warn + the
308
+ * marker-mirrored pin flag) — refreshed tree-wide each run by design, so a
309
+ * concurrent curator run's lifecycle changes are never reverted by them.
310
+ */
311
+ function applyCuratorMetaFields(disk, curated) {
299
312
  disk.quality_score = curated.quality_score;
300
313
  disk.quality_warn = curated.quality_warn;
301
314
  disk.pinned = curated.pinned;
@@ -305,13 +318,20 @@ function applyCuratorFields(disk, curated) {
305
318
  * each curated record is projected onto its disk peer by copying only the
306
319
  * curator-owned fields, so a concurrent tool-side bump between snapshot and
307
320
  * save survives. Records absent from the snapshot are left untouched; a
308
- * curated record with no disk peer is seeded from the snapshot.
321
+ * curated record with no disk peer is seeded from the snapshot. `stateOwned`
322
+ * (rc.72 H-1) restricts the lifecycle pair to the names this run ACTUALLY
323
+ * transitioned — a concurrent curator run's archive/restore is never reverted
324
+ * by a stale snapshot; without it both pairs apply everywhere.
309
325
  */
310
- function foldCuratorFields(disk, curated) {
326
+ function foldCuratorFields(disk, curated, stateOwned) {
311
327
  for (const [name, record] of curated) {
312
328
  const diskRecord = disk.get(name);
313
- if (diskRecord) applyCuratorFields(diskRecord, record);
314
- else disk.set(name, { ...record });
329
+ if (!diskRecord) {
330
+ disk.set(name, { ...record });
331
+ continue;
332
+ }
333
+ applyCuratorMetaFields(diskRecord, record);
334
+ if (stateOwned === void 0 || stateOwned.has(name)) applyCuratorLifecycleFields(diskRecord, record);
315
335
  }
316
336
  }
317
337
  async function saveUsage(root, map, io = nodeEvolutionIo()) {
@@ -728,10 +748,29 @@ function computeLifecycleTransitions(usage, config, now = /* @__PURE__ */ new Da
728
748
  * learn on target X" is answerable. The aggregate `feedback.json` is a
729
749
  * rebuildable boot cache, never the truth.
730
750
  *
731
- * The malformed-refusal posture matches the whole sidecar family (rc.65): an
732
- * append NEVER rewrites a corrupt log — the bytes stay untouched.
751
+ * Rotation (rc.71, 007 design): when the active log reaches
752
+ * `EVENT_LOG_ROTATE_AT` the older half is split into an archive
753
+ * (`events-<lastArchivedSeq>.json`); the boot timeline merges active +
754
+ * archives and dedupes by seq (active copy wins), so the rotation crash window
755
+ * yields the identical timeline. Archival naming is STRICTLY numeric
756
+ * (`/^events-\d+\.json$/`) — user files under the same directory are never
757
+ * read as archives and never pruned (rc.72 G-2).
733
758
  */
734
759
  const EVENT_LOG_VERSION = 1;
760
+ /** Active-log split point (rc.71): when the active log reaches this many events
761
+ * the older half is rotated into an archive; the active stays bounded so a
762
+ * single append stays O(active) instead of O(total-history). Tunable default —
763
+ * callers may override per append (the tests use small values). */
764
+ const EVENT_LOG_ROTATE_AT = 4e3;
765
+ /** Number of archives retained (rc.71): older archives are pruned at rotation,
766
+ * mirroring retainReports. The horizon covers the loop-analysis window. */
767
+ const EVENT_LOG_RETAIN_ARCHIVES = 10;
768
+ /** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
769
+ * `events.json` and never matches this glob. */
770
+ const EVENT_ARCHIVE_PREFIX = "events-";
771
+ /** Archive naming is strictly numeric: a user file such as `events-backup.json`
772
+ * under the same directory is neither read into the timeline nor pruned. */
773
+ const EVENT_ARCHIVE_RE = /^events-(\d+)\.json$/;
735
774
  function eventsFile(home) {
736
775
  return join(home, "evolution", "events.json");
737
776
  }
@@ -759,39 +798,108 @@ function parseEvolutionEvents(raw) {
759
798
  }
760
799
  }
761
800
  /**
801
+ * List the numeric archives under the log's directory, sorted ascending by
802
+ * their last-archived seq. Single glob predicate for the timeline, the
803
+ * retention pass and the feedback migration check (rc.72 H-3).
804
+ */
805
+ async function listEventArchives(io, path) {
806
+ const dir = dirname(path);
807
+ return (await io.list(dir)).filter((name) => EVENT_ARCHIVE_RE.test(name)).sort((a, b) => {
808
+ return Number.parseInt(a.slice(7, a.length - 5), 10) - Number.parseInt(b.slice(7, b.length - 5), 10);
809
+ });
810
+ }
811
+ /**
762
812
  * Append one event under the write lock (rc.68): `seq` = current max + 1
763
813
  * computed inside the transact, so two processes appending concurrently never
764
814
  * collide. A malformed log is refused (bytes preserved) and the append fails.
765
815
  * Returns the assigned seq.
816
+ *
817
+ * Rotation (rc.71, 007 design): when the active log reaches `rotateAt`, the
818
+ * older half is copied into an archive inside the SAME transact (the archive
819
+ * path has its own lock, so no recursion) and the active is replaced with the
820
+ * newer half + the new event. seqs stay globally monotonic; a crash between
821
+ * archive write and active write leaves both copies, which the timeline merge
822
+ * dedupes by seq. An archive-write failure aborts the append (active keeps the
823
+ * full old content — no loss) and the caller's best-effort handling applies.
824
+ *
825
+ * rc.72 G-1: when the ACTIVE is missing/whitespace but archives exist (a
826
+ * deleted active, or B-2 self-heal), seq derivation consults the archive names
827
+ * — the active restarts AFTER the highest archived seq, never at 1, so a new
828
+ * event can never shadow an archived one in the seq-deduped timeline.
766
829
  */
767
- async function appendEvolutionEvent(io, path, event) {
830
+ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
768
831
  let assigned = 0;
769
- await transactIo(io, path, (current) => {
832
+ await transactIo(io, path, async (current) => {
770
833
  if (current !== null && current.trim() !== "") try {
771
834
  JSON.parse(current);
772
835
  } catch {
773
- return Promise.resolve(current);
836
+ return current;
774
837
  }
775
- const events = parseEvolutionEvents(current);
776
- const maxSeq = events.reduce((max, entry) => Math.max(max, entry.seq), 0);
838
+ const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
839
+ let maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
840
+ if (maxSeq === 0) for (const name of await listEventArchives(io, path)) maxSeq = Math.max(maxSeq, Number.parseInt(name.slice(7, name.length - 5), 10));
777
841
  const record = {
778
842
  ...event,
779
843
  seq: maxSeq + 1,
780
844
  at: (/* @__PURE__ */ new Date()).toISOString()
781
845
  };
782
846
  assigned = record.seq;
783
- return Promise.resolve(JSON.stringify({
847
+ return JSON.stringify({
784
848
  version: 1,
785
- events: [...events, record]
786
- }, null, 2));
849
+ events: [...nextEvents, record]
850
+ }, null, 2);
787
851
  });
788
852
  if (assigned === 0) throw new Error(`evolution event log is malformed and was not touched: ${path}`);
789
853
  return assigned;
790
854
  }
855
+ /**
856
+ * Split the active log at its midpoint when due: the older half is written to
857
+ * `events-<lastArchivedSeq>.json` (await — a failed archive write aborts the
858
+ * append so the active is never truncated without its copy), old archives are
859
+ * pruned, and the newer half is returned as the next active body. No-op when
860
+ * under the threshold; `rotateAt < 2` is a guarded no-op (rc.72 G-1: a
861
+ * one-event rotate would archive everything and restart seqs at 1).
862
+ */
863
+ async function rotateIfDue(io, path, events, rotateAt) {
864
+ if (rotateAt < 2 || events.length < rotateAt) return events;
865
+ const mid = Math.ceil(events.length / 2);
866
+ const head = events.slice(0, mid);
867
+ const tail = events.slice(mid);
868
+ if (tail.length === 0) return events;
869
+ const anchor = tail[0]?.seq ?? 0;
870
+ const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
871
+ await io.writeText(archivePath, JSON.stringify({
872
+ version: 1,
873
+ events: head
874
+ }, null, 2));
875
+ await retainEventArchives(io, path);
876
+ return tail;
877
+ }
878
+ /**
879
+ * Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
880
+ * The name's numeric part is the last archived seq, so ordering is NUMERIC —
881
+ * lexicographic would rank `events-10` before `events-2`. Only strictly
882
+ * numeric names participate (rc.72 G-2: user files are never deleted).
883
+ * Best-effort per removal; exported for the retention test.
884
+ */
885
+ async function retainEventArchives(io, path) {
886
+ const dir = dirname(path);
887
+ const names = await listEventArchives(io, path);
888
+ const excess = names.slice(0, Math.max(0, names.length - 10));
889
+ for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
890
+ }
791
891
  /** Read the event log; a missing/whitespace-only file reads as empty,
792
892
  * corrupt content is flagged (and refused on append). */
793
893
  async function readEvolutionEvents(io, path) {
794
- const raw = await io.readText(path);
894
+ let raw;
895
+ try {
896
+ raw = await io.readText(path);
897
+ } catch {
898
+ return {
899
+ events: [],
900
+ malformed: true
901
+ };
902
+ }
795
903
  if (raw === null || raw.trim() === "") return {
796
904
  events: [],
797
905
  malformed: false
@@ -813,6 +921,30 @@ async function readEvolutionEvents(io, path) {
813
921
  };
814
922
  }
815
923
  }
924
+ /**
925
+ * Read the full timeline (rc.71): active log + all archives, merged by seq
926
+ * (active copy wins, duplicates only arise from the rotation crash window),
927
+ * sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
928
+ * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
929
+ * it is still flagged.
930
+ */
931
+ async function readEvolutionTimeline(io, path) {
932
+ const dir = dirname(path);
933
+ let malformed = false;
934
+ const bySeq = /* @__PURE__ */ new Map();
935
+ for (const name of await listEventArchives(io, path)) {
936
+ const read = await readEvolutionEvents(io, join(dir, name));
937
+ if (read.malformed) malformed = true;
938
+ for (const event of read.events) bySeq.set(event.seq, event);
939
+ }
940
+ const active = await readEvolutionEvents(io, path);
941
+ if (active.malformed) malformed = true;
942
+ for (const event of active.events) bySeq.set(event.seq, event);
943
+ return {
944
+ events: [...bySeq.values()].sort((a, b) => a.seq - b.seq),
945
+ malformed
946
+ };
947
+ }
816
948
  //#endregion
817
949
  //#region lib/types/prompts.js
818
950
  /**
@@ -3130,4 +3262,4 @@ function evolutionHome(env = process.env) {
3130
3262
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "evolution");
3131
3263
  }
3132
3264
  //#endregion
3133
- export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_LOG_VERSION, EvolutionGateSet, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, foldCuratorFields, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutateUsage, mutationsFile, nodeEvolutionIo, normalizeUsageRecord, observeEvent, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, recordMutation, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, validateFrontmatter, verifyPromptBundle };
3265
+ export { AUTHORING_DESCRIPTION_BAR, COMBINED_REVIEW_PLAN_PROMPT, COMBINED_REVIEW_PROMPT, COMPLETION_SKILL_REVIEW_PROMPT, CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MIN_IDLE_HOURS, DEFAULT_MUTATION_CAP, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_STALE_AFTER_DAYS, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, LOW_QUALITY_THRESHOLD, MAX_DESCRIPTION_LENGTH, MAX_SKILL_CONTENT_CHARS, MAX_SKILL_FILE_BYTES, MAX_SKILL_NAME_LENGTH, MEMORY_REVIEW_PROMPT, MUTATIONS_FILE_VERSION, MemoryStore, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, PROMPT_BUNDLE_VERSION, PROTECTED_BUILTIN_SKILLS, QUALITY_WEIGHTS, SKILLS_GUIDANCE, SKILL_NAME_RE, SKILL_REVIEW_PLAN_PROMPT, SKILL_REVIEW_PROMPT, SNAPSHOT_EXTRA_NAME_RE, SUPPORT_DIRS, SUPPRESSED_FILE_VERSION, SkillLibrary, advanceReview, appendEvolutionEvent, applyCuratorFields, applyCuratorLifecycleFields, applyCuratorMetaFields, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, foldCuratorFields, foldTurn, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, mutateUsage, mutationsFile, nodeEvolutionIo, normalizeUsageRecord, observeEvent, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, validateFrontmatter, verifyPromptBundle };
@@ -6,11 +6,27 @@
6
6
  * learn on target X" is answerable. The aggregate `feedback.json` is a
7
7
  * rebuildable boot cache, never the truth.
8
8
  *
9
- * The malformed-refusal posture matches the whole sidecar family (rc.65): an
10
- * append NEVER rewrites a corrupt log — the bytes stay untouched.
9
+ * Rotation (rc.71, 007 design): when the active log reaches
10
+ * `EVENT_LOG_ROTATE_AT` the older half is split into an archive
11
+ * (`events-<lastArchivedSeq>.json`); the boot timeline merges active +
12
+ * archives and dedupes by seq (active copy wins), so the rotation crash window
13
+ * yields the identical timeline. Archival naming is STRICTLY numeric
14
+ * (`/^events-\d+\.json$/`) — user files under the same directory are never
15
+ * read as archives and never pruned (rc.72 G-2).
11
16
  */
12
17
  import { type EvolutionIoLike } from './io.ts';
13
18
  export declare const EVENT_LOG_VERSION = 1;
19
+ /** Active-log split point (rc.71): when the active log reaches this many events
20
+ * the older half is rotated into an archive; the active stays bounded so a
21
+ * single append stays O(active) instead of O(total-history). Tunable default —
22
+ * callers may override per append (the tests use small values). */
23
+ export declare const EVENT_LOG_ROTATE_AT = 4000;
24
+ /** Number of archives retained (rc.71): older archives are pruned at rotation,
25
+ * mirroring retainReports. The horizon covers the loop-analysis window. */
26
+ export declare const EVENT_LOG_RETAIN_ARCHIVES = 10;
27
+ /** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
28
+ * `events.json` and never matches this glob. */
29
+ export declare const EVENT_ARCHIVE_PREFIX = "events-";
14
30
  export interface EvolutionEvent {
15
31
  /** Global monotonic order key, assigned inside the append transact. */
16
32
  seq: number;
@@ -37,22 +53,59 @@ export declare function eventsFile(home: string): string;
37
53
  * sidecar's per-field normalization on read).
38
54
  */
39
55
  export declare function parseEvolutionEvents(raw: string | null): EvolutionEvent[];
56
+ /**
57
+ * List the numeric archives under the log's directory, sorted ascending by
58
+ * their last-archived seq. Single glob predicate for the timeline, the
59
+ * retention pass and the feedback migration check (rc.72 H-3).
60
+ */
61
+ export declare function listEventArchives(io: EvolutionIoLike, path: string): Promise<string[]>;
40
62
  /**
41
63
  * Append one event under the write lock (rc.68): `seq` = current max + 1
42
64
  * computed inside the transact, so two processes appending concurrently never
43
65
  * collide. A malformed log is refused (bytes preserved) and the append fails.
44
66
  * Returns the assigned seq.
67
+ *
68
+ * Rotation (rc.71, 007 design): when the active log reaches `rotateAt`, the
69
+ * older half is copied into an archive inside the SAME transact (the archive
70
+ * path has its own lock, so no recursion) and the active is replaced with the
71
+ * newer half + the new event. seqs stay globally monotonic; a crash between
72
+ * archive write and active write leaves both copies, which the timeline merge
73
+ * dedupes by seq. An archive-write failure aborts the append (active keeps the
74
+ * full old content — no loss) and the caller's best-effort handling applies.
75
+ *
76
+ * rc.72 G-1: when the ACTIVE is missing/whitespace but archives exist (a
77
+ * deleted active, or B-2 self-heal), seq derivation consults the archive names
78
+ * — the active restarts AFTER the highest archived seq, never at 1, so a new
79
+ * event can never shadow an archived one in the seq-deduped timeline.
45
80
  */
46
- export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>): Promise<number>;
81
+ export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>, rotateAt?: number): Promise<number>;
82
+ /**
83
+ * Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
84
+ * The name's numeric part is the last archived seq, so ordering is NUMERIC —
85
+ * lexicographic would rank `events-10` before `events-2`. Only strictly
86
+ * numeric names participate (rc.72 G-2: user files are never deleted).
87
+ * Best-effort per removal; exported for the retention test.
88
+ */
89
+ export declare function retainEventArchives(io: EvolutionIoLike, path: string): Promise<void>;
47
90
  export interface EventLogRead {
48
91
  events: EvolutionEvent[];
49
92
  /** True when the body is not valid JSON (syntax-level damage): refused on
50
93
  * append, bytes untouched. Well-formed JSON with a damaged `events` field
51
94
  * is REPLACEABLE garbage — reads as empty and is rewritten at the next
52
- * append (rc.70 F-1: read and append agree on the same boundary). */
95
+ * append (rc.70 F-1: read and append agree on the same boundary). A READ
96
+ * error (EISDIR etc.) also flags malformed — the file is unusable either
97
+ * way and is never overwritten (the append read would fail identically). */
53
98
  malformed: boolean;
54
99
  }
55
100
  /** Read the event log; a missing/whitespace-only file reads as empty,
56
101
  * corrupt content is flagged (and refused on append). */
57
102
  export declare function readEvolutionEvents(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
103
+ /**
104
+ * Read the full timeline (rc.71): active log + all archives, merged by seq
105
+ * (active copy wins, duplicates only arise from the rotation crash window),
106
+ * sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
107
+ * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
108
+ * it is still flagged.
109
+ */
110
+ export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
58
111
  //# sourceMappingURL=evolution-events.d.ts.map
@@ -53,14 +53,26 @@ export declare function mutateUsage(root: string, io: EvolutionIoLike, task: (ma
53
53
  * folds copy this set only.
54
54
  */
55
55
  export declare function applyCuratorFields(disk: UsageRecord, curated: UsageRecord): void;
56
+ /** Copy only the lifecycle pair (state/archived_at) — see the ownership split
57
+ * rationale on {@link applyCuratorMetaFields}. */
58
+ export declare function applyCuratorLifecycleFields(disk: UsageRecord, curated: UsageRecord): void;
59
+ /**
60
+ * Copy the recomputed meta pair (quality_score/quality_warn + the
61
+ * marker-mirrored pin flag) — refreshed tree-wide each run by design, so a
62
+ * concurrent curator run's lifecycle changes are never reverted by them.
63
+ */
64
+ export declare function applyCuratorMetaFields(disk: UsageRecord, curated: UsageRecord): void;
56
65
  /**
57
66
  * Fold a curator run-start snapshot onto the current on-disk map (rc.67 K-2):
58
67
  * each curated record is projected onto its disk peer by copying only the
59
68
  * curator-owned fields, so a concurrent tool-side bump between snapshot and
60
69
  * save survives. Records absent from the snapshot are left untouched; a
61
- * curated record with no disk peer is seeded from the snapshot.
70
+ * curated record with no disk peer is seeded from the snapshot. `stateOwned`
71
+ * (rc.72 H-1) restricts the lifecycle pair to the names this run ACTUALLY
72
+ * transitioned — a concurrent curator run's archive/restore is never reverted
73
+ * by a stale snapshot; without it both pairs apply everywhere.
62
74
  */
63
- export declare function foldCuratorFields(disk: UsageMap, curated: UsageMap): void;
75
+ export declare function foldCuratorFields(disk: UsageMap, curated: UsageMap, stateOwned?: ReadonlySet<string>): void;
64
76
  export declare function saveUsage(root: string, map: UsageMap, io?: EvolutionIoLike): Promise<void>;
65
77
  export declare function getRecord(map: UsageMap, name: string): UsageRecord;
66
78
  export declare function bumpView(map: UsageMap, name: string, when?: Date): void;
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.1.0-rc.70",
4
+ "version": "0.1.0-rc.72",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },