@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 +148 -16
- package/lib/types/evolution-events.d.ts +57 -4
- package/lib/types/usage.d.ts +14 -2
- package/package.json +1 -1
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)
|
|
314
|
-
|
|
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
|
-
*
|
|
732
|
-
*
|
|
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
|
|
836
|
+
return current;
|
|
774
837
|
}
|
|
775
|
-
const
|
|
776
|
-
|
|
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
|
|
847
|
+
return JSON.stringify({
|
|
784
848
|
version: 1,
|
|
785
|
-
events: [...
|
|
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
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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'
|
|
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
|
package/lib/types/usage.d.ts
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.1.0-rc.72",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|