@lmzhen/dsh-evolution-core 0.30.36 → 0.30.38

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.
Files changed (36) hide show
  1. package/lib/index.js +3776 -2300
  2. package/lib/types/evolution-events.d.ts +6 -2
  3. package/lib/types/evolution-events.js +6 -0
  4. package/lib/types/extract-plan.js +17 -11
  5. package/lib/types/extract-prompt.d.ts +1 -5
  6. package/lib/types/extract-prompt.js +2 -0
  7. package/lib/types/extract-request.d.ts +32 -0
  8. package/lib/types/extract-request.js +155 -0
  9. package/lib/types/index.d.ts +4 -0
  10. package/lib/types/index.js +4 -0
  11. package/lib/types/mutation-storage-adapter.d.ts +14 -0
  12. package/lib/types/mutation-storage-adapter.js +107 -0
  13. package/lib/types/mutation-unit.d.ts +164 -0
  14. package/lib/types/mutation-unit.js +509 -0
  15. package/lib/types/prompts.d.ts +3 -3
  16. package/lib/types/prompts.js +8 -8
  17. package/lib/types/proposal-view.d.ts +4 -0
  18. package/lib/types/proposal-view.js +3 -1
  19. package/lib/types/skill-health.d.ts +5 -0
  20. package/lib/types/skill-health.js +3 -0
  21. package/lib/types/skill-history.d.ts +2 -0
  22. package/lib/types/skill-history.js +3 -0
  23. package/lib/types/skill-lint.d.ts +2 -0
  24. package/lib/types/skill-lint.js +13 -2
  25. package/lib/types/skill-model.d.ts +45 -0
  26. package/lib/types/skill-model.js +74 -0
  27. package/lib/types/skill-store.d.ts +22 -1
  28. package/lib/types/skill-store.js +415 -274
  29. package/lib/types/unit-members.js +3 -3
  30. package/lib/types/unit-storage.d.ts +10 -0
  31. package/lib/types/unit-transaction.d.ts +1 -1
  32. package/lib/types/unit-undo-plan.js +5 -3
  33. package/lib/types/unit-undo.d.ts +1 -1
  34. package/lib/types/unit-undo.js +18 -3
  35. package/package.json +1 -1
  36. package/persisted-write-inventory.json +3 -3
@@ -23,6 +23,8 @@ export interface SkillHealthThresholds {
23
23
  /** Patch count at/above this with zero reads -> 'warn' (write-ghost: the
24
24
  * skill is churned but nothing ever loads it). */
25
25
  churnMinPatches: number;
26
+ /** Hard-cap hits at/above this with a large support closure -> scope-overloaded advisory. */
27
+ hardCapMinHits: number;
26
28
  }
27
29
  export declare const DEFAULT_HEALTH_THRESHOLDS: SkillHealthThresholds;
28
30
  /**
@@ -67,6 +69,8 @@ interface SkillHealthSnapshot {
67
69
  patchCount?: number | undefined;
68
70
  /** Usage-side view count, when the caller has it (A2 churn dimension). */
69
71
  readCount?: number | undefined;
72
+ /** Mechanical hard-cap-hit count, when the caller has it; null = unknown. */
73
+ hardCapHits?: number | null | undefined;
70
74
  }
71
75
  interface SkillHealthDim {
72
76
  bodyChars: number;
@@ -81,6 +85,7 @@ interface SkillHealthDim {
81
85
  /** Usage churn facts; null when the caller supplied no counts. */
82
86
  churnPatches: number | null;
83
87
  churnReads: number | null;
88
+ hardCapHits: number | null;
84
89
  }
85
90
  export interface SkillHealthAssessment {
86
91
  verdict: SkillHealthVerdict;
@@ -23,6 +23,7 @@ export const DEFAULT_HEALTH_THRESHOLDS = {
23
23
  softBodyChars: AUTHORING_SPLIT_LINE_CHARS,
24
24
  stampDensityPerKb: 2,
25
25
  churnMinPatches: 20,
26
+ hardCapMinHits: 3,
26
27
  };
27
28
  /**
28
29
  * Stamp regex shared by health assessment and the maintenance probe (single
@@ -72,6 +73,7 @@ export function assessStructureHealth(snapshot, thresholds = DEFAULT_HEALTH_THRE
72
73
  supportFiles: snapshot.supportFiles ?? null,
73
74
  churnPatches: null,
74
75
  churnReads: null,
76
+ hardCapHits: snapshot.hardCapHits ?? null,
75
77
  };
76
78
  // V6: the JUDGMENT runs on the token scale. The configured number stays the
77
79
  // borrowed UPSTREAM character line, converted per body composition, so the
@@ -115,6 +117,7 @@ export function assessStructureHealth(snapshot, thresholds = DEFAULT_HEALTH_THRE
115
117
  overTwiceSoftLine: overTwice,
116
118
  stampLimitPerKb: thresholds.stampDensityPerKb,
117
119
  churnMinPatches: thresholds.churnMinPatches,
120
+ hardCapMinHits: thresholds.hardCapMinHits,
118
121
  // This engine's snapshot always carries a MEASURED body (its type has no null arm yet); the
119
122
  // unreadable case becomes expressible when the caller's type does, and the rule already waits.
120
123
  readable: true,
@@ -73,6 +73,8 @@ export interface SkillVersion {
73
73
  * can interleave (the index is appended after the content lock is released); this link is computed
74
74
  * from the bytes a write lock actually read, so it is the authoritative predecessor. */
75
75
  beforeHash?: string;
76
+ /** True on the synthetic entry that records a REMOVAL; its hash is the removed bytes. */
77
+ removed?: boolean;
76
78
  /** One line saying what this write changed, when the deployment turned the summarizer on
77
79
  * (registry `skillVersionSummary`). Absent is a first-class state: the faces say so instead of
78
80
  * inventing a sentence, and a version recorded before the feature existed simply has none. */
@@ -213,6 +213,7 @@ function readVersionEntry(entry) {
213
213
  const summary = typeof candidate.summary === 'string' && candidate.summary !== '' ? candidate.summary : undefined;
214
214
  const path = typeof candidate.path === 'string' && candidate.path !== '' ? candidate.path : undefined;
215
215
  const unitId = typeof candidate.unitId === 'string' && candidate.unitId !== '' ? candidate.unitId : undefined;
216
+ const removed = candidate.removed === true ? true : undefined;
216
217
  // The fold must carry unitHead or the next write drops it for good (the G3a lesson).
217
218
  const unitHead = candidate.unitHead;
218
219
  const base = {
@@ -224,6 +225,7 @@ function readVersionEntry(entry) {
224
225
  ...summary === undefined ? {} : { summary },
225
226
  ...path === undefined ? {} : { path },
226
227
  ...unitId === undefined ? {} : { unitId },
228
+ ...removed === undefined ? {} : { removed },
227
229
  ...unitHead === undefined ? {} : { unitHead },
228
230
  };
229
231
  return typeof candidate.beforeHash === 'string' && candidate.beforeHash !== ''
@@ -353,6 +355,7 @@ export function nextHistoryIndex(versions, input, keep) {
353
355
  action: input.action,
354
356
  hash: removalHash,
355
357
  chars: input.before?.length ?? 0,
358
+ removed: true,
356
359
  ...pathField,
357
360
  ...input.unitId === undefined ? {} : { unitId: input.unitId },
358
361
  // F-G3b-1: a removal-only landing IS the last entry of its unit, so it carries the head;
@@ -21,6 +21,7 @@ export interface SkillDims {
21
21
  stampHits: number | null;
22
22
  churnPatches: number | null;
23
23
  churnReads: number | null;
24
+ hardCapHits: number | null;
24
25
  }
25
26
  /** The judgement itself, computed by `skill-health.ts` on its own scales; this module never
26
27
  * recomputes it from raw numbers (that is exactly how a second set of thresholds appears). */
@@ -33,6 +34,7 @@ export interface LintFacts {
33
34
  stampLimitPerKb: number;
34
35
  /** The engine's own churn threshold — the rule must use the SAME number, not a literal. */
35
36
  churnMinPatches: number;
37
+ hardCapMinHits: number;
36
38
  /** True when the caller could read the body at all. */
37
39
  readable: boolean;
38
40
  }
@@ -25,14 +25,14 @@ export const RULES = [
25
25
  id: 'body-oversize-2x',
26
26
  severity: 'error',
27
27
  run: facts => facts.overTwiceSoftLine
28
- ? { severity: 'error', rule: 'body-oversize-2x', message: msg([facts.skillName, ': body is at or past twice the soft line (', facts.softLineChars, ' chars)']), count: 1 }
28
+ ? { severity: 'error', rule: 'body-oversize-2x', message: msg([facts.skillName, ': body is at or past twice the soft line (', facts.softLineChars, ' chars); keeping the current skill is valid']), count: 1 }
29
29
  : null,
30
30
  },
31
31
  {
32
32
  id: 'body-oversize',
33
33
  severity: 'warn',
34
34
  run: facts => !facts.overTwiceSoftLine && facts.overSoftLine
35
- ? { severity: 'warn', rule: 'body-oversize', message: msg([facts.skillName, ': body is at or above the soft line (', facts.softLineChars, ' chars)']), count: 1 }
35
+ ? { severity: 'warn', rule: 'body-oversize', message: msg([facts.skillName, ': body is at or above the soft line (', facts.softLineChars, ' chars); keep is valid — size alone does not require a split']), count: 1 }
36
36
  : null,
37
37
  },
38
38
  {
@@ -63,6 +63,17 @@ export const RULES = [
63
63
  return { severity: 'warn', rule: 'references-sprawl', message: msg([facts.skillName, ': ', files, ' support files']), count: files };
64
64
  },
65
65
  },
66
+ {
67
+ id: 'scope-overloaded',
68
+ severity: 'warn',
69
+ run: (facts) => {
70
+ const hits = facts.dims.hardCapHits;
71
+ const files = facts.dims.supportFiles;
72
+ if (hits === null || files === null || hits < facts.hardCapMinHits || files < 3)
73
+ return null;
74
+ return { severity: 'warn', rule: 'scope-overloaded', message: msg([facts.skillName, ': ', hits, ' hard-cap hit(s) with ', files, ' support files — remaining growth may be scope']), count: hits };
75
+ },
76
+ },
66
77
  {
67
78
  id: 'write-ghost',
68
79
  severity: 'warn',
@@ -0,0 +1,45 @@
1
+ import type { SkillSummary } from './skill-store.ts';
2
+ export interface SkillListQuery {
3
+ withContent?: boolean;
4
+ withFrontmatter?: boolean;
5
+ withSupport?: boolean;
6
+ }
7
+ export interface SkillSupportFile {
8
+ path: string;
9
+ bytes: number;
10
+ sha256: string;
11
+ }
12
+ export interface SkillDocumentHashes {
13
+ threeField: string;
14
+ content: string;
15
+ support: string;
16
+ }
17
+ export interface SkillDocument {
18
+ summary: SkillSummary;
19
+ content: string;
20
+ frontmatter: Record<string, unknown>;
21
+ support: SkillSupportFile[];
22
+ hashes: SkillDocumentHashes;
23
+ readable: boolean;
24
+ }
25
+ export interface SkillModelProvider {
26
+ list(query?: SkillListQuery): Promise<SkillSummary[]>;
27
+ read(name: string): Promise<string | null>;
28
+ listSupportFiles(name: string): Promise<readonly string[]>;
29
+ readSupportFile(name: string, path: string): Promise<string | null>;
30
+ }
31
+ /**
32
+ * Canonical read face for skill consumers.
33
+ *
34
+ * This is an adapter, not a second store: the provider owns IO and summary
35
+ * extraction. Every consumer reads the same SkillDocument contract so owner,
36
+ * health, panel and future domain/search surfaces cannot drift on parsing,
37
+ * whenToUse or support hashing.
38
+ */
39
+ export declare class SkillModel {
40
+ private readonly provider;
41
+ constructor(provider: SkillModelProvider);
42
+ list(query?: SkillListQuery): Promise<SkillSummary[]>;
43
+ get(name: string): Promise<SkillDocument | null>;
44
+ }
45
+ //# sourceMappingURL=skill-model.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { contentHash } from "./mutations.js";
2
+ function canonical(value) {
3
+ if (value === null || typeof value !== 'object')
4
+ return JSON.stringify(value);
5
+ if (Array.isArray(value))
6
+ return '[' + value.map(canonical).join(',') + ']';
7
+ const record = value;
8
+ return '{' + Object.keys(record).sort().map(key => JSON.stringify(key) + ':' + canonical(record[key])).join(',') + '}';
9
+ }
10
+ function threeFieldHash(summary) {
11
+ return contentHash(canonical([
12
+ summary.name,
13
+ summary.description,
14
+ summary.whenToUse ?? '',
15
+ ]));
16
+ }
17
+ function supportHash(files) {
18
+ return contentHash(canonical(files.map(file => [file.path, file.sha256, file.bytes])));
19
+ }
20
+ /**
21
+ * Canonical read face for skill consumers.
22
+ *
23
+ * This is an adapter, not a second store: the provider owns IO and summary
24
+ * extraction. Every consumer reads the same SkillDocument contract so owner,
25
+ * health, panel and future domain/search surfaces cannot drift on parsing,
26
+ * whenToUse or support hashing.
27
+ */
28
+ export class SkillModel {
29
+ provider;
30
+ constructor(provider) {
31
+ this.provider = provider;
32
+ }
33
+ async list(query = {}) {
34
+ return await this.provider.list(query);
35
+ }
36
+ async get(name) {
37
+ const summaries = await this.provider.list({ withFrontmatter: true });
38
+ const summary = summaries.find(candidate => candidate.name === name);
39
+ if (summary === undefined)
40
+ return null;
41
+ let content = null;
42
+ let readable = true;
43
+ try {
44
+ content = await this.provider.read(name);
45
+ }
46
+ catch {
47
+ readable = false;
48
+ }
49
+ if (content === null)
50
+ readable = false;
51
+ const paths = readable ? await this.provider.listSupportFiles(name) : [];
52
+ const support = [];
53
+ for (const path of paths) {
54
+ const file = await this.provider.readSupportFile(name, path);
55
+ if (file === null)
56
+ continue;
57
+ support.push({ path, bytes: Buffer.byteLength(file, 'utf8'), sha256: contentHash(file) });
58
+ }
59
+ support.sort((a, b) => a.path.localeCompare(b.path));
60
+ return {
61
+ summary,
62
+ content: content ?? '',
63
+ frontmatter: summary.frontmatter ?? {},
64
+ support,
65
+ hashes: {
66
+ threeField: threeFieldHash(summary),
67
+ content: contentHash(content ?? ''),
68
+ support: supportHash(support),
69
+ },
70
+ readable,
71
+ };
72
+ }
73
+ }
74
+ //# sourceMappingURL=skill-model.js.map
@@ -36,6 +36,8 @@ import { type WriteAuthority } from './write-authority.ts';
36
36
  import { transactIo, type EvolutionIoLike } from './io.ts';
37
37
  import { type Probe } from './probe.ts';
38
38
  import type { SkillLimits } from './limits.ts';
39
+ import { type MutationResult } from './mutation-unit.ts';
40
+ import { SkillModel } from './skill-model.ts';
39
41
  import { type ExtractPlanDraft } from './extract-plan.ts';
40
42
  import { type MutationRecord } from './mutations.ts';
41
43
  import { type RevisionAnchor, type RevisionFile } from './skill-revision.ts';
@@ -326,6 +328,8 @@ export declare function validateRestructureTarget(filePath: string): string | nu
326
328
  export declare function validateSupportPath(filePath: string): string | null;
327
329
  export declare class SkillLibrary {
328
330
  readonly root: string;
331
+ /** The ONE read face owner/health/panel consume; a thin adapter, not a store. */
332
+ readonly model: SkillModel;
329
333
  readonly limits: SkillLimits;
330
334
  private readonly io;
331
335
  private readonly onMutation;
@@ -349,6 +353,20 @@ export declare class SkillLibrary {
349
353
  * but it is a deployment condition, not a per-write event, so it warns once per instance. */
350
354
  private historyWarned;
351
355
  constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits, onMutation?: (event: EvolutionSkillMutatedEvent) => void, transact?: typeof transactIo, threatExemptLabels?: readonly string[], authority?: WriteAuthority);
356
+ /**
357
+ * Transitional root lock for mutators that still own their write path. The
358
+ * extract/restructure path takes the same lock through MutationUnit, so those
359
+ * two paths cannot interleave with create/update/patch across processes.
360
+ */
361
+ private unitHeadForOps;
362
+ private withMutationLock;
363
+ private recovery;
364
+ /** Run the boot recovery once per library instance, before the first write. */
365
+ private ensureRecovered;
366
+ /** Public boot hook: idempotent with the first-write recovery. */
367
+ recoverOnBoot(): Promise<void>;
368
+ /** Recover every prepared tree-change snapshot in the shared unit store. */
369
+ recoverPreparedUnits(): Promise<MutationResult[]>;
352
370
  /**
353
371
  * Run one single-file read-modify-write for a mutator. When `transact` was
354
372
  * injected the read and the write run inside it (cross-process atomicity);
@@ -358,7 +376,6 @@ export declare class SkillLibrary {
358
376
  * mutation event are issued ONLY when a write actually lands, so a no-op
359
377
  * never inflates the mutation-maturity counter.
360
378
  */
361
- private runSingleWrite;
362
379
  /**
363
380
  * v28 G1.1 (EVO-IO-02): shared compensating cleanup for a locked write that
364
381
  * found no SKILL.md — remove the possibly-resurrected directory ONLY when it
@@ -519,7 +536,10 @@ export declare class SkillLibrary {
519
536
  assessHealth(rawName: string, thresholds?: SkillHealthThresholds, counts?: {
520
537
  patchCount?: number;
521
538
  readCount?: number;
539
+ hardCapHits?: number | null;
522
540
  }): Promise<SkillHealthAssessment | null>;
541
+ /** S2: mechanical hard-cap event; failure must not change the refusal. */
542
+ private recordHardCapHit;
523
543
  /** Best-effort audit trail entry; never blocks the mutation.
524
544
  *
525
545
  * Two independent best-effort writes run here, because either one failing must not cost the
@@ -565,6 +585,7 @@ export declare class SkillLibrary {
565
585
  create(name: string, content: string, origin?: WriteOrigin, options?: {
566
586
  unitId?: string;
567
587
  unitHead?: UnitHead;
588
+ managed?: boolean;
568
589
  }): Promise<SkillActionResult>;
569
590
  private createCore;
570
591
  update(rawName: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;