superbee 0.2.1-pre.2 → 0.3.0-pre.1

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,44 +3658,43 @@ 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);
@@ -3659,24 +3713,89 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs) {
3659
3713
  });
3660
3714
  throw err;
3661
3715
  }
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
- );
3716
+ let inFlight;
3717
+ return () => {
3718
+ if (inFlight) return inFlight;
3719
+ inFlight = (async () => {
3720
+ const started2 = Date.now();
3721
+ const current = await resolveOwnerRecord(lockPath, started2, waitMs, pollMs);
3722
+ if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
3723
+ if (current.state !== "record" || current.owner.token !== owner.token) {
3724
+ throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
3725
+ }
3726
+ await removeReleasedLock(lockPath, owner, started2, waitMs, pollMs, policy);
3727
+ })();
3728
+ return inFlight.finally(() => {
3729
+ inFlight = void 0;
3730
+ });
3731
+ };
3732
+ }
3733
+ }
3734
+ function changedOwnerRefusal(lockPath, current) {
3735
+ return new FilesystemMutationLockError(
3736
+ `refusing to release filesystem mutation lock '${lockPath}' because its owner token changed; the mutation may have completed, inspect the lock before retrying.`,
3737
+ { lockPath, owner: current, stale: false, malformed: current === null }
3738
+ );
3739
+ }
3740
+ function unreadableOwnerRefusal(lockPath, error) {
3741
+ const message = error instanceof Error ? error.message : String(error);
3742
+ return new FilesystemMutationLockError(
3743
+ `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.`,
3744
+ { lockPath, owner: null, stale: false, malformed: true }
3745
+ );
3746
+ }
3747
+ function releasedLockRemnantPath(lockPath, owner) {
3748
+ const tokenHash = createHash("sha256").update(owner.token).digest("hex");
3749
+ return `${lockPath}.released-${tokenHash}`;
3750
+ }
3751
+ function removalFailure(inspectPath, owner, err, attempts, detail) {
3752
+ const message = err instanceof Error ? err.message : String(err);
3753
+ const suffix = attempts > 1 ? ` after ${attempts} bounded attempts` : "";
3754
+ return new FilesystemMutationLockError(
3755
+ `mutation completed but ${detail}${suffix} (${message}); inspect the lock before retrying.`,
3756
+ { lockPath: inspectPath, owner, stale: false, malformed: false }
3757
+ );
3758
+ }
3759
+ async function removeReleasedLock(lockPath, owner, started, waitMs, pollMs, policy) {
3760
+ const remnant = releasedLockRemnantPath(lockPath, owner);
3761
+ let attempts = 0;
3762
+ while (true) {
3763
+ try {
3764
+ await fs.rename(lockPath, remnant);
3765
+ break;
3766
+ } catch (err) {
3767
+ attempts += 1;
3768
+ if (attempts > 1 && err.code === "ENOENT") return;
3769
+ if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
3770
+ throw removalFailure(lockPath, owner, err, attempts, `filesystem lock '${lockPath}' could not be removed`);
3669
3771
  }
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 }
3772
+ }
3773
+ await delay(pollMs);
3774
+ const current = await resolveOwnerRecord(lockPath, started, waitMs, pollMs);
3775
+ if (current.state === "record" && current.owner.token === owner.token) continue;
3776
+ if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
3777
+ if (current.state === "absent" && !await pathExists(lockPath)) return;
3778
+ throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
3779
+ }
3780
+ attempts = 0;
3781
+ while (true) {
3782
+ try {
3783
+ await fs.rm(remnant, { recursive: true, force: false });
3784
+ return;
3785
+ } catch (err) {
3786
+ attempts += 1;
3787
+ if (err.code === "ENOENT") return;
3788
+ if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
3789
+ throw removalFailure(
3790
+ remnant,
3791
+ owner,
3792
+ err,
3793
+ attempts,
3794
+ `filesystem lock '${lockPath}' was released yet its remnant '${remnant}' could not be removed`
3677
3795
  );
3678
3796
  }
3679
- };
3797
+ }
3798
+ await delay(pollMs);
3680
3799
  }
3681
3800
  }
3682
3801
  function newOwner(target) {
@@ -3694,21 +3813,20 @@ function assertIdentityKey(key) {
3694
3813
  }
3695
3814
  async function acquireFilesystemIdentityLock(key, identity, options = {}) {
3696
3815
  assertIdentityKey(key);
3816
+ const policy = captureFilesystemHostPolicy(options.hostPolicy);
3697
3817
  const waitMs = positiveOption(options.waitMs, DEFAULT_WAIT_MS, "waitMs");
3698
3818
  const pollMs = positiveOption(options.pollMs, DEFAULT_POLL_MS, "pollMs");
3699
3819
  const owner = newOwner(identity);
3700
- const lockRoot = await selectLockRoot(options);
3701
- return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs);
3820
+ const lockRoot = await selectLockRoot(options, policy);
3821
+ return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs, policy);
3702
3822
  }
3703
3823
 
3704
3824
  // ../core/src/filesystem-identity.ts
3705
- var WINDOWS_REPLACE_CONFLICT_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
3706
3825
  async function replaceExistingLeaf(port, tmp, target, rel) {
3707
3826
  try {
3708
3827
  await port.rename(tmp, target);
3709
3828
  } catch (err) {
3710
- const code = err.code ?? "";
3711
- if ((port.platform ?? process.platform) === "win32" && WINDOWS_REPLACE_CONFLICT_CODES.has(code)) {
3829
+ if (port.isReplacementConflict?.(err)) {
3712
3830
  throw new ConcurrentReplacementError(rel, 1, 10);
3713
3831
  }
3714
3832
  throw err;
@@ -3875,7 +3993,7 @@ async function observeExact(port, root, rel, read) {
3875
3993
  opened = await port.open(target);
3876
3994
  } catch (err) {
3877
3995
  if (isAbsentPathError(err)) return { state: "absent" };
3878
- if (process.platform === "win32" && err.code === "EPERM") {
3996
+ if (port.isTransientOpenError?.(err)) {
3879
3997
  restarts = countRestart(restarts, rel);
3880
3998
  continue;
3881
3999
  }
@@ -3909,7 +4027,15 @@ async function probeExact(port, root, rel) {
3909
4027
  restarts = countRestart(restarts, rel);
3910
4028
  }
3911
4029
  }
3912
- var linkUnsupportedRoots = /* @__PURE__ */ new Set();
4030
+ var linkUnsupportedRoots = /* @__PURE__ */ new WeakMap();
4031
+ function unsupportedLinkRoots(port) {
4032
+ let roots = linkUnsupportedRoots.get(port);
4033
+ if (!roots) {
4034
+ roots = /* @__PURE__ */ new Set();
4035
+ linkUnsupportedRoots.set(port, roots);
4036
+ }
4037
+ return roots;
4038
+ }
3913
4039
  var queues = /* @__PURE__ */ new Map();
3914
4040
  function enqueue(key, fn) {
3915
4041
  const tail = queues.get(key) ?? Promise.resolve();
@@ -3991,7 +4117,7 @@ async function createExactLeaf(port, tmp, target, rel, leaf, rootResolved) {
3991
4117
  return;
3992
4118
  }
3993
4119
  if (outcome === "unsupported") {
3994
- linkUnsupportedRoots.add(rootResolved);
4120
+ unsupportedLinkRoots(port).add(rootResolved);
3995
4121
  await port.rename(tmp, target);
3996
4122
  return;
3997
4123
  }
@@ -4027,7 +4153,7 @@ async function mutateExact(port, root, rel, body) {
4027
4153
  await replaceExistingLeaf(port, tmp, target, rel);
4028
4154
  return;
4029
4155
  }
4030
- if (linkUnsupportedRoots.has(rootResolved)) {
4156
+ if (unsupportedLinkRoots(port).has(rootResolved)) {
4031
4157
  await port.rename(tmp, target);
4032
4158
  return;
4033
4159
  }
@@ -4054,80 +4180,84 @@ function kindOf(stats) {
4054
4180
  if (stats.isFile()) return "file";
4055
4181
  return "other";
4056
4182
  }
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;
4183
+ function createNodeFilesystemIdentityPort(hostPolicy) {
4184
+ const policy = captureFilesystemHostPolicy(hostPolicy);
4185
+ return Object.freeze({
4186
+ isTransientOpenError: policy.isTransientOpenError,
4187
+ isReplacementConflict: policy.isReplacementConflict,
4188
+ async probe(target) {
4189
+ let lstats;
4190
+ try {
4191
+ lstats = await fs2.lstat(target);
4192
+ } catch (err) {
4193
+ if (isAbsentPathError(err)) return null;
4194
+ throw err;
4195
+ }
4196
+ return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
4197
+ },
4198
+ async entries(dir) {
4199
+ try {
4200
+ const dirents = await fs2.readdir(dir, { withFileTypes: true });
4201
+ return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
4202
+ } catch (err) {
4203
+ if (isAbsentPathError(err)) return null;
4204
+ throw err;
4205
+ }
4206
+ },
4207
+ async open(target) {
4208
+ const handle = await fs2.open(target, "r");
4209
+ try {
4210
+ const stats = await handle.stat();
4211
+ return { handle, dev: stats.dev, ino: stats.ino };
4212
+ } catch (err) {
4213
+ await handle.close().catch(() => {
4214
+ });
4215
+ throw err;
4216
+ }
4217
+ },
4218
+ readAll(handle) {
4219
+ return handle.readFile();
4220
+ },
4221
+ close(handle) {
4222
+ return handle.close();
4223
+ },
4224
+ async stat(target) {
4225
+ return { mtime: (await fs2.stat(target)).mtime };
4226
+ },
4227
+ async mkdir(dir) {
4228
+ try {
4229
+ await fs2.mkdir(dir);
4230
+ return "created";
4231
+ } catch (err) {
4232
+ if (err.code === "EEXIST") return "exists";
4233
+ throw err;
4234
+ }
4235
+ },
4236
+ async writeTemp(dir, name, bytes) {
4237
+ await fs2.writeFile(path2.join(dir, name), bytes);
4238
+ },
4239
+ async link(from, to) {
4240
+ try {
4241
+ await fs2.link(from, to);
4242
+ return "linked";
4243
+ } catch (err) {
4244
+ const code = err.code;
4245
+ if (code === "EEXIST") return "exists";
4246
+ if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV") return "unsupported";
4247
+ throw err;
4248
+ }
4249
+ },
4250
+ async rename(from, to) {
4251
+ await fs2.rename(from, to);
4252
+ },
4253
+ async unlink(target) {
4254
+ await fs2.unlink(target);
4255
+ },
4256
+ claim(key, identity) {
4257
+ return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root, hostPolicy: policy });
4119
4258
  }
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
- });
4259
+ });
4260
+ }
4131
4261
 
4132
4262
  // ../core/src/sha256.ts
4133
4263
  var K = new Uint32Array([
@@ -4286,6 +4416,9 @@ function sha256HexOfUtf8(input) {
4286
4416
  }
4287
4417
 
4288
4418
  // ../core/src/version-transport.ts
4419
+ function isContentVersion(value) {
4420
+ return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
4421
+ }
4289
4422
  var VersionConflict = class extends Error {
4290
4423
  name = "VersionConflict";
4291
4424
  id;
@@ -4317,7 +4450,7 @@ function defaultActor() {
4317
4450
  }
4318
4451
 
4319
4452
  // ../core/src/backend.ts
4320
- var publicationRoots = /* @__PURE__ */ new WeakMap();
4453
+ var publicationContexts = /* @__PURE__ */ new WeakMap();
4321
4454
  function firstString(...vals) {
4322
4455
  for (const v of vals) {
4323
4456
  if (typeof v === "string" && v.trim() !== "") return v;
@@ -4336,22 +4469,22 @@ function notFound(root, rel) {
4336
4469
  err.path = target;
4337
4470
  return err;
4338
4471
  }
4339
- async function readBytes(handle) {
4472
+ async function readBytes(port, handle) {
4340
4473
  try {
4341
- return await nodeFilesystemIdentityPort.readAll(handle);
4474
+ return await port.readAll(handle);
4342
4475
  } catch (err) {
4343
4476
  if (isAbsentFileError(err)) return null;
4344
4477
  throw err;
4345
4478
  }
4346
4479
  }
4347
- async function walkFiles(root, keep, sub = "") {
4348
- const entries = await nodeFilesystemIdentityPort.entries(path3.join(root, sub)) ?? [];
4480
+ async function walkFiles(port, root, keep, sub = "") {
4481
+ const entries = await port.entries(path3.join(root, sub)) ?? [];
4349
4482
  const out = [];
4350
4483
  for (const entry of entries) {
4351
4484
  if (entry.name.startsWith(".")) continue;
4352
4485
  const rel = sub === "" ? entry.name : `${sub}/${entry.name}`;
4353
4486
  if (entry.kind === "directory") {
4354
- out.push(...await walkFiles(root, keep, rel));
4487
+ out.push(...await walkFiles(port, root, keep, rel));
4355
4488
  } else if (entry.kind === "file" && keep(entry.name)) {
4356
4489
  out.push(rel);
4357
4490
  }
@@ -4359,19 +4492,20 @@ async function walkFiles(root, keep, sub = "") {
4359
4492
  return out;
4360
4493
  }
4361
4494
  function reservedPath(dir, name) {
4362
- const d = toPosix(dir).replace(/^\.?\//, "").replace(/\/$/, "");
4363
- return d === "" ? name : `${d}/${name}`;
4495
+ return dir === "" ? name : `${dir}/${name}`;
4364
4496
  }
4365
4497
  var FilesystemBackend = class _FilesystemBackend {
4366
4498
  #root;
4499
+ #port;
4367
4500
  /**
4368
4501
  * The root is resolved once here: a relative root would otherwise re-resolve against the
4369
4502
  * process's current directory on every operation, so a later `chdir` would silently move the
4370
4503
  * bundle and derive a different identity key.
4371
4504
  */
4372
- constructor(root) {
4505
+ constructor(root, options = {}) {
4506
+ this.#port = createNodeFilesystemIdentityPort(options.hostPolicy);
4373
4507
  this.#root = path3.resolve(root);
4374
- publicationRoots.set(this, this.#root);
4508
+ publicationContexts.set(this, { root: this.#root, port: this.#port });
4375
4509
  }
4376
4510
  async read(id) {
4377
4511
  return this.#readWithEdition(id, () => this.#readEdition());
@@ -4387,7 +4521,7 @@ var FilesystemBackend = class _FilesystemBackend {
4387
4521
  async #readWithEdition(id, edition) {
4388
4522
  assertSafeConceptId(id);
4389
4523
  const rel = pathFromConceptId(id);
4390
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, rel, readBytes);
4524
+ const observed = await observeExact(this.#port, this.#root, rel, (handle) => readBytes(this.#port, handle));
4391
4525
  if (observed.state === "absent") throw notFound(this.#root, rel);
4392
4526
  const raw = observed.value.toString("utf8");
4393
4527
  const { frontmatter: frontmatter2, body } = parseMarkdown(raw, rel, { okfVersion: await edition() });
@@ -4409,7 +4543,7 @@ var FilesystemBackend = class _FilesystemBackend {
4409
4543
  assertSafeConceptId(id);
4410
4544
  const raw = stringifyDoc(doc.frontmatter, doc.body ?? "");
4411
4545
  const rel = pathFromConceptId(id);
4412
- return mutateExact(nodeFilesystemIdentityPort, this.#root, rel, async (target) => {
4546
+ return mutateExact(this.#port, this.#root, rel, async (target) => {
4413
4547
  if (options.expectedVersion !== void 0) {
4414
4548
  const bytes = await target.current();
4415
4549
  const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
@@ -4423,7 +4557,7 @@ var FilesystemBackend = class _FilesystemBackend {
4423
4557
  }
4424
4558
  async delete(id, options = {}) {
4425
4559
  assertSafeConceptId(id);
4426
- return mutateExact(nodeFilesystemIdentityPort, this.#root, pathFromConceptId(id), async (target) => {
4560
+ return mutateExact(this.#port, this.#root, pathFromConceptId(id), async (target) => {
4427
4561
  const bytes = await target.current();
4428
4562
  if (bytes === null) return false;
4429
4563
  const current = versionOfBytes(bytes.toString("utf8"));
@@ -4437,10 +4571,10 @@ var FilesystemBackend = class _FilesystemBackend {
4437
4571
  async versions(id) {
4438
4572
  assertSafeConceptId(id);
4439
4573
  const rel = pathFromConceptId(id);
4440
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, rel, async (handle, target) => {
4574
+ const observed = await observeExact(this.#port, this.#root, rel, async (handle, target) => {
4441
4575
  try {
4442
- const bytes = await nodeFilesystemIdentityPort.readAll(handle);
4443
- const { mtime: mtime2 } = await nodeFilesystemIdentityPort.stat(target);
4576
+ const bytes = await this.#port.readAll(handle);
4577
+ const { mtime: mtime2 } = await this.#port.stat(target);
4444
4578
  return { raw: bytes.toString("utf8"), mtime: mtime2 };
4445
4579
  } catch {
4446
4580
  return null;
@@ -4455,10 +4589,10 @@ var FilesystemBackend = class _FilesystemBackend {
4455
4589
  }
4456
4590
  async exists(id) {
4457
4591
  assertSafeConceptId(id);
4458
- return (await probeExact(nodeFilesystemIdentityPort, this.#root, pathFromConceptId(id))).state === "exact";
4592
+ return (await probeExact(this.#port, this.#root, pathFromConceptId(id))).state === "exact";
4459
4593
  }
4460
4594
  async list(prefix) {
4461
- const files = await walkFiles(this.#root, (name) => name.endsWith(".md"));
4595
+ const files = await walkFiles(this.#port, this.#root, (name) => name.endsWith(".md"));
4462
4596
  const ids = [];
4463
4597
  for (const rel of files) {
4464
4598
  if (isReservedFile(rel)) continue;
@@ -4472,20 +4606,22 @@ var FilesystemBackend = class _FilesystemBackend {
4472
4606
  if (prefix && !id.startsWith(prefix)) continue;
4473
4607
  ids.push(id);
4474
4608
  }
4475
- ids.sort((a, b) => a.localeCompare(b));
4609
+ ids.sort(compareStorageKeys);
4476
4610
  return ids;
4477
4611
  }
4478
4612
  async readReserved(dir, name) {
4479
4613
  assertSafeReservedDir(dir);
4480
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, reservedPath(dir, name), readBytes);
4614
+ assertSafeReservedFilename(name);
4615
+ const observed = await observeExact(this.#port, this.#root, reservedPath(dir, name), (handle) => readBytes(this.#port, handle));
4481
4616
  if (observed.state === "absent") return null;
4482
4617
  const content = observed.value.toString("utf8");
4483
4618
  return { content, version: versionOfBytes(content) };
4484
4619
  }
4485
4620
  async writeReserved(dir, name, content, options = {}) {
4486
4621
  assertSafeReservedDir(dir);
4622
+ assertSafeReservedFilename(name);
4487
4623
  const rel = reservedPath(dir, name);
4488
- return mutateExact(nodeFilesystemIdentityPort, this.#root, rel, async (target) => {
4624
+ return mutateExact(this.#port, this.#root, rel, async (target) => {
4489
4625
  if (options.expectedVersion !== void 0) {
4490
4626
  const bytes = await target.current();
4491
4627
  const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
@@ -4500,14 +4636,14 @@ var FilesystemBackend = class _FilesystemBackend {
4500
4636
  // ── blobs: opaque bytes + a content-type ──────────────────────────────────
4501
4637
  async readBlob(key) {
4502
4638
  assertSafeBlobKey(key);
4503
- const observed = await observeExact(nodeFilesystemIdentityPort, this.#root, key, readBytes);
4639
+ const observed = await observeExact(this.#port, this.#root, key, (handle) => readBytes(this.#port, handle));
4504
4640
  if (observed.state === "absent") return null;
4505
4641
  const bytes = observed.value;
4506
4642
  return { bytes, contentType: resolveContentType(key), version: blobVersion(bytes) };
4507
4643
  }
4508
4644
  async writeBlob(key, bytes, _contentType, options = {}) {
4509
4645
  assertSafeBlobKey(key);
4510
- return mutateExact(nodeFilesystemIdentityPort, this.#root, key, async (target) => {
4646
+ return mutateExact(this.#port, this.#root, key, async (target) => {
4511
4647
  if (options.expectedVersion !== void 0) {
4512
4648
  const existing = await target.current();
4513
4649
  const current = existing === null ? null : blobVersion(existing);
@@ -4521,7 +4657,7 @@ var FilesystemBackend = class _FilesystemBackend {
4521
4657
  }
4522
4658
  async deleteBlob(key, options = {}) {
4523
4659
  assertSafeBlobKey(key);
4524
- return mutateExact(nodeFilesystemIdentityPort, this.#root, key, async (target) => {
4660
+ return mutateExact(this.#port, this.#root, key, async (target) => {
4525
4661
  const existing = await target.current();
4526
4662
  if (existing === null) return false;
4527
4663
  const current = blobVersion(existing);
@@ -4534,14 +4670,14 @@ var FilesystemBackend = class _FilesystemBackend {
4534
4670
  }
4535
4671
  async existsBlob(key) {
4536
4672
  assertSafeBlobKey(key);
4537
- const observed = await probeExact(nodeFilesystemIdentityPort, this.#root, key);
4673
+ const observed = await probeExact(this.#port, this.#root, key);
4538
4674
  return observed.state === "exact" && observed.value.kind === "file";
4539
4675
  }
4540
4676
  async listBlobs(prefix) {
4541
- const files = await walkFiles(this.#root, (name) => !name.toLowerCase().endsWith(".md"));
4677
+ const files = await walkFiles(this.#port, this.#root, (name) => !name.toLowerCase().endsWith(".md"));
4542
4678
  const keys = files.map(toPosix);
4543
4679
  const filtered = prefix ? keys.filter((k) => k.startsWith(prefix)) : keys;
4544
- filtered.sort((a, b) => a.localeCompare(b));
4680
+ filtered.sort(compareStorageKeys);
4545
4681
  return filtered;
4546
4682
  }
4547
4683
  capabilities() {
@@ -4592,7 +4728,7 @@ setDefaultBackendFactory((root) => new FilesystemBackend(root));
4592
4728
  var CONVENTIONS_PREFIX = "conventions/";
4593
4729
  var CONVENTION_TYPE = "Convention";
4594
4730
  function buildKindRegistry(docs, initialWarnings = [], options = {}) {
4595
- const kinds = /* @__PURE__ */ new Map();
4731
+ const accepted = /* @__PURE__ */ new Map();
4596
4732
  const warnings = [...initialWarnings];
4597
4733
  for (const doc of [...docs].sort((a, b) => a.id.localeCompare(b.id))) {
4598
4734
  const parsed = parseConventionDoc(doc, options);
@@ -4615,10 +4751,10 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
4615
4751
  severity: "warning"
4616
4752
  });
4617
4753
  }
4618
- if (kinds.has(kind.governs)) {
4754
+ if (accepted.has(kind.governs)) {
4619
4755
  warnings.push({
4620
4756
  code: "KIND_DUPLICATE_GOVERNS",
4621
- message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${kinds.get(kind.governs).id}'.`,
4757
+ message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${accepted.get(kind.governs).id}'.`,
4622
4758
  field: kind.governs,
4623
4759
  severity: "warning"
4624
4760
  });
@@ -4632,10 +4768,18 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
4632
4768
  severity: "warning"
4633
4769
  });
4634
4770
  }
4635
- kinds.set(kind.governs, kind);
4771
+ accepted.set(kind.governs, kind);
4636
4772
  }
4773
+ const kinds = /* @__PURE__ */ new Map();
4774
+ for (const kind of [...accepted.values()].sort(compareReadingOrder)) kinds.set(kind.governs, kind);
4637
4775
  return { kinds, warnings };
4638
4776
  }
4777
+ function compareReadingOrder(a, b) {
4778
+ if (a.order !== void 0 && b.order !== void 0 && a.order !== b.order) return a.order - b.order;
4779
+ if (a.order !== void 0 && b.order === void 0) return -1;
4780
+ if (a.order === void 0 && b.order !== void 0) return 1;
4781
+ return a.id.localeCompare(b.id);
4782
+ }
4639
4783
  var PROGRESS_STATUS_FIELD = "progress_status";
4640
4784
  var SUPERBEE_PROGRESS_STATUS_FIELD = "superbee_progress_status";
4641
4785
  function progressStatusStorageField(okfVersion) {
@@ -5104,6 +5248,19 @@ function parseConventionDoc(doc, options = {}) {
5104
5248
  const path4 = typeof fm.path === "string" && fm.path.trim() !== "" ? fm.path.trim() : void 0;
5105
5249
  const freshnessHorizon = typeof fm.freshness_horizon === "string" && fm.freshness_horizon.trim() !== "" ? fm.freshness_horizon.trim() : void 0;
5106
5250
  const browseCollapsed = fm.browse_collapsed === true ? true : void 0;
5251
+ let order;
5252
+ if (fm.order !== void 0) {
5253
+ if (typeof fm.order === "number" && Number.isFinite(fm.order)) {
5254
+ order = fm.order;
5255
+ } else {
5256
+ warnings.push({
5257
+ code: "KIND_CONVENTION_BAD_SHAPE",
5258
+ message: `kind convention '${doc.id}' has an invalid 'order' (${describeShape(fm.order)}; expected a finite number such as 10); ignoring it.`,
5259
+ field: "order",
5260
+ severity: "warning"
5261
+ });
5262
+ }
5263
+ }
5107
5264
  const kind = {
5108
5265
  id: doc.id,
5109
5266
  title,
@@ -5118,6 +5275,7 @@ function parseConventionDoc(doc, options = {}) {
5118
5275
  if (sections && sections.length > 0) kind.sections = sections;
5119
5276
  if (freshnessHorizon !== void 0) kind.freshnessHorizon = freshnessHorizon;
5120
5277
  if (browseCollapsed !== void 0) kind.browseCollapsed = browseCollapsed;
5278
+ if (order !== void 0) kind.order = order;
5121
5279
  if (claim !== void 0) kind.claim = claim;
5122
5280
  return {
5123
5281
  ok: true,
@@ -5259,7 +5417,7 @@ function isAnyEntryKey(entry) {
5259
5417
  return isPageEntryKey(entry) || isViewEntryKey(entry);
5260
5418
  }
5261
5419
  function isViewEntryVersion(value) {
5262
- return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
5420
+ return isContentVersion(value);
5263
5421
  }
5264
5422
  function parseRegistration(id, frontmatter2) {
5265
5423
  if (!isAnyRegistryId(id) || !isPageTypeName(frontmatter2.type) || !isAnyEntryKey(frontmatter2.entry)) return null;
@@ -5286,9 +5444,45 @@ var MAX_QUERY_ROWS = 500;
5286
5444
  var MAX_EDGE_ROWS = 1e3;
5287
5445
  var MAX_DOCUMENT_BODY_BYTES = 1024 * 1024;
5288
5446
  var MAX_REPLY_BYTES = 2 * 1024 * 1024;
5447
+ var GRAPH_MAX_DOCUMENTS = 1e3;
5448
+ var GRAPH_MAX_RELATIONSHIPS = 1e4;
5289
5449
  var MAX_CHANGE_ROWS = 100;
5290
5450
  var MAX_CHANGE_BYTES = 256 * 1024;
5291
5451
  var MAX_SUBSCRIPTION_HEADS = 1e4;
5452
+ var MAX_HOST_CAPABILITY_BYTES = 128;
5453
+ var MAX_HOST_REQUEST_BYTES = 64 * 1024;
5454
+ var HOST_CAPABILITY_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
5455
+ var BRIDGE_SERVICE_LIMITS = Object.freeze({
5456
+ query: MAX_QUERY_ROWS,
5457
+ edges: MAX_EDGE_ROWS,
5458
+ graphDocuments: GRAPH_MAX_DOCUMENTS,
5459
+ graphRelationships: GRAPH_MAX_RELATIONSHIPS,
5460
+ replyBytes: MAX_REPLY_BYTES
5461
+ });
5462
+ var BRIDGE_HOST_CAPABILITIES = Object.freeze({
5463
+ queryKindProjection: "query.kind-projection",
5464
+ queryFieldOr: "query.field-or",
5465
+ queryOpen: "query.open",
5466
+ queryCount: "query.count",
5467
+ edges: "edges",
5468
+ renderDocument: "render-document",
5469
+ openPage: "open-page",
5470
+ subscribeDeltas: "subscribe-deltas",
5471
+ graph: "graph",
5472
+ graphModel: "graph.model",
5473
+ recordOpen: "record.open",
5474
+ frameResize: "frame.resize"
5475
+ });
5476
+ var BRIDGE_SERVICE_CAPABILITIES = Object.freeze([
5477
+ BRIDGE_HOST_CAPABILITIES.queryKindProjection,
5478
+ BRIDGE_HOST_CAPABILITIES.queryFieldOr,
5479
+ BRIDGE_HOST_CAPABILITIES.queryOpen,
5480
+ BRIDGE_HOST_CAPABILITIES.queryCount,
5481
+ BRIDGE_HOST_CAPABILITIES.edges,
5482
+ BRIDGE_HOST_CAPABILITIES.graph,
5483
+ BRIDGE_HOST_CAPABILITIES.renderDocument
5484
+ ]);
5485
+ var V0_REQUEST_TYPES = /* @__PURE__ */ new Set(["hello", "query", "read", "render-document", "edges", "graph", "subscribe", "host", "open-page"]);
5292
5486
  function isPlainRecord(value) {
5293
5487
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
5294
5488
  const prototype = Object.getPrototypeOf(value);
@@ -5421,6 +5615,45 @@ function parseBridgeRequest(value) {
5421
5615
  const params = normalizeEdgeParams(value.params);
5422
5616
  return params ? { bridge: BRIDGE_PROTOCOL, type: "edges", id, params } : null;
5423
5617
  }
5618
+ if (value.type === "graph") {
5619
+ const expected = value.includeBodies === void 0 ? ["bridge", "type", "id"] : ["bridge", "type", "id", "includeBodies"];
5620
+ if (!exactKeys(value, expected)) return null;
5621
+ if (value.includeBodies !== void 0 && typeof value.includeBodies !== "boolean") return null;
5622
+ return { bridge: BRIDGE_PROTOCOL, type: "graph", id, includeBodies: value.includeBodies === true };
5623
+ }
5624
+ if (value.type === "host") {
5625
+ const expected = value.input === void 0 ? ["bridge", "type", "id", "capability"] : ["bridge", "type", "id", "capability", "input"];
5626
+ if (!exactKeys(value, expected)) return null;
5627
+ const capability = boundedString(value.capability, MAX_HOST_CAPABILITY_BYTES);
5628
+ if (!capability || !HOST_CAPABILITY_NAME.test(capability)) return null;
5629
+ if (value.input !== void 0 && !isPlainRecord(value.input)) return null;
5630
+ let bytes;
5631
+ try {
5632
+ bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
5633
+ } catch {
5634
+ return null;
5635
+ }
5636
+ if (bytes > MAX_HOST_REQUEST_BYTES) return null;
5637
+ return {
5638
+ bridge: BRIDGE_PROTOCOL,
5639
+ type: "host",
5640
+ id,
5641
+ capability,
5642
+ ...value.input === void 0 ? {} : { input: value.input }
5643
+ };
5644
+ }
5645
+ return null;
5646
+ }
5647
+ function unsupportedRequest(value) {
5648
+ if (!isPlainRecord(value) || typeof value.type !== "string") return null;
5649
+ if (value.bridge === BRIDGE_PROTOCOL) {
5650
+ return V0_REQUEST_TYPES.has(value.type) ? null : { bridge: BRIDGE_PROTOCOL, id: requestId(value.id) ?? void 0 };
5651
+ }
5652
+ if (value.bridge === ACTION_BRIDGE_PROTOCOL) {
5653
+ if (value.type === "read-versioned") return null;
5654
+ const id = value.type === "action.propose" ? value.requestId : value.id;
5655
+ return { bridge: ACTION_BRIDGE_PROTOCOL, id: requestId(id) ?? void 0 };
5656
+ }
5424
5657
  return null;
5425
5658
  }
5426
5659
  function ok(id, bridge, type, result) {
@@ -5445,6 +5678,17 @@ var BridgeService = class {
5445
5678
  async handle(launchId, rawRequest) {
5446
5679
  const request = parseBridgeRequest(rawRequest);
5447
5680
  if (!request) {
5681
+ const unsupported = unsupportedRequest(rawRequest);
5682
+ if (unsupported) {
5683
+ return {
5684
+ reply: fail(
5685
+ unsupported.id,
5686
+ unsupported.bridge,
5687
+ "FORBIDDEN",
5688
+ "this host does not offer the requested bridge operation"
5689
+ )
5690
+ };
5691
+ }
5448
5692
  return {
5449
5693
  reply: fail(
5450
5694
  invalidV0RequestId(rawRequest),
@@ -5454,16 +5698,6 @@ var BridgeService = class {
5454
5698
  )
5455
5699
  };
5456
5700
  }
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
5701
  const dataBearing = request.type !== "open-page";
5468
5702
  const before = await this.options.launches.resolve(launchId, dataBearing);
5469
5703
  if (!before) {
@@ -5569,6 +5803,73 @@ var BridgeService = class {
5569
5803
  [...rows].sort((a, b) => a.id.localeCompare(b.id)).map((row) => [row.id, row.version])
5570
5804
  );
5571
5805
  }
5806
+ /**
5807
+ * Whole-bundle projection for graph-shaped Views. The documents come from one head scan and
5808
+ * the relationships from `queryEdges`, which is a second full-bundle scan; the two scans are
5809
+ * accepted because the reply is bounded by the exported graph limits and the host never
5810
+ * caches bundle state on a View's behalf. No `model` or `definitions` are answered here: the
5811
+ * owner of that shape is undecided, and a host that has one declares the `graph.model`
5812
+ * capability before adding them.
5813
+ */
5814
+ async graph(launch, request) {
5815
+ const heads = await queryHeads2(this.options.bundle, {});
5816
+ if (heads.length > GRAPH_MAX_DOCUMENTS) {
5817
+ return {
5818
+ reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_DOCUMENTS} documents`)
5819
+ };
5820
+ }
5821
+ const [registry, declaredOkfVersion, edges] = await Promise.all([
5822
+ loadKinds(this.options.bundle),
5823
+ readBundleOkfVersion2(this.options.bundle),
5824
+ queryEdges2(this.options.bundle, {})
5825
+ ]);
5826
+ if (edges.length > GRAPH_MAX_RELATIONSHIPS) {
5827
+ return {
5828
+ reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_RELATIONSHIPS} relationships`)
5829
+ };
5830
+ }
5831
+ const okfVersion = declaredOkfVersion ?? "0.1";
5832
+ const includeBodies = request.includeBodies && (launch.capability === "bundle-read" || launch.capability === "bundle-propose");
5833
+ const documents = [];
5834
+ for (const head of heads) {
5835
+ const source = includeBodies ? await readDocVersioned2(this.options.bundle, head.id).then((result) => ({
5836
+ version: result.version,
5837
+ frontmatter: result.doc.frontmatter,
5838
+ body: result.doc.body
5839
+ })) : { version: head.version, frontmatter: head.frontmatter };
5840
+ if (source.body !== void 0 && Buffer.byteLength(source.body, "utf8") > MAX_DOCUMENT_BODY_BYTES) {
5841
+ return { reply: fail(request.id, request.bridge, "TOO_LARGE", "a document body exceeded the 1 MiB View limit") };
5842
+ }
5843
+ const kind = registry.kinds.get(String(source.frontmatter.type ?? ""));
5844
+ const frontmatter2 = kind ? projectLogicalKindFields(okfVersion, kind, source.frontmatter) : source.frontmatter;
5845
+ documents.push({
5846
+ id: head.id,
5847
+ version: source.version,
5848
+ frontmatter: frontmatter2,
5849
+ ...source.body === void 0 ? {} : { body: source.body }
5850
+ });
5851
+ }
5852
+ const relationships = edges.map(({ from, to, text }) => ({ from, to, text }));
5853
+ return {
5854
+ reply: ok(request.id, request.bridge, request.type, {
5855
+ okfVersion,
5856
+ documents,
5857
+ relationships,
5858
+ counts: { documents: documents.length, relationships: relationships.length }
5859
+ })
5860
+ };
5861
+ }
5862
+ hostDescriptor() {
5863
+ const declared = new Set(this.options.host.capabilities);
5864
+ for (const name of Object.keys(this.options.hostHandlers ?? {})) declared.add(name);
5865
+ return {
5866
+ kind: this.options.host.kind,
5867
+ capabilities: [...declared].sort(),
5868
+ limits: { ...this.options.host.limits },
5869
+ ...this.options.host.frame ? { frame: { ...this.options.host.frame } } : {},
5870
+ ...this.options.host.theme ? { theme: { ...this.options.host.theme } } : {}
5871
+ };
5872
+ }
5572
5873
  async execute(launch, request) {
5573
5874
  if (request.type === "open-page") {
5574
5875
  if (this.options.consumeOpenPage === true) {
@@ -5592,10 +5893,24 @@ var BridgeService = class {
5592
5893
  bundle: { root: config.root, name: config.name },
5593
5894
  mode: config.mode,
5594
5895
  protocol: BRIDGE_PROTOCOL,
5595
- grant: launch.capability === "bundle-propose" ? "propose" : "read"
5896
+ grant: launch.capability === "bundle-propose" ? "propose" : "read",
5897
+ host: this.hostDescriptor()
5596
5898
  })
5597
5899
  };
5598
5900
  }
5901
+ if (request.type === "host") {
5902
+ const handlers = this.options.hostHandlers ?? {};
5903
+ if (!Object.hasOwn(handlers, request.capability)) {
5904
+ return { reply: fail(request.id, request.bridge, "FORBIDDEN", "this host does not offer the requested capability") };
5905
+ }
5906
+ const outcome = await handlers[request.capability]({
5907
+ capability: request.capability,
5908
+ input: request.input,
5909
+ launch
5910
+ });
5911
+ if (!outcome.ok) return { reply: fail(request.id, request.bridge, outcome.code, outcome.message) };
5912
+ return { reply: ok(request.id, request.bridge, request.type, { capability: request.capability, output: outcome.output }) };
5913
+ }
5599
5914
  if (request.type === "query") {
5600
5915
  const rows = await queryHeads2(this.options.bundle, {
5601
5916
  ...request.params.type ? { type: request.params.type } : {},
@@ -5673,6 +5988,9 @@ var BridgeService = class {
5673
5988
  const projected = edges.map(({ from, to, text }) => ({ from, to, text }));
5674
5989
  return { reply: ok(request.id, request.bridge, request.type, { edges: projected, count: projected.length }) };
5675
5990
  }
5991
+ if (request.type === "graph") {
5992
+ return this.graph(launch, request);
5993
+ }
5676
5994
  if (this.options.enablePolling) {
5677
5995
  this.subscriptions.set(launch.launchId, {
5678
5996
  baseline: await this.subscriptionSnapshot()
@@ -5821,6 +6139,10 @@ var STATIC_LAUNCH_ID = "publication-snapshot";
5821
6139
  function digest(bytes) {
5822
6140
  return `sha256:${createHash3("sha256").update(bytes).digest("hex")}`;
5823
6141
  }
6142
+ var STATIC_BRIDGE_CAPABILITIES = Object.freeze([
6143
+ ...BRIDGE_SERVICE_CAPABILITIES,
6144
+ BRIDGE_HOST_CAPABILITIES.openPage
6145
+ ]);
5824
6146
  function createPublicationBridge(options) {
5825
6147
  if (options.protocol !== PUBLICATION_BRIDGE_V0) {
5826
6148
  throw new PublicationError("CAPABILITY_UNAVAILABLE", "the requested publication bridge protocol is unsupported");
@@ -5868,7 +6190,11 @@ function createPublicationBridge(options) {
5868
6190
  if (!rendered || rendered.id !== id) throw new Error(`document '${id}' is not available`);
5869
6191
  return { html: rendered.html, bounded: rendered.bounded };
5870
6192
  },
5871
- allowActionProtocol: false,
6193
+ host: {
6194
+ kind: options.host?.kind ?? "oss",
6195
+ capabilities: options.host?.capabilities ?? STATIC_BRIDGE_CAPABILITIES,
6196
+ limits: BRIDGE_SERVICE_LIMITS
6197
+ },
5872
6198
  enablePolling: false,
5873
6199
  consumeOpenPage: false
5874
6200
  });