superbee 0.2.1 → 0.3.0-pre.2

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.
@@ -2963,21 +2963,28 @@ function normalizeFrontmatter(data, okfVersion) {
2963
2963
  function parseMarkdown(raw, context, options = {}) {
2964
2964
  const split = splitLeadingFrontmatter(raw, context);
2965
2965
  if (!("yamlSource" in split)) return { frontmatter: {}, body: split.body };
2966
+ return { frontmatter: parseFrontmatter(split.yamlSource, context, options), body: split.body };
2967
+ }
2968
+ function parseFrontmatter(source, context, options = {}) {
2966
2969
  let parsed;
2967
2970
  try {
2968
- parsed = yamlEngine.parse(split.yamlSource);
2971
+ parsed = yamlEngine.parse(source);
2969
2972
  } catch (err) {
2970
2973
  throw new MalformedDocumentError(context, err);
2971
2974
  }
2972
- const frontmatter2 = normalizeFrontmatter(parsed, options.okfVersion);
2973
- return { frontmatter: frontmatter2, body: split.body };
2975
+ return normalizeFrontmatter(parsed, options.okfVersion);
2976
+ }
2977
+ function stringifyFrontmatter(data) {
2978
+ return import_js_yaml.default.safeDump(data).trim();
2974
2979
  }
2975
2980
  function normalizeDocumentBodyForStorage(body) {
2976
2981
  return body.endsWith("\n") ? body : `${body}
2977
2982
  `;
2978
2983
  }
2979
2984
  function stringifyWithData(data, body) {
2980
- const dumped = import_js_yaml.default.safeDump(data).trim();
2985
+ return stringifyWithSerializedFrontmatter(stringifyFrontmatter(data), body);
2986
+ }
2987
+ function stringifyWithSerializedFrontmatter(dumped, body) {
2981
2988
  const content = body ?? "";
2982
2989
  const newline = (value) => value.endsWith("\n") ? value : `${value}
2983
2990
  `;
@@ -3038,6 +3045,29 @@ function mutationActorFromFrontmatter(frontmatter2) {
3038
3045
 
3039
3046
  // ../core/src/paths.ts
3040
3047
  var RESERVED_FILENAMES = ["index.md", "log.md"];
3048
+ function compareStorageKeys(a, b) {
3049
+ const primary = a.localeCompare(b);
3050
+ if (primary !== 0 || a === b) return primary;
3051
+ const left = Array.from(a, (character) => character.codePointAt(0));
3052
+ const right = Array.from(b, (character) => character.codePointAt(0));
3053
+ for (let i = 0; i < Math.min(left.length, right.length); i++) {
3054
+ if (left[i] !== right[i]) return left[i] - right[i];
3055
+ }
3056
+ return left.length - right.length;
3057
+ }
3058
+ function assertPortablePath(value, label) {
3059
+ if (/[\u0000-\u001f\u007f]/u.test(value) || /^[a-z]:/i.test(value)) {
3060
+ throw new InvalidInputError(`${label} must not contain controls or start with a drive prefix.`);
3061
+ }
3062
+ if (value.includes("\\") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
3063
+ throw new InvalidInputError(`${label} must be canonical and bundle-relative.`);
3064
+ }
3065
+ }
3066
+ function assertSafeReservedFilename(name) {
3067
+ if (!RESERVED_FILENAMES.includes(name)) {
3068
+ throw new InvalidInputError("Reserved filename must be index.md or log.md.");
3069
+ }
3070
+ }
3041
3071
  function toPosix(p) {
3042
3072
  return p.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
3043
3073
  }
@@ -3057,21 +3087,8 @@ function assertSafeConceptId(id) {
3057
3087
  if (typeof id !== "string" || id.trim() === "") {
3058
3088
  throw new InvalidInputError("Concept id must be a non-empty string.");
3059
3089
  }
3060
- if (id.startsWith("/")) {
3061
- throw new InvalidInputError(`Concept id must be bundle-relative, got absolute '${id}'.`);
3062
- }
3063
- if (id.includes("\\")) {
3064
- throw new InvalidInputError(`Concept id must use forward slashes: '${id}'.`);
3065
- }
3090
+ assertPortablePath(id, "Concept id");
3066
3091
  const segments = id.split("/");
3067
- if (segments.some((seg) => seg === "..")) {
3068
- throw new InvalidInputError(`Concept id must not contain '..' segments: '${id}'.`);
3069
- }
3070
- if (segments.some((seg) => seg === "." || seg === "")) {
3071
- throw new InvalidInputError(
3072
- `Concept id must be canonical (no '.', duplicate-slash, leading './', or trailing-slash segments): '${id}'.`
3073
- );
3074
- }
3075
3092
  if (segments.slice(0, -1).some((seg) => seg.toLowerCase().endsWith(".md"))) {
3076
3093
  throw new InvalidInputError(
3077
3094
  `Concept id must not contain a non-final segment ending in '.md': '${id}' (it collides with a concept file at that path).`
@@ -3082,21 +3099,11 @@ function assertSafeBlobKey(key) {
3082
3099
  if (typeof key !== "string" || key.trim() === "") {
3083
3100
  throw new InvalidInputError("Blob key must be a non-empty string.");
3084
3101
  }
3085
- const norm = toPosix(key);
3086
- if (norm.startsWith("/")) {
3087
- throw new InvalidInputError(`Blob key must be bundle-relative, got absolute '${key}'.`);
3088
- }
3089
- const segments = norm.split("/");
3090
- if (segments.some((seg) => seg === "..")) {
3091
- throw new InvalidInputError(`Blob key must not contain '..' segments: '${key}'.`);
3092
- }
3102
+ assertPortablePath(key, "Blob key");
3103
+ const segments = key.split("/");
3093
3104
  if (segments.some((seg) => seg.startsWith("."))) {
3094
3105
  throw new InvalidInputError(`Blob key must not contain dot-prefixed segments: '${key}'.`);
3095
3106
  }
3096
- const last = segments[segments.length - 1] ?? "";
3097
- if (last === "") {
3098
- throw new InvalidInputError(`Blob key must name a file, not end with '/': '${key}'.`);
3099
- }
3100
3107
  if (segments.some((seg) => seg.toLowerCase().endsWith(".md"))) {
3101
3108
  throw new InvalidInputError(
3102
3109
  `Blob key '${key}' has a path segment ending in '.md' (checked case-insensitively, at any depth), which collides with the concept-document namespace \u2014 write it as a doc instead.`
@@ -3107,13 +3114,7 @@ function assertSafeReservedDir(dir) {
3107
3114
  if (typeof dir !== "string") {
3108
3115
  throw new InvalidInputError("Reserved-file directory must be a string.");
3109
3116
  }
3110
- const norm = toPosix(dir);
3111
- if (norm.startsWith("/")) {
3112
- throw new InvalidInputError(`Reserved-file directory must be bundle-relative, got absolute '${dir}'.`);
3113
- }
3114
- if (norm.split("/").some((seg) => seg === "..")) {
3115
- throw new InvalidInputError(`Reserved-file directory must not contain '..' segments: '${dir}'.`);
3116
- }
3117
+ if (dir !== "") assertPortablePath(dir, "Reserved-file directory");
3117
3118
  }
3118
3119
 
3119
3120
  // ../core/src/links.ts
@@ -3418,7 +3419,52 @@ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypt
3418
3419
 
3419
3420
  // ../core/src/filesystem-lock.ts
3420
3421
  import { promises as fs, realpathSync } from "node:fs";
3421
- import { homedir, hostname, userInfo } from "node:os";
3422
+ import { homedir, hostname } from "node:os";
3423
+
3424
+ // ../core/src/host-policy-snapshot.ts
3425
+ function snapshotHostPolicy(input, seen = /* @__PURE__ */ new WeakMap()) {
3426
+ const existing = seen.get(input);
3427
+ if (existing) return existing;
3428
+ const result = Array.isArray(input) ? [] : {};
3429
+ seen.set(input, result);
3430
+ const keys = /* @__PURE__ */ new Set();
3431
+ for (let owner = input; owner && owner !== Object.prototype && owner !== Array.prototype; owner = Object.getPrototypeOf(owner)) {
3432
+ for (const key of Reflect.ownKeys(owner)) {
3433
+ if (key !== "constructor" && !(Array.isArray(input) && key === "length")) keys.add(key);
3434
+ }
3435
+ }
3436
+ for (const key of keys) {
3437
+ const value = input[key];
3438
+ Object.defineProperty(result, key, {
3439
+ value: typeof value === "function" ? value.bind(result) : value && typeof value === "object" ? snapshotHostPolicy(value, seen) : value,
3440
+ enumerable: true
3441
+ });
3442
+ }
3443
+ if (Array.isArray(input)) result.length = input.length;
3444
+ return Object.freeze(result);
3445
+ }
3446
+
3447
+ // ../core/src/filesystem-host.ts
3448
+ function defaultFilesystemHostPolicy() {
3449
+ if (process.platform !== "darwin" && process.platform !== "linux") {
3450
+ throw new InvalidInputError(
3451
+ "This host requires an explicit filesystem host policy; the default filesystem supports macOS and Linux."
3452
+ );
3453
+ }
3454
+ return {
3455
+ runtimeLockParent: () => "/tmp",
3456
+ runtimeOwnerKey: () => `uid-${process.getuid()}`,
3457
+ enforcePrivateMode: true,
3458
+ isTransientOpenError: () => false,
3459
+ isReplacementConflict: () => false,
3460
+ isDirectoryContentionError: () => false
3461
+ };
3462
+ }
3463
+ function captureFilesystemHostPolicy(policy) {
3464
+ return snapshotHostPolicy(policy ?? defaultFilesystemHostPolicy());
3465
+ }
3466
+
3467
+ // ../core/src/filesystem-lock.ts
3422
3468
  import path from "node:path";
3423
3469
  import { createHash, randomUUID } from "node:crypto";
3424
3470
  var OWNER_FILE = "owner.json";
@@ -3438,16 +3484,6 @@ var FilesystemMutationLockError = class extends Error {
3438
3484
  this.malformed = details.malformed;
3439
3485
  }
3440
3486
  };
3441
- function runtimeOwnerKey() {
3442
- const uid = process.getuid?.();
3443
- if (uid !== void 0) return `uid-${uid}`;
3444
- let username = "unknown";
3445
- try {
3446
- username = userInfo().username;
3447
- } catch {
3448
- }
3449
- return `user-${createHash("sha256").update(username).digest("hex").slice(0, 16)}`;
3450
- }
3451
3487
  function canonicalExistingPath(value) {
3452
3488
  try {
3453
3489
  return realpathSync(value);
@@ -3459,11 +3495,12 @@ function pathContains(root, candidate) {
3459
3495
  const rel = path.relative(root, candidate);
3460
3496
  return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
3461
3497
  }
3462
- function filesystemMutationLockRoot(portableRoot) {
3463
- const runtimeParent = process.platform === "win32" ? path.join(homedir(), "AppData", "Local") : "/tmp";
3498
+ function filesystemMutationLockRoot(portableRoot, hostPolicy) {
3499
+ const policy = captureFilesystemHostPolicy(hostPolicy);
3500
+ const runtimeParent = policy.runtimeLockParent();
3464
3501
  const tempParent = canonicalExistingPath(runtimeParent);
3465
3502
  const homeParent = canonicalExistingPath(homedir());
3466
- const ownerKey = runtimeOwnerKey();
3503
+ const ownerKey = policy.runtimeOwnerKey();
3467
3504
  const candidates = [
3468
3505
  path.join(tempParent, `agentstate-lite-mutation-locks-${ownerKey}`),
3469
3506
  path.join(homeParent, ".agentstate", `mutation-locks-${ownerKey}`)
@@ -3503,15 +3540,33 @@ function parseFilesystemMutationLockOwner(value) {
3503
3540
  target: value.target
3504
3541
  };
3505
3542
  }
3506
- async function readOwner(lockPath) {
3543
+ async function readOwnerRecord(lockPath) {
3544
+ let raw;
3507
3545
  try {
3508
- return parseFilesystemMutationLockOwner(
3509
- JSON.parse(await fs.readFile(path.join(lockPath, OWNER_FILE), "utf8"))
3510
- );
3546
+ raw = await fs.readFile(path.join(lockPath, OWNER_FILE), "utf8");
3547
+ } catch (err) {
3548
+ const code = err.code;
3549
+ return code === "ENOENT" || code === "ENOTDIR" ? { state: "absent" } : { state: "unreadable", error: err };
3550
+ }
3551
+ let owner = null;
3552
+ try {
3553
+ owner = parseFilesystemMutationLockOwner(JSON.parse(raw));
3511
3554
  } catch {
3512
- return null;
3555
+ owner = null;
3556
+ }
3557
+ return owner === null ? { state: "absent" } : { state: "record", owner };
3558
+ }
3559
+ async function resolveOwnerRecord(lockPath, started, waitMs, pollMs) {
3560
+ while (true) {
3561
+ const record = await readOwnerRecord(lockPath);
3562
+ if (record.state !== "unreadable" || Date.now() - started >= waitMs) return record;
3563
+ await delay(pollMs);
3513
3564
  }
3514
3565
  }
3566
+ async function readOwner(lockPath) {
3567
+ const record = await readOwnerRecord(lockPath);
3568
+ return record.state === "record" ? record.owner : null;
3569
+ }
3515
3570
  function processExists(pid) {
3516
3571
  try {
3517
3572
  process.kill(pid, 0);
@@ -3533,7 +3588,7 @@ async function pathExists(candidate) {
3533
3588
  throw err;
3534
3589
  }
3535
3590
  }
3536
- async function quarantineStaleLock(lockPath, owner) {
3591
+ async function quarantineStaleLock(lockPath, owner, policy) {
3537
3592
  if (owner.hostname !== hostname() || processExists(owner.pid)) return false;
3538
3593
  const quarantinePath = staleLockQuarantinePath(lockPath, owner);
3539
3594
  try {
@@ -3543,7 +3598,7 @@ async function quarantineStaleLock(lockPath, owner) {
3543
3598
  const code = err.code;
3544
3599
  if (code === "ENOENT") return false;
3545
3600
  if (await pathExists(quarantinePath)) return false;
3546
- if (process.platform === "win32" && WINDOWS_DIRECTORY_CONTENTION_CODES.has(code ?? "")) return false;
3601
+ if (policy.isDirectoryContentionError(err)) return false;
3547
3602
  throw err;
3548
3603
  }
3549
3604
  }
@@ -3562,7 +3617,7 @@ function isPrivateFilesystemMutationLockRoot(facts) {
3562
3617
  const unsafeMode = facts.enforcePrivateMode && (facts.mode & 511) !== 448;
3563
3618
  return facts.directory && !facts.symbolicLink && !wrongOwner && !unsafeMode;
3564
3619
  }
3565
- async function ensurePrivateLockRoot(root) {
3620
+ async function ensurePrivateLockRoot(root, policy) {
3566
3621
  try {
3567
3622
  await fs.mkdir(root, { recursive: true, mode: 448 });
3568
3623
  } catch (err) {
@@ -3576,7 +3631,7 @@ async function ensurePrivateLockRoot(root) {
3576
3631
  ownerUid: stat.uid,
3577
3632
  expectedUid: uid,
3578
3633
  mode: stat.mode,
3579
- enforcePrivateMode: process.platform !== "win32"
3634
+ enforcePrivateMode: policy.enforcePrivateMode
3580
3635
  })) {
3581
3636
  throw new FilesystemMutationLockError(
3582
3637
  `refusing unsafe filesystem mutation lock root '${root}'; it must be a private directory owned by this user`,
@@ -3603,50 +3658,50 @@ async function resolvedPortableRoot(portableRoot) {
3603
3658
  if (portableRoot === void 0) return void 0;
3604
3659
  return fs.realpath(portableRoot).catch(() => path.resolve(portableRoot));
3605
3660
  }
3606
- async function selectLockRoot(options) {
3661
+ async function selectLockRoot(options, policy) {
3607
3662
  const portableRoot = await resolvedPortableRoot(options.portableRoot);
3608
- const lockRoot = options.lockRoot !== void 0 ? explicitFilesystemMutationLockRoot(options.lockRoot, portableRoot) : filesystemMutationLockRoot(portableRoot);
3609
- await ensurePrivateLockRoot(lockRoot);
3663
+ const lockRoot = options.lockRoot !== void 0 ? explicitFilesystemMutationLockRoot(options.lockRoot, portableRoot) : filesystemMutationLockRoot(portableRoot, policy);
3664
+ await ensurePrivateLockRoot(lockRoot, policy);
3610
3665
  return lockRoot;
3611
3666
  }
3612
- var WINDOWS_DIRECTORY_CONTENTION_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
3613
- async function classifyLockClaimFailure(error, lockPath) {
3667
+ async function classifyLockClaimFailure(error, lockPath, policy) {
3614
3668
  const code = error.code;
3615
3669
  if (code === "EEXIST") return "contention";
3616
- if (process.platform !== "win32" || !WINDOWS_DIRECTORY_CONTENTION_CODES.has(code ?? "")) return "terminal";
3670
+ if (!policy.isDirectoryContentionError(error)) return "terminal";
3617
3671
  try {
3618
3672
  await fs.lstat(lockPath);
3619
3673
  return "contention";
3620
3674
  } catch (probeError) {
3621
3675
  const probeCode = probeError.code;
3622
- return probeCode === "ENOENT" || probeCode === "ENOTDIR" ? "unwitnessed-windows-sharing-error" : "terminal";
3676
+ return probeCode === "ENOENT" || probeCode === "ENOTDIR" ? "unwitnessed-contention-error" : "terminal";
3623
3677
  }
3624
3678
  }
3625
- async function claimLockPath(lockPath, owner, waitMs, pollMs) {
3679
+ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
3626
3680
  const started = owner.created_at_ms;
3627
- let unwitnessedWindowsRetryUsed = false;
3681
+ let unwitnessedRetryUsed = false;
3628
3682
  while (true) {
3629
3683
  try {
3630
3684
  await fs.mkdir(lockPath, { mode: 448 });
3631
3685
  } catch (err) {
3632
- const failure = await classifyLockClaimFailure(err, lockPath);
3686
+ const failure = await classifyLockClaimFailure(err, lockPath, policy);
3633
3687
  if (failure === "terminal") throw err;
3634
- if (failure === "unwitnessed-windows-sharing-error") {
3635
- if (unwitnessedWindowsRetryUsed) throw err;
3636
- unwitnessedWindowsRetryUsed = true;
3688
+ if (failure === "unwitnessed-contention-error") {
3689
+ if (unwitnessedRetryUsed) throw err;
3690
+ unwitnessedRetryUsed = true;
3637
3691
  continue;
3638
3692
  } else {
3639
- unwitnessedWindowsRetryUsed = false;
3693
+ unwitnessedRetryUsed = false;
3640
3694
  }
3641
3695
  let existingOwner = await readOwner(lockPath);
3642
3696
  if (existingOwner !== null) {
3643
- if (await quarantineStaleLock(lockPath, existingOwner)) continue;
3697
+ if (await quarantineStaleLock(lockPath, existingOwner, policy)) continue;
3644
3698
  existingOwner = await readOwner(lockPath);
3645
3699
  }
3646
3700
  if (Date.now() - started >= waitMs) throw timeoutError(lockPath, existingOwner, owner.target);
3647
3701
  await delay(pollMs);
3648
3702
  continue;
3649
3703
  }
3704
+ const claimed = await fs.lstat(lockPath, { bigint: true }).catch(() => null);
3650
3705
  try {
3651
3706
  await fs.writeFile(path.join(lockPath, OWNER_FILE), `${JSON.stringify(owner)}
3652
3707
  `, {
@@ -3655,28 +3710,113 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs) {
3655
3710
  mode: 384
3656
3711
  });
3657
3712
  } catch (err) {
3658
- await fs.rm(lockPath, { recursive: true, force: true }).catch(() => {
3713
+ const code = err.code;
3714
+ if (code === "EEXIST") continue;
3715
+ await rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy).catch(() => {
3659
3716
  });
3717
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
3660
3718
  throw err;
3661
3719
  }
3662
- return async () => {
3663
- const current = await readOwner(lockPath);
3664
- if (current?.token !== owner.token) {
3665
- throw new FilesystemMutationLockError(
3666
- `refusing to release filesystem mutation lock '${lockPath}' because its owner token changed; the mutation may have completed, inspect the lock before retrying.`,
3667
- { lockPath, owner: current, stale: false, malformed: current === null }
3668
- );
3720
+ let inFlight;
3721
+ return () => {
3722
+ if (inFlight) return inFlight;
3723
+ inFlight = (async () => {
3724
+ const started2 = Date.now();
3725
+ const current = await resolveOwnerRecord(lockPath, started2, waitMs, pollMs);
3726
+ if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
3727
+ if (current.state !== "record" || current.owner.token !== owner.token) {
3728
+ throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
3729
+ }
3730
+ await removeReleasedLock(lockPath, owner, started2, waitMs, pollMs, policy);
3731
+ })();
3732
+ return inFlight.finally(() => {
3733
+ inFlight = void 0;
3734
+ });
3735
+ };
3736
+ }
3737
+ }
3738
+ async function rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy) {
3739
+ const started = Date.now();
3740
+ const record = await resolveOwnerRecord(lockPath, started, waitMs, pollMs);
3741
+ if (record.state === "record") {
3742
+ if (record.owner.token === owner.token) await removeReleasedLock(lockPath, owner, started, waitMs, pollMs, policy);
3743
+ return;
3744
+ }
3745
+ if (record.state === "unreadable" || !await isClaimedDirectory(lockPath, claimed)) return;
3746
+ const remnant = releasedLockRemnantPath(lockPath, owner);
3747
+ await fs.rename(lockPath, remnant);
3748
+ await fs.rm(remnant, { recursive: true, force: true });
3749
+ }
3750
+ async function isClaimedDirectory(lockPath, claimed) {
3751
+ if (claimed === null || claimed.birthtimeNs <= 0n) return false;
3752
+ const current = await fs.lstat(lockPath, { bigint: true }).catch(() => null);
3753
+ return current !== null && current.isDirectory() && current.dev === claimed.dev && current.ino === claimed.ino && current.birthtimeNs === claimed.birthtimeNs;
3754
+ }
3755
+ function changedOwnerRefusal(lockPath, current) {
3756
+ return new FilesystemMutationLockError(
3757
+ `refusing to release filesystem mutation lock '${lockPath}' because its owner token changed; the mutation may have completed, inspect the lock before retrying.`,
3758
+ { lockPath, owner: current, stale: false, malformed: current === null }
3759
+ );
3760
+ }
3761
+ function unreadableOwnerRefusal(lockPath, error) {
3762
+ const message = error instanceof Error ? error.message : String(error);
3763
+ return new FilesystemMutationLockError(
3764
+ `refusing to release filesystem mutation lock '${lockPath}' because its owner record could not be read within the wait budget (${message}); the mutation may have completed, inspect the lock before retrying.`,
3765
+ { lockPath, owner: null, stale: false, malformed: true }
3766
+ );
3767
+ }
3768
+ function releasedLockRemnantPath(lockPath, owner) {
3769
+ const tokenHash = createHash("sha256").update(owner.token).digest("hex");
3770
+ return `${lockPath}.released-${tokenHash}`;
3771
+ }
3772
+ function removalFailure(inspectPath, owner, err, attempts, detail) {
3773
+ const message = err instanceof Error ? err.message : String(err);
3774
+ const suffix = attempts > 1 ? ` after ${attempts} bounded attempts` : "";
3775
+ return new FilesystemMutationLockError(
3776
+ `mutation completed but ${detail}${suffix} (${message}); inspect the lock before retrying.`,
3777
+ { lockPath: inspectPath, owner, stale: false, malformed: false }
3778
+ );
3779
+ }
3780
+ async function removeReleasedLock(lockPath, owner, started, waitMs, pollMs, policy) {
3781
+ const remnant = releasedLockRemnantPath(lockPath, owner);
3782
+ let attempts = 0;
3783
+ while (true) {
3784
+ try {
3785
+ await fs.rename(lockPath, remnant);
3786
+ break;
3787
+ } catch (err) {
3788
+ attempts += 1;
3789
+ if (attempts > 1 && err.code === "ENOENT") return;
3790
+ if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
3791
+ throw removalFailure(lockPath, owner, err, attempts, `filesystem lock '${lockPath}' could not be removed`);
3669
3792
  }
3670
- try {
3671
- await fs.rm(lockPath, { recursive: true, force: false });
3672
- } catch (err) {
3673
- const message = err instanceof Error ? err.message : String(err);
3674
- throw new FilesystemMutationLockError(
3675
- `mutation completed but filesystem lock '${lockPath}' could not be removed (${message}); inspect the lock before retrying.`,
3676
- { lockPath, owner: current, stale: false, malformed: false }
3793
+ }
3794
+ await delay(pollMs);
3795
+ const current = await resolveOwnerRecord(lockPath, started, waitMs, pollMs);
3796
+ if (current.state === "record" && current.owner.token === owner.token) continue;
3797
+ if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
3798
+ if (current.state === "absent" && !await pathExists(lockPath)) return;
3799
+ throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
3800
+ }
3801
+ attempts = 0;
3802
+ while (true) {
3803
+ try {
3804
+ await fs.rm(remnant, { recursive: true, force: false });
3805
+ return;
3806
+ } catch (err) {
3807
+ attempts += 1;
3808
+ if (err.code === "ENOENT") return;
3809
+ if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
3810
+ throw removalFailure(
3811
+ remnant,
3812
+ owner,
3813
+ err,
3814
+ attempts,
3815
+ `filesystem lock '${lockPath}' was released yet its remnant '${remnant}' could not be removed`
3677
3816
  );
3678
3817
  }
3679
- };
3818
+ }
3819
+ await delay(pollMs);
3680
3820
  }
3681
3821
  }
3682
3822
  function newOwner(target) {
@@ -3694,21 +3834,20 @@ function assertIdentityKey(key) {
3694
3834
  }
3695
3835
  async function acquireFilesystemIdentityLock(key, identity, options = {}) {
3696
3836
  assertIdentityKey(key);
3837
+ const policy = captureFilesystemHostPolicy(options.hostPolicy);
3697
3838
  const waitMs = positiveOption(options.waitMs, DEFAULT_WAIT_MS, "waitMs");
3698
3839
  const pollMs = positiveOption(options.pollMs, DEFAULT_POLL_MS, "pollMs");
3699
3840
  const owner = newOwner(identity);
3700
- const lockRoot = await selectLockRoot(options);
3701
- return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs);
3841
+ const lockRoot = await selectLockRoot(options, policy);
3842
+ return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs, policy);
3702
3843
  }
3703
3844
 
3704
3845
  // ../core/src/filesystem-identity.ts
3705
- var WINDOWS_REPLACE_CONFLICT_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
3706
3846
  async function replaceExistingLeaf(port, tmp, target, rel) {
3707
3847
  try {
3708
3848
  await port.rename(tmp, target);
3709
3849
  } catch (err) {
3710
- const code = err.code ?? "";
3711
- if ((port.platform ?? process.platform) === "win32" && WINDOWS_REPLACE_CONFLICT_CODES.has(code)) {
3850
+ if (port.isReplacementConflict?.(err)) {
3712
3851
  throw new ConcurrentReplacementError(rel, 1, 10);
3713
3852
  }
3714
3853
  throw err;
@@ -3875,7 +4014,7 @@ async function observeExact(port, root, rel, read) {
3875
4014
  opened = await port.open(target);
3876
4015
  } catch (err) {
3877
4016
  if (isAbsentPathError(err)) return { state: "absent" };
3878
- if (process.platform === "win32" && err.code === "EPERM") {
4017
+ if (port.isTransientOpenError?.(err)) {
3879
4018
  restarts = countRestart(restarts, rel);
3880
4019
  continue;
3881
4020
  }
@@ -3909,7 +4048,15 @@ async function probeExact(port, root, rel) {
3909
4048
  restarts = countRestart(restarts, rel);
3910
4049
  }
3911
4050
  }
3912
- var linkUnsupportedRoots = /* @__PURE__ */ new Set();
4051
+ var linkUnsupportedRoots = /* @__PURE__ */ new WeakMap();
4052
+ function unsupportedLinkRoots(port) {
4053
+ let roots = linkUnsupportedRoots.get(port);
4054
+ if (!roots) {
4055
+ roots = /* @__PURE__ */ new Set();
4056
+ linkUnsupportedRoots.set(port, roots);
4057
+ }
4058
+ return roots;
4059
+ }
3913
4060
  var queues = /* @__PURE__ */ new Map();
3914
4061
  function enqueue(key, fn) {
3915
4062
  const tail = queues.get(key) ?? Promise.resolve();
@@ -3991,7 +4138,7 @@ async function createExactLeaf(port, tmp, target, rel, leaf, rootResolved) {
3991
4138
  return;
3992
4139
  }
3993
4140
  if (outcome === "unsupported") {
3994
- linkUnsupportedRoots.add(rootResolved);
4141
+ unsupportedLinkRoots(port).add(rootResolved);
3995
4142
  await port.rename(tmp, target);
3996
4143
  return;
3997
4144
  }
@@ -4027,7 +4174,7 @@ async function mutateExact(port, root, rel, body) {
4027
4174
  await replaceExistingLeaf(port, tmp, target, rel);
4028
4175
  return;
4029
4176
  }
4030
- if (linkUnsupportedRoots.has(rootResolved)) {
4177
+ if (unsupportedLinkRoots(port).has(rootResolved)) {
4031
4178
  await port.rename(tmp, target);
4032
4179
  return;
4033
4180
  }
@@ -4054,80 +4201,84 @@ function kindOf(stats) {
4054
4201
  if (stats.isFile()) return "file";
4055
4202
  return "other";
4056
4203
  }
4057
- var nodeFilesystemIdentityPort = Object.freeze({
4058
- platform: process.platform,
4059
- async probe(target) {
4060
- let lstats;
4061
- try {
4062
- lstats = await fs2.lstat(target);
4063
- } catch (err) {
4064
- if (isAbsentPathError(err)) return null;
4065
- throw err;
4066
- }
4067
- return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
4068
- },
4069
- async entries(dir) {
4070
- try {
4071
- const dirents = await fs2.readdir(dir, { withFileTypes: true });
4072
- return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
4073
- } catch (err) {
4074
- if (isAbsentPathError(err)) return null;
4075
- throw err;
4076
- }
4077
- },
4078
- async open(target) {
4079
- const handle = await fs2.open(target, "r");
4080
- try {
4081
- const stats = await handle.stat();
4082
- return { handle, dev: stats.dev, ino: stats.ino };
4083
- } catch (err) {
4084
- await handle.close().catch(() => {
4085
- });
4086
- throw err;
4087
- }
4088
- },
4089
- readAll(handle) {
4090
- return handle.readFile();
4091
- },
4092
- close(handle) {
4093
- return handle.close();
4094
- },
4095
- async stat(target) {
4096
- return { mtime: (await fs2.stat(target)).mtime };
4097
- },
4098
- async mkdir(dir) {
4099
- try {
4100
- await fs2.mkdir(dir);
4101
- return "created";
4102
- } catch (err) {
4103
- if (err.code === "EEXIST") return "exists";
4104
- throw err;
4105
- }
4106
- },
4107
- async writeTemp(dir, name, bytes) {
4108
- await fs2.writeFile(path2.join(dir, name), bytes);
4109
- },
4110
- async link(from, to) {
4111
- try {
4112
- await fs2.link(from, to);
4113
- return "linked";
4114
- } catch (err) {
4115
- const code = err.code;
4116
- if (code === "EEXIST") return "exists";
4117
- if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV") return "unsupported";
4118
- throw err;
4204
+ function createNodeFilesystemIdentityPort(hostPolicy) {
4205
+ const policy = captureFilesystemHostPolicy(hostPolicy);
4206
+ return Object.freeze({
4207
+ isTransientOpenError: policy.isTransientOpenError,
4208
+ isReplacementConflict: policy.isReplacementConflict,
4209
+ async probe(target) {
4210
+ let lstats;
4211
+ try {
4212
+ lstats = await fs2.lstat(target);
4213
+ } catch (err) {
4214
+ if (isAbsentPathError(err)) return null;
4215
+ throw err;
4216
+ }
4217
+ return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
4218
+ },
4219
+ async entries(dir) {
4220
+ try {
4221
+ const dirents = await fs2.readdir(dir, { withFileTypes: true });
4222
+ return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
4223
+ } catch (err) {
4224
+ if (isAbsentPathError(err)) return null;
4225
+ throw err;
4226
+ }
4227
+ },
4228
+ async open(target) {
4229
+ const handle = await fs2.open(target, "r");
4230
+ try {
4231
+ const stats = await handle.stat();
4232
+ return { handle, dev: stats.dev, ino: stats.ino };
4233
+ } catch (err) {
4234
+ await handle.close().catch(() => {
4235
+ });
4236
+ throw err;
4237
+ }
4238
+ },
4239
+ readAll(handle) {
4240
+ return handle.readFile();
4241
+ },
4242
+ close(handle) {
4243
+ return handle.close();
4244
+ },
4245
+ async stat(target) {
4246
+ return { mtime: (await fs2.stat(target)).mtime };
4247
+ },
4248
+ async mkdir(dir) {
4249
+ try {
4250
+ await fs2.mkdir(dir);
4251
+ return "created";
4252
+ } catch (err) {
4253
+ if (err.code === "EEXIST") return "exists";
4254
+ throw err;
4255
+ }
4256
+ },
4257
+ async writeTemp(dir, name, bytes) {
4258
+ await fs2.writeFile(path2.join(dir, name), bytes);
4259
+ },
4260
+ async link(from, to) {
4261
+ try {
4262
+ await fs2.link(from, to);
4263
+ return "linked";
4264
+ } catch (err) {
4265
+ const code = err.code;
4266
+ if (code === "EEXIST") return "exists";
4267
+ if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV") return "unsupported";
4268
+ throw err;
4269
+ }
4270
+ },
4271
+ async rename(from, to) {
4272
+ await fs2.rename(from, to);
4273
+ },
4274
+ async unlink(target) {
4275
+ await fs2.unlink(target);
4276
+ },
4277
+ claim(key, identity) {
4278
+ return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root, hostPolicy: policy });
4119
4279
  }
4120
- },
4121
- async rename(from, to) {
4122
- await fs2.rename(from, to);
4123
- },
4124
- async unlink(target) {
4125
- await fs2.unlink(target);
4126
- },
4127
- claim(key, identity) {
4128
- return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root });
4129
- }
4130
- });
4280
+ });
4281
+ }
4131
4282
 
4132
4283
  // ../core/src/sha256.ts
4133
4284
  var K = new Uint32Array([
@@ -4286,6 +4437,9 @@ function sha256HexOfUtf8(input) {
4286
4437
  }
4287
4438
 
4288
4439
  // ../core/src/version-transport.ts
4440
+ function isContentVersion(value) {
4441
+ return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
4442
+ }
4289
4443
  var VersionConflict = class extends Error {
4290
4444
  name = "VersionConflict";
4291
4445
  id;
@@ -4317,7 +4471,7 @@ function defaultActor() {
4317
4471
  }
4318
4472
 
4319
4473
  // ../core/src/backend.ts
4320
- var publicationRoots = /* @__PURE__ */ new WeakMap();
4474
+ var publicationContexts = /* @__PURE__ */ new WeakMap();
4321
4475
  function firstString(...vals) {
4322
4476
  for (const v of vals) {
4323
4477
  if (typeof v === "string" && v.trim() !== "") return v;
@@ -4336,22 +4490,22 @@ function notFound(root, rel) {
4336
4490
  err.path = target;
4337
4491
  return err;
4338
4492
  }
4339
- async function readBytes(handle) {
4493
+ async function readBytes(port, handle) {
4340
4494
  try {
4341
- return await nodeFilesystemIdentityPort.readAll(handle);
4495
+ return await port.readAll(handle);
4342
4496
  } catch (err) {
4343
4497
  if (isAbsentFileError(err)) return null;
4344
4498
  throw err;
4345
4499
  }
4346
4500
  }
4347
- async function walkFiles(root, keep, sub = "") {
4348
- const entries = await nodeFilesystemIdentityPort.entries(path3.join(root, sub)) ?? [];
4501
+ async function walkFiles(port, root, keep, sub = "") {
4502
+ const entries = await port.entries(path3.join(root, sub)) ?? [];
4349
4503
  const out = [];
4350
4504
  for (const entry of entries) {
4351
4505
  if (entry.name.startsWith(".")) continue;
4352
4506
  const rel = sub === "" ? entry.name : `${sub}/${entry.name}`;
4353
4507
  if (entry.kind === "directory") {
4354
- out.push(...await walkFiles(root, keep, rel));
4508
+ out.push(...await walkFiles(port, root, keep, rel));
4355
4509
  } else if (entry.kind === "file" && keep(entry.name)) {
4356
4510
  out.push(rel);
4357
4511
  }
@@ -4359,19 +4513,20 @@ async function walkFiles(root, keep, sub = "") {
4359
4513
  return out;
4360
4514
  }
4361
4515
  function reservedPath(dir, name) {
4362
- const d = toPosix(dir).replace(/^\.?\//, "").replace(/\/$/, "");
4363
- return d === "" ? name : `${d}/${name}`;
4516
+ return dir === "" ? name : `${dir}/${name}`;
4364
4517
  }
4365
4518
  var FilesystemBackend = class _FilesystemBackend {
4366
4519
  #root;
4520
+ #port;
4367
4521
  /**
4368
4522
  * The root is resolved once here: a relative root would otherwise re-resolve against the
4369
4523
  * process's current directory on every operation, so a later `chdir` would silently move the
4370
4524
  * bundle and derive a different identity key.
4371
4525
  */
4372
- constructor(root) {
4526
+ constructor(root, options = {}) {
4527
+ this.#port = createNodeFilesystemIdentityPort(options.hostPolicy);
4373
4528
  this.#root = path3.resolve(root);
4374
- publicationRoots.set(this, this.#root);
4529
+ publicationContexts.set(this, { root: this.#root, port: this.#port });
4375
4530
  }
4376
4531
  async read(id) {
4377
4532
  return this.#readWithEdition(id, () => this.#readEdition());
@@ -4387,7 +4542,7 @@ var FilesystemBackend = class _FilesystemBackend {
4387
4542
  async #readWithEdition(id, edition) {
4388
4543
  assertSafeConceptId(id);
4389
4544
  const rel = pathFromConceptId(id);
4390
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, rel, readBytes);
4545
+ const observed = await observeExact(this.#port, this.#root, rel, (handle) => readBytes(this.#port, handle));
4391
4546
  if (observed.state === "absent") throw notFound(this.#root, rel);
4392
4547
  const raw = observed.value.toString("utf8");
4393
4548
  const { frontmatter: frontmatter2, body } = parseMarkdown(raw, rel, { okfVersion: await edition() });
@@ -4409,7 +4564,7 @@ var FilesystemBackend = class _FilesystemBackend {
4409
4564
  assertSafeConceptId(id);
4410
4565
  const raw = stringifyDoc(doc.frontmatter, doc.body ?? "");
4411
4566
  const rel = pathFromConceptId(id);
4412
- return mutateExact(nodeFilesystemIdentityPort, this.#root, rel, async (target) => {
4567
+ return mutateExact(this.#port, this.#root, rel, async (target) => {
4413
4568
  if (options.expectedVersion !== void 0) {
4414
4569
  const bytes = await target.current();
4415
4570
  const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
@@ -4423,7 +4578,7 @@ var FilesystemBackend = class _FilesystemBackend {
4423
4578
  }
4424
4579
  async delete(id, options = {}) {
4425
4580
  assertSafeConceptId(id);
4426
- return mutateExact(nodeFilesystemIdentityPort, this.#root, pathFromConceptId(id), async (target) => {
4581
+ return mutateExact(this.#port, this.#root, pathFromConceptId(id), async (target) => {
4427
4582
  const bytes = await target.current();
4428
4583
  if (bytes === null) return false;
4429
4584
  const current = versionOfBytes(bytes.toString("utf8"));
@@ -4437,10 +4592,10 @@ var FilesystemBackend = class _FilesystemBackend {
4437
4592
  async versions(id) {
4438
4593
  assertSafeConceptId(id);
4439
4594
  const rel = pathFromConceptId(id);
4440
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, rel, async (handle, target) => {
4595
+ const observed = await observeExact(this.#port, this.#root, rel, async (handle, target) => {
4441
4596
  try {
4442
- const bytes = await nodeFilesystemIdentityPort.readAll(handle);
4443
- const { mtime: mtime2 } = await nodeFilesystemIdentityPort.stat(target);
4597
+ const bytes = await this.#port.readAll(handle);
4598
+ const { mtime: mtime2 } = await this.#port.stat(target);
4444
4599
  return { raw: bytes.toString("utf8"), mtime: mtime2 };
4445
4600
  } catch {
4446
4601
  return null;
@@ -4455,10 +4610,10 @@ var FilesystemBackend = class _FilesystemBackend {
4455
4610
  }
4456
4611
  async exists(id) {
4457
4612
  assertSafeConceptId(id);
4458
- return (await probeExact(nodeFilesystemIdentityPort, this.#root, pathFromConceptId(id))).state === "exact";
4613
+ return (await probeExact(this.#port, this.#root, pathFromConceptId(id))).state === "exact";
4459
4614
  }
4460
4615
  async list(prefix) {
4461
- const files = await walkFiles(this.#root, (name) => name.endsWith(".md"));
4616
+ const files = await walkFiles(this.#port, this.#root, (name) => name.endsWith(".md"));
4462
4617
  const ids = [];
4463
4618
  for (const rel of files) {
4464
4619
  if (isReservedFile(rel)) continue;
@@ -4472,20 +4627,22 @@ var FilesystemBackend = class _FilesystemBackend {
4472
4627
  if (prefix && !id.startsWith(prefix)) continue;
4473
4628
  ids.push(id);
4474
4629
  }
4475
- ids.sort((a, b) => a.localeCompare(b));
4630
+ ids.sort(compareStorageKeys);
4476
4631
  return ids;
4477
4632
  }
4478
4633
  async readReserved(dir, name) {
4479
4634
  assertSafeReservedDir(dir);
4480
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, reservedPath(dir, name), readBytes);
4635
+ assertSafeReservedFilename(name);
4636
+ const observed = await observeExact(this.#port, this.#root, reservedPath(dir, name), (handle) => readBytes(this.#port, handle));
4481
4637
  if (observed.state === "absent") return null;
4482
4638
  const content = observed.value.toString("utf8");
4483
4639
  return { content, version: versionOfBytes(content) };
4484
4640
  }
4485
4641
  async writeReserved(dir, name, content, options = {}) {
4486
4642
  assertSafeReservedDir(dir);
4643
+ assertSafeReservedFilename(name);
4487
4644
  const rel = reservedPath(dir, name);
4488
- return mutateExact(nodeFilesystemIdentityPort, this.#root, rel, async (target) => {
4645
+ return mutateExact(this.#port, this.#root, rel, async (target) => {
4489
4646
  if (options.expectedVersion !== void 0) {
4490
4647
  const bytes = await target.current();
4491
4648
  const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
@@ -4500,14 +4657,14 @@ var FilesystemBackend = class _FilesystemBackend {
4500
4657
  // ── blobs: opaque bytes + a content-type ──────────────────────────────────
4501
4658
  async readBlob(key) {
4502
4659
  assertSafeBlobKey(key);
4503
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, key, readBytes);
4660
+ const observed = await observeExact(this.#port, this.#root, key, (handle) => readBytes(this.#port, handle));
4504
4661
  if (observed.state === "absent") return null;
4505
4662
  const bytes = observed.value;
4506
4663
  return { bytes, contentType: resolveContentType(key), version: blobVersion(bytes) };
4507
4664
  }
4508
4665
  async writeBlob(key, bytes, _contentType, options = {}) {
4509
4666
  assertSafeBlobKey(key);
4510
- return mutateExact(nodeFilesystemIdentityPort, this.#root, key, async (target) => {
4667
+ return mutateExact(this.#port, this.#root, key, async (target) => {
4511
4668
  if (options.expectedVersion !== void 0) {
4512
4669
  const existing = await target.current();
4513
4670
  const current = existing === null ? null : blobVersion(existing);
@@ -4521,7 +4678,7 @@ var FilesystemBackend = class _FilesystemBackend {
4521
4678
  }
4522
4679
  async deleteBlob(key, options = {}) {
4523
4680
  assertSafeBlobKey(key);
4524
- return mutateExact(nodeFilesystemIdentityPort, this.#root, key, async (target) => {
4681
+ return mutateExact(this.#port, this.#root, key, async (target) => {
4525
4682
  const existing = await target.current();
4526
4683
  if (existing === null) return false;
4527
4684
  const current = blobVersion(existing);
@@ -4534,14 +4691,14 @@ var FilesystemBackend = class _FilesystemBackend {
4534
4691
  }
4535
4692
  async existsBlob(key) {
4536
4693
  assertSafeBlobKey(key);
4537
- const observed = await probeExact(nodeFilesystemIdentityPort, this.#root, key);
4694
+ const observed = await probeExact(this.#port, this.#root, key);
4538
4695
  return observed.state === "exact" && observed.value.kind === "file";
4539
4696
  }
4540
4697
  async listBlobs(prefix) {
4541
- const files = await walkFiles(this.#root, (name) => !name.toLowerCase().endsWith(".md"));
4698
+ const files = await walkFiles(this.#port, this.#root, (name) => !name.toLowerCase().endsWith(".md"));
4542
4699
  const keys = files.map(toPosix);
4543
4700
  const filtered = prefix ? keys.filter((k) => k.startsWith(prefix)) : keys;
4544
- filtered.sort((a, b) => a.localeCompare(b));
4701
+ filtered.sort(compareStorageKeys);
4545
4702
  return filtered;
4546
4703
  }
4547
4704
  capabilities() {
@@ -4592,7 +4749,7 @@ setDefaultBackendFactory((root) => new FilesystemBackend(root));
4592
4749
  var CONVENTIONS_PREFIX = "conventions/";
4593
4750
  var CONVENTION_TYPE = "Convention";
4594
4751
  function buildKindRegistry(docs, initialWarnings = [], options = {}) {
4595
- const kinds = /* @__PURE__ */ new Map();
4752
+ const accepted = /* @__PURE__ */ new Map();
4596
4753
  const warnings = [...initialWarnings];
4597
4754
  for (const doc of [...docs].sort((a, b) => a.id.localeCompare(b.id))) {
4598
4755
  const parsed = parseConventionDoc(doc, options);
@@ -4615,10 +4772,10 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
4615
4772
  severity: "warning"
4616
4773
  });
4617
4774
  }
4618
- if (kinds.has(kind.governs)) {
4775
+ if (accepted.has(kind.governs)) {
4619
4776
  warnings.push({
4620
4777
  code: "KIND_DUPLICATE_GOVERNS",
4621
- message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${kinds.get(kind.governs).id}'.`,
4778
+ message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${accepted.get(kind.governs).id}'.`,
4622
4779
  field: kind.governs,
4623
4780
  severity: "warning"
4624
4781
  });
@@ -4632,10 +4789,18 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
4632
4789
  severity: "warning"
4633
4790
  });
4634
4791
  }
4635
- kinds.set(kind.governs, kind);
4792
+ accepted.set(kind.governs, kind);
4636
4793
  }
4794
+ const kinds = /* @__PURE__ */ new Map();
4795
+ for (const kind of [...accepted.values()].sort(compareReadingOrder)) kinds.set(kind.governs, kind);
4637
4796
  return { kinds, warnings };
4638
4797
  }
4798
+ function compareReadingOrder(a, b) {
4799
+ if (a.order !== void 0 && b.order !== void 0 && a.order !== b.order) return a.order - b.order;
4800
+ if (a.order !== void 0 && b.order === void 0) return -1;
4801
+ if (a.order === void 0 && b.order !== void 0) return 1;
4802
+ return a.id.localeCompare(b.id);
4803
+ }
4639
4804
  var PROGRESS_STATUS_FIELD = "progress_status";
4640
4805
  var SUPERBEE_PROGRESS_STATUS_FIELD = "superbee_progress_status";
4641
4806
  function progressStatusStorageField(okfVersion) {
@@ -5104,6 +5269,19 @@ function parseConventionDoc(doc, options = {}) {
5104
5269
  const path4 = typeof fm.path === "string" && fm.path.trim() !== "" ? fm.path.trim() : void 0;
5105
5270
  const freshnessHorizon = typeof fm.freshness_horizon === "string" && fm.freshness_horizon.trim() !== "" ? fm.freshness_horizon.trim() : void 0;
5106
5271
  const browseCollapsed = fm.browse_collapsed === true ? true : void 0;
5272
+ let order;
5273
+ if (fm.order !== void 0) {
5274
+ if (typeof fm.order === "number" && Number.isFinite(fm.order)) {
5275
+ order = fm.order;
5276
+ } else {
5277
+ warnings.push({
5278
+ code: "KIND_CONVENTION_BAD_SHAPE",
5279
+ message: `kind convention '${doc.id}' has an invalid 'order' (${describeShape(fm.order)}; expected a finite number such as 10); ignoring it.`,
5280
+ field: "order",
5281
+ severity: "warning"
5282
+ });
5283
+ }
5284
+ }
5107
5285
  const kind = {
5108
5286
  id: doc.id,
5109
5287
  title,
@@ -5118,6 +5296,7 @@ function parseConventionDoc(doc, options = {}) {
5118
5296
  if (sections && sections.length > 0) kind.sections = sections;
5119
5297
  if (freshnessHorizon !== void 0) kind.freshnessHorizon = freshnessHorizon;
5120
5298
  if (browseCollapsed !== void 0) kind.browseCollapsed = browseCollapsed;
5299
+ if (order !== void 0) kind.order = order;
5121
5300
  if (claim !== void 0) kind.claim = claim;
5122
5301
  return {
5123
5302
  ok: true,
@@ -5259,7 +5438,7 @@ function isAnyEntryKey(entry) {
5259
5438
  return isPageEntryKey(entry) || isViewEntryKey(entry);
5260
5439
  }
5261
5440
  function isViewEntryVersion(value) {
5262
- return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
5441
+ return isContentVersion(value);
5263
5442
  }
5264
5443
  function parseRegistration(id, frontmatter2) {
5265
5444
  if (!isAnyRegistryId(id) || !isPageTypeName(frontmatter2.type) || !isAnyEntryKey(frontmatter2.entry)) return null;
@@ -5286,9 +5465,45 @@ var MAX_QUERY_ROWS = 500;
5286
5465
  var MAX_EDGE_ROWS = 1e3;
5287
5466
  var MAX_DOCUMENT_BODY_BYTES = 1024 * 1024;
5288
5467
  var MAX_REPLY_BYTES = 2 * 1024 * 1024;
5468
+ var GRAPH_MAX_DOCUMENTS = 1e3;
5469
+ var GRAPH_MAX_RELATIONSHIPS = 1e4;
5289
5470
  var MAX_CHANGE_ROWS = 100;
5290
5471
  var MAX_CHANGE_BYTES = 256 * 1024;
5291
5472
  var MAX_SUBSCRIPTION_HEADS = 1e4;
5473
+ var MAX_HOST_CAPABILITY_BYTES = 128;
5474
+ var MAX_HOST_REQUEST_BYTES = 64 * 1024;
5475
+ var HOST_CAPABILITY_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
5476
+ var BRIDGE_SERVICE_LIMITS = Object.freeze({
5477
+ query: MAX_QUERY_ROWS,
5478
+ edges: MAX_EDGE_ROWS,
5479
+ graphDocuments: GRAPH_MAX_DOCUMENTS,
5480
+ graphRelationships: GRAPH_MAX_RELATIONSHIPS,
5481
+ replyBytes: MAX_REPLY_BYTES
5482
+ });
5483
+ var BRIDGE_HOST_CAPABILITIES = Object.freeze({
5484
+ queryKindProjection: "query.kind-projection",
5485
+ queryFieldOr: "query.field-or",
5486
+ queryOpen: "query.open",
5487
+ queryCount: "query.count",
5488
+ edges: "edges",
5489
+ renderDocument: "render-document",
5490
+ openPage: "open-page",
5491
+ subscribeDeltas: "subscribe-deltas",
5492
+ graph: "graph",
5493
+ graphModel: "graph.model",
5494
+ recordOpen: "record.open",
5495
+ frameResize: "frame.resize"
5496
+ });
5497
+ var BRIDGE_SERVICE_CAPABILITIES = Object.freeze([
5498
+ BRIDGE_HOST_CAPABILITIES.queryKindProjection,
5499
+ BRIDGE_HOST_CAPABILITIES.queryFieldOr,
5500
+ BRIDGE_HOST_CAPABILITIES.queryOpen,
5501
+ BRIDGE_HOST_CAPABILITIES.queryCount,
5502
+ BRIDGE_HOST_CAPABILITIES.edges,
5503
+ BRIDGE_HOST_CAPABILITIES.graph,
5504
+ BRIDGE_HOST_CAPABILITIES.renderDocument
5505
+ ]);
5506
+ var V0_REQUEST_TYPES = /* @__PURE__ */ new Set(["hello", "query", "read", "render-document", "edges", "graph", "subscribe", "host", "open-page"]);
5292
5507
  function isPlainRecord(value) {
5293
5508
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
5294
5509
  const prototype = Object.getPrototypeOf(value);
@@ -5421,6 +5636,45 @@ function parseBridgeRequest(value) {
5421
5636
  const params = normalizeEdgeParams(value.params);
5422
5637
  return params ? { bridge: BRIDGE_PROTOCOL, type: "edges", id, params } : null;
5423
5638
  }
5639
+ if (value.type === "graph") {
5640
+ const expected = value.includeBodies === void 0 ? ["bridge", "type", "id"] : ["bridge", "type", "id", "includeBodies"];
5641
+ if (!exactKeys(value, expected)) return null;
5642
+ if (value.includeBodies !== void 0 && typeof value.includeBodies !== "boolean") return null;
5643
+ return { bridge: BRIDGE_PROTOCOL, type: "graph", id, includeBodies: value.includeBodies === true };
5644
+ }
5645
+ if (value.type === "host") {
5646
+ const expected = value.input === void 0 ? ["bridge", "type", "id", "capability"] : ["bridge", "type", "id", "capability", "input"];
5647
+ if (!exactKeys(value, expected)) return null;
5648
+ const capability = boundedString(value.capability, MAX_HOST_CAPABILITY_BYTES);
5649
+ if (!capability || !HOST_CAPABILITY_NAME.test(capability)) return null;
5650
+ if (value.input !== void 0 && !isPlainRecord(value.input)) return null;
5651
+ let bytes;
5652
+ try {
5653
+ bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
5654
+ } catch {
5655
+ return null;
5656
+ }
5657
+ if (bytes > MAX_HOST_REQUEST_BYTES) return null;
5658
+ return {
5659
+ bridge: BRIDGE_PROTOCOL,
5660
+ type: "host",
5661
+ id,
5662
+ capability,
5663
+ ...value.input === void 0 ? {} : { input: value.input }
5664
+ };
5665
+ }
5666
+ return null;
5667
+ }
5668
+ function unsupportedRequest(value) {
5669
+ if (!isPlainRecord(value) || typeof value.type !== "string") return null;
5670
+ if (value.bridge === BRIDGE_PROTOCOL) {
5671
+ return V0_REQUEST_TYPES.has(value.type) ? null : { bridge: BRIDGE_PROTOCOL, id: requestId(value.id) ?? void 0 };
5672
+ }
5673
+ if (value.bridge === ACTION_BRIDGE_PROTOCOL) {
5674
+ if (value.type === "read-versioned") return null;
5675
+ const id = value.type === "action.propose" ? value.requestId : value.id;
5676
+ return { bridge: ACTION_BRIDGE_PROTOCOL, id: requestId(id) ?? void 0 };
5677
+ }
5424
5678
  return null;
5425
5679
  }
5426
5680
  function ok(id, bridge, type, result) {
@@ -5445,6 +5699,17 @@ var BridgeService = class {
5445
5699
  async handle(launchId, rawRequest) {
5446
5700
  const request = parseBridgeRequest(rawRequest);
5447
5701
  if (!request) {
5702
+ const unsupported = unsupportedRequest(rawRequest);
5703
+ if (unsupported) {
5704
+ return {
5705
+ reply: fail(
5706
+ unsupported.id,
5707
+ unsupported.bridge,
5708
+ "FORBIDDEN",
5709
+ "this host does not offer the requested bridge operation"
5710
+ )
5711
+ };
5712
+ }
5448
5713
  return {
5449
5714
  reply: fail(
5450
5715
  invalidV0RequestId(rawRequest),
@@ -5454,16 +5719,6 @@ var BridgeService = class {
5454
5719
  )
5455
5720
  };
5456
5721
  }
5457
- if (request.bridge === ACTION_BRIDGE_PROTOCOL && this.options.allowActionProtocol === false) {
5458
- return {
5459
- reply: fail(
5460
- request.id,
5461
- request.bridge,
5462
- "FORBIDDEN",
5463
- "this host admits only the read-only v0 View bridge"
5464
- )
5465
- };
5466
- }
5467
5722
  const dataBearing = request.type !== "open-page";
5468
5723
  const before = await this.options.launches.resolve(launchId, dataBearing);
5469
5724
  if (!before) {
@@ -5569,6 +5824,73 @@ var BridgeService = class {
5569
5824
  [...rows].sort((a, b) => a.id.localeCompare(b.id)).map((row) => [row.id, row.version])
5570
5825
  );
5571
5826
  }
5827
+ /**
5828
+ * Whole-bundle projection for graph-shaped Views. The documents come from one head scan and
5829
+ * the relationships from `queryEdges`, which is a second full-bundle scan; the two scans are
5830
+ * accepted because the reply is bounded by the exported graph limits and the host never
5831
+ * caches bundle state on a View's behalf. No `model` or `definitions` are answered here: the
5832
+ * owner of that shape is undecided, and a host that has one declares the `graph.model`
5833
+ * capability before adding them.
5834
+ */
5835
+ async graph(launch, request) {
5836
+ const heads = await queryHeads2(this.options.bundle, {});
5837
+ if (heads.length > GRAPH_MAX_DOCUMENTS) {
5838
+ return {
5839
+ reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_DOCUMENTS} documents`)
5840
+ };
5841
+ }
5842
+ const [registry, declaredOkfVersion, edges] = await Promise.all([
5843
+ loadKinds(this.options.bundle),
5844
+ readBundleOkfVersion2(this.options.bundle),
5845
+ queryEdges2(this.options.bundle, {})
5846
+ ]);
5847
+ if (edges.length > GRAPH_MAX_RELATIONSHIPS) {
5848
+ return {
5849
+ reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_RELATIONSHIPS} relationships`)
5850
+ };
5851
+ }
5852
+ const okfVersion = declaredOkfVersion ?? "0.1";
5853
+ const includeBodies = request.includeBodies && (launch.capability === "bundle-read" || launch.capability === "bundle-propose");
5854
+ const documents = [];
5855
+ for (const head of heads) {
5856
+ const source = includeBodies ? await readDocVersioned2(this.options.bundle, head.id).then((result) => ({
5857
+ version: result.version,
5858
+ frontmatter: result.doc.frontmatter,
5859
+ body: result.doc.body
5860
+ })) : { version: head.version, frontmatter: head.frontmatter };
5861
+ if (source.body !== void 0 && Buffer.byteLength(source.body, "utf8") > MAX_DOCUMENT_BODY_BYTES) {
5862
+ return { reply: fail(request.id, request.bridge, "TOO_LARGE", "a document body exceeded the 1 MiB View limit") };
5863
+ }
5864
+ const kind = registry.kinds.get(String(source.frontmatter.type ?? ""));
5865
+ const frontmatter2 = kind ? projectLogicalKindFields(okfVersion, kind, source.frontmatter) : source.frontmatter;
5866
+ documents.push({
5867
+ id: head.id,
5868
+ version: source.version,
5869
+ frontmatter: frontmatter2,
5870
+ ...source.body === void 0 ? {} : { body: source.body }
5871
+ });
5872
+ }
5873
+ const relationships = edges.map(({ from, to, text }) => ({ from, to, text }));
5874
+ return {
5875
+ reply: ok(request.id, request.bridge, request.type, {
5876
+ okfVersion,
5877
+ documents,
5878
+ relationships,
5879
+ counts: { documents: documents.length, relationships: relationships.length }
5880
+ })
5881
+ };
5882
+ }
5883
+ hostDescriptor() {
5884
+ const declared = new Set(this.options.host.capabilities);
5885
+ for (const name of Object.keys(this.options.hostHandlers ?? {})) declared.add(name);
5886
+ return {
5887
+ kind: this.options.host.kind,
5888
+ capabilities: [...declared].sort(),
5889
+ limits: { ...this.options.host.limits },
5890
+ ...this.options.host.frame ? { frame: { ...this.options.host.frame } } : {},
5891
+ ...this.options.host.theme ? { theme: { ...this.options.host.theme } } : {}
5892
+ };
5893
+ }
5572
5894
  async execute(launch, request) {
5573
5895
  if (request.type === "open-page") {
5574
5896
  if (this.options.consumeOpenPage === true) {
@@ -5592,10 +5914,25 @@ var BridgeService = class {
5592
5914
  bundle: { root: config.root, name: config.name },
5593
5915
  mode: config.mode,
5594
5916
  protocol: BRIDGE_PROTOCOL,
5595
- grant: launch.capability === "bundle-propose" ? "propose" : "read"
5917
+ grant: launch.capability === "bundle-propose" ? "propose" : "read",
5918
+ ...config.mode === "dir" && this.options.host.kind === "oss" && launch.capability === "bundle-propose" ? { actionProtocol: "v1", actions: ["document.set-field", "document.set-body", "document.update"] } : {},
5919
+ host: this.hostDescriptor()
5596
5920
  })
5597
5921
  };
5598
5922
  }
5923
+ if (request.type === "host") {
5924
+ const handlers = this.options.hostHandlers ?? {};
5925
+ if (!Object.hasOwn(handlers, request.capability)) {
5926
+ return { reply: fail(request.id, request.bridge, "FORBIDDEN", "this host does not offer the requested capability") };
5927
+ }
5928
+ const outcome = await handlers[request.capability]({
5929
+ capability: request.capability,
5930
+ input: request.input,
5931
+ launch
5932
+ });
5933
+ if (!outcome.ok) return { reply: fail(request.id, request.bridge, outcome.code, outcome.message) };
5934
+ return { reply: ok(request.id, request.bridge, request.type, { capability: request.capability, output: outcome.output }) };
5935
+ }
5599
5936
  if (request.type === "query") {
5600
5937
  const rows = await queryHeads2(this.options.bundle, {
5601
5938
  ...request.params.type ? { type: request.params.type } : {},
@@ -5673,6 +6010,9 @@ var BridgeService = class {
5673
6010
  const projected = edges.map(({ from, to, text }) => ({ from, to, text }));
5674
6011
  return { reply: ok(request.id, request.bridge, request.type, { edges: projected, count: projected.length }) };
5675
6012
  }
6013
+ if (request.type === "graph") {
6014
+ return this.graph(launch, request);
6015
+ }
5676
6016
  if (this.options.enablePolling) {
5677
6017
  this.subscriptions.set(launch.launchId, {
5678
6018
  baseline: await this.subscriptionSnapshot()
@@ -5821,6 +6161,10 @@ var STATIC_LAUNCH_ID = "publication-snapshot";
5821
6161
  function digest(bytes) {
5822
6162
  return `sha256:${createHash3("sha256").update(bytes).digest("hex")}`;
5823
6163
  }
6164
+ var STATIC_BRIDGE_CAPABILITIES = Object.freeze([
6165
+ ...BRIDGE_SERVICE_CAPABILITIES,
6166
+ BRIDGE_HOST_CAPABILITIES.openPage
6167
+ ]);
5824
6168
  function createPublicationBridge(options) {
5825
6169
  if (options.protocol !== PUBLICATION_BRIDGE_V0) {
5826
6170
  throw new PublicationError("CAPABILITY_UNAVAILABLE", "the requested publication bridge protocol is unsupported");
@@ -5868,7 +6212,11 @@ function createPublicationBridge(options) {
5868
6212
  if (!rendered || rendered.id !== id) throw new Error(`document '${id}' is not available`);
5869
6213
  return { html: rendered.html, bounded: rendered.bounded };
5870
6214
  },
5871
- allowActionProtocol: false,
6215
+ host: {
6216
+ kind: options.host?.kind ?? "oss",
6217
+ capabilities: options.host?.capabilities ?? STATIC_BRIDGE_CAPABILITIES,
6218
+ limits: BRIDGE_SERVICE_LIMITS
6219
+ },
5872
6220
  enablePolling: false,
5873
6221
  consumeOpenPage: false
5874
6222
  });