@avantf/dsh-identity 0.0.0-stage → 0.1.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -2
  3. package/cordis.patch.yml +13 -0
  4. package/lib/assets/presets/analyst/en/IDENTITY.md +1 -0
  5. package/lib/assets/presets/analyst/en/RULES.md +8 -0
  6. package/lib/assets/presets/analyst/en/SOUL.md +5 -0
  7. package/lib/assets/presets/analyst/zh/IDENTITY.md +1 -0
  8. package/lib/assets/presets/analyst/zh/RULES.md +8 -0
  9. package/lib/assets/presets/analyst/zh/SOUL.md +5 -0
  10. package/lib/assets/presets/assistant/en/IDENTITY.md +1 -0
  11. package/lib/assets/presets/assistant/en/RULES.md +8 -0
  12. package/lib/assets/presets/assistant/en/SOUL.md +5 -0
  13. package/lib/assets/presets/assistant/zh/IDENTITY.md +1 -0
  14. package/lib/assets/presets/assistant/zh/RULES.md +8 -0
  15. package/lib/assets/presets/assistant/zh/SOUL.md +5 -0
  16. package/lib/assets/presets/coder/en/IDENTITY.md +1 -0
  17. package/lib/assets/presets/coder/en/RULES.md +8 -0
  18. package/lib/assets/presets/coder/en/SOUL.md +5 -0
  19. package/lib/assets/presets/coder/zh/IDENTITY.md +1 -0
  20. package/lib/assets/presets/coder/zh/RULES.md +8 -0
  21. package/lib/assets/presets/coder/zh/SOUL.md +5 -0
  22. package/lib/client.js +61 -0
  23. package/lib/dsh-build.json +6 -0
  24. package/lib/envinit-bootstrap.js +341 -0
  25. package/lib/index.js +1517 -0
  26. package/lib/interface-version.json +4 -0
  27. package/lib/types/atomic-write.d.ts +3 -0
  28. package/lib/types/atomic-write.d.ts.map +1 -0
  29. package/lib/types/base.d.ts +45 -0
  30. package/lib/types/base.d.ts.map +1 -0
  31. package/lib/types/client/IdentitySection.d.ts +29 -0
  32. package/lib/types/client/IdentitySection.d.ts.map +1 -0
  33. package/lib/types/client/api.d.ts +204 -0
  34. package/lib/types/client/api.d.ts.map +1 -0
  35. package/lib/types/client/index.d.ts +57 -0
  36. package/lib/types/client/index.d.ts.map +1 -0
  37. package/lib/types/documents.d.ts +65 -0
  38. package/lib/types/documents.d.ts.map +1 -0
  39. package/lib/types/envinit.d.ts +22 -0
  40. package/lib/types/envinit.d.ts.map +1 -0
  41. package/lib/types/gateway.d.ts +124 -0
  42. package/lib/types/gateway.d.ts.map +1 -0
  43. package/lib/types/index.d.ts +97 -0
  44. package/lib/types/index.d.ts.map +1 -0
  45. package/lib/types/interface_gate.d.ts +121 -0
  46. package/lib/types/interface_gate.d.ts.map +1 -0
  47. package/lib/types/log.d.ts +18 -0
  48. package/lib/types/log.d.ts.map +1 -0
  49. package/lib/types/paths.d.ts +45 -0
  50. package/lib/types/paths.d.ts.map +1 -0
  51. package/lib/types/presets.d.ts +201 -0
  52. package/lib/types/presets.d.ts.map +1 -0
  53. package/lib/types/session-freeze.d.ts +33 -0
  54. package/lib/types/session-freeze.d.ts.map +1 -0
  55. package/lib/types/state.d.ts +190 -0
  56. package/lib/types/state.d.ts.map +1 -0
  57. package/lib/types/versions.d.ts +19 -0
  58. package/lib/types/versions.d.ts.map +1 -0
  59. package/lib/types/volatile.d.ts +28 -0
  60. package/lib/types/volatile.d.ts.map +1 -0
  61. package/lib/types/wire.d.ts +86 -0
  62. package/lib/types/wire.d.ts.map +1 -0
  63. package/package.json +104 -3
package/lib/index.js ADDED
@@ -0,0 +1,1517 @@
1
+ // lib/index.js
2
+ import { PERSONA_PREFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
3
+ import z2 from "@deepseek-ai/schemastery";
4
+ import { readFileSync as readFileSync3 } from "node:fs";
5
+
6
+ // lib/base.js
7
+ import { loadFramework } from "./envinit-bootstrap.js";
8
+
9
+ // lib/interface_gate.js
10
+ function member(module, name2) {
11
+ try {
12
+ return module?.[name2];
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+ function cannotTell(reason) {
18
+ return { status: "cannot-tell", reason };
19
+ }
20
+ function baseIsUsable(verdict) {
21
+ return verdict.status !== "incompatible";
22
+ }
23
+ var BAKED_CANDIDATES = ["./interface-version.json", "../interface-version.json"];
24
+ function requirementOf(read, bakedUrl) {
25
+ const candidates = bakedUrl === void 0 ? BAKED_CANDIDATES.map((relative) => new URL(relative, import.meta.url)) : [bakedUrl];
26
+ for (const url of candidates) {
27
+ let requirement;
28
+ try {
29
+ requirement = read(url);
30
+ } catch {
31
+ continue;
32
+ }
33
+ const version = requirement?.interfaceVersion;
34
+ if (typeof version === "number" && Number.isInteger(version) && version > 0)
35
+ return { interfaceVersion: version };
36
+ }
37
+ return void 0;
38
+ }
39
+ function interfaceVerdict(module, bakedUrl) {
40
+ const check = member(module, "checkInterface");
41
+ const read = member(module, "readInterfaceRequirement");
42
+ if (typeof check !== "function" || typeof read !== "function") {
43
+ return cannotTell("the loaded base has no interface gate (checkInterface / readInterfaceRequirement missing) — it predates the runtime interface contract, so which generation it implements cannot be told");
44
+ }
45
+ const requirement = requirementOf(read, bakedUrl);
46
+ if (requirement === void 0) {
47
+ return cannotTell("this build has no baked interface requirement (lib/interface-version.json is missing or malformed), so the runtime interface gate cannot run");
48
+ }
49
+ let verdict;
50
+ try {
51
+ verdict = check(requirement.interfaceVersion, module);
52
+ } catch {
53
+ return cannotTell("the loaded base threw while judging the interface generation");
54
+ }
55
+ const status = verdict?.status;
56
+ if (status !== "ok" && status !== "incompatible" && status !== "cannot-tell") {
57
+ return cannotTell("the loaded base returned no usable interface verdict");
58
+ }
59
+ return verdict;
60
+ }
61
+ function warn(log, message) {
62
+ try {
63
+ log?.warn(message);
64
+ } catch {
65
+ }
66
+ }
67
+ function reasonOf(error) {
68
+ return error instanceof Error ? error.message : String(error);
69
+ }
70
+ function gateOrDegrade(module, log, words) {
71
+ const verdict = interfaceVerdict(module);
72
+ if (!baseIsUsable(verdict)) {
73
+ warn(log, `${words.prefix} WARNING — interface: ${verdict.reason ?? "the loaded base implements another interface generation"}; the base's shared capabilities are NOT used (${words.withheld}) and the plugin mounts anyway`);
74
+ return false;
75
+ }
76
+ if (verdict.status === "cannot-tell") {
77
+ warn(log, `${words.prefix} WARNING — interface: ${verdict.reason ?? "the interface generation cannot be told"}; using the loaded base anyway ("cannot tell" is never "incompatible")`);
78
+ }
79
+ return true;
80
+ }
81
+ async function loadGuarded(options, loadOnce, log, words) {
82
+ try {
83
+ return await loadOnce(options);
84
+ } catch (error) {
85
+ warn(log, `${words.prefix} WARNING — environment initialisation failed (${reasonOf(error)}); ${words.loadFailure}`);
86
+ return void 0;
87
+ }
88
+ }
89
+ function createLoadCache(guarded) {
90
+ let loaded;
91
+ let loading;
92
+ return {
93
+ load(options) {
94
+ if (loaded !== void 0)
95
+ return Promise.resolve(loaded);
96
+ if (loading !== void 0)
97
+ return loading;
98
+ loading = guarded(options).then((runtime) => {
99
+ if (runtime === void 0)
100
+ loading = void 0;
101
+ else
102
+ loaded = runtime;
103
+ return runtime;
104
+ }, () => {
105
+ loading = void 0;
106
+ return void 0;
107
+ });
108
+ return loading;
109
+ },
110
+ clear() {
111
+ loaded = void 0;
112
+ loading = void 0;
113
+ }
114
+ };
115
+ }
116
+ function stillActive(ctx) {
117
+ const fiber = ctx?.fiber;
118
+ return fiber === void 0 || fiber.uid !== null;
119
+ }
120
+
121
+ // lib/paths.js
122
+ import { homedir } from "node:os";
123
+ import { join } from "node:path";
124
+ var IDENTITY_FILES = ["IDENTITY", "SOUL", "RULES"];
125
+ var LOCALE_IDS = ["zh", "en"];
126
+ var FALLBACK_LOCALE = "en";
127
+ var PRESET_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
128
+ function expandHomeMirror(path, home = homedir()) {
129
+ if (path === "~")
130
+ return home;
131
+ return path.startsWith("~/") ? join(home, path.slice(2)) : path;
132
+ }
133
+ function resolveDataHomeMirror(input = {}) {
134
+ const layer5 = input.explicit?.trim() ?? "";
135
+ const env = input.env ?? process.env;
136
+ const fromEnv = env["AVANTF_HOME"]?.trim() ?? "";
137
+ const layer2 = input.configured?.trim() ?? "";
138
+ const base = layer5 !== "" ? layer5 : fromEnv !== "" ? fromEnv : layer2 !== "" ? layer2 : join(homedir(), ".avantf");
139
+ return expandHomeMirror(base);
140
+ }
141
+ function identityRoot(dataHome) {
142
+ return join(dataHome, "identity");
143
+ }
144
+ function profileDir(root, profile) {
145
+ return join(root, "profiles", profile);
146
+ }
147
+ function presetsRoot(root) {
148
+ return join(root, "presets");
149
+ }
150
+ function profileEditsRoot(root, profile) {
151
+ return join(profileDir(root, profile), ".edits");
152
+ }
153
+ function profileEditDir(root, profile, id) {
154
+ return join(profileEditsRoot(root, profile), id);
155
+ }
156
+ function presetLocaleDir(root, id, locale) {
157
+ return join(presetsRoot(root), id, locale);
158
+ }
159
+ function provisionMarker(root) {
160
+ return join(root, ".provisioned");
161
+ }
162
+
163
+ // lib/base.js
164
+ var FRAMEWORK_PACKAGE = "@avantf/dsh-plugin-base";
165
+ var DEGRADE_WORDS = {
166
+ prefix: "envinit:",
167
+ loadFailure: "the identity plugin mounts with its own data-home resolution and no shared base capabilities",
168
+ withheld: `the family data-home rule comes from this plugin's own mirror instead of ${FRAMEWORK_PACKAGE}`
169
+ };
170
+ async function loadBaseOnce(options) {
171
+ const log = options.log;
172
+ const module = options.framework ?? await loadFramework({
173
+ logger: {
174
+ warn: (message) => {
175
+ log?.warn(`${FRAMEWORK_PACKAGE}: ${message}`);
176
+ },
177
+ info: (message) => {
178
+ log?.info(`${FRAMEWORK_PACKAGE}: ${message}`);
179
+ }
180
+ }
181
+ });
182
+ if (module === void 0) {
183
+ log?.warn(`${FRAMEWORK_PACKAGE} could not be made available; the identity plugin mounts anyway and resolves the data home with its own mirror`);
184
+ return void 0;
185
+ }
186
+ if (!gateOrDegrade(module, log, DEGRADE_WORDS))
187
+ return void 0;
188
+ return { module, dataHome: (input) => readDataHome(module, input) };
189
+ }
190
+ function readDataHome(module, input) {
191
+ try {
192
+ if (typeof module.resolveDataHome === "function")
193
+ return module.resolveDataHome(input);
194
+ } catch {
195
+ }
196
+ return resolveDataHomeMirror(input);
197
+ }
198
+ var cache = createLoadCache((options) => loadGuarded(options, loadBaseOnce, options.log, DEGRADE_WORDS));
199
+ function loadBase(options = {}) {
200
+ return cache.load(options);
201
+ }
202
+
203
+ // lib/gateway.js
204
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
205
+
206
+ // lib/presets.js
207
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
208
+ import { join as join2 } from "node:path";
209
+ import { fileURLToPath } from "node:url";
210
+
211
+ // lib/atomic-write.js
212
+ import { renameSync, unlinkSync, writeFileSync } from "node:fs";
213
+ function writeFileAtomically(path, text2) {
214
+ const temporary = `${path}.tmp-${String(process.pid)}`;
215
+ try {
216
+ writeFileSync(temporary, text2, "utf8");
217
+ renameSync(temporary, path);
218
+ } catch (error) {
219
+ try {
220
+ unlinkSync(temporary);
221
+ } catch {
222
+ }
223
+ throw error;
224
+ }
225
+ }
226
+
227
+ // lib/presets.js
228
+ var LOCALE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
229
+ var DEFAULT_PRESET_ID = "assistant";
230
+ function isValidPresetId(id) {
231
+ return PRESET_ID_PATTERN.test(id);
232
+ }
233
+ function isValidLocale(locale) {
234
+ return LOCALE_PATTERN.test(locale);
235
+ }
236
+ function resolveAssetsDir(from = import.meta.url) {
237
+ for (const relative of ["./assets/presets", "../assets/presets"]) {
238
+ try {
239
+ const candidate = fileURLToPath(new URL(relative, from));
240
+ if (existsSync(candidate))
241
+ return candidate;
242
+ } catch {
243
+ }
244
+ }
245
+ return void 0;
246
+ }
247
+ function packagedPresetIds(assetsDir) {
248
+ try {
249
+ return readdirSync(assetsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isValidPresetId(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
250
+ } catch {
251
+ return [];
252
+ }
253
+ }
254
+ function materializePresets(options) {
255
+ const copied = [];
256
+ const skipped = [];
257
+ const target = presetsRoot(options.root);
258
+ const marker = provisionMarker(options.root);
259
+ const packaged = packagedPresetIds(options.assetsDir);
260
+ const released = readReleasedPresets(marker);
261
+ const known = released ?? packaged;
262
+ try {
263
+ mkdirSync(target, { recursive: true });
264
+ for (const id of packaged) {
265
+ const destination = join2(target, id);
266
+ if (known.includes(id)) {
267
+ skipped.push(id);
268
+ continue;
269
+ }
270
+ if (copyMissing(join2(options.assetsDir, id), destination))
271
+ copied.push(id);
272
+ else
273
+ skipped.push(id);
274
+ }
275
+ writeFileAtomically(marker, `${JSON.stringify({
276
+ version: options.version,
277
+ at: (/* @__PURE__ */ new Date()).toISOString(),
278
+ presets: [.../* @__PURE__ */ new Set([...known, ...copied])].sort((left, right) => left.localeCompare(right)),
279
+ copied
280
+ }, null, 2)}
281
+ `);
282
+ } catch (error) {
283
+ options.logger?.warn(`releasing the built-in presets into ${target} failed (${reasonOf2(error)}); the plugin mounts with the presets already on disk`);
284
+ return { copied, skipped };
285
+ }
286
+ if (copied.length > 0)
287
+ options.logger?.info(`released built-in presets: ${copied.join(", ")}`);
288
+ return { copied, skipped };
289
+ }
290
+ function copyMissing(source, destination) {
291
+ if (!existsSync(source))
292
+ return false;
293
+ let wrote = false;
294
+ mkdirSync(destination, { recursive: true });
295
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
296
+ const from = join2(source, entry.name);
297
+ const to = join2(destination, entry.name);
298
+ if (entry.isDirectory()) {
299
+ if (copyMissing(from, to))
300
+ wrote = true;
301
+ continue;
302
+ }
303
+ if (existsSync(to))
304
+ continue;
305
+ copyFileSync(from, to);
306
+ wrote = true;
307
+ }
308
+ return wrote;
309
+ }
310
+ function readReleasedPresets(marker) {
311
+ let raw;
312
+ try {
313
+ raw = readFileSync(marker, "utf8");
314
+ } catch {
315
+ return [];
316
+ }
317
+ try {
318
+ const parsed = JSON.parse(raw);
319
+ if (!Array.isArray(parsed.presets))
320
+ return void 0;
321
+ return parsed.presets.filter((id) => typeof id === "string" && isValidPresetId(id));
322
+ } catch {
323
+ return void 0;
324
+ }
325
+ }
326
+ function listPresets(root) {
327
+ const base = presetsRoot(root);
328
+ let ids = [];
329
+ try {
330
+ ids = readdirSync(base, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isValidPresetId(entry.name)).map((entry) => entry.name);
331
+ } catch {
332
+ return [];
333
+ }
334
+ return ids.sort((left, right) => left.localeCompare(right)).map((id) => {
335
+ let locales = [];
336
+ try {
337
+ locales = readdirSync(join2(base, id), { withFileTypes: true }).filter((entry) => entry.isDirectory() && isValidLocale(entry.name)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
338
+ } catch {
339
+ locales = [];
340
+ }
341
+ return { id, locales };
342
+ });
343
+ }
344
+ function resolvingLocale(root, id, requested) {
345
+ if (isValidLocale(requested) && existsSync(presetLocaleDir(root, id, requested)))
346
+ return requested;
347
+ if (existsSync(presetLocaleDir(root, id, FALLBACK_LOCALE)))
348
+ return FALLBACK_LOCALE;
349
+ return void 0;
350
+ }
351
+ function identityFilesIn(dir) {
352
+ return IDENTITY_FILES.map((name2) => {
353
+ const file = `${name2}.md`;
354
+ let text2 = "";
355
+ try {
356
+ text2 = readFileSync(join2(dir, file), "utf8");
357
+ } catch {
358
+ text2 = "";
359
+ }
360
+ return { name: name2, file, text: text2, present: text2.trim() !== "", bytes: Buffer.byteLength(text2, "utf8") };
361
+ });
362
+ }
363
+ function writeIdentityFiles(dir, files) {
364
+ const written = [];
365
+ const missing = [];
366
+ let ready = false;
367
+ for (const file of files) {
368
+ if (!file.present) {
369
+ missing.push(file.file);
370
+ continue;
371
+ }
372
+ if (!ready) {
373
+ mkdirSync(dir, { recursive: true });
374
+ ready = true;
375
+ }
376
+ writeFileAtomically(join2(dir, file.file), file.text);
377
+ written.push(file.file);
378
+ }
379
+ return { written, missing };
380
+ }
381
+ function readPreset(root, id, requestedLocale) {
382
+ if (!isValidPresetId(id))
383
+ return void 0;
384
+ const requested = isValidLocale(requestedLocale) ? requestedLocale : FALLBACK_LOCALE;
385
+ const resolved = resolvingLocale(root, id, requested);
386
+ if (resolved === void 0)
387
+ return void 0;
388
+ return {
389
+ id,
390
+ requestedLocale: requested,
391
+ resolvedLocale: resolved,
392
+ fellBack: resolved !== requested,
393
+ files: identityFilesIn(presetLocaleDir(root, id, resolved))
394
+ };
395
+ }
396
+ function writePresetFile(root, id, locale, name2, text2) {
397
+ if (!isValidPresetId(id))
398
+ throw new Error(`invalid preset id: ${id}`);
399
+ if (!isValidLocale(locale))
400
+ throw new Error(`invalid locale: ${locale}`);
401
+ const dir = presetLocaleDir(root, id, locale);
402
+ mkdirSync(dir, { recursive: true });
403
+ writeFileAtomically(join2(dir, `${name2}.md`), text2);
404
+ }
405
+ function deletePreset(root, id) {
406
+ if (!isValidPresetId(id))
407
+ return false;
408
+ const dir = join2(presetsRoot(root), id);
409
+ if (!existsSync(dir))
410
+ return false;
411
+ rmSync(dir, { recursive: true, force: true });
412
+ return true;
413
+ }
414
+ function applyPresetToProfile(options) {
415
+ const preset = readPreset(options.root, options.id, options.locale);
416
+ if (preset === void 0)
417
+ return void 0;
418
+ const { written, missing } = writeIdentityFiles(profileDir(options.root, options.profile), preset.files);
419
+ return { resolvedLocale: preset.resolvedLocale, fellBack: preset.fellBack, written, missing };
420
+ }
421
+ function seedProfileIfEmpty(options) {
422
+ const dir = profileDir(options.root, options.profile);
423
+ if (identityFilesIn(dir).some((file) => file.present))
424
+ return { seeded: false, written: [] };
425
+ const available = listPresets(options.root).map((preset) => preset.id);
426
+ const id = available.includes(DEFAULT_PRESET_ID) ? DEFAULT_PRESET_ID : available[0];
427
+ if (id === void 0) {
428
+ return { seeded: false, written: [], error: "no preset is available to seed this identity; apply a preset or write the files" };
429
+ }
430
+ const applied = applyPresetToProfile({ root: options.root, profile: options.profile, id, locale: options.locale });
431
+ if (applied === void 0)
432
+ return { seeded: false, written: [], error: `preset could not be read: ${id}` };
433
+ return {
434
+ seeded: true,
435
+ preset: id,
436
+ resolvedLocale: applied.resolvedLocale,
437
+ fellBack: applied.fellBack,
438
+ written: applied.written
439
+ };
440
+ }
441
+ function readProfileEdit(options) {
442
+ if (!isValidPresetId(options.id))
443
+ return void 0;
444
+ const dir = profileEditDir(options.root, options.profile, options.id);
445
+ if (!existsSync(dir))
446
+ return void 0;
447
+ const files = identityFilesIn(dir);
448
+ return { files, bytes: files.reduce((total, file) => total + file.bytes, 0) };
449
+ }
450
+ function snapshotProfileEdit(options) {
451
+ if (!isValidPresetId(options.id))
452
+ return void 0;
453
+ const files = options.files ?? identityFilesIn(profileDir(options.root, options.profile));
454
+ const { written } = writeIdentityFiles(profileEditDir(options.root, options.profile, options.id), files);
455
+ return { written };
456
+ }
457
+ function applyProfileEditToProfile(options) {
458
+ const edit = readProfileEdit(options);
459
+ if (edit === void 0)
460
+ return void 0;
461
+ const { written } = writeIdentityFiles(profileDir(options.root, options.profile), edit.files);
462
+ return { written };
463
+ }
464
+ function dropProfileEdit(options) {
465
+ if (!isValidPresetId(options.id))
466
+ return false;
467
+ const dir = profileEditDir(options.root, options.profile, options.id);
468
+ if (!existsSync(dir))
469
+ return false;
470
+ rmSync(dir, { recursive: true, force: true });
471
+ return true;
472
+ }
473
+ function profileDiffersFromPreset(options) {
474
+ if (!isValidPresetId(options.id))
475
+ return false;
476
+ const files = options.effectiveFiles ?? identityFilesIn(profileDir(options.root, options.profile));
477
+ const effective = files.map((file) => file.text.trim());
478
+ const locales = [.../* @__PURE__ */ new Set([options.locale, ...LOCALE_IDS])];
479
+ const matchesSomeLocale = locales.some((locale) => {
480
+ const preset = readPreset(options.root, options.id, locale);
481
+ if (preset === void 0)
482
+ return false;
483
+ return IDENTITY_FILES.every((name2, index) => {
484
+ const file = preset.files.find((candidate) => candidate.file === `${name2}.md`);
485
+ return effective[index] === (file?.present === true ? file.text.trim() : "");
486
+ });
487
+ });
488
+ return !matchesSomeLocale;
489
+ }
490
+ function saveProfileAsPreset(options) {
491
+ if (!isValidPresetId(options.id))
492
+ return void 0;
493
+ const locale = isValidLocale(options.locale) ? options.locale : FALLBACK_LOCALE;
494
+ const dir = presetLocaleDir(options.root, options.id, locale);
495
+ if (existsSync(dir))
496
+ return void 0;
497
+ const files = identityFilesIn(profileDir(options.root, options.profile));
498
+ const { written } = writeIdentityFiles(dir, files);
499
+ return { written };
500
+ }
501
+ function reasonOf2(error) {
502
+ return error instanceof Error ? error.message : String(error);
503
+ }
504
+
505
+ // lib/wire.js
506
+ import * as z from "zod";
507
+ var PACKAGE = "@avantf/dsh-identity";
508
+ var NAMESPACE = "avantfIdentity";
509
+ function strict(schema, typeSymbol) {
510
+ return { mode: "strict", typeSymbol, schema, create: () => schema };
511
+ }
512
+ var endpointId = (method) => `${PACKAGE}#${NAMESPACE}/${method}`;
513
+ var fieldSymbol = (method, field) => `${PACKAGE}#${NAMESPACE}/${method}:${field}`;
514
+ function direct(method, args) {
515
+ return {
516
+ id: endpointId(method),
517
+ service: NAMESPACE,
518
+ namespace: NAMESPACE,
519
+ method,
520
+ invocation: { kind: "direct" },
521
+ parameters: [{ name: "args", wire: "args", source: "json", codec: strict(args, fieldSymbol(method, "args")) }],
522
+ result: strict(z.any(), fieldSymbol(method, "result"))
523
+ };
524
+ }
525
+ function directNoArgs(method) {
526
+ return {
527
+ id: endpointId(method),
528
+ service: NAMESPACE,
529
+ namespace: NAMESPACE,
530
+ method,
531
+ invocation: { kind: "direct" },
532
+ parameters: [],
533
+ result: strict(z.any(), fieldSymbol(method, "result"))
534
+ };
535
+ }
536
+ var optionalText = z.string().optional();
537
+ var text = z.string();
538
+ var descriptors = [
539
+ direct("status", z.object({ locale: optionalText })),
540
+ directNoArgs("listPresets"),
541
+ directNoArgs("readProfile"),
542
+ direct("writeProfileFile", z.object({ name: text, text })),
543
+ direct("ensureProfile", z.object({ locale: optionalText })),
544
+ direct("applyPreset", z.object({ id: text, locale: optionalText })),
545
+ direct("resetToPreset", z.object({ id: text, locale: optionalText })),
546
+ direct("saveAsPreset", z.object({ id: text, locale: optionalText })),
547
+ direct("readPreset", z.object({ id: text, locale: optionalText })),
548
+ direct("writePresetFile", z.object({ id: text, name: text, text, locale: optionalText })),
549
+ direct("deletePreset", z.object({ id: text }))
550
+ ];
551
+ var fileTextSchema = z.object({
552
+ name: text,
553
+ file: text,
554
+ text,
555
+ present: z.boolean(),
556
+ bytes: z.number()
557
+ });
558
+ var fileStatusSchema = z.object({
559
+ name: text,
560
+ file: text,
561
+ path: text,
562
+ present: z.boolean(),
563
+ bytes: z.number()
564
+ });
565
+ var presetSummarySchema = z.object({
566
+ id: text,
567
+ locales: z.array(text),
568
+ active: z.boolean()
569
+ });
570
+ var statusResultSchema = z.object({
571
+ enabled: z.boolean(),
572
+ profile: text,
573
+ dataHome: text,
574
+ identityDir: text,
575
+ presetsDir: text,
576
+ fileCount: z.number(),
577
+ totalBytes: z.number(),
578
+ maxBytes: z.number(),
579
+ interpolate: z.boolean(),
580
+ replaceScope: text,
581
+ drop: z.array(text),
582
+ fallbackToNative: z.boolean(),
583
+ activePreset: text,
584
+ activeLocale: text,
585
+ hasLocalEdit: z.boolean(),
586
+ locale: text,
587
+ localeSource: text,
588
+ files: z.array(fileStatusSchema),
589
+ presets: z.array(presetSummarySchema)
590
+ });
591
+ var profileResultSchema = z.object({
592
+ profile: text,
593
+ identityDir: text,
594
+ totalBytes: z.number(),
595
+ files: z.array(fileTextSchema)
596
+ });
597
+ var presetResultSchema = z.object({
598
+ found: z.boolean(),
599
+ id: text,
600
+ requestedLocale: text,
601
+ resolvedLocale: text,
602
+ fellBack: z.boolean(),
603
+ files: z.array(fileTextSchema)
604
+ });
605
+ var writeResultSchema = z.object({
606
+ ok: z.boolean(),
607
+ error: optionalText,
608
+ resolvedLocale: optionalText,
609
+ fellBack: z.boolean().optional(),
610
+ files: z.array(text).optional()
611
+ });
612
+ var declare = (name2, schema) => ({ name: name2, schema, create: () => schema });
613
+ var declaredSchemas = [
614
+ ...descriptors.flatMap((descriptor) => descriptor.parameters.map((parameter) => declare(`${descriptor.method}${parameter.name}`, parameter.codec.schema))),
615
+ declare("statusResult", statusResultSchema),
616
+ declare("profileResult", profileResultSchema),
617
+ declare("writeResult", writeResultSchema),
618
+ declare("presetResult", presetResultSchema)
619
+ ];
620
+ var model = {
621
+ services: [
622
+ {
623
+ key: NAMESPACE,
624
+ exportName: NAMESPACE,
625
+ description: "Identity files: the profile identity, its built-in/edited presets, and the replacement switch",
626
+ summary: "avantf-identity identity files",
627
+ tags: [],
628
+ members: [],
629
+ types: []
630
+ }
631
+ ],
632
+ // Empty on purpose, but declared: the client mount reads every field of the model.
633
+ events: [],
634
+ objects: []
635
+ };
636
+ var hostContribution = {
637
+ package: PACKAGE,
638
+ face: "host",
639
+ schemas: declaredSchemas,
640
+ model,
641
+ invocations: descriptors
642
+ };
643
+
644
+ // lib/gateway.js
645
+ var __runInitializers = function(thisArg, initializers, value) {
646
+ var useValue = arguments.length > 2;
647
+ for (var i = 0; i < initializers.length; i++) {
648
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
649
+ }
650
+ return useValue ? value : void 0;
651
+ };
652
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
653
+ function accept(f) {
654
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
655
+ return f;
656
+ }
657
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
658
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
659
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
660
+ var _, done = false;
661
+ for (var i = decorators.length - 1; i >= 0; i--) {
662
+ var context = {};
663
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
664
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
665
+ context.addInitializer = function(f) {
666
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
667
+ extraInitializers.push(accept(f || null));
668
+ };
669
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
670
+ if (kind === "accessor") {
671
+ if (result === void 0) continue;
672
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
673
+ if (_ = accept(result.get)) descriptor.get = _;
674
+ if (_ = accept(result.set)) descriptor.set = _;
675
+ if (_ = accept(result.init)) initializers.unshift(_);
676
+ } else if (_ = accept(result)) {
677
+ if (kind === "field") initializers.unshift(_);
678
+ else descriptor[key] = _;
679
+ }
680
+ }
681
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
682
+ done = true;
683
+ };
684
+ var ENTRY_ID = "avantf-identity";
685
+ var LOCALE_NAMESPACE = "locale";
686
+ var LOCALE_PREFERENCE_FIELD = "preference";
687
+ var IdentityGateway = (() => {
688
+ let _classSuper = TypertRemoteService;
689
+ let _instanceExtraInitializers = [];
690
+ let _status_decorators;
691
+ let _listPresets_decorators;
692
+ let _readProfile_decorators;
693
+ let _writeProfileFile_decorators;
694
+ let _ensureProfile_decorators;
695
+ let _applyPreset_decorators;
696
+ let _resetToPreset_decorators;
697
+ let _saveAsPreset_decorators;
698
+ let _readPreset_decorators;
699
+ let _writePresetFile_decorators;
700
+ let _deletePreset_decorators;
701
+ return class IdentityGateway extends _classSuper {
702
+ static {
703
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
704
+ _status_decorators = [Remote("status")];
705
+ _listPresets_decorators = [Remote("listPresets")];
706
+ _readProfile_decorators = [Remote("readProfile")];
707
+ _writeProfileFile_decorators = [Remote("writeProfileFile")];
708
+ _ensureProfile_decorators = [Remote("ensureProfile")];
709
+ _applyPreset_decorators = [Remote("applyPreset")];
710
+ _resetToPreset_decorators = [Remote("resetToPreset")];
711
+ _saveAsPreset_decorators = [Remote("saveAsPreset")];
712
+ _readPreset_decorators = [Remote("readPreset")];
713
+ _writePresetFile_decorators = [Remote("writePresetFile")];
714
+ _deletePreset_decorators = [Remote("deletePreset")];
715
+ __esDecorate(this, null, _status_decorators, { kind: "method", name: "status", static: false, private: false, access: { has: (obj) => "status" in obj, get: (obj) => obj.status }, metadata: _metadata }, null, _instanceExtraInitializers);
716
+ __esDecorate(this, null, _listPresets_decorators, { kind: "method", name: "listPresets", static: false, private: false, access: { has: (obj) => "listPresets" in obj, get: (obj) => obj.listPresets }, metadata: _metadata }, null, _instanceExtraInitializers);
717
+ __esDecorate(this, null, _readProfile_decorators, { kind: "method", name: "readProfile", static: false, private: false, access: { has: (obj) => "readProfile" in obj, get: (obj) => obj.readProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
718
+ __esDecorate(this, null, _writeProfileFile_decorators, { kind: "method", name: "writeProfileFile", static: false, private: false, access: { has: (obj) => "writeProfileFile" in obj, get: (obj) => obj.writeProfileFile }, metadata: _metadata }, null, _instanceExtraInitializers);
719
+ __esDecorate(this, null, _ensureProfile_decorators, { kind: "method", name: "ensureProfile", static: false, private: false, access: { has: (obj) => "ensureProfile" in obj, get: (obj) => obj.ensureProfile }, metadata: _metadata }, null, _instanceExtraInitializers);
720
+ __esDecorate(this, null, _applyPreset_decorators, { kind: "method", name: "applyPreset", static: false, private: false, access: { has: (obj) => "applyPreset" in obj, get: (obj) => obj.applyPreset }, metadata: _metadata }, null, _instanceExtraInitializers);
721
+ __esDecorate(this, null, _resetToPreset_decorators, { kind: "method", name: "resetToPreset", static: false, private: false, access: { has: (obj) => "resetToPreset" in obj, get: (obj) => obj.resetToPreset }, metadata: _metadata }, null, _instanceExtraInitializers);
722
+ __esDecorate(this, null, _saveAsPreset_decorators, { kind: "method", name: "saveAsPreset", static: false, private: false, access: { has: (obj) => "saveAsPreset" in obj, get: (obj) => obj.saveAsPreset }, metadata: _metadata }, null, _instanceExtraInitializers);
723
+ __esDecorate(this, null, _readPreset_decorators, { kind: "method", name: "readPreset", static: false, private: false, access: { has: (obj) => "readPreset" in obj, get: (obj) => obj.readPreset }, metadata: _metadata }, null, _instanceExtraInitializers);
724
+ __esDecorate(this, null, _writePresetFile_decorators, { kind: "method", name: "writePresetFile", static: false, private: false, access: { has: (obj) => "writePresetFile" in obj, get: (obj) => obj.writePresetFile }, metadata: _metadata }, null, _instanceExtraInitializers);
725
+ __esDecorate(this, null, _deletePreset_decorators, { kind: "method", name: "deletePreset", static: false, private: false, access: { has: (obj) => "deletePreset" in obj, get: (obj) => obj.deletePreset }, metadata: _metadata }, null, _instanceExtraInitializers);
726
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
727
+ }
728
+ state = __runInitializers(this, _instanceExtraInitializers);
729
+ settings;
730
+ log;
731
+ constructor(ctx, state, settings, log) {
732
+ super(ctx, NAMESPACE);
733
+ this.state = state;
734
+ this.settings = settings;
735
+ this.log = log;
736
+ }
737
+ /** The settings page's ONE entry point: the header, the three file slots, and the preset library. */
738
+ status(args) {
739
+ return this.state.status(this.settings(), this.localeInfo(args.locale));
740
+ }
741
+ /** The preset library, with the active one marked. */
742
+ listPresets() {
743
+ return { presets: this.state.presetSummaries(this.settings().activePreset) };
744
+ }
745
+ /** The three files of the effective identity, with their text. */
746
+ readProfile() {
747
+ return this.state.readProfile();
748
+ }
749
+ /** Write one effective-identity file. The next assembly reads it; nothing else is touched. */
750
+ writeProfileFile(args) {
751
+ const result = this.state.writeProfileFile(args.name, args.text);
752
+ if (result.ok)
753
+ this.log.info(`wrote ${args.name}.md (${String(Buffer.byteLength(args.text, "utf8"))} bytes); the next assembly reads it`);
754
+ return result;
755
+ }
756
+ /**
757
+ * Materialize an identity for the current profile when it has none: the switch's FIRST enable calls
758
+ * this, so turning the identity on produces something to edit instead of three empty editors over a
759
+ * prompt that still says `harness:identity` (the first run releases the preset LIBRARY only).
760
+ *
761
+ * A no-op once any file carries text — it never overwrites what the user wrote — and a reported error,
762
+ * never a silent nothing, when the library is empty.
763
+ */
764
+ async ensureProfile(args) {
765
+ const { locale } = this.localeInfo(args.locale);
766
+ const seed = this.state.ensureProfile(locale);
767
+ if (seed.error !== void 0)
768
+ return { ok: false, error: seed.error };
769
+ if (seed.seeded) {
770
+ this.log.info(`seeded the effective identity for profile ${this.state.profile} from preset ${String(seed.preset ?? "")} (${String(seed.resolvedLocale ?? locale)})`);
771
+ await this.remember({ activePreset: seed.preset ?? "", activeLocale: seed.resolvedLocale ?? locale });
772
+ }
773
+ return {
774
+ ok: true,
775
+ ...seed.resolvedLocale === void 0 ? {} : { resolvedLocale: seed.resolvedLocale },
776
+ ...seed.fellBack === void 0 ? {} : { fellBack: seed.fellBack },
777
+ ...seed.written.length === 0 ? {} : { files: seed.written }
778
+ };
779
+ }
780
+ /**
781
+ * Switch to a preset, then record which preset/locale is in force.
782
+ *
783
+ * The identity being LEFT is named to the state, which files its current content away under that id
784
+ * before the write — that is what makes coming back to it restore the user's own text instead of the
785
+ * library's. Leaving an identity that no preset owns is refused (see `IdentityState.applyPreset`).
786
+ */
787
+ async applyPreset(args) {
788
+ const { locale } = this.localeInfo(args.locale);
789
+ const result = this.state.applyPreset(args.id, locale, this.settings().activePreset);
790
+ if (result.ok) {
791
+ this.log.info(result.restoredEdit === true ? `switched to ${args.id}: restored this profile's own version` : `applied preset ${args.id} (${String(result.resolvedLocale ?? locale)}${result.fellBack === true ? ", fell back to en" : ""})`);
792
+ await this.remember({ activePreset: args.id, activeLocale: result.resolvedLocale ?? this.settings().activeLocale ?? locale });
793
+ }
794
+ return result;
795
+ }
796
+ /**
797
+ * Drop this profile's own version of `id` and put the library preset's text back in force — the way
798
+ * out of a customized identity. Destructive by intent; the page confirms it.
799
+ */
800
+ async resetToPreset(args) {
801
+ const { locale } = this.localeInfo(args.locale);
802
+ const result = this.state.resetToPreset(args.id, locale);
803
+ if (result.ok)
804
+ this.log.info(`restored the preset text of ${args.id} (dropped this profile's version: ${String(result.dropped === true)})`);
805
+ return result;
806
+ }
807
+ /** Save the effective identity as a new preset under the locale the caller is showing. */
808
+ saveAsPreset(args) {
809
+ const { locale } = this.localeInfo(args.locale);
810
+ return this.state.saveAsPreset(args.id, locale);
811
+ }
812
+ /** Read one preset (English fallback when the requested locale has no directory). */
813
+ readPreset(args) {
814
+ const { locale } = this.localeInfo(args.locale);
815
+ return this.state.readPreset(args.id, locale);
816
+ }
817
+ /** Write one preset file. Never touches the effective identity (the page says so out loud). */
818
+ writePresetFile(args) {
819
+ const { locale } = this.localeInfo(args.locale);
820
+ return this.state.writePresetFile(args.id, args.name, args.text, locale);
821
+ }
822
+ /**
823
+ * Delete one preset from the library (and, with it, this profile's version of it).
824
+ *
825
+ * Deleting the identity that is IN FORCE also forgets it: the effective files stay where they are, but
826
+ * their id is gone, so the state must treat them as unowned from here on — otherwise the next switch
827
+ * would file them away under a name the dropdown no longer shows.
828
+ */
829
+ async deletePreset(args) {
830
+ const wasActive = this.settings().activePreset === args.id;
831
+ const result = this.state.deletePreset(args.id);
832
+ if (result.ok) {
833
+ this.log.info(`deleted preset ${args.id}`);
834
+ if (wasActive)
835
+ await this.remember({ activePreset: "", activeLocale: "" });
836
+ }
837
+ return result;
838
+ }
839
+ /**
840
+ * Record which preset/locale is in force, so a later language change can offer "apply it again in
841
+ * `<new language>`" WITHOUT ever rewriting the user's files. Best-effort: a host whose settings
842
+ * service is absent or read-only mounts and works, it just cannot remember this.
843
+ */
844
+ async remember(patch) {
845
+ try {
846
+ const settings = this.ctx.get("settings");
847
+ if (settings === void 0 || typeof settings.update !== "function")
848
+ return;
849
+ await settings.update(ENTRY_ID, patch);
850
+ } catch (error) {
851
+ this.log.warn(`could not record the active preset in the profile config (${reasonOf3(error)}); the identity itself is applied`);
852
+ }
853
+ }
854
+ /**
855
+ * The language to apply in: the caller's own (UI-triggered applies always carry it), else the host's
856
+ * explicit `locale` preference, else English.
857
+ *
858
+ * The browser's `locale` service exists only on the CLIENT half, so a headless caller reads the same
859
+ * preference through the settings projection — the field `dsh-client-locale` writes only when a user
860
+ * has explicitly chosen a language, which is exactly the "no browser to delegate to" case.
861
+ */
862
+ localeInfo(requested) {
863
+ if (typeof requested === "string" && isValidLocale(requested))
864
+ return { locale: requested, source: "client" };
865
+ const host = this.hostLocale();
866
+ if (host !== void 0)
867
+ return { locale: host, source: "host" };
868
+ return { locale: FALLBACK_LOCALE, source: "fallback" };
869
+ }
870
+ hostLocale() {
871
+ try {
872
+ const settings = this.ctx.get("settings");
873
+ const descriptors2 = settings?.describe?.();
874
+ if (!Array.isArray(descriptors2))
875
+ return void 0;
876
+ for (const descriptor of descriptors2) {
877
+ const record = descriptor;
878
+ if (record?.ns !== LOCALE_NAMESPACE)
879
+ continue;
880
+ const value = record.value;
881
+ const preference = value?.[LOCALE_PREFERENCE_FIELD];
882
+ if (typeof preference === "string" && isValidLocale(preference))
883
+ return preference;
884
+ }
885
+ } catch {
886
+ }
887
+ return void 0;
888
+ }
889
+ };
890
+ })();
891
+ function reasonOf3(error) {
892
+ return error instanceof Error ? error.message : String(error);
893
+ }
894
+
895
+ // lib/log.js
896
+ var LOG_PREFIX = "[avantf-identity]";
897
+ function createLogger(host, sink = (line) => {
898
+ console.error(line);
899
+ }) {
900
+ const emit = (level, message) => {
901
+ sink(`${LOG_PREFIX} ${level.toUpperCase()} ${message}`);
902
+ try {
903
+ host?.[level](`${LOG_PREFIX} ${message}`);
904
+ } catch {
905
+ }
906
+ };
907
+ return {
908
+ info: (message) => {
909
+ emit("info", message);
910
+ },
911
+ warn: (message) => {
912
+ emit("warn", message);
913
+ },
914
+ error: (message) => {
915
+ emit("error", message);
916
+ }
917
+ };
918
+ }
919
+
920
+ // lib/session-freeze.js
921
+ var SessionFreeze = class {
922
+ held = /* @__PURE__ */ new WeakMap();
923
+ /**
924
+ * The frozen text for `key`, calling `read` exactly once per key.
925
+ *
926
+ * @param key - the session object, or `undefined` to read live on every call.
927
+ * @param read - reads the current text; called only when no value is held yet.
928
+ * @returns the text this session is stuck with.
929
+ */
930
+ text(key, read) {
931
+ if (key === void 0)
932
+ return read();
933
+ const existing = this.held.get(key);
934
+ if (existing !== void 0)
935
+ return existing;
936
+ const value = read();
937
+ this.held.set(key, value);
938
+ return value;
939
+ }
940
+ };
941
+
942
+ // lib/state.js
943
+ import { existsSync as existsSync2 } from "node:fs";
944
+
945
+ // lib/documents.js
946
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync } from "node:fs";
947
+ import { dirname, join as join3 } from "node:path";
948
+ function fileNameOf(name2) {
949
+ return `${name2}.md`;
950
+ }
951
+ function rulesHeading(body) {
952
+ return /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/u.test(body) ? "**规则**" : "**Rules**";
953
+ }
954
+ var IdentityDocuments = class {
955
+ dir;
956
+ options;
957
+ cache = /* @__PURE__ */ new Map();
958
+ constructor(dir, options = { maxBytes: () => 0 }) {
959
+ this.dir = dir;
960
+ this.options = options;
961
+ }
962
+ /** The directory the three files live in. */
963
+ get directory() {
964
+ return this.dir;
965
+ }
966
+ /** One file's path. */
967
+ pathOf(name2) {
968
+ return join3(this.dir, fileNameOf(name2));
969
+ }
970
+ /**
971
+ * Read one file, or `''` when it is absent or unreadable.
972
+ *
973
+ * Cached on `(mtimeMs, size)`: the two together are what a text editor changes, and the stat is an
974
+ * order of magnitude cheaper than the read for the tens-of-KB files this holds.
975
+ */
976
+ read(name2) {
977
+ const path = this.pathOf(name2);
978
+ try {
979
+ const stat = statSync(path);
980
+ const cached = this.cache.get(name2);
981
+ if (cached !== void 0 && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
982
+ return cached.text;
983
+ const text2 = readFileSync2(path, "utf8");
984
+ this.cache.set(name2, { mtimeMs: stat.mtimeMs, size: stat.size, text: text2 });
985
+ return text2;
986
+ } catch {
987
+ this.cache.delete(name2);
988
+ return "";
989
+ }
990
+ }
991
+ /** Every file's text, in render order. */
992
+ readAll() {
993
+ return IDENTITY_FILES.map((name2) => {
994
+ const text2 = this.read(name2);
995
+ return { name: name2, file: fileNameOf(name2), text: text2, present: text2.trim() !== "", bytes: Buffer.byteLength(text2, "utf8") };
996
+ });
997
+ }
998
+ /** Every file's presence and size, without rendering. */
999
+ status() {
1000
+ return IDENTITY_FILES.map((name2) => {
1001
+ const text2 = this.read(name2);
1002
+ return {
1003
+ name: name2,
1004
+ file: fileNameOf(name2),
1005
+ path: this.pathOf(name2),
1006
+ present: text2.trim() !== "",
1007
+ bytes: Buffer.byteLength(text2, "utf8")
1008
+ };
1009
+ });
1010
+ }
1011
+ /**
1012
+ * The prompt text: the non-empty bodies joined by one blank line, the rules part under its own
1013
+ * heading (see {@link RULES_HEADING}), truncated to `maxBytes` (when it is positive) with a visible
1014
+ * notice appended and a warning naming the files that overflowed.
1015
+ */
1016
+ render() {
1017
+ const parts = IDENTITY_FILES.map((name2) => ({ name: name2, body: this.read(name2).trim() })).filter((part) => part.body !== "");
1018
+ if (parts.length === 0)
1019
+ return "";
1020
+ const text2 = parts.map((part) => part.name === "RULES" ? `${rulesHeading(part.body)}
1021
+
1022
+ ${part.body}` : part.body).join("\n\n");
1023
+ const maxBytes = this.options.maxBytes();
1024
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0)
1025
+ return text2;
1026
+ if (Buffer.byteLength(text2, "utf8") <= maxBytes)
1027
+ return text2;
1028
+ const truncated = Buffer.from(text2, "utf8").subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
1029
+ this.options.logger?.warn(`identity files exceed maxBytes=${String(maxBytes)}; truncated (files: ${this.status().filter((s) => s.present).map((s) => s.file).join(", ")})`);
1030
+ return `${truncated}
1031
+
1032
+ [identity files truncated at ${String(maxBytes)} bytes; the full text is in ${this.dir}]`;
1033
+ }
1034
+ /** Total rendered-model bytes across the three files, without truncation. */
1035
+ bytes() {
1036
+ return this.status().reduce((total, file) => total + file.bytes, 0);
1037
+ }
1038
+ /** Drop the read cache, so the next read comes from disk. Called after every write. */
1039
+ invalidate() {
1040
+ this.cache.clear();
1041
+ }
1042
+ /**
1043
+ * Write one file atomically, so an assembly reading concurrently sees either the old text or the new
1044
+ * one, never a half-written file. The write itself is `writeFileAtomically`'s (shared with the preset
1045
+ * writers); the cache is dropped here, in a `finally`, so a failed write cannot leave it serving text
1046
+ * that is no longer on disk.
1047
+ */
1048
+ write(name2, text2) {
1049
+ if (!IDENTITY_FILES.includes(name2))
1050
+ throw new Error(`unknown identity file: ${String(name2)}`);
1051
+ mkdirSync2(this.dir, { recursive: true });
1052
+ try {
1053
+ writeFileAtomically(this.pathOf(name2), text2);
1054
+ } finally {
1055
+ this.invalidate();
1056
+ }
1057
+ }
1058
+ /** The identity directory itself; exported so a caller need not re-derive it. */
1059
+ get root() {
1060
+ return dirname(this.dir);
1061
+ }
1062
+ };
1063
+
1064
+ // lib/state.js
1065
+ var IdentityState = class {
1066
+ options;
1067
+ dataHomeValue;
1068
+ documentsCache;
1069
+ constructor(options) {
1070
+ this.options = options;
1071
+ this.dataHomeValue = options.dataHome;
1072
+ }
1073
+ /** The data home currently in force. */
1074
+ get dataHome() {
1075
+ return this.dataHomeValue;
1076
+ }
1077
+ /** `<data home>/identity`. */
1078
+ get root() {
1079
+ return identityRoot(this.dataHomeValue);
1080
+ }
1081
+ /** The profile whose identity is effective (the profile context's name, or the configured one). */
1082
+ get profile() {
1083
+ const profile = this.options.profile().trim();
1084
+ return profile === "" ? "default" : profile;
1085
+ }
1086
+ /** `profiles/<profile>/` — the ONE source of the effective identity. */
1087
+ get profileDir() {
1088
+ return profileDir(this.root, this.profile);
1089
+ }
1090
+ /** `presets/`. */
1091
+ get presetsDir() {
1092
+ return presetsRoot(this.root);
1093
+ }
1094
+ /** The three files of the effective identity (recreated when the data home or profile moves). */
1095
+ get documents() {
1096
+ const dir = this.profileDir;
1097
+ if (this.documentsCache?.dir !== dir) {
1098
+ this.documentsCache = {
1099
+ dir,
1100
+ documents: new IdentityDocuments(dir, {
1101
+ ...this.options.logger === void 0 ? {} : { logger: this.options.logger },
1102
+ maxBytes: this.options.maxBytes
1103
+ })
1104
+ };
1105
+ }
1106
+ return this.documentsCache.documents;
1107
+ }
1108
+ /** Point the state at the base's answer. A different root drops the cached reader. */
1109
+ setDataHome(next) {
1110
+ if (next === this.dataHomeValue)
1111
+ return;
1112
+ this.dataHomeValue = next;
1113
+ this.documentsCache = void 0;
1114
+ }
1115
+ /**
1116
+ * The section text: the rendered files when the switch is on, `''` when it is off. `''` is what
1117
+ * makes "off" byte-identical to native — the empty section is dropped by `renderPrompt`, and the
1118
+ * waterfall removes it as well.
1119
+ */
1120
+ sectionText(enabled) {
1121
+ if (!enabled)
1122
+ return "";
1123
+ try {
1124
+ return this.documents.render();
1125
+ } catch (error) {
1126
+ this.options.logger?.warn(`rendering the identity files failed (${reasonOf4(error)}); falling back to the native prompt`);
1127
+ return "";
1128
+ }
1129
+ }
1130
+ /** Release the built-in presets (missing-only). Never throws. */
1131
+ provision(assetsDir) {
1132
+ const source = assetsDir ?? this.options.assetsDir ?? resolveAssetsDir();
1133
+ if (source === void 0 || !existsSync2(source)) {
1134
+ this.options.logger?.warn("the built-in preset assets are missing from this build; no preset was released");
1135
+ return { copied: [], skipped: [] };
1136
+ }
1137
+ return materializePresets({
1138
+ assetsDir: source,
1139
+ root: this.root,
1140
+ version: this.options.version,
1141
+ ...this.options.logger === void 0 ? {} : { logger: this.options.logger }
1142
+ });
1143
+ }
1144
+ /** Every preset on disk, marking the one this profile is currently using. */
1145
+ presetSummaries(activePreset) {
1146
+ return listPresets(this.root).map((preset) => ({
1147
+ id: preset.id,
1148
+ locales: preset.locales,
1149
+ active: preset.id === activePreset
1150
+ }));
1151
+ }
1152
+ /** The effective identity's three files, with their text. */
1153
+ readProfile() {
1154
+ const files = this.documents.readAll();
1155
+ return {
1156
+ profile: this.profile,
1157
+ identityDir: this.profileDir,
1158
+ totalBytes: files.reduce((total, file) => total + file.bytes, 0),
1159
+ files
1160
+ };
1161
+ }
1162
+ /** Write one effective-identity file. Answers an error string instead of throwing. */
1163
+ writeProfileFile(name2, text2) {
1164
+ const file = identityNameOfLoose(name2);
1165
+ if (file === void 0)
1166
+ return { ok: false, error: `unknown identity file: ${name2}` };
1167
+ try {
1168
+ this.documents.write(file, text2);
1169
+ return { ok: true };
1170
+ } catch (error) {
1171
+ return { ok: false, error: reasonOf4(error) };
1172
+ }
1173
+ }
1174
+ /** Read one preset with locale fallback; `undefined` when the preset or its English copy is absent. */
1175
+ readPreset(id, locale) {
1176
+ const read = readPreset(this.root, id, locale);
1177
+ if (read === void 0) {
1178
+ return { found: false, id, requestedLocale: locale, resolvedLocale: FALLBACK_LOCALE, fellBack: false, files: [] };
1179
+ }
1180
+ return { found: true, ...read };
1181
+ }
1182
+ /** Write one preset file in one locale. */
1183
+ writePresetFile(id, name2, text2, locale) {
1184
+ const file = identityNameOfLoose(name2);
1185
+ if (file === void 0)
1186
+ return { ok: false, error: `unknown identity file: ${name2}` };
1187
+ if (!isValidPresetId(id))
1188
+ return { ok: false, error: `invalid preset id: ${id}` };
1189
+ try {
1190
+ writePresetFile(this.root, id, locale, file, text2);
1191
+ return { ok: true };
1192
+ } catch (error) {
1193
+ return { ok: false, error: reasonOf4(error) };
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Switch the effective identity to `id`.
1198
+ *
1199
+ * `previousId` is the identity being LEFT (the caller's live `activePreset`): its current content is
1200
+ * filed away under that id FIRST, so coming back to it later restores what the user wrote instead of
1201
+ * the library's factory text. The incoming identity is then taken from this profile's own version when
1202
+ * one exists, and from the library preset otherwise.
1203
+ *
1204
+ * `previousId === ''` with a non-empty effective identity means the user hand-wrote content that no
1205
+ * identity owns: switching away would leave it nowhere to come back to, so it is REFUSED with an
1206
+ * explanation rather than silently destroyed.
1207
+ */
1208
+ applyPreset(id, locale, previousId = "") {
1209
+ try {
1210
+ const effective = this.documents.readAll();
1211
+ if (previousId === "" && effective.some((file) => file.present)) {
1212
+ return { ok: false, error: 'this identity has no preset id, so switching away would destroy it; use "save as preset" first' };
1213
+ }
1214
+ if (previousId !== "")
1215
+ snapshotProfileEdit({ root: this.root, profile: this.profile, id: previousId, files: effective });
1216
+ const restored = applyProfileEditToProfile({ root: this.root, profile: this.profile, id });
1217
+ if (restored !== void 0) {
1218
+ this.documents.invalidate();
1219
+ return { ok: true, files: restored.written, restoredEdit: true };
1220
+ }
1221
+ return this.fromLibraryPreset(id, locale);
1222
+ } catch (error) {
1223
+ return { ok: false, error: reasonOf4(error) };
1224
+ }
1225
+ }
1226
+ /** Discard this profile's own version of `id` and put the library preset's text back in force. */
1227
+ resetToPreset(id, locale) {
1228
+ try {
1229
+ const dropped = dropProfileEdit({ root: this.root, profile: this.profile, id });
1230
+ const applied = this.fromLibraryPreset(id, locale);
1231
+ return applied.ok ? { ...applied, dropped } : applied;
1232
+ } catch (error) {
1233
+ return { ok: false, error: reasonOf4(error) };
1234
+ }
1235
+ }
1236
+ /**
1237
+ * Write a library preset's text into the effective identity and shape the answer both callers return.
1238
+ *
1239
+ * Always invalidates the reader: the write went behind its back, and a stale cache would keep serving
1240
+ * the old identity until the mtime happened to move.
1241
+ */
1242
+ fromLibraryPreset(id, locale) {
1243
+ const applied = applyPresetToProfile({ root: this.root, profile: this.profile, id, locale });
1244
+ if (applied === void 0)
1245
+ return { ok: false, error: `preset not found or has no ${FALLBACK_LOCALE} copy: ${id}` };
1246
+ this.documents.invalidate();
1247
+ return { ok: true, resolvedLocale: applied.resolvedLocale, fellBack: applied.fellBack, files: applied.written };
1248
+ }
1249
+ /**
1250
+ * Whether the effective identity differs from what preset `id` ships in `locale`.
1251
+ * @param effectiveFiles - the effective files already read, when the caller has them.
1252
+ */
1253
+ hasLocalEdit(id, locale, effectiveFiles) {
1254
+ return profileDiffersFromPreset({ root: this.root, profile: this.profile, id, locale, ...effectiveFiles === void 0 ? {} : { effectiveFiles } });
1255
+ }
1256
+ /**
1257
+ * Materialize an effective identity for THIS profile when it has none — what the switch's first
1258
+ * enable asks the host for (see `presets.seedProfileIfEmpty`). Never overwrites the user's files.
1259
+ */
1260
+ ensureProfile(locale) {
1261
+ try {
1262
+ const seed = seedProfileIfEmpty({ root: this.root, profile: this.profile, locale });
1263
+ this.documents.invalidate();
1264
+ return seed;
1265
+ } catch (error) {
1266
+ return { seeded: false, written: [], error: reasonOf4(error) };
1267
+ }
1268
+ }
1269
+ /** Save the effective identity as a new preset. */
1270
+ saveAsPreset(id, locale) {
1271
+ if (!isValidPresetId(id))
1272
+ return { ok: false, error: `invalid preset id: ${id}` };
1273
+ try {
1274
+ const saved = saveProfileAsPreset({ root: this.root, profile: this.profile, id, locale });
1275
+ if (saved === void 0)
1276
+ return { ok: false, error: `preset already exists: ${id}` };
1277
+ if (saved.written.length === 0)
1278
+ return { ok: false, error: "the effective identity is empty; nothing to save" };
1279
+ return { ok: true, files: saved.written };
1280
+ } catch (error) {
1281
+ return { ok: false, error: reasonOf4(error) };
1282
+ }
1283
+ }
1284
+ /**
1285
+ * Delete one preset from the library.
1286
+ *
1287
+ * This profile's own version of it goes too: once the identity is out of the dropdown, that version
1288
+ * is unreachable — keeping it would only leave a shadow that a future release could resurrect.
1289
+ */
1290
+ deletePreset(id) {
1291
+ try {
1292
+ if (!deletePreset(this.root, id))
1293
+ return { ok: false, error: `preset not found: ${id}` };
1294
+ dropProfileEdit({ root: this.root, profile: this.profile, id });
1295
+ return { ok: true };
1296
+ } catch (error) {
1297
+ return { ok: false, error: reasonOf4(error) };
1298
+ }
1299
+ }
1300
+ /**
1301
+ * The header's one call: everything the settings page renders above the editors.
1302
+ *
1303
+ * The three effective files are read ONCE here and reused for both the status rows and the
1304
+ * "you have your own version" comparison — reading them a second time was pure work, and this call
1305
+ * also walks the preset library (see `presetSummaries`).
1306
+ */
1307
+ status(settings, locale) {
1308
+ const entries = this.documents.readAll();
1309
+ const files = entries.map((file) => ({
1310
+ name: file.name,
1311
+ file: file.file,
1312
+ path: this.documents.pathOf(file.name),
1313
+ present: file.present,
1314
+ bytes: file.bytes
1315
+ }));
1316
+ const present = files.filter((file) => file.present);
1317
+ return {
1318
+ enabled: settings.enabled,
1319
+ profile: this.profile,
1320
+ dataHome: this.dataHomeValue,
1321
+ identityDir: this.profileDir,
1322
+ presetsDir: this.presetsDir,
1323
+ fileCount: present.length,
1324
+ totalBytes: files.reduce((total, file) => total + file.bytes, 0),
1325
+ maxBytes: settings.maxBytes,
1326
+ interpolate: settings.interpolate,
1327
+ replaceScope: settings.replaceScope,
1328
+ drop: settings.drop,
1329
+ fallbackToNative: !settings.enabled || present.length === 0,
1330
+ activePreset: settings.activePreset,
1331
+ activeLocale: settings.activeLocale,
1332
+ // "You have your own version of the identity in force" — what enables 「恢复预设原文」.
1333
+ hasLocalEdit: settings.activePreset !== "" && this.hasLocalEdit(settings.activePreset, locale.locale, entries),
1334
+ locale: locale.locale,
1335
+ localeSource: locale.source,
1336
+ files,
1337
+ presets: this.presetSummaries(settings.activePreset)
1338
+ };
1339
+ }
1340
+ /** The preset locale directory (read-only accessor for diagnostics and tests). */
1341
+ presetDir(id, locale) {
1342
+ return presetLocaleDir(this.root, id, locale);
1343
+ }
1344
+ };
1345
+ function identityNameOfLoose(name2) {
1346
+ const upper = name2.trim().toUpperCase().replace(/\.MD$/u, "");
1347
+ return IDENTITY_FILES.includes(upper) ? upper : void 0;
1348
+ }
1349
+ function reasonOf4(error) {
1350
+ return error instanceof Error ? error.message : String(error);
1351
+ }
1352
+
1353
+ // lib/volatile.js
1354
+ function volatileValue(field, fallback) {
1355
+ if (field === void 0)
1356
+ return fallback;
1357
+ const get = field.get;
1358
+ if (typeof get === "function") {
1359
+ try {
1360
+ const value = get.call(field);
1361
+ return value === void 0 ? fallback : value;
1362
+ } catch {
1363
+ return fallback;
1364
+ }
1365
+ }
1366
+ return field;
1367
+ }
1368
+
1369
+ // lib/index.js
1370
+ var name = "avantf-identity";
1371
+ var inject = ["systemPrompt"];
1372
+ var OWN_SECTION = "avantf:identity";
1373
+ var HARNESS_IDENTITY_SECTION = "harness:identity";
1374
+ var DEFAULT_DROP = [HARNESS_IDENTITY_SECTION, PERSONA_PREFIX_SECTION];
1375
+ var DEFAULT_MAX_BYTES = 65536;
1376
+ var Config = z2.object({
1377
+ enabled: z2.boolean().default(false).volatile(),
1378
+ interpolate: z2.boolean().default(false).volatile(),
1379
+ replaceScope: z2.union([z2.const("session"), z2.const("all")]).default("session").volatile(),
1380
+ drop: z2.array(z2.string()).default([...DEFAULT_DROP]).volatile(),
1381
+ maxBytes: z2.natural().default(DEFAULT_MAX_BYTES).volatile(),
1382
+ profile: z2.string().default("").volatile(),
1383
+ dataHome: z2.string().default("").volatile(),
1384
+ activePreset: z2.string().default("").volatile(),
1385
+ activeLocale: z2.string().default("").volatile()
1386
+ });
1387
+ function isMainAgent(context) {
1388
+ const agent = context.agent;
1389
+ const header = agent?.session?.header;
1390
+ return header?.origin !== "subagent" && (header?.delegationDepth ?? 0) === 0;
1391
+ }
1392
+ function sessionKeyOf(context) {
1393
+ const agent = context.agent;
1394
+ const session = agent?.session;
1395
+ return session !== null && typeof session === "object" ? session : void 0;
1396
+ }
1397
+ function profileContextName(ctx) {
1398
+ try {
1399
+ const profileContext = ctx.get("profileContext");
1400
+ const value = profileContext?.name;
1401
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
1402
+ } catch {
1403
+ return void 0;
1404
+ }
1405
+ }
1406
+ function packageVersion() {
1407
+ for (const relative of ["../package.json"]) {
1408
+ try {
1409
+ const parsed = JSON.parse(readFileSync3(new URL(relative, import.meta.url), "utf8"));
1410
+ if (typeof parsed.version === "string" && parsed.version !== "")
1411
+ return parsed.version;
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ return "unknown";
1416
+ }
1417
+ function reasonOf5(error) {
1418
+ return error instanceof Error ? error.message : String(error);
1419
+ }
1420
+ async function initialize(options) {
1421
+ const { ctx, state, settings, log } = options;
1422
+ try {
1423
+ const base = await loadBase({ log });
1424
+ if (!stillActive(ctx)) {
1425
+ log.warn("unmounted while preparing the environment; leaving the data home as it was resolved");
1426
+ return;
1427
+ }
1428
+ const resolve = base?.dataHome ?? resolveDataHomeMirror;
1429
+ state.setDataHome(resolve({ configured: settings().dataHome }));
1430
+ log.info(base === void 0 ? `data home resolved by this plugin's own mirror: ${state.dataHome}` : `data home resolved by @avantf/dsh-plugin-base: ${state.dataHome}`);
1431
+ const assetsDir = resolveAssetsDir();
1432
+ if (assetsDir === void 0) {
1433
+ log.warn("the built-in preset assets are missing from this build; no preset was released");
1434
+ return;
1435
+ }
1436
+ const provisioned = state.provision(assetsDir);
1437
+ log.info(`preset library ready at ${state.presetsDir} (released: ${provisioned.copied.length === 0 ? "nothing new" : provisioned.copied.join(", ")})`);
1438
+ } catch (error) {
1439
+ log.warn(`startup initialisation failed (${reasonOf5(error)}); the plugin mounts with the data home it can resolve`);
1440
+ }
1441
+ }
1442
+ async function apply(ctx, config) {
1443
+ const log = createLogger(ctx.logger);
1444
+ const settings = () => ({
1445
+ enabled: volatileValue(config.enabled, false),
1446
+ interpolate: volatileValue(config.interpolate, false),
1447
+ replaceScope: volatileValue(config.replaceScope, "session"),
1448
+ drop: volatileValue(config.drop, DEFAULT_DROP),
1449
+ maxBytes: volatileValue(config.maxBytes, DEFAULT_MAX_BYTES),
1450
+ profile: volatileValue(config.profile, ""),
1451
+ dataHome: volatileValue(config.dataHome, ""),
1452
+ activePreset: volatileValue(config.activePreset, ""),
1453
+ activeLocale: volatileValue(config.activeLocale, "")
1454
+ });
1455
+ const state = new IdentityState({
1456
+ // The provisional answer, replaced by the base's own rule once it is loaded: the family mirror is
1457
+ // what the base would say with no base present, so nothing is invented in the meantime.
1458
+ dataHome: resolveDataHomeMirror({ configured: settings().dataHome }),
1459
+ profile: () => profileContextName(ctx) ?? settings().profile,
1460
+ maxBytes: () => settings().maxBytes,
1461
+ version: packageVersion(),
1462
+ logger: log
1463
+ });
1464
+ log.info(`mounting: enabled=${String(settings().enabled)} profile=${state.profile} assets=${String(resolveAssetsDir() ?? "MISSING")}`);
1465
+ const freeze = new SessionFreeze();
1466
+ const ownSection = {
1467
+ name: OWN_SECTION,
1468
+ order: ctx.systemPrompt.getSectionOrder("HARNESS_IDENTITY"),
1469
+ interpolate: settings().interpolate,
1470
+ text: (context) => freeze.text(sessionKeyOf(context), () => state.sectionText(settings().enabled))
1471
+ };
1472
+ ctx.effect(() => ctx.systemPrompt.section(ownSection), "avantf-identity:section");
1473
+ ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
1474
+ const out = await next();
1475
+ const snapshot = settings();
1476
+ const own = out.sections.find((section) => section.name === OWN_SECTION);
1477
+ const applicable = own !== void 0 && own.text.length > 0 && (snapshot.replaceScope === "all" || isMainAgent(context));
1478
+ const drop = new Set(applicable ? snapshot.drop : [OWN_SECTION]);
1479
+ if (drop.size === 0)
1480
+ return out;
1481
+ const sections = out.sections.filter((section) => !drop.has(section.name));
1482
+ return sections.length === out.sections.length ? out : { ...out, sections };
1483
+ });
1484
+ new IdentityGateway(ctx, state, settings, log);
1485
+ const typert = ctx.get("typert");
1486
+ if (typert === void 0 || typeof typert.register !== "function") {
1487
+ log.warn("no typert registry mounted; the settings page will report it instead of reading the identity files");
1488
+ } else {
1489
+ try {
1490
+ typert.register(hostContribution);
1491
+ log.info(`typert host face registered (namespace avantfIdentity, ${String(hostContribution.invocations?.length ?? 0)} invocations)`);
1492
+ } catch (error) {
1493
+ log.error(`typert host face FAILED to register: the settings page will report it; the prompt mechanism mounts anyway — ${reasonOf5(error)}`);
1494
+ }
1495
+ }
1496
+ ctx.inject(["settings"], (child) => {
1497
+ child.effect(() => {
1498
+ const forms = child.get("settings");
1499
+ if (forms === void 0 || typeof forms.configure !== "function")
1500
+ return () => void 0;
1501
+ return forms.configure({ auto: false }, ctx.fiber) ?? (() => void 0);
1502
+ }, "avantf-identity:no-auto-form");
1503
+ });
1504
+ await initialize({ ctx, state, settings, log });
1505
+ }
1506
+ export {
1507
+ Config,
1508
+ DEFAULT_DROP,
1509
+ DEFAULT_MAX_BYTES,
1510
+ HARNESS_IDENTITY_SECTION,
1511
+ OWN_SECTION,
1512
+ apply,
1513
+ inject,
1514
+ isMainAgent,
1515
+ name,
1516
+ sessionKeyOf
1517
+ };