@lmzhen/dsh-evolution-core 0.3.13 → 0.3.14

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
@@ -2,6 +2,7 @@ import { basename, dirname, join } from "node:path";
2
2
  import { cp, lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { createHash, randomBytes } from "node:crypto";
4
4
  import { homedir } from "node:os";
5
+ import { load } from "js-yaml";
5
6
  //#region lib/types/io.js
6
7
  /**
7
8
  * Structural IO seam for the evolution plugin family.
@@ -2501,15 +2502,42 @@ function markerEntryName(marker) {
2501
2502
  function markerPath(dir, marker) {
2502
2503
  return join(dir, markerEntryName(marker));
2503
2504
  }
2504
- function parseFrontmatter(content) {
2505
+ /**
2506
+ * Shared frontmatter block detection (P3-3 single owner): opening line `---`
2507
+ * and closing line exactly `---` (both trimmed). Used by `parseFrontmatter`,
2508
+ * `frontmatterYamlUnsafeValues` and `normalizeFrontmatter` so the three can
2509
+ * never disagree about where the block ends (the loose `indexOf('\n---')`
2510
+ * form matched `\n----` and was replaced by this strict line rule).
2511
+ */
2512
+ function frontmatterBlock(content) {
2505
2513
  if (!content.trimStart().startsWith("---")) return null;
2506
- const end = content.indexOf("\n---", 3);
2514
+ const nl = content.includes("\r\n") ? "\r\n" : "\n";
2515
+ const lines = content.split(nl);
2516
+ if ((lines[0] ?? "").trim() !== "---") return null;
2517
+ let end = -1;
2518
+ for (let i = 1; i < lines.length; i++) {
2519
+ const line = lines[i];
2520
+ if (line === void 0) continue;
2521
+ if (line.trim() === "---") {
2522
+ end = i;
2523
+ break;
2524
+ }
2525
+ }
2507
2526
  if (end < 0) return null;
2508
- const block = content.slice(3, end);
2509
- const body = content.slice(end + 4).trim();
2527
+ return {
2528
+ block: lines.slice(1, end).join(nl),
2529
+ lines,
2530
+ end,
2531
+ nl
2532
+ };
2533
+ }
2534
+ function parseFrontmatter(content) {
2535
+ const found = frontmatterBlock(content);
2536
+ if (!found) return null;
2537
+ const body = found.lines.slice(found.end + 1).join(found.nl).trim();
2510
2538
  if (!body) return null;
2511
2539
  const frontmatter = {};
2512
- for (const line of block.split("\n")) {
2540
+ for (const line of found.block.split(found.nl)) {
2513
2541
  const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
2514
2542
  if (match) {
2515
2543
  const [, key, value] = match;
@@ -2528,7 +2556,10 @@ function parseFrontmatter(content) {
2528
2556
  * silently split family-visibility from platform-visibility (0.3.11
2529
2557
  * inkos-harness case: the description carried "…: " and the catalog dropped
2530
2558
  * the whole skill). Already-quoted values and well-formed flow collections
2531
- * (`[a, b]` / `{a: b}`) are considered safe. */
2559
+ * (`[a, b]` / `{a: b}`) are considered safe. This rule is only the FAST
2560
+ * PATH — the write path re-verifies every rewrite with the real YAML parser
2561
+ * (see normalizeFrontmatter), so an incomplete approximation can never
2562
+ * corrupt a multiline flow value (P3-4). */
2532
2563
  function yamlPlainScalarNeedsQuotes(value) {
2533
2564
  if (value.length === 0) return false;
2534
2565
  if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return false;
@@ -2545,23 +2576,9 @@ function yamlPlainScalarNeedsQuotes(value) {
2545
2576
  * path. Single-line entries only; lines with embedded line breaks skip. */
2546
2577
  function frontmatterYamlUnsafeValues(content) {
2547
2578
  const found = [];
2548
- if (!content.trimStart().startsWith("---")) return found;
2549
- const nl = content.includes("\r\n") ? "\r\n" : "\n";
2550
- const lines = content.split(nl);
2551
- if ((lines[0] ?? "").trim() !== "---") return found;
2552
- let end = -1;
2553
- for (let i = 1; i < lines.length; i++) {
2554
- const line = lines[i];
2555
- if (line === void 0) continue;
2556
- if (line.trim() === "---") {
2557
- end = i;
2558
- break;
2559
- }
2560
- }
2561
- if (end < 0) return found;
2562
- for (let i = 1; i < end; i++) {
2563
- const line = lines[i];
2564
- if (line === void 0) continue;
2579
+ const block = frontmatterBlock(content);
2580
+ if (!block) return found;
2581
+ for (const line of block.block.split(block.nl)) {
2565
2582
  if (line.includes("\n") || line.includes("\r")) continue;
2566
2583
  const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
2567
2584
  if (!match) continue;
@@ -2579,43 +2596,25 @@ function frontmatterYamlUnsafeValues(content) {
2579
2596
  * Normalize a SKILL.md frontmatter block into catalog-loadable YAML: values
2580
2597
  * that YAML forbids unquoted get quotes — double quotes normally, single
2581
2598
  * quotes (with `''` doubling) when the value contains `"` or `\` (both legal
2582
- * unescaped inside single-quoted YAML). Idempotent (a normalized block
2583
- * passes through unchanged); only single-line `key: value` entries are
2584
- * touched; body text is never modified; a key line carrying an embedded
2585
- * line break (mixed ending styles) is left untouched rather than risking
2586
- * continuation-line data loss. Line-ending style of the block is preserved.
2599
+ * unescaped inside single-quoted YAML). Idempotent; only single-line
2600
+ * `key: value` entries are touched; body text is never modified; line-ending
2601
+ * style is preserved. **Every rewrite is re-verified with the real YAML
2602
+ * parser** (js-yaml — the same parser the platform catalog uses): if the
2603
+ * rewritten block no longer parses, or a rewritten value's parsed content
2604
+ * differs from the original, the rewrite is rolled back and reported in
2605
+ * `issues` (fail-loud, never a silent value corruption — P3-4).
2587
2606
  */
2588
2607
  function normalizeFrontmatter(content) {
2589
- if (!content.trimStart().startsWith("---")) return {
2590
- content,
2591
- changed: false,
2592
- fields: [],
2593
- issues: []
2594
- };
2595
- const nl = content.includes("\r\n") ? "\r\n" : "\n";
2596
- const lines = content.split(nl);
2597
- if ((lines[0] ?? "").trim() !== "---") return {
2598
- content,
2599
- changed: false,
2600
- fields: [],
2601
- issues: []
2602
- };
2603
- let end = -1;
2604
- for (let i = 1; i < lines.length; i++) {
2605
- const line = lines[i];
2606
- if (line === void 0) continue;
2607
- if (line.trim() === "---") {
2608
- end = i;
2609
- break;
2610
- }
2611
- }
2612
- if (end < 0) return {
2608
+ const block = frontmatterBlock(content);
2609
+ if (!block) return {
2613
2610
  content,
2614
2611
  changed: false,
2615
2612
  fields: [],
2616
2613
  issues: []
2617
2614
  };
2615
+ const { lines, end, nl } = block;
2618
2616
  const unsafe = new Map(frontmatterYamlUnsafeValues(content).map((entry) => [entry.key, entry.value]));
2617
+ const originalValues = new Map(unsafe);
2619
2618
  const fields = [];
2620
2619
  const issues = [];
2621
2620
  let changed = false;
@@ -2635,12 +2634,30 @@ function normalizeFrontmatter(content) {
2635
2634
  fields.push(key);
2636
2635
  changed = true;
2637
2636
  }
2638
- return {
2639
- content: changed ? lines.join(nl) : content,
2640
- changed,
2641
- fields,
2637
+ if (!changed) return {
2638
+ content,
2639
+ changed: false,
2640
+ fields: [],
2642
2641
  issues
2643
2642
  };
2643
+ const rewrittenBlock = lines.slice(1, end).join(nl);
2644
+ try {
2645
+ const parsed = load(rewrittenBlock);
2646
+ for (const key of fields) if (String(parsed[key]) !== originalValues.get(key)) throw new Error(`rewritten value for ${key} differs from the original`);
2647
+ return {
2648
+ content: lines.join(nl),
2649
+ changed: true,
2650
+ fields,
2651
+ issues
2652
+ };
2653
+ } catch (error) {
2654
+ return {
2655
+ content,
2656
+ changed: false,
2657
+ fields: [],
2658
+ issues: [`frontmatter rewrite verification failed (${error instanceof Error ? error.message : String(error)}) — quoting skipped; wrap this value manually`]
2659
+ };
2660
+ }
2644
2661
  }
2645
2662
  /**
2646
2663
  * Skill names referenced by a SKILL.md's `related_skills` frontmatter
@@ -4154,4 +4171,4 @@ function evolutionHome(env = process.env) {
4154
4171
  return join(env.DSH_HOME ?? join(homedir(), ".dsh"), "evolution");
4155
4172
  }
4156
4173
  //#endregion
4157
- 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_HEALTH_THRESHOLDS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, 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, RESTRUCTURE_TARGET_RE, 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, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
4174
+ 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_HEALTH_THRESHOLDS, 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, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, DSH_AUTHORING_STANDARDS, ENTRY_DELIMITER, EVENT_ARCHIVE_PREFIX, EVENT_LOG_RETAIN_ARCHIVES, EVENT_LOG_ROTATE_AT, EVENT_LOG_VERSION, EvolutionGateSet, HEALTH_STAMP_RE, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, MAX_DESCRIPTION_LENGTH, MAX_RESTRUCTURE_MOVES, 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, RESTRUCTURE_TARGET_RE, 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, assessStructureHealth, authoringFeedback, buildCuratorRunReport, buildLearnPrompt, bumpPatch, bumpUse, bumpView, computeDedupGroups, computeDriftSignals, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, createGateSet, duplicateHeadings, emptyRecord, evaluateThreat, eventsFile, evolutionHome, evolutionIoAdapter, findDriftSignal, foldCuratorFields, foldTurn, frontmatterBlock, frontmatterYamlUnsafeValues, getRecord, latestActivityAt, lifecycleCandidate, listEventArchives, loadMutations, loadSuppressedNames, loadUsage, markAgentCreated, memoryRoot, missingSupportPointers, mutateUsage, mutationsFile, narrowNameMatches, nodeEvolutionIo, normalizeFrontmatter, normalizeUsageRecord, observeEvent, overlongLines, parseCuratorNominations, parseEvolutionEvents, parseFrontmatter, readEvolutionEvents, readEvolutionTimeline, recordMutation, redactSecrets, relatedSkillNames, renderCuratorReportMarkdown, resolveOrigins, retainEventArchives, reviewPrompt, saveSuppressedNames, saveUsage, scanContentThreats, scanMemoryThreats, scanThreats, skillsRoot, suppressedFile, transactIo, updateSuppressedNames, usageFile, usageObserved, validateFrontmatter, verifyPromptBundle, yamlPlainScalarNeedsQuotes };
@@ -100,6 +100,19 @@ export interface Frontmatter {
100
100
  description?: string;
101
101
  [key: string]: unknown;
102
102
  }
103
+ /**
104
+ * Shared frontmatter block detection (P3-3 single owner): opening line `---`
105
+ * and closing line exactly `---` (both trimmed). Used by `parseFrontmatter`,
106
+ * `frontmatterYamlUnsafeValues` and `normalizeFrontmatter` so the three can
107
+ * never disagree about where the block ends (the loose `indexOf('\n---')`
108
+ * form matched `\n----` and was replaced by this strict line rule).
109
+ */
110
+ export declare function frontmatterBlock(content: string): {
111
+ block: string;
112
+ lines: string[];
113
+ end: number;
114
+ nl: string;
115
+ } | null;
103
116
  export declare function parseFrontmatter(content: string): {
104
117
  frontmatter: Frontmatter;
105
118
  body: string;
@@ -111,7 +124,10 @@ export declare function parseFrontmatter(content: string): {
111
124
  * silently split family-visibility from platform-visibility (0.3.11
112
125
  * inkos-harness case: the description carried "…: " and the catalog dropped
113
126
  * the whole skill). Already-quoted values and well-formed flow collections
114
- * (`[a, b]` / `{a: b}`) are considered safe. */
127
+ * (`[a, b]` / `{a: b}`) are considered safe. This rule is only the FAST
128
+ * PATH — the write path re-verifies every rewrite with the real YAML parser
129
+ * (see normalizeFrontmatter), so an incomplete approximation can never
130
+ * corrupt a multiline flow value (P3-4). */
115
131
  export declare function yamlPlainScalarNeedsQuotes(value: string): boolean;
116
132
  /** Raw-line scan of the frontmatter block: entries whose UNQUOTED value is
117
133
  * YAML-unsafe for the strict platform catalog. Operates on the ORIGINAL line
@@ -127,20 +143,23 @@ export interface FrontmatterNormalizeResult {
127
143
  changed: boolean;
128
144
  /** Frontmatter keys whose values were auto-quoted. */
129
145
  fields: string[];
130
- /** Values that cannot be auto-quoted safely (control characters only —
131
- * double/single-quote fallback covers `"`/`\`/`'`, so a quote-containing
132
- * value no longer traps the write path in an unfixable loop). */
146
+ /** Values that cannot be auto-quoted safely (control characters, or a
147
+ * rewrite that failed the real-parser verification — a multiline flow
148
+ * collection line etc. is left untouched and reported here, so the write
149
+ * path rejects instead of silently damaging a value; 0.3.14). */
133
150
  issues: string[];
134
151
  }
135
152
  /**
136
153
  * Normalize a SKILL.md frontmatter block into catalog-loadable YAML: values
137
154
  * that YAML forbids unquoted get quotes — double quotes normally, single
138
155
  * quotes (with `''` doubling) when the value contains `"` or `\` (both legal
139
- * unescaped inside single-quoted YAML). Idempotent (a normalized block
140
- * passes through unchanged); only single-line `key: value` entries are
141
- * touched; body text is never modified; a key line carrying an embedded
142
- * line break (mixed ending styles) is left untouched rather than risking
143
- * continuation-line data loss. Line-ending style of the block is preserved.
156
+ * unescaped inside single-quoted YAML). Idempotent; only single-line
157
+ * `key: value` entries are touched; body text is never modified; line-ending
158
+ * style is preserved. **Every rewrite is re-verified with the real YAML
159
+ * parser** (js-yaml — the same parser the platform catalog uses): if the
160
+ * rewritten block no longer parses, or a rewritten value's parsed content
161
+ * differs from the original, the rewrite is rolled back and reported in
162
+ * `issues` (fail-loud, never a silent value corruption — P3-4).
144
163
  */
145
164
  export declare function normalizeFrontmatter(content: string): FrontmatterNormalizeResult;
146
165
  /**
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.3.13",
4
+ "version": "0.3.14",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,12 +31,16 @@
31
31
  "lib/types/invariant.d.ts"
32
32
  ],
33
33
  "license": "MIT",
34
+ "dependencies": {
35
+ "js-yaml": "^4.2.0"
36
+ },
34
37
  "peerDependencies": {
35
38
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
36
39
  "@deepseek-ai/cordis": "^4.0.1",
37
40
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2"
38
41
  },
39
42
  "devDependencies": {
43
+ "@types/js-yaml": "^4.0.9",
40
44
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
41
45
  "@deepseek-ai/cordis": "^4.0.1",
42
46
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2"