@lmzhen/dsh-evolution-core 0.4.0 → 0.5.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,90 @@
1
+ /**
2
+ * Reference re-homing plans (0.5.0 V1, design §16.7).
3
+ *
4
+ * A consolidation moves one skill's body under ANOTHER skill root, where every
5
+ * support-dir reference resolves against that other root (design §2.1). This
6
+ * module plans what has to happen to the support files and to the body's
7
+ * references for the move to leave no dangling link. Pure: the caller owns IO,
8
+ * decides the destination names, and decides whether the plan is applied (V2)
9
+ * or only reported (V1). Cross-skill rewriting is out of scope by design (§14) —
10
+ * a plan covers ONE body.
11
+ */
12
+ /** One re-homed support file: source-relative path -> target-relative path. */
13
+ export interface ReferenceMove {
14
+ /** Path inside the skill the body comes FROM. */
15
+ from: string;
16
+ /** Path inside the skill the body moves TO. */
17
+ to: string;
18
+ }
19
+ /** One reference the plan rewrites, in body line order. */
20
+ export interface ReferenceRewriteEdit {
21
+ /** 1-based line in the body being moved. */
22
+ line: number;
23
+ /** The resolved target before the move. */
24
+ from: string;
25
+ /** The resolved target after the move. */
26
+ to: string;
27
+ /** The verbatim token as it appears in the body (`raw`), so a rewrite replaces
28
+ * exactly what is there and leaves a trailing `#fragment` in place. */
29
+ raw: string;
30
+ }
31
+ /** What one consolidation plan does to one body's references. */
32
+ export interface ReferenceRewritePlan {
33
+ /** References whose target changes (a re-homed file). */
34
+ edits: readonly ReferenceRewriteEdit[];
35
+ /** Cited files the moves give no destination: applying this plan would dangle. */
36
+ unresolved: readonly string[];
37
+ /** Targets still absent from the target file list after the moves. */
38
+ residualDangling: readonly string[];
39
+ /** The moves the body actually NEEDS: a support file the body never cites stays
40
+ * with the archived package instead of being copied into the target for
41
+ * nothing (upstream's wording is "every needed support file"). */
42
+ moves: readonly ReferenceMove[];
43
+ }
44
+ /**
45
+ * Provisional re-homing rule. The naming style for re-homed files is still an
46
+ * open decision (design §16.3 B5); this rule only fires on a COLLISION, where
47
+ * something has to give: a path that the target already occupies gets the source
48
+ * name as a prefix (`references/a.md` -> `references/<source>-a.md`). Nested
49
+ * destinations are deliberately avoided: the support listing is one level deep,
50
+ * so a file moved into a subdirectory would drop out of the listing entirely.
51
+ * @param sourceFiles - the moving skill's support files.
52
+ * @param targetFiles - the destination skill's support files.
53
+ * @param sourceName - the moving skill's name, used as the collision prefix.
54
+ * @returns the moves plus the paths that collided (for the report).
55
+ */
56
+ export declare function planRehoming(sourceFiles: readonly string[], targetFiles: readonly string[], sourceName: string): {
57
+ moves: ReferenceMove[];
58
+ collisions: string[];
59
+ };
60
+ /**
61
+ * Plan the reference rewrites for one body that is moving into another root.
62
+ * @param input - the body, the MOVING skill's file list, the re-homing moves and
63
+ * the destination's file list (with the moved paths already included).
64
+ * @returns the edits, the cited files without a destination, and any target that
65
+ * the destination file list cannot prove (applying such a plan would dangle).
66
+ */
67
+ export declare function planReferenceRewrite(input: {
68
+ content: string;
69
+ files: readonly string[];
70
+ moves: readonly ReferenceMove[];
71
+ targetFiles: readonly string[];
72
+ }): ReferenceRewritePlan;
73
+ /**
74
+ * Apply a plan's edits to the body (V2). Line-wise and literal: line N gets its
75
+ * FIRST occurrence of the recorded token replaced, everything else is copied
76
+ * byte for byte — no regex, so a token that happens to contain regex
77
+ * metacharacters cannot rewrite more than it matched.
78
+ * @param content - the body the plan was computed against.
79
+ * @param edits - the plan's edits.
80
+ * @returns the rewritten body (identical to the input when there is nothing to do).
81
+ */
82
+ export declare function applyReferenceRewrite(content: string, edits: readonly ReferenceRewriteEdit[]): string;
83
+ /**
84
+ * One-line, model-facing summary of a plan, for the consolidation refusal and
85
+ * for the V2 apply path's audit text.
86
+ * @param plan - the plan to describe.
87
+ * @returns a compact sentence naming counts, the moves and any residual risk.
88
+ */
89
+ export declare function describeReferenceRewrite(plan: ReferenceRewritePlan): string;
90
+ //# sourceMappingURL=reference-rewrite.d.ts.map
@@ -67,6 +67,8 @@ interface SkillHealthSnapshot {
67
67
  }
68
68
  interface SkillHealthDim {
69
69
  bodyChars: number;
70
+ /** The same body on the token scale (limit basis) — the judged quantity. */
71
+ bodyTokens: number;
70
72
  stampDensityPerKb: number | null;
71
73
  supportGroups: number;
72
74
  /** Usage churn facts; null when the caller supplied no counts. */
@@ -368,11 +368,27 @@ export declare class SkillLibrary {
368
368
  * promises the present branch is complete (011 §7 enrichment, probe reads).
369
369
  */
370
370
  listSupportFiles(rawName: string): Promise<Probe<string[]>>;
371
+ /**
372
+ * V4 (design §16.6): exact character counts of the support files that can
373
+ * POSSIBLY exceed the content cap. The pre-filter is BYTE size, and that is
374
+ * sound rather than approximate: a UTF-16 code unit never costs less than one
375
+ * UTF-8 byte, so a file whose bytes are within the cap provably cannot exceed
376
+ * the cap in characters. The returned map is therefore COMPLETE for the
377
+ * oversize question without reading the small files.
378
+ * @param rawName - the skill's name.
379
+ * @returns path -> character count (possibly empty), or null when the listing or
380
+ * a size probe cannot answer — unknown is never an empty map.
381
+ */
382
+ supportFileChars(rawName: string): Promise<Record<string, number> | null>;
371
383
  /**
372
384
  * Structure-health facts for one skill (rc.73 A1, 008 design): body
373
385
  * chars/density from SKILL.md, support groups from countSupportDirs, plus
374
386
  * optional usage counts (A2 churn dimension) when the caller has them.
375
- * Derived, never persisted; null when the skill is unreadable.
387
+ * Derived, never persisted. CONTRACT: `null` whenever the skill cannot be
388
+ * read — a missing file AND any read failure (EACCES/EIO/…) both answer
389
+ * null, so a whole health view degrades one ROW instead of throwing out of
390
+ * its per-skill loop (A6, audit P2-10: the former code absorbed only
391
+ * missing/EISDIR and let a transient win32 hold kill the entire view).
376
392
  */
377
393
  assessHealth(rawName: string, thresholds?: SkillHealthThresholds, counts?: {
378
394
  patchCount?: number;
@@ -436,9 +452,19 @@ export declare class SkillLibrary {
436
452
  * stolen by the sweep. A dead-pid residue would otherwise permanently
437
453
  * refuse archive/restore. */
438
454
  private deleteStrandedLocks;
455
+ /** R2 follow-up (audit of A2): the recycled-pid window from io's
456
+ * `ALIVE_LOCK_TAKEOVER_MS`, applied to the stranded-lock sweepers. A lock
457
+ * whose holder pid probes alive but whose mtime is older than the alive
458
+ * window is a recycled pid, not a live writer (no write in this family
459
+ * holds a lock for more than minutes) — without this, one recycled pid
460
+ * blocked whole-tree snapshot recovery forever with a "retry once the
461
+ * write completes" message that could never become true. A backend without
462
+ * `mtime` keeps the conservative refuse-on-alive posture. */
463
+ private lockHolderAgedOut;
439
464
  /** Remove `lockPath` only when its body has the writer-lock `pid:token`
440
- * shape AND the holder pid is not alive; anything else (a user support file
441
- * or a live writer's lock) is left untouched. */
465
+ * shape AND the holder pid is not alive (or is an aged-out recycled pid —
466
+ * R2 follow-up); anything else (a user support file or a live writer's
467
+ * lock) is left untouched. */
442
468
  private sweepLockIfStranded;
443
469
  /** A1-4 (v18): a manifest-declared name is copied with `join(root, name)`;
444
470
  * only a single, non-traversing path component is safe. Dotfiles
@@ -451,6 +477,17 @@ export declare class SkillLibrary {
451
477
  * recovery. A non-lock body shape is left alone (user file). */
452
478
  private refuseLiveLockOrSweep;
453
479
  archive(rawName: string, options?: ArchiveOptions): Promise<SkillActionResult>;
480
+ /**
481
+ * V2 (design §16.7): what an apply-mode consolidation would do for ONE source,
482
+ * computed BEFORE any side effect. `blocked` means behaviour is exactly the
483
+ * plan-mode refusal (with the reason named); `apply` carries the rewritten body
484
+ * and the support files that must be copied into the target.
485
+ * @param source - the moving skill's name.
486
+ * @param targetName - the destination skill's name.
487
+ * @param body - the moving body (frontmatter stripped).
488
+ * @returns the decision plus the note to append to a refusal.
489
+ */
490
+ private planSourceRehoming;
454
491
  /**
455
492
  * Merge the bodies of `sources` into `target` and archive the sources with
456
493
  * an absorbed-into marker. Hermes-style consolidation: overlapping skills
@@ -468,6 +505,14 @@ export declare class SkillLibrary {
468
505
  * `target/references/<source>.md` and archives the source — the demote path
469
506
  * (009-II). A source body with support-directory links is refused there too
470
507
  * (the references file would carry links whose files were archived).
508
+ *
509
+ * V2 (design §16.7) lifts that refusal when `referenceRewrite:'apply'` and the
510
+ * plan proves the move is safe: the support files the body NEEDS are copied into
511
+ * the target under their own paths (renamed only on a collision) and the body's
512
+ * references are rewritten to match, in the same commit. Anything the plan
513
+ * cannot place — a cited file the source no longer has, an unreadable listing —
514
+ * still refuses, and the refusal names it. The archived source keeps whatever
515
+ * the body did not need.
471
516
  */
472
517
  consolidate(target: string, sources: string[], origin?: WriteOrigin, options?: {
473
518
  mode?: 'append' | 'reference';
@@ -514,6 +559,19 @@ export declare class SkillLibrary {
514
559
  * Backends without the mtime probe skip pruning (no false deletes on
515
560
  * unknown age).
516
561
  */
562
+ /**
563
+ * 0.5.0 V1 (design §16.6-④): the retention window's READ half. Names every
564
+ * archived entry past the window without touching it — the report path the
565
+ * default policy uses, and the input the curator's run report carries.
566
+ * @returns the expired entry names ([] when the backend has no mtime probe,
567
+ * because an unknown age must never read as expired).
568
+ */
569
+ expiredArchives(): Promise<string[] | null>;
570
+ /** V4 (design §16.6): support-file char policy, resolved at the call site. */
571
+ supportFileCharPolicy(): 'report' | 'enforce';
572
+ /** Retention policy resolved at the call site (absent limits object = report).
573
+ * Public so a run report can say WHICH policy produced its numbers. */
574
+ archiveRetentionPolicy(): 'report' | 'prune';
517
575
  private pruneExpiredArchives;
518
576
  /**
519
577
  * Snapshot the recoverable skills state: active tree, usage/suppression
@@ -601,8 +659,8 @@ export interface NewSkillLibraryOptions {
601
659
  threatExemptLabels?: readonly string[] | undefined;
602
660
  }
603
661
  export declare function newSkillLibrary(options: NewSkillLibraryOptions): SkillLibrary;
604
- export { DEFAULT_SKILL_LIMITS } from './limits.ts';
605
- export type { SkillLimits } from './limits.ts';
662
+ export { DEFAULT_SKILL_LIMITS, DEFAULT_CITATION_POLICY, DEFAULT_REFERENCE_REWRITE_POLICY, DEFAULT_ARCHIVE_RETENTION_POLICY, DEFAULT_SUPPORT_FILE_CHAR_POLICY, POLICY_STAGE_DEFAULTS, policyStageLimits, } from './limits.ts';
663
+ export type { SkillLimits, CitationPolicy, ReferenceRewritePolicy, ArchiveRetentionPolicy, SupportFileCharPolicy, PolicyStageFields, } from './limits.ts';
606
664
  export { authoringFeedback, frontmatterBlock, frontmatterCatalogInvalid, normalizeFrontmatter, parseFrontmatter, relatedSkillNames, validateFrontmatter, yamlPlainScalarNeedsQuotes } from './frontmatter.ts';
607
665
  export type { AuthoringFeedback, Frontmatter, FrontmatterNormalizeResult, FrontmatterRead, PlatformStringSplit } from './frontmatter.ts';
608
666
  //# sourceMappingURL=skill-store.d.ts.map
@@ -186,6 +186,29 @@ export declare function isSkillReadToolName(name: string): boolean;
186
186
  * counts as a read: the platform settles every started sub-dispatch, so pending
187
187
  * is a live-window state, not a failure.
188
188
  */
189
+ /** Tool names whose dispatch reads one FILE — the deployment's file tool
190
+ * (default `read`). A differently named tool is configured, not guessed. */
191
+ export declare const DEFAULT_SUPPORT_READ_TOOL_NAMES: readonly string[];
192
+ /** One support-file read attributed to a skill (design §5.5). */
193
+ export interface SupportReadHit {
194
+ skill: string;
195
+ /** Skill-root-relative path, e.g. `references/design-x.md`. */
196
+ rel: string;
197
+ }
198
+ /**
199
+ * Read one dispatch as a SUPPORT-FILE read attributed to a skill: a read of
200
+ * `<root>/<skill>/<support dir>/…` is demand evidence for that one file, the
201
+ * way `skillReadNameOf` is demand evidence for a whole body.
202
+ * @param signal - a settled dispatch.
203
+ * @param options - the deployment's file-tool names and its skills root; an
204
+ * empty root falls back to the `/skills/` marker in the path.
205
+ * @returns the skill name and the skill-root-relative path, or `null` when the
206
+ * dispatch is not a support-file read. Pure: no IO, no service lookup.
207
+ */
208
+ export declare function supportFileReadOf(signal: ToolDispatchSignal, options: {
209
+ toolNames: readonly string[];
210
+ root: string;
211
+ }): SupportReadHit | null;
189
212
  export declare function skillReadNameOf(signal: ToolDispatchSignal): string | undefined;
190
213
  /**
191
214
  * Fold one session log into its deduplicated dispatches.
@@ -28,6 +28,11 @@ export interface UsageRecord {
28
28
  * relevant again. */
29
29
  feedback_score?: number | undefined;
30
30
  feedback_warn?: boolean | undefined;
31
+ /** Demand evidence per SUPPORT FILE (design §5.5): how many observed reads
32
+ * landed on each `references/…`-style path of this skill. Absent on every
33
+ * record written before this field existed, and on skills whose support
34
+ * files were never read — absence means "no evidence", never "zero demand". */
35
+ support_reads?: Record<string, number> | undefined;
31
36
  }
32
37
  export type UsageMap = Map<string, UsageRecord>;
33
38
  export declare function usageFile(root: string): string;
@@ -44,6 +49,8 @@ export declare function emptyRecord(): UsageRecord;
44
49
  * Pure — exported for unit tests; `loadUsage` is the production caller.
45
50
  */
46
51
  export declare function normalizeUsageRecord(record: unknown): UsageRecord;
52
+ /** Sidecar bound: a pathological path set must not grow the usage file forever. */
53
+ export declare const MAX_SUPPORT_READ_PATHS = 200;
47
54
  export declare function loadUsage(root: string, io?: EvolutionIoLike): Promise<UsageMap>;
48
55
  /**
49
56
  * Atomic read-modify-write on the usage sidecar (rc.50 P2-2): `task` receives
@@ -100,8 +107,19 @@ export declare function getRecord(map: UsageMap, name: string): UsageRecord;
100
107
  export declare function bumpView(map: UsageMap, name: string, when?: Date): void;
101
108
  export declare function bumpUse(map: UsageMap, name: string, when?: Date): void;
102
109
  export declare function bumpPatch(map: UsageMap, name: string, when?: Date): void;
110
+ /** Count one observed support-file read (design §5.5). The map is created on
111
+ * first evidence only — a skill with no support reads keeps the field absent. */
112
+ export declare function bumpSupportRead(map: UsageMap, name: string, rel: string, when?: Date): void;
103
113
  export declare function markAgentCreated(map: UsageMap, name: string): void;
104
114
  export declare function latestActivityAt(record: UsageRecord): string | null;
115
+ /** Days between an ISO timestamp (falling back to `created`) and `now`. An
116
+ * unparseable date counts as 0 days: NaN silently froze every age comparison
117
+ * (A2-9). */
118
+ export declare function daysSinceIso(iso: string | null, created: string, nowMs: number): number;
119
+ /** Idle days of one skill since its lifecycle age anchor (`last activity ??
120
+ * created_at`) — the SAME anchor the curator's transitions use, so the
121
+ * maintenance view and the lifecycle can never disagree about staleness. */
122
+ export declare function idleDays(record: UsageRecord, now?: Date): number;
105
123
  /**
106
124
  * Whether the library has ANY observed read evidence (C observation window):
107
125
  * reads were invisible to the usage sidecar before A2, so `view_count` zero
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.4.0",
4
+ "version": "0.5.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },