@lmzhen/dsh-evolution-core 0.26.15 → 0.27.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.
Files changed (116) hide show
  1. package/README.md +16 -1
  2. package/lib/composition/index.js +3 -0
  3. package/lib/composition-D8Y3-ROd.js +604 -0
  4. package/lib/ensure-MEYMj9jn.js +222 -0
  5. package/lib/errors-Br2-uUSK.js +57 -0
  6. package/lib/http-routes-BP-S3KV-.js +149 -0
  7. package/lib/http-routes.js +2 -0
  8. package/lib/index.js +777 -840
  9. package/lib/restrictor.js +37 -0
  10. package/lib/subagent/index.js +186 -0
  11. package/lib/types/citations.js +192 -0
  12. package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
  13. package/lib/types/composition/composer.js +681 -0
  14. package/lib/types/composition/ensure.d.ts +10 -0
  15. package/lib/types/composition/ensure.js +95 -0
  16. package/lib/types/composition/index.d.ts +5 -0
  17. package/lib/types/composition/index.js +7 -0
  18. package/lib/types/composition/preset-ids.d.ts +17 -0
  19. package/lib/types/composition/preset-ids.js +21 -0
  20. package/lib/types/composition/restricted-preset.d.ts +36 -0
  21. package/lib/types/composition/restricted-preset.js +42 -0
  22. package/lib/types/constants.js +223 -0
  23. package/lib/types/cost.js +67 -0
  24. package/lib/types/curator.d.ts +2 -0
  25. package/lib/types/curator.js +346 -0
  26. package/lib/types/diagnostics.d.ts +26 -0
  27. package/lib/types/diagnostics.js +23 -0
  28. package/lib/types/document-body.js +91 -0
  29. package/lib/types/drift-signals.js +319 -0
  30. package/lib/types/env.js +25 -0
  31. package/lib/types/errors.d.ts +4 -1
  32. package/lib/types/errors.js +59 -0
  33. package/lib/types/events.js +16 -0
  34. package/lib/types/evidence.js +65 -0
  35. package/lib/types/evolution-events.js +479 -0
  36. package/lib/types/fold-untrusted.js +44 -0
  37. package/lib/types/frontmatter.js +623 -0
  38. package/lib/types/fuzzy-match.js +135 -0
  39. package/lib/types/gates.js +51 -0
  40. package/lib/types/guards.js +27 -0
  41. package/lib/types/health-baseline.js +63 -0
  42. package/lib/types/http-routes.d.ts +67 -35
  43. package/lib/types/http-routes.js +135 -0
  44. package/lib/types/index.d.ts +13 -2
  45. package/lib/types/index.js +101 -0
  46. package/lib/types/instance-scope.js +69 -0
  47. package/lib/types/io.js +675 -0
  48. package/lib/types/learn-prompt.js +42 -0
  49. package/lib/types/limits.js +63 -0
  50. package/lib/types/live.js +107 -0
  51. package/lib/types/long-runs.js +96 -0
  52. package/lib/types/memory-store.d.ts +0 -16
  53. package/lib/types/memory-store.js +606 -0
  54. package/lib/types/mutations.js +95 -0
  55. package/lib/types/namespace-migration.js +156 -0
  56. package/lib/types/numeric.js +47 -0
  57. package/lib/types/opt-in.js +101 -0
  58. package/lib/types/package-identity.d.ts +14 -0
  59. package/lib/types/package-identity.js +47 -0
  60. package/lib/types/params.js +328 -0
  61. package/lib/types/payload.js +36 -0
  62. package/lib/types/platform-line.generated.d.ts +10 -0
  63. package/lib/types/platform-line.generated.js +12 -0
  64. package/lib/types/platform-services.d.ts +2 -2
  65. package/lib/types/platform-services.js +52 -0
  66. package/lib/types/probe.d.ts +9 -3
  67. package/lib/types/probe.js +122 -0
  68. package/lib/types/prompts.js +409 -0
  69. package/lib/types/quality.js +308 -0
  70. package/lib/types/reachability.d.ts +46 -0
  71. package/lib/types/reachability.js +122 -0
  72. package/lib/types/redact.js +257 -0
  73. package/lib/types/reference-rewrite.js +144 -0
  74. package/lib/types/reports.js +110 -0
  75. package/lib/types/restrictor.d.ts +11 -0
  76. package/lib/types/restrictor.js +48 -0
  77. package/lib/types/review-channel.js +59 -0
  78. package/lib/types/run-registry.js +345 -0
  79. package/lib/types/scope.js +7 -0
  80. package/lib/types/serial.js +17 -0
  81. package/lib/types/session-fold.d.ts +15 -0
  82. package/lib/types/session-fold.js +28 -0
  83. package/lib/types/session-projection.js +295 -0
  84. package/lib/types/signals.js +209 -0
  85. package/lib/types/skill-discovery.js +66 -0
  86. package/lib/types/skill-health.js +134 -0
  87. package/lib/types/skill-history.js +745 -0
  88. package/lib/types/skill-lint.js +112 -0
  89. package/lib/types/skill-reads.js +67 -0
  90. package/lib/types/skill-row-cells.js +123 -0
  91. package/lib/types/skill-store.d.ts +34 -3
  92. package/lib/types/skill-store.js +3528 -0
  93. package/lib/types/state-store.js +60 -0
  94. package/lib/types/store-ledger.d.ts +44 -0
  95. package/lib/types/store-ledger.js +117 -0
  96. package/lib/types/subagent/channel.d.ts +4 -0
  97. package/lib/types/subagent/channel.js +131 -0
  98. package/lib/types/subagent/index.d.ts +4 -0
  99. package/lib/types/subagent/index.js +5 -0
  100. package/lib/types/subagent/readiness.d.ts +6 -0
  101. package/lib/types/subagent/readiness.js +35 -0
  102. package/lib/types/subagent/types.d.ts +58 -0
  103. package/lib/types/subagent/types.js +2 -0
  104. package/lib/types/threats.js +338 -0
  105. package/lib/types/tool-dispatch.d.ts +5 -0
  106. package/lib/types/tool-dispatch.js +535 -0
  107. package/lib/types/tree-write.d.ts +53 -0
  108. package/lib/types/tree-write.js +40 -0
  109. package/lib/types/unit-members.js +121 -0
  110. package/lib/types/usage.js +474 -0
  111. package/lib/types/write-authority.d.ts +87 -0
  112. package/lib/types/write-authority.js +23 -0
  113. package/lib/types/write-gates.d.ts +111 -0
  114. package/lib/types/write-gates.js +344 -0
  115. package/lib/types/write-inventory.js +158 -0
  116. package/package.json +39 -11
@@ -0,0 +1,112 @@
1
+ /*
2
+ * skill-lint — the FORM of a judgement, not a second judgement (G1, contract §3/§3b/§4, D1).
3
+ *
4
+ * The engine that DECIDES (token-scale line, /1024 stamp density, soft/2 support threshold) is
5
+ * `skill-health.ts`; this module only re-expresses the facts it already computed as findings, so a
6
+ * conclusion carries a rule id, a severity and a count instead of free text. Severity is declared
7
+ * HERE, per rule — never scored by a model, never derived from statistics — so the same facts always
8
+ * produce the same findings, on any machine, which is what lets CI replay a conclusion.
9
+ *
10
+ * Pure: no IO, no ctx, no thresholds of its own.
11
+ */
12
+ import { SUPPORT_SPRAWL_FILES } from "./constants.js";
13
+ const msg = (parts) => parts.map(p => (typeof p === 'string' ? p : String(p))).join('');
14
+ /** One rule yields AT MOST one finding (contract §3). `body-oversize-2x` is the only rule whose
15
+ * severity can escalate a verdict to needs-restructure. */
16
+ export const RULES = [
17
+ {
18
+ id: 'unreadable-skill',
19
+ severity: 'error',
20
+ run: facts => facts.readable && facts.dims.bodyChars !== null
21
+ ? null
22
+ : { severity: 'error', rule: 'unreadable-skill', message: msg([facts.skillName, ': SKILL.md could not be read — no health claim is possible']), count: 1 },
23
+ },
24
+ {
25
+ id: 'body-oversize-2x',
26
+ severity: 'error',
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 }
29
+ : null,
30
+ },
31
+ {
32
+ id: 'body-oversize',
33
+ severity: 'warn',
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 }
36
+ : null,
37
+ },
38
+ {
39
+ id: 'stamp-density',
40
+ severity: 'warn',
41
+ run: (facts) => {
42
+ const density = facts.dims.stampDensityPerKb;
43
+ const hits = facts.dims.stampHits;
44
+ if (density === null || hits === null || density < facts.stampLimitPerKb)
45
+ return null;
46
+ return { severity: 'warn', rule: 'stamp-density', message: msg([facts.skillName, ': stamp density ', density.toFixed(1), '/KB (rc/sha/date lines — log-like content)']), count: hits };
47
+ },
48
+ },
49
+ {
50
+ id: 'body-without-support',
51
+ severity: 'warn',
52
+ run: facts => facts.dims.supportGroups === 0 && facts.dims.bodyChars !== null && facts.dims.bodyChars >= facts.softLineChars / 2
53
+ ? { severity: 'warn', rule: 'body-without-support', message: msg([facts.skillName, ': large body (', facts.dims.bodyChars, ' chars) with NO support files']), count: 1 }
54
+ : null,
55
+ },
56
+ {
57
+ id: 'references-sprawl',
58
+ severity: 'warn',
59
+ run: (facts) => {
60
+ const files = facts.dims.supportFiles;
61
+ if (files === null || files < SUPPORT_SPRAWL_FILES)
62
+ return null;
63
+ return { severity: 'warn', rule: 'references-sprawl', message: msg([facts.skillName, ': ', files, ' support files']), count: files };
64
+ },
65
+ },
66
+ {
67
+ id: 'write-ghost',
68
+ severity: 'warn',
69
+ run: (facts) => {
70
+ const patches = facts.dims.churnPatches;
71
+ const reads = facts.dims.churnReads;
72
+ if (patches === null || reads === null || patches < facts.churnMinPatches || reads > 0)
73
+ return null;
74
+ return { severity: 'warn', rule: 'write-ghost', message: msg([facts.skillName, ': ', patches, ' patch(es), never read']), count: patches };
75
+ },
76
+ },
77
+ ];
78
+ export function lintSkill(facts) {
79
+ const out = [];
80
+ for (const rule of RULES) {
81
+ const hit = rule.run(facts);
82
+ if (hit !== null)
83
+ out.push(hit);
84
+ }
85
+ return out;
86
+ }
87
+ /** What a write introduced, what got worse, what got fixed. A rule whose count grew (one site to
88
+ * nine) counts as worsened — comparing by rule name alone reported nothing. */
89
+ export function diffFindings(before, after) {
90
+ const beforeByRule = new Map(before.map(f => [f.rule, f]));
91
+ const afterByRule = new Map(after.map(f => [f.rule, f]));
92
+ const introduced = after.filter(f => !beforeByRule.has(f.rule));
93
+ const worsened = after.filter((f) => {
94
+ const old = beforeByRule.get(f.rule);
95
+ return old !== undefined && f.count > old.count;
96
+ });
97
+ const resolved = before.filter(f => !afterByRule.has(f.rule)).map(f => f.rule);
98
+ return { introduced, worsened, resolved };
99
+ }
100
+ /** Support files = markdown files under references/ (contract §4: unit frozen, underscore-prefixed
101
+ * excluded). Paths are supplied by the caller — this module never touches the filesystem. */
102
+ export function countReferenceFiles(paths) {
103
+ return paths.filter((p) => {
104
+ const n = p.replace(/\\/g, '/');
105
+ const inReferences = n.includes('/references/') || n.startsWith('references/');
106
+ if (!inReferences || !n.endsWith('.md'))
107
+ return false;
108
+ const base = n.slice(n.lastIndexOf('/') + 1);
109
+ return !base.startsWith('_');
110
+ }).length;
111
+ }
112
+ //# sourceMappingURL=skill-lint.js.map
@@ -0,0 +1,67 @@
1
+ import { collectReadSkillNames } from "./tool-dispatch.js";
2
+ import { sessionReadNames } from "./session-projection.js";
3
+ /**
4
+ * The skill names ONE session read, or `undefined` when its log is not readable.
5
+ *
6
+ * Two readers, in this order: the family's `evolutionReads` projection for a platform `Session`
7
+ * (0.2.x's live read — the deprecated synchronous log read is gone from production), then the
8
+ * STRUCTURAL accessor for a view that is not a `Session` at all. The tool path's exec view may be a
9
+ * bare stub, which is what this module exists for; a real Session always takes the first path.
10
+ *
11
+ * `undefined` is NOT an empty set: a caller that must decide (the tool path's read-before-write
12
+ * gate) can then keep its previous behavior instead of refusing every write in a composition whose
13
+ * session objects expose no log at all.
14
+ * @param ctx - a context of the runtime.
15
+ * @param session - the session whose reads are wanted; `undefined` when the call carries none.
16
+ * @returns the names read through a non-failed `skill` dispatch, or `undefined` when unreadable.
17
+ */
18
+ export function sessionReadSkillNames(ctx, session) {
19
+ if (session === undefined)
20
+ return undefined;
21
+ const projected = sessionReadNames(ctx, session);
22
+ if (projected !== undefined)
23
+ return projected;
24
+ const events = session.readEvents?.();
25
+ return events === undefined ? undefined : collectReadSkillNames(events);
26
+ }
27
+ /** Actions whose target must have been read first (create is authorship, so it is exempt). */
28
+ export const READ_REQUIRED_ACTIONS = Object.freeze([
29
+ 'edit', 'update', 'patch', 'delete', 'write_file', 'remove_file', 'restructure',
30
+ ]);
31
+ /**
32
+ * Does this one op write a skill the caller never read?
33
+ *
34
+ * V6-26 (0.3.37): the validator normalizes a missing action to `patch`, so a missing action is a
35
+ * read-required write, never silently exempt.
36
+ * @param op - one skill operation (from a tool call or a review plan).
37
+ * @param readNames - names read this session, from {@link collectReadSkillNames}.
38
+ * @returns true when the op is a read-required write to an unread target.
39
+ */
40
+ export function isUnreadWrite(op, readNames) {
41
+ if (!op.name)
42
+ return false;
43
+ return READ_REQUIRED_ACTIONS.includes(op.action ?? 'patch') && !readNames.has(op.name);
44
+ }
45
+ /**
46
+ * Drop mutating ops whose target was not read this session, in place.
47
+ *
48
+ * Covers the same mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
49
+ * background review cannot blind-touch support files or edits of skills it never loaded.
50
+ * @param ops - the plan's ops; dropped entries are removed in place.
51
+ * @param readNames - names read this session.
52
+ * @returns the count of dropped ops, so the plan event can report them as rejected.
53
+ */
54
+ export function filterUnreadSkillOps(ops, readNames) {
55
+ let dropped = 0;
56
+ for (let index = ops.length - 1; index >= 0; index -= 1) {
57
+ const op = ops[index];
58
+ if (!op)
59
+ continue;
60
+ if (isUnreadWrite(op, readNames)) {
61
+ ops.splice(index, 1);
62
+ dropped += 1;
63
+ }
64
+ }
65
+ return dropped;
66
+ }
67
+ //# sourceMappingURL=skill-reads.js.map
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The cells one row shows, decided here and rendered by the client.
3
+ *
4
+ * Why it exists: until 0.14.1 a row's information architecture lived inside one JSX expression
5
+ * (description + ' · ' + state + ' · ' + age), so which facts survived the browser's single-line
6
+ * ellipsis was an accident of concatenation order — on a real library the skill row's age and
7
+ * curation state were clipped away on EVERY row (measured 2026-09-28: 6 skills, descriptions
8
+ * 49–61 characters, ~240px of line). A row now arrives as an ORDERED list of cells, each declaring
9
+ * its slot, and the renderer lays them out: `aside` cells are right-aligned and never truncate, so a
10
+ * decision fact cannot be eaten by a long description again.
11
+ *
12
+ * Invariants: no words live here. A cell carries either a locale key (`copy`, with the values its
13
+ * template needs) or a verbatim fact (`literal` — numbers, hashes, recorded paths). Facts come from
14
+ * the host; the dictionary owns every sentence and every number format, including the ones the panel
15
+ * used to build itself (`row.delta.up`, `time.<unit>.one`, `versions.count`).
16
+ * @module @lmzhen/dsh-evolution-core/skill-row-cells
17
+ */
18
+ /** The actions whose row carries an explanatory hover text. */
19
+ const ACTION_NOTES = {
20
+ delete: 'action.delete.note',
21
+ archive: 'action.archive.note',
22
+ };
23
+ /** The actions whose bytes come from a support file rather than from SKILL.md. */
24
+ const SUPPORT_ACTIONS = ['support-write', 'support-remove'];
25
+ /**
26
+ * The relative-time key for one bucket: a count of one is spelled on its own.
27
+ * @param age - the bucket the host reported.
28
+ * @returns the locale key, singular or plural.
29
+ */
30
+ export function ageCopyKey(age) {
31
+ // 'now' is its own sentence: there is no count to spell, and no `time.now.one`.
32
+ if (age.unit === 'now')
33
+ return 'time.now';
34
+ return age.n === 1 ? 'time.' + age.unit + '.one' : 'time.' + age.unit;
35
+ }
36
+ /**
37
+ * The state word one skill row carries, from the facts the listing reported.
38
+ * @param skill - the row's facts.
39
+ * @returns the locale key for its state.
40
+ */
41
+ export function skillStateKey(skill) {
42
+ if (skill.protectionUnknown)
43
+ return 'state.unknown';
44
+ if (skill.protectedBy !== null)
45
+ return 'state.protected';
46
+ return skill.managed ? 'state.managed' : 'state.foreign';
47
+ }
48
+ /** The hover text one action word needs, or nothing when it needs none. */
49
+ function actionNote(actionKind) {
50
+ const note = ACTION_NOTES[actionKind];
51
+ return note === undefined ? {} : { note };
52
+ }
53
+ /**
54
+ * The cells of one version row: the facts on the title line, the summary (if any) under it.
55
+ * @param row - the row's facts.
56
+ * @returns both lines.
57
+ */
58
+ export function versionRowCells(row) {
59
+ const title = [
60
+ { key: 'v', copy: 'row.version', values: { n: row.v }, tone: 'tertiary', slot: 'meta' },
61
+ { key: 'action', copy: 'action.' + row.actionKind, tone: 'primary', slot: 'lead', ...actionNote(row.actionKind) },
62
+ { key: 'time', copy: ageCopyKey(row.age), values: { n: row.age.n }, tone: 'tertiary', slot: 'meta' },
63
+ ];
64
+ if (row.path !== undefined) {
65
+ title.push({ key: 'path', literal: row.path, tone: 'tertiary', slot: 'meta', note: row.path });
66
+ }
67
+ else if (SUPPORT_ACTIONS.includes(row.actionKind)) {
68
+ title.push({ key: 'path', copy: 'path.unrecorded', tone: 'tertiary', slot: 'meta' });
69
+ }
70
+ const delta = row.charsDelta;
71
+ if (delta !== undefined && delta !== 0) {
72
+ title.push({
73
+ key: 'delta',
74
+ copy: delta > 0 ? 'row.delta.up' : 'row.delta.down',
75
+ values: { n: Math.abs(delta) },
76
+ tone: 'tertiary',
77
+ slot: 'meta',
78
+ });
79
+ }
80
+ // U-5: the recorded hash, as a SHORT code (seven chars) whose hover carries the full value — the
81
+ // platform already puts `hash` on the row; nothing rendered it, so the fact was declared and unused.
82
+ if (row.hash !== undefined && row.hash !== '') {
83
+ title.push({ key: 'hash', literal: row.hash.slice(0, 7), tone: 'tertiary', slot: 'meta', note: row.hash });
84
+ }
85
+ const meta = [];
86
+ // The summary is the one line a deployment may or may not generate; the host sends it already
87
+ // verbatim, so it needs no dictionary key.
88
+ if (row.summary !== undefined && row.summary !== '') {
89
+ meta.push({ key: 'summary', literal: row.summary, tone: 'secondary', slot: 'lead' });
90
+ }
91
+ return { title, meta };
92
+ }
93
+ /**
94
+ * The cells of one skill row: the name line, then the facts that describe it.
95
+ *
96
+ * The description is a `lead` cell (it is the one allowed to truncate, with the full text on hover),
97
+ * while the version count and the last-changed time are `aside` cells — they survive any description.
98
+ * The description is NOT pre-truncated in JavaScript: the browser's ellipsis already clips it at the
99
+ * layout's real width, and a character cut on top of that only hides where the fact ends.
100
+ * @param skill - the row's facts.
101
+ * @returns both lines.
102
+ */
103
+ export function skillRowCells(skill) {
104
+ const title = [
105
+ { key: 'name', literal: skill.name, tone: 'primary', slot: 'lead', note: skill.name },
106
+ { key: 'count', copy: 'versions.count', values: { n: skill.versions }, tone: 'tertiary', slot: 'aside' },
107
+ ];
108
+ const meta = [
109
+ { key: 'description', literal: skill.description, tone: 'tertiary', slot: 'lead', note: skill.description },
110
+ { key: 'state', copy: skillStateKey(skill), tone: 'tertiary', slot: 'meta' },
111
+ ];
112
+ // U-5 (history half): a protected skill says WHO holds it, not only that it is held. The state cell
113
+ // above answers 'protected or not' from the same field; this cell carries the guard's own name, a
114
+ // verbatim fact (no dictionary key — literals are values the host owns).
115
+ if (skill.protectedBy !== null) {
116
+ meta.push({ key: 'protectedBy', literal: skill.protectedBy, tone: 'tertiary', slot: 'meta', note: skill.protectedBy });
117
+ }
118
+ if (skill.age !== null) {
119
+ meta.push({ key: 'age', copy: ageCopyKey(skill.age), values: { n: skill.age.n }, tone: 'tertiary', slot: 'aside' });
120
+ }
121
+ return { title, meta };
122
+ }
123
+ //# sourceMappingURL=skill-row-cells.js.map
@@ -32,6 +32,7 @@
32
32
  * re-probes after its copies so a mid-copy writer demotes to `skipped`
33
33
  * (OPT-07); the movers' probe→rename window is owned by the io.ts protocol.
34
34
  */
35
+ import { type WriteAuthority } from './write-authority.ts';
35
36
  import { transactIo, type EvolutionIoLike } from './io.ts';
36
37
  import { type Probe } from './probe.ts';
37
38
  import type { SkillLimits } from './limits.ts';
@@ -287,10 +288,13 @@ export declare class SkillLibrary {
287
288
  /** V10-03 (P2-18): see the constructor's threatExemptLabels. Empty by
288
289
  * default — the strict ANY-hit-blocks policy is unchanged. */
289
290
  private readonly threatExemptLabels;
291
+ /** G1/1.1: default authority; a call may override (origin is per-invocation). */
292
+ /** G1/1.1/Stage 2: the ONE default every mutator reads. Per-call overrides still win. */
293
+ private readonly authority;
290
294
  /** skill-history.ts: the content history is best-effort, so a failure must not be SILENT —
291
295
  * but it is a deployment condition, not a per-write event, so it warns once per instance. */
292
296
  private historyWarned;
293
- constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits, onMutation?: (event: EvolutionSkillMutatedEvent) => void, transact?: typeof transactIo, threatExemptLabels?: readonly string[]);
297
+ constructor(root?: string, io?: EvolutionIoLike, limits?: SkillLimits, onMutation?: (event: EvolutionSkillMutatedEvent) => void, transact?: typeof transactIo, threatExemptLabels?: readonly string[], authority?: WriteAuthority);
294
298
  /**
295
299
  * Run one single-file read-modify-write for a mutator. When `transact` was
296
300
  * injected the read and the write run inside it (cross-process atomicity);
@@ -328,6 +332,21 @@ export declare class SkillLibrary {
328
332
  * a swallowed error would silently downgrade the anchor to
329
333
  * last-writer-wins.
330
334
  */
335
+ /**
336
+ * S4-01 (G0/0.7): the ONE three-state read the staging/replay anchors use.
337
+ *
338
+ * The two absorbing reads below absorb ENOENT/EISDIR and rethrow everything else, and the callers then
339
+ * each wrote their own try/catch to turn a rethrow into a decision - three near-copies of one posture,
340
+ * and the newest of them silently dropped the anchor instead of refusing. A read failure is its own
341
+ * state, so it gets its own name here: `unknown` with the reason, next to `present` and `absent`.
342
+ * Failing closed is then the CALLER decision (they refuse on unknown), not something each call site has
343
+ * to re-derive from a thrown error.
344
+ */
345
+ readSkillProbe(rawName: string): Promise<Probe<string>>;
346
+ /** The support-file face of {@link readSkillProbe}. */
347
+ readSupportFileProbe(rawName: string, filePath: string): Promise<Probe<string>>;
348
+ /** One place that turns the absorbing reads into three states. `null` is absent; a throw is unknown. */
349
+ private probeOf;
331
350
  readSupportFile(name: string, filePath: string): Promise<string | null>;
332
351
  /**
333
352
  * Summarize the skill tree.
@@ -499,7 +518,7 @@ export declare class SkillLibrary {
499
518
  * P2-4 (v19): a non-string entry (`skills: [123]`) is refused structurally
500
519
  * instead of throwing `name.includes is not a function`. */
501
520
  private safeSnapshotEntryName;
502
- archive(rawName: string, options?: ArchiveOptions): Promise<SkillActionResult>;
521
+ archive(rawName: string, options?: ArchiveOptions, origin?: WriteOrigin): Promise<SkillActionResult>;
503
522
  /**
504
523
  * V2 (design §16.7): what an apply-mode consolidation would do for ONE source,
505
524
  * computed BEFORE any side effect. `blocked` means behaviour is exactly the
@@ -567,7 +586,7 @@ export declare class SkillLibrary {
567
586
  * recoverability: archival never deletes, and this is the control-plane
568
587
  * path back. The `.archive-reason` marker is dropped on restore.
569
588
  */
570
- restoreFromArchive(rawName: string): Promise<SkillActionResult>;
589
+ restoreFromArchive(rawName: string, origin?: WriteOrigin): Promise<SkillActionResult>;
571
590
  writeSupportFile(rawName: string, filePath: string, content: string, origin?: WriteOrigin, anchor?: WriteAnchor): Promise<SkillActionResult>;
572
591
  private writeSupportFileCore;
573
592
  removeSupportFile(rawName: string, filePath: string, origin?: WriteOrigin, anchor?: WriteAnchor, unitId?: string): Promise<SkillActionResult>;
@@ -624,6 +643,16 @@ export declare class SkillLibrary {
624
643
  private sanitizeExtraNames;
625
644
  readSnapshotManifest(path: string): Promise<SnapshotManifest | null>;
626
645
  /** Keep only the newest N snapshots (Hermes keep=5 parity); older ones are removed outright. */
646
+ /**
647
+ * G3/3.4: can a snapshot actually be used right now?
648
+ *
649
+ * The recovery messages used to promise "restore it from a snapshot" without asking whether a usable
650
+ * snapshot exists (audit S1-01: the message promised recoverability from a chain that is five deep and
651
+ * whose newest member held 9 of 35 skills). Three states, like every other reading here: a fragment
652
+ * naming the usable snapshot, a fragment saying there is none, or a fragment saying the directory could
653
+ * not be read - the caller embeds whichever it gets, so the sentence is generated by the capability.
654
+ */
655
+ private snapshotRecoveryAvailability;
627
656
  private retainSnapshots;
628
657
  /** Snapshots as a THREE-state read (N14): absent = no .backups directory,
629
658
  * unknown = it exists but could not be listed (a caller must not read a
@@ -680,6 +709,8 @@ export interface NewSkillLibraryOptions {
680
709
  ctx?: SkillMutationSink | undefined;
681
710
  transact?: typeof transactIo | undefined;
682
711
  threatExemptLabels?: readonly string[] | undefined;
712
+ /** G1/1.1: WHO writes through this library. Required: no call site can omit it. */
713
+ authority: WriteAuthority;
683
714
  }
684
715
  export declare function newSkillLibrary(options: NewSkillLibraryOptions): SkillLibrary;
685
716
  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';