@lmzhen/dsh-evolution-core 0.1.0-rc.71 → 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,8 +748,13 @@ 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;
735
760
  /** Active-log split point (rc.71): when the active log reaches this many events
@@ -743,6 +768,9 @@ const EVENT_LOG_RETAIN_ARCHIVES = 10;
743
768
  /** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
744
769
  * `events.json` and never matches this glob. */
745
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$/;
746
774
  function eventsFile(home) {
747
775
  return join(home, "evolution", "events.json");
748
776
  }
@@ -770,6 +798,17 @@ function parseEvolutionEvents(raw) {
770
798
  }
771
799
  }
772
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
+ /**
773
812
  * Append one event under the write lock (rc.68): `seq` = current max + 1
774
813
  * computed inside the transact, so two processes appending concurrently never
775
814
  * collide. A malformed log is refused (bytes preserved) and the append fails.
@@ -782,6 +821,11 @@ function parseEvolutionEvents(raw) {
782
821
  * archive write and active write leaves both copies, which the timeline merge
783
822
  * dedupes by seq. An archive-write failure aborts the append (active keeps the
784
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.
785
829
  */
786
830
  async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
787
831
  let assigned = 0;
@@ -792,7 +836,8 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
792
836
  return current;
793
837
  }
794
838
  const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
795
- const maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
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));
796
841
  const record = {
797
842
  ...event,
798
843
  seq: maxSeq + 1,
@@ -812,14 +857,16 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
812
857
  * `events-<lastArchivedSeq>.json` (await — a failed archive write aborts the
813
858
  * append so the active is never truncated without its copy), old archives are
814
859
  * pruned, and the newer half is returned as the next active body. No-op when
815
- * under the threshold.
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).
816
862
  */
817
863
  async function rotateIfDue(io, path, events, rotateAt) {
818
- if (events.length < rotateAt) return events;
864
+ if (rotateAt < 2 || events.length < rotateAt) return events;
819
865
  const mid = Math.ceil(events.length / 2);
820
866
  const head = events.slice(0, mid);
821
867
  const tail = events.slice(mid);
822
- const anchor = tail[0]?.seq ?? events[events.length - 1]?.seq ?? 0;
868
+ if (tail.length === 0) return events;
869
+ const anchor = tail[0]?.seq ?? 0;
823
870
  const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
824
871
  await io.writeText(archivePath, JSON.stringify({
825
872
  version: 1,
@@ -831,21 +878,28 @@ async function rotateIfDue(io, path, events, rotateAt) {
831
878
  /**
832
879
  * Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
833
880
  * The name's numeric part is the last archived seq, so ordering is NUMERIC —
834
- * lexicographic would rank `events-10` before `events-2`. Best-effort per
835
- * removal; exported for the retention test.
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.
836
884
  */
837
885
  async function retainEventArchives(io, path) {
838
886
  const dir = dirname(path);
839
- const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json"));
840
- const archiveSeq = (name) => Number.parseInt(name.slice(7, name.length - 5), 10) || 0;
841
- names.sort((a, b) => archiveSeq(a) - archiveSeq(b));
887
+ const names = await listEventArchives(io, path);
842
888
  const excess = names.slice(0, Math.max(0, names.length - 10));
843
889
  for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
844
890
  }
845
891
  /** Read the event log; a missing/whitespace-only file reads as empty,
846
892
  * corrupt content is flagged (and refused on append). */
847
893
  async function readEvolutionEvents(io, path) {
848
- 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
+ }
849
903
  if (raw === null || raw.trim() === "") return {
850
904
  events: [],
851
905
  malformed: false
@@ -871,14 +925,14 @@ async function readEvolutionEvents(io, path) {
871
925
  * Read the full timeline (rc.71): active log + all archives, merged by seq
872
926
  * (active copy wins, duplicates only arise from the rotation crash window),
873
927
  * sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
874
- * malformed ARCHIVE is skipped (never bricks the boot) and still flagged.
928
+ * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
929
+ * it is still flagged.
875
930
  */
876
931
  async function readEvolutionTimeline(io, path) {
877
932
  const dir = dirname(path);
878
- const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json")).sort();
879
933
  let malformed = false;
880
934
  const bySeq = /* @__PURE__ */ new Map();
881
- for (const name of names) {
935
+ for (const name of await listEventArchives(io, path)) {
882
936
  const read = await readEvolutionEvents(io, join(dir, name));
883
937
  if (read.malformed) malformed = true;
884
938
  for (const event of read.events) bySeq.set(event.seq, event);
@@ -3208,4 +3262,4 @@ function evolutionHome(env = process.env) {
3208
3262
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "evolution");
3209
3263
  }
3210
3264
  //#endregion
3211
- 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, 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, readEvolutionTimeline, recordMutation, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, 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,8 +6,13 @@
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;
@@ -48,6 +53,12 @@ export declare function eventsFile(home: string): string;
48
53
  * sidecar's per-field normalization on read).
49
54
  */
50
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[]>;
51
62
  /**
52
63
  * Append one event under the write lock (rc.68): `seq` = current max + 1
53
64
  * computed inside the transact, so two processes appending concurrently never
@@ -61,13 +72,19 @@ export declare function parseEvolutionEvents(raw: string | null): EvolutionEvent
61
72
  * archive write and active write leaves both copies, which the timeline merge
62
73
  * dedupes by seq. An archive-write failure aborts the append (active keeps the
63
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.
64
80
  */
65
81
  export declare function appendEvolutionEvent(io: EvolutionIoLike, path: string, event: Omit<EvolutionEvent, 'seq' | 'at'>, rotateAt?: number): Promise<number>;
66
82
  /**
67
83
  * Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
68
84
  * The name's numeric part is the last archived seq, so ordering is NUMERIC —
69
- * lexicographic would rank `events-10` before `events-2`. Best-effort per
70
- * removal; exported for the retention test.
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.
71
88
  */
72
89
  export declare function retainEventArchives(io: EvolutionIoLike, path: string): Promise<void>;
73
90
  export interface EventLogRead {
@@ -75,7 +92,9 @@ export interface EventLogRead {
75
92
  /** True when the body is not valid JSON (syntax-level damage): refused on
76
93
  * append, bytes untouched. Well-formed JSON with a damaged `events` field
77
94
  * is REPLACEABLE garbage — reads as empty and is rewritten at the next
78
- * 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). */
79
98
  malformed: boolean;
80
99
  }
81
100
  /** Read the event log; a missing/whitespace-only file reads as empty,
@@ -85,7 +104,8 @@ export declare function readEvolutionEvents(io: EvolutionIoLike, path: string):
85
104
  * Read the full timeline (rc.71): active log + all archives, merged by seq
86
105
  * (active copy wins, duplicates only arise from the rotation crash window),
87
106
  * sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
88
- * malformed ARCHIVE is skipped (never bricks the boot) and still flagged.
107
+ * malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
108
+ * it is still flagged.
89
109
  */
90
110
  export declare function readEvolutionTimeline(io: EvolutionIoLike, path: string): Promise<EventLogRead>;
91
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.71",
4
+ "version": "0.1.0-rc.72",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },