superbee 0.4.5 → 0.4.6-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.
- package/README.md +12 -3
- package/SKILL.md +13 -5
- package/dist/THIRD-PARTY-NOTICES.txt +2 -2
- package/dist/publication/types.d.ts +8 -0
- package/dist/publication-bridge.mjs +252 -89
- package/dist/publication.mjs +269 -91
- package/dist/superbee.mjs +55766 -50582
- package/package.json +2 -2
- package/references/hosted-checkout.md +1 -1
- package/references/modeling-and-delivery.md +1 -1
- package/references/recipes/review-workflow/references/view-authoring-v0.md +1 -2
- package/references/views/references/view-authoring-v0.md +1 -2
|
@@ -2890,15 +2890,30 @@ var FrontmatterRefusedError = class extends InvalidInputError {
|
|
|
2890
2890
|
}
|
|
2891
2891
|
};
|
|
2892
2892
|
var FilesystemIdentityAliasError = class extends InvalidInputError {
|
|
2893
|
+
reason = "filesystem_alias";
|
|
2893
2894
|
rel;
|
|
2894
2895
|
segment;
|
|
2895
|
-
|
|
2896
|
+
existing;
|
|
2897
|
+
suggestedPath;
|
|
2898
|
+
/**
|
|
2899
|
+
* `noun` names the existing entry ("folder" or "file"); `index` is the position of `segment`
|
|
2900
|
+
* in `rel`. Both default from the position of the first segment spelled `segment`.
|
|
2901
|
+
*/
|
|
2902
|
+
constructor(rel, segment, existing, noun, index) {
|
|
2903
|
+
const segments = rel.split("/").filter((part) => part !== "" && part !== ".");
|
|
2904
|
+
const at = index ?? segments.indexOf(segment);
|
|
2905
|
+
const kind = noun ?? (at === segments.length - 1 ? "file" : "folder");
|
|
2906
|
+
const clash = existing === void 0 ? `but a ${kind} with that name in a different case or Unicode form already exists` : existing.normalize("NFC") === segment.normalize("NFC") ? `but ${kind} '${existing}' already exists, written in a different Unicode form` : `but ${kind} '${existing}' already exists`;
|
|
2907
|
+
const difference = existing === void 0 ? "case or Unicode form" : existing.normalize("NFC") === segment.normalize("NFC") ? "Unicode form" : existing.toLowerCase() === segment.toLowerCase() ? "case" : "case and Unicode form";
|
|
2896
2908
|
super(
|
|
2897
|
-
`Path '${rel}'
|
|
2909
|
+
`Path '${rel}' uses ${kind} '${segment}', ${clash}, and this disk treats names that differ only by ${difference} as the same name. Use the existing spelling.`
|
|
2898
2910
|
);
|
|
2899
2911
|
this.name = "FilesystemIdentityAliasError";
|
|
2900
2912
|
this.rel = rel;
|
|
2901
2913
|
this.segment = segment;
|
|
2914
|
+
this.existing = existing;
|
|
2915
|
+
const unusable = existing === void 0 || at < 0 || segments[at] !== segment || kind === "file" && at < segments.length - 1 || existing.normalize("NFC") === segment.normalize("NFC");
|
|
2916
|
+
this.suggestedPath = unusable ? void 0 : segments.map((part, position) => position === at ? existing : part).join("/");
|
|
2902
2917
|
}
|
|
2903
2918
|
};
|
|
2904
2919
|
var ConcurrentReplacementError = class extends Error {
|
|
@@ -3103,6 +3118,9 @@ function delimiterLineEnd(input, offset) {
|
|
|
3103
3118
|
if (input[suffix] === "\r" && input[suffix + 1] === "\n") return suffix + 2;
|
|
3104
3119
|
return null;
|
|
3105
3120
|
}
|
|
3121
|
+
function startsWithFrontmatterSyntax(text) {
|
|
3122
|
+
return text.startsWith("\uFEFF") || delimiterLineEnd(text, 0) !== null;
|
|
3123
|
+
}
|
|
3106
3124
|
function splitLeadingFrontmatter(raw, context) {
|
|
3107
3125
|
const input = raw.startsWith("\uFEFF") ? raw.slice(1) : raw;
|
|
3108
3126
|
const openingEnd = delimiterLineEnd(input, 0);
|
|
@@ -3208,7 +3226,10 @@ function stringifyWithSerializedFrontmatter(dumped, body) {
|
|
|
3208
3226
|
const content = body ?? "";
|
|
3209
3227
|
const newline = (value) => value.endsWith("\n") ? value : `${value}
|
|
3210
3228
|
`;
|
|
3211
|
-
if (dumped === "{}")
|
|
3229
|
+
if (dumped === "{}") {
|
|
3230
|
+
const stored = normalizeDocumentBodyForStorage(content);
|
|
3231
|
+
return `${startsWithFrontmatterSyntax(stored) ? "---\n---\n" : ""}${stored}`;
|
|
3232
|
+
}
|
|
3212
3233
|
return `---
|
|
3213
3234
|
${newline(dumped)}---
|
|
3214
3235
|
${normalizeDocumentBodyForStorage(content)}`;
|
|
@@ -4019,36 +4040,57 @@ function hasSourceIdentity(sources, identity) {
|
|
|
4019
4040
|
return Array.isArray(sources) && sources.some((entry) => matchesSourceIdentity(entry, identity));
|
|
4020
4041
|
}
|
|
4021
4042
|
|
|
4043
|
+
// ../core/src/skip-reasons.ts
|
|
4044
|
+
var PERMISSION_DENIED_PREFIX = "permission denied (";
|
|
4045
|
+
function permissionDeniedReason(code) {
|
|
4046
|
+
return `${PERMISSION_DENIED_PREFIX}${code})`;
|
|
4047
|
+
}
|
|
4048
|
+
function isPermissionDeniedSkip(skip) {
|
|
4049
|
+
return skip.reason.startsWith(PERMISSION_DENIED_PREFIX);
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4022
4052
|
// ../core/src/document-scan.ts
|
|
4023
4053
|
function isEnoent(err) {
|
|
4024
4054
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
4025
4055
|
}
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
}
|
|
4043
|
-
|
|
4044
|
-
onAliased({ id, reason: candidate.message });
|
|
4045
|
-
continue;
|
|
4046
|
-
}
|
|
4047
|
-
throw candidate;
|
|
4056
|
+
function deniedReason(err) {
|
|
4057
|
+
const code = typeof err === "object" && err !== null ? err.code : void 0;
|
|
4058
|
+
return code === "EACCES" || code === "EPERM" ? permissionDeniedReason(code) : void 0;
|
|
4059
|
+
}
|
|
4060
|
+
async function readManyExisting(backend, ids, onMalformed, onAliased, onDenied) {
|
|
4061
|
+
const tolerated = (err) => isEnoent(err) || err instanceof MalformedDocumentError && onMalformed !== void 0 || err instanceof FilesystemIdentityAliasError && onAliased !== void 0 || deniedReason(err) !== void 0 && onDenied !== void 0;
|
|
4062
|
+
const readOne = async (id) => {
|
|
4063
|
+
try {
|
|
4064
|
+
return [await backend.read(id)];
|
|
4065
|
+
} catch (candidate) {
|
|
4066
|
+
if (isEnoent(candidate)) return [];
|
|
4067
|
+
if (candidate instanceof MalformedDocumentError && onMalformed) {
|
|
4068
|
+
onMalformed({ id, reason: candidate.detail });
|
|
4069
|
+
return [];
|
|
4070
|
+
}
|
|
4071
|
+
if (candidate instanceof FilesystemIdentityAliasError && onAliased) {
|
|
4072
|
+
onAliased({ id, reason: candidate.message });
|
|
4073
|
+
return [];
|
|
4048
4074
|
}
|
|
4075
|
+
const reason = deniedReason(candidate);
|
|
4076
|
+
if (reason !== void 0 && onDenied) {
|
|
4077
|
+
onDenied({ id, reason });
|
|
4078
|
+
return [];
|
|
4079
|
+
}
|
|
4080
|
+
throw candidate;
|
|
4049
4081
|
}
|
|
4050
|
-
|
|
4051
|
-
|
|
4082
|
+
};
|
|
4083
|
+
const readBatch = async (batch) => {
|
|
4084
|
+
try {
|
|
4085
|
+
return await backend.readMany(batch);
|
|
4086
|
+
} catch (err) {
|
|
4087
|
+
if (!tolerated(err)) throw err;
|
|
4088
|
+
if (batch.length === 1) return readOne(batch[0]);
|
|
4089
|
+
const half = Math.ceil(batch.length / 2);
|
|
4090
|
+
return [...await readBatch(batch.slice(0, half)), ...await readBatch(batch.slice(half))];
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
return readBatch(ids);
|
|
4052
4094
|
}
|
|
4053
4095
|
|
|
4054
4096
|
// ../core/src/engine.ts
|
|
@@ -4072,7 +4114,7 @@ async function readDocVersioned(backend, id) {
|
|
|
4072
4114
|
async function scanMatching(backend, filter, onSkip) {
|
|
4073
4115
|
const ids = await backend.list(filter.prefix);
|
|
4074
4116
|
const results = [];
|
|
4075
|
-
for (const result of await readManyExisting(backend, ids, onSkip)) {
|
|
4117
|
+
for (const result of await readManyExisting(backend, ids, onSkip, void 0, onSkip)) {
|
|
4076
4118
|
if (matchesFilter(result.doc, filter)) results.push(result);
|
|
4077
4119
|
}
|
|
4078
4120
|
results.sort((a, b) => compareCodeUnits(a.doc.id, b.doc.id));
|
|
@@ -4197,7 +4239,7 @@ function assertStorableReservedWrite(dir, name, content) {
|
|
|
4197
4239
|
// ../core/src/filesystem-identity.ts
|
|
4198
4240
|
import { promises as fs2 } from "node:fs";
|
|
4199
4241
|
import path2 from "node:path";
|
|
4200
|
-
import { createHash as createHash2
|
|
4242
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4201
4243
|
|
|
4202
4244
|
// ../core/src/filesystem-lock.ts
|
|
4203
4245
|
import { promises as fs, realpathSync } from "node:fs";
|
|
@@ -4252,6 +4294,8 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4252
4294
|
var OWNER_FILE = "owner.json";
|
|
4253
4295
|
var DEFAULT_WAIT_MS = 5e3;
|
|
4254
4296
|
var DEFAULT_POLL_MS = 25;
|
|
4297
|
+
var UNREADABLE_OWNER_POLL_MS = 2e3;
|
|
4298
|
+
var ABANDONED_CLAIM_AGE_MS = 2e3;
|
|
4255
4299
|
var FilesystemMutationLockError = class extends Error {
|
|
4256
4300
|
lockPath;
|
|
4257
4301
|
owner;
|
|
@@ -4339,9 +4383,13 @@ async function readOwnerRecord(lockPath) {
|
|
|
4339
4383
|
return owner === null ? { state: "absent" } : { state: "record", owner };
|
|
4340
4384
|
}
|
|
4341
4385
|
async function resolveOwnerRecord(lockPath, started, waitMs, pollMs) {
|
|
4386
|
+
let firstUnreadable;
|
|
4342
4387
|
while (true) {
|
|
4343
4388
|
const record2 = await readOwnerRecord(lockPath);
|
|
4344
|
-
if (record2.state !== "unreadable"
|
|
4389
|
+
if (record2.state !== "unreadable") return record2;
|
|
4390
|
+
const now = Date.now();
|
|
4391
|
+
firstUnreadable ??= now;
|
|
4392
|
+
if (now - started >= waitMs || now - firstUnreadable >= UNREADABLE_OWNER_POLL_MS) return record2;
|
|
4345
4393
|
await delay(pollMs);
|
|
4346
4394
|
}
|
|
4347
4395
|
}
|
|
@@ -4392,6 +4440,17 @@ async function quarantineStaleLock(lockPath, owner, policy) {
|
|
|
4392
4440
|
throw err;
|
|
4393
4441
|
}
|
|
4394
4442
|
}
|
|
4443
|
+
async function removeAbandonedClaim(lockPath) {
|
|
4444
|
+
try {
|
|
4445
|
+
const info = await fs.lstat(lockPath);
|
|
4446
|
+
const age = Date.now() - info.mtimeMs;
|
|
4447
|
+
if (!info.isDirectory() || age < ABANDONED_CLAIM_AGE_MS) return false;
|
|
4448
|
+
await fs.rmdir(lockPath);
|
|
4449
|
+
return true;
|
|
4450
|
+
} catch {
|
|
4451
|
+
return false;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4395
4454
|
function delay(ms) {
|
|
4396
4455
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4397
4456
|
}
|
|
@@ -4442,7 +4501,7 @@ async function timeoutError(lockPath, snapshot, guarded) {
|
|
|
4442
4501
|
const sameHost = owner?.hostname === hostname();
|
|
4443
4502
|
let message;
|
|
4444
4503
|
if (malformed) {
|
|
4445
|
-
message = `timed out waiting for filesystem mutation lock '${lockPath}'; its owner metadata is missing or malformed.
|
|
4504
|
+
message = `timed out waiting for filesystem mutation lock '${lockPath}'; its owner metadata is missing or malformed. Remove the lock directory once no Superbee process is running, then retry.`;
|
|
4446
4505
|
} else if (stale) {
|
|
4447
4506
|
message = `stale filesystem mutation lock '${lockPath}' belongs to absent PID ${owner.pid} on ${owner.hostname}. Inspect and remove the lock, then retry.`;
|
|
4448
4507
|
} else if (!sameHost) {
|
|
@@ -4491,7 +4550,9 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
|
|
|
4491
4550
|
} else {
|
|
4492
4551
|
unwitnessedRetryUsed = false;
|
|
4493
4552
|
}
|
|
4494
|
-
|
|
4553
|
+
const existing = await readOwnerRecord(lockPath);
|
|
4554
|
+
let existingOwner = existing.state === "record" ? existing.owner : null;
|
|
4555
|
+
if (existing.state === "absent" && await removeAbandonedClaim(lockPath)) continue;
|
|
4495
4556
|
if (existingOwner !== null) {
|
|
4496
4557
|
if (await quarantineStaleLock(lockPath, existingOwner, policy)) continue;
|
|
4497
4558
|
existingOwner = await readOwner(lockPath);
|
|
@@ -4511,14 +4572,13 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
|
|
|
4511
4572
|
});
|
|
4512
4573
|
} catch (err) {
|
|
4513
4574
|
const code = err.code;
|
|
4514
|
-
if (code === "EEXIST") {
|
|
4575
|
+
if (code === "EEXIST" || code === "ENOENT" || code === "ENOTDIR" || await claimDirectoryLost(lockPath, claimed)) {
|
|
4515
4576
|
heldTokens.delete(owner.token);
|
|
4516
4577
|
continue;
|
|
4517
4578
|
}
|
|
4518
4579
|
await rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy).catch(() => {
|
|
4519
4580
|
});
|
|
4520
4581
|
heldTokens.delete(owner.token);
|
|
4521
|
-
if (code === "ENOENT" || code === "ENOTDIR") continue;
|
|
4522
4582
|
throw err;
|
|
4523
4583
|
}
|
|
4524
4584
|
let inFlight;
|
|
@@ -4548,9 +4608,18 @@ async function rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy
|
|
|
4548
4608
|
return;
|
|
4549
4609
|
}
|
|
4550
4610
|
if (record2.state === "unreadable" || !await isClaimedDirectory(lockPath, claimed)) return;
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4611
|
+
await fs.rmdir(lockPath);
|
|
4612
|
+
}
|
|
4613
|
+
async function claimDirectoryLost(lockPath, claimed) {
|
|
4614
|
+
try {
|
|
4615
|
+
const current = await fs.lstat(lockPath, { bigint: true });
|
|
4616
|
+
if (claimed === null) return false;
|
|
4617
|
+
const reborn = claimed.birthtimeNs > 0n && current.birthtimeNs > 0n && current.birthtimeNs !== claimed.birthtimeNs;
|
|
4618
|
+
return current.dev !== claimed.dev || current.ino !== claimed.ino || reborn;
|
|
4619
|
+
} catch (err) {
|
|
4620
|
+
const code = err.code;
|
|
4621
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
4622
|
+
}
|
|
4554
4623
|
}
|
|
4555
4624
|
async function isClaimedDirectory(lockPath, claimed) {
|
|
4556
4625
|
if (claimed === null || claimed.birthtimeNs <= 0n) return false;
|
|
@@ -4652,6 +4721,12 @@ function foldSegment(segment) {
|
|
|
4652
4721
|
return segment.normalize("NFKD").toLowerCase().toUpperCase().toLowerCase();
|
|
4653
4722
|
}
|
|
4654
4723
|
|
|
4724
|
+
// ../core/src/filesystem-temp-name.ts
|
|
4725
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4726
|
+
function identityTempName(leaf) {
|
|
4727
|
+
return `.${leaf}.${process.pid}.${Date.now()}.${randomUUID2()}.tmp`;
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4655
4730
|
// ../core/src/filesystem-identity.ts
|
|
4656
4731
|
async function replaceExistingLeaf(port, tmp, target, rel) {
|
|
4657
4732
|
try {
|
|
@@ -4796,10 +4871,32 @@ async function probeSegment(port, candidate, rel, segment) {
|
|
|
4796
4871
|
function hasExact(listing, segment) {
|
|
4797
4872
|
return listing.some((entry) => entry.name === segment);
|
|
4798
4873
|
}
|
|
4874
|
+
function existingSpelling(names, segment) {
|
|
4875
|
+
const folded = foldSegment(segment);
|
|
4876
|
+
const matches = [...names].filter((name) => name !== segment && foldSegment(name) === folded);
|
|
4877
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
4878
|
+
}
|
|
4879
|
+
function entryNoun(kind, leaf) {
|
|
4880
|
+
if (kind === void 0) return leaf ? "file" : "folder";
|
|
4881
|
+
return kind === "directory" ? "folder" : "file";
|
|
4882
|
+
}
|
|
4883
|
+
function aliasRefusal(rel, segments, index, names, kind) {
|
|
4884
|
+
const segment = segments[index];
|
|
4885
|
+
return new FilesystemIdentityAliasError(rel, segment, existingSpelling(names, segment), entryNoun(kind, index === segments.length - 1), index);
|
|
4886
|
+
}
|
|
4887
|
+
function aliasRefusalFromListing(rel, index, listing, leaf) {
|
|
4888
|
+
const segment = relSegments(rel)[index];
|
|
4889
|
+
const existing = existingSpelling(listing.map((entry) => entry.name), segment);
|
|
4890
|
+
const kind = existing === void 0 ? void 0 : listing.find((entry) => entry.name === existing)?.kind;
|
|
4891
|
+
return new FilesystemIdentityAliasError(rel, segment, existing, entryNoun(kind, leaf), index);
|
|
4892
|
+
}
|
|
4893
|
+
function listingView(names) {
|
|
4894
|
+
return { has: (name) => names.has(name), names: () => names };
|
|
4895
|
+
}
|
|
4799
4896
|
function freshListings(port) {
|
|
4800
4897
|
return async (dir) => {
|
|
4801
4898
|
const listing = await port.entries(dir);
|
|
4802
|
-
return listing === null ? null : { has: (name) => hasExact(listing, name) };
|
|
4899
|
+
return listing === null ? null : { has: (name) => hasExact(listing, name), names: () => listing.map((entry) => entry.name) };
|
|
4803
4900
|
};
|
|
4804
4901
|
}
|
|
4805
4902
|
async function walkExact(port, rootResolved, segments, rel) {
|
|
@@ -4811,7 +4908,7 @@ async function walkExact(port, rootResolved, segments, rel) {
|
|
|
4811
4908
|
if (probed === null) return { state: "absent", depth: index };
|
|
4812
4909
|
const listing = await port.entries(parent);
|
|
4813
4910
|
if (listing === null) return { state: "absent", depth: index };
|
|
4814
|
-
if (!hasExact(listing, segment)) throw
|
|
4911
|
+
if (!hasExact(listing, segment)) throw aliasRefusal(rel, segments, index, listing.map((entry) => entry.name), probed.kind);
|
|
4815
4912
|
if (index === segments.length - 1) return { state: "exact", leaf: probed };
|
|
4816
4913
|
if (probed.kind !== "directory") return { state: "shape-mismatch", segment };
|
|
4817
4914
|
parent = candidate;
|
|
@@ -4826,35 +4923,53 @@ async function recordWalk(port, rootResolved, segments, rel) {
|
|
|
4826
4923
|
const segment = segments[index];
|
|
4827
4924
|
const candidate = path2.join(parent, segment);
|
|
4828
4925
|
const probed = await probeSegment(port, candidate, rel, segment);
|
|
4829
|
-
if (probed === null) return
|
|
4830
|
-
if (index < segments.length - 1 && probed.kind !== "directory") return null;
|
|
4926
|
+
if (probed === null) return { recorded, complete: false };
|
|
4831
4927
|
recorded.push(probed);
|
|
4928
|
+
if (index < segments.length - 1 && probed.kind !== "directory") return { recorded, complete: false };
|
|
4832
4929
|
parent = candidate;
|
|
4833
4930
|
}
|
|
4834
|
-
return recorded;
|
|
4931
|
+
return { recorded, complete: true };
|
|
4835
4932
|
}
|
|
4836
|
-
async function confirmedWalk(port, rootResolved, segments, rel, recorded, handle, listings = freshListings(port)) {
|
|
4933
|
+
async function confirmedWalk(port, rootResolved, segments, rel, recorded, handle, listings = freshListings(port), stopAt = segments.length) {
|
|
4837
4934
|
let parent = rootResolved;
|
|
4838
|
-
for (let index = 0; index <
|
|
4935
|
+
for (let index = 0; index < stopAt; index++) {
|
|
4839
4936
|
const segment = segments[index];
|
|
4840
4937
|
const candidate = path2.join(parent, segment);
|
|
4841
4938
|
const listing = await listings(parent);
|
|
4842
4939
|
if (listing === null) return "absent";
|
|
4843
4940
|
const listed = listing.has(segment);
|
|
4844
4941
|
if (handle !== null && index === segments.length - 1) {
|
|
4845
|
-
const
|
|
4846
|
-
|
|
4942
|
+
const probe = await probeSegment(port, candidate, rel, segment);
|
|
4943
|
+
const verdict = classifyLeaf({ listed, probe, handle });
|
|
4944
|
+
if (verdict === "aliased") throw aliasRefusal(rel, segments, index, listing.names(), probe?.kind);
|
|
4847
4945
|
return verdict === "replaced" ? "restart" : verdict;
|
|
4848
4946
|
}
|
|
4849
4947
|
if (index < segments.length - 1 || !listed) {
|
|
4850
|
-
const
|
|
4851
|
-
|
|
4948
|
+
const probe = await probeSegment(port, candidate, rel, segment);
|
|
4949
|
+
const verdict = confirmAlias(listed, probe, recorded[index]);
|
|
4950
|
+
if (verdict === "aliased") throw aliasRefusal(rel, segments, index, listing.names(), probe?.kind);
|
|
4852
4951
|
if (verdict !== "continue") return verdict;
|
|
4853
4952
|
}
|
|
4854
4953
|
parent = candidate;
|
|
4855
4954
|
}
|
|
4856
4955
|
return "continue";
|
|
4857
4956
|
}
|
|
4957
|
+
async function confirmPrefix(port, rootResolved, segments, rel, recorded) {
|
|
4958
|
+
if (recorded.length === 0) return "absent";
|
|
4959
|
+
let kept = prefixListings.get(port);
|
|
4960
|
+
if (kept === void 0) prefixListings.set(port, kept = /* @__PURE__ */ new Map());
|
|
4961
|
+
const cached = witnessedListings(port, kept, Date.now, PREFIX_LISTING_CAPACITY);
|
|
4962
|
+
let verdict;
|
|
4963
|
+
try {
|
|
4964
|
+
verdict = await confirmedWalk(port, rootResolved, segments, rel, recorded, null, cached.source, recorded.length);
|
|
4965
|
+
} catch (error) {
|
|
4966
|
+
if (!cached.reused()) throw error;
|
|
4967
|
+
kept.clear();
|
|
4968
|
+
verdict = await confirmedWalk(port, rootResolved, segments, rel, recorded, null, freshListings(port), recorded.length);
|
|
4969
|
+
}
|
|
4970
|
+
if (verdict === "restart" && cached.reused()) kept.clear();
|
|
4971
|
+
return verdict === "restart" ? "restart" : "absent";
|
|
4972
|
+
}
|
|
4858
4973
|
function countRestart(restarts, rel) {
|
|
4859
4974
|
if (restarts >= MAX_RESTARTS) throw new ConcurrentReplacementError(rel, restarts + 1);
|
|
4860
4975
|
return restarts + 1;
|
|
@@ -4866,8 +4981,14 @@ async function observeWith(port, rootResolved, rel, read, listings, onExact) {
|
|
|
4866
4981
|
const segments = relSegments(rel);
|
|
4867
4982
|
const target = path2.join(rootResolved, ...segments);
|
|
4868
4983
|
for (let restarts = 0; ; ) {
|
|
4869
|
-
const
|
|
4870
|
-
if (
|
|
4984
|
+
const walk = await recordWalk(port, rootResolved, segments, rel);
|
|
4985
|
+
if (!walk.complete) {
|
|
4986
|
+
const prefix = await confirmPrefix(port, rootResolved, segments, rel, walk.recorded);
|
|
4987
|
+
if (prefix === "absent") return { state: "absent" };
|
|
4988
|
+
restarts = countRestart(restarts, rel);
|
|
4989
|
+
continue;
|
|
4990
|
+
}
|
|
4991
|
+
const recorded = walk.recorded;
|
|
4871
4992
|
const verified = await confirmedWalk(port, rootResolved, segments, rel, recorded, null, listings);
|
|
4872
4993
|
if (verified === "absent") return { state: "absent" };
|
|
4873
4994
|
if (verified === "restart") {
|
|
@@ -4906,8 +5027,14 @@ async function probeExact(port, root, rel) {
|
|
|
4906
5027
|
const segments = relSegments(rel);
|
|
4907
5028
|
const rootResolved = path2.resolve(root);
|
|
4908
5029
|
for (let restarts = 0; ; ) {
|
|
4909
|
-
const
|
|
4910
|
-
if (
|
|
5030
|
+
const walk = await recordWalk(port, rootResolved, segments, rel);
|
|
5031
|
+
if (!walk.complete) {
|
|
5032
|
+
const prefix = await confirmPrefix(port, rootResolved, segments, rel, walk.recorded);
|
|
5033
|
+
if (prefix === "absent") return { state: "absent" };
|
|
5034
|
+
restarts = countRestart(restarts, rel);
|
|
5035
|
+
continue;
|
|
5036
|
+
}
|
|
5037
|
+
const recorded = walk.recorded;
|
|
4911
5038
|
const leaf = recorded[recorded.length - 1];
|
|
4912
5039
|
const verified = await confirmedWalk(port, rootResolved, segments, rel, recorded, null);
|
|
4913
5040
|
const verdict = verified === "continue" ? await confirmedWalk(port, rootResolved, segments, rel, recorded, leaf) : verified;
|
|
@@ -4917,45 +5044,56 @@ async function probeExact(port, root, rel) {
|
|
|
4917
5044
|
}
|
|
4918
5045
|
}
|
|
4919
5046
|
var SETTLED_AFTER_NS = 3000000000n;
|
|
5047
|
+
function witnessCanDetectChange(witness) {
|
|
5048
|
+
return witness.mtimeNs > 0n && witness.ctimeNs > 0n;
|
|
5049
|
+
}
|
|
4920
5050
|
function witnessKey(witness) {
|
|
4921
5051
|
return `${witness.dev}:${witness.ino}:${witness.mtimeNs}:${witness.ctimeNs}`;
|
|
4922
5052
|
}
|
|
4923
|
-
function
|
|
4924
|
-
|
|
4925
|
-
const
|
|
4926
|
-
|
|
4927
|
-
const
|
|
4928
|
-
|
|
5053
|
+
function witnessedListings(port, kept, now, capacity = Infinity) {
|
|
5054
|
+
let reused = false;
|
|
5055
|
+
const portWitness = port.directoryWitness?.bind(port);
|
|
5056
|
+
if (portWitness === void 0) return { source: freshListings(port), reused: () => false };
|
|
5057
|
+
const witnessOf = async (dir) => {
|
|
5058
|
+
const witness = await portWitness(dir);
|
|
5059
|
+
return witness !== null && witnessCanDetectChange(witness) ? witness : null;
|
|
5060
|
+
};
|
|
4929
5061
|
const settled = (witness) => {
|
|
4930
5062
|
const nowNs = BigInt(Math.floor(now())) * 1000000n;
|
|
4931
5063
|
const latest = witness.mtimeNs > witness.ctimeNs ? witness.mtimeNs : witness.ctimeNs;
|
|
4932
5064
|
return nowNs - latest >= SETTLED_AFTER_NS;
|
|
4933
5065
|
};
|
|
4934
|
-
const
|
|
4935
|
-
|
|
4936
|
-
if (
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
return names;
|
|
4956
|
-
};
|
|
4957
|
-
return { source, reused: () => reused };
|
|
5066
|
+
const source = async (dir) => {
|
|
5067
|
+
const keep = kept.get(dir);
|
|
5068
|
+
if (keep !== void 0) {
|
|
5069
|
+
const current = await witnessOf(dir);
|
|
5070
|
+
if (current !== null && witnessKey(current) === keep.witness) {
|
|
5071
|
+
reused = true;
|
|
5072
|
+
return listingView(keep.names);
|
|
5073
|
+
}
|
|
5074
|
+
kept.delete(dir);
|
|
5075
|
+
}
|
|
5076
|
+
const before = await witnessOf(dir);
|
|
5077
|
+
const listing = await port.entries(dir);
|
|
5078
|
+
if (listing === null) return null;
|
|
5079
|
+
const names = new Set(listing.map((entry) => entry.name));
|
|
5080
|
+
const after = await witnessOf(dir);
|
|
5081
|
+
if (before !== null && after !== null && witnessKey(before) === witnessKey(after) && settled(after)) {
|
|
5082
|
+
kept.delete(dir);
|
|
5083
|
+
kept.set(dir, { witness: witnessKey(after), names });
|
|
5084
|
+
while (kept.size > capacity) kept.delete(kept.keys().next().value);
|
|
5085
|
+
}
|
|
5086
|
+
return listingView(names);
|
|
4958
5087
|
};
|
|
5088
|
+
return { source, reused: () => reused };
|
|
5089
|
+
}
|
|
5090
|
+
var prefixListings = /* @__PURE__ */ new WeakMap();
|
|
5091
|
+
var PREFIX_LISTING_CAPACITY = 64;
|
|
5092
|
+
function openObservationPass(port, root, now = Date.now) {
|
|
5093
|
+
const rootResolved = path2.resolve(root);
|
|
5094
|
+
const kept = /* @__PURE__ */ new Map();
|
|
5095
|
+
const reusedVerdicts = [];
|
|
5096
|
+
const listingsFor = () => witnessedListings(port, kept, now);
|
|
4959
5097
|
return {
|
|
4960
5098
|
async observe(rel, read) {
|
|
4961
5099
|
const listings = listingsFor();
|
|
@@ -5058,7 +5196,7 @@ async function readWhole(port, target) {
|
|
|
5058
5196
|
await port.close(opened.handle);
|
|
5059
5197
|
}
|
|
5060
5198
|
}
|
|
5061
|
-
async function mkdirExact(port, parent, segment, isTail, rel) {
|
|
5199
|
+
async function mkdirExact(port, parent, segment, isTail, rel, index = -1) {
|
|
5062
5200
|
const outcome = await port.mkdir(path2.join(parent, segment));
|
|
5063
5201
|
if (outcome === "created") return;
|
|
5064
5202
|
const listing = await port.entries(parent);
|
|
@@ -5067,7 +5205,9 @@ async function mkdirExact(port, parent, segment, isTail, rel) {
|
|
|
5067
5205
|
throw new FilesystemSymlinkEntryError(rel, segment);
|
|
5068
5206
|
}
|
|
5069
5207
|
const verdict = classifyMkdir(outcome, listing, segment, isTail);
|
|
5070
|
-
if (verdict === "aliased")
|
|
5208
|
+
if (verdict === "aliased") {
|
|
5209
|
+
throw aliasRefusalFromListing(rel, index, listing, false);
|
|
5210
|
+
}
|
|
5071
5211
|
if (verdict === "shape-mismatch") throw new FilesystemShapeMismatchError(rel, segment);
|
|
5072
5212
|
}
|
|
5073
5213
|
async function ensureExactDirectories(port, rootResolved, segments, existingDepth, rel) {
|
|
@@ -5087,8 +5227,9 @@ async function ensureExactDirectories(port, rootResolved, segments, existingDept
|
|
|
5087
5227
|
}
|
|
5088
5228
|
}
|
|
5089
5229
|
let parent = path2.join(rootResolved, ...segments.slice(0, existingDepth));
|
|
5090
|
-
for (
|
|
5091
|
-
|
|
5230
|
+
for (let index = existingDepth; index < segments.length - 1; index++) {
|
|
5231
|
+
const segment = segments[index];
|
|
5232
|
+
await mkdirExact(port, parent, segment, false, rel, index);
|
|
5092
5233
|
parent = path2.join(parent, segment);
|
|
5093
5234
|
}
|
|
5094
5235
|
}
|
|
@@ -5106,7 +5247,7 @@ async function createExactLeaf(port, tmp, target, rel, leaf, rootResolved) {
|
|
|
5106
5247
|
}
|
|
5107
5248
|
const listing = await port.entries(path2.dirname(target)) ?? [];
|
|
5108
5249
|
if (hasExact(listing, leaf)) throw new ConcurrentReplacementError(rel, 1);
|
|
5109
|
-
throw
|
|
5250
|
+
throw aliasRefusalFromListing(rel, relSegments(rel).length - 1, listing, true);
|
|
5110
5251
|
}
|
|
5111
5252
|
async function mutateExact(port, root, rel, body) {
|
|
5112
5253
|
const segments = relSegments(rel);
|
|
@@ -5128,11 +5269,13 @@ async function mutateExact(port, root, rel, body) {
|
|
|
5128
5269
|
async replace(bytes) {
|
|
5129
5270
|
await ensureExactDirectories(port, rootResolved, segments, existingDepth, rel);
|
|
5130
5271
|
const dir = path2.dirname(target);
|
|
5131
|
-
const tmpName =
|
|
5272
|
+
const tmpName = identityTempName(leaf);
|
|
5132
5273
|
const tmp = path2.join(dir, tmpName);
|
|
5133
5274
|
await port.writeTemp(dir, tmpName, bytes);
|
|
5134
5275
|
try {
|
|
5135
5276
|
if (realized.state === "exact") {
|
|
5277
|
+
await port.copyMode?.(target, tmp).catch(() => {
|
|
5278
|
+
});
|
|
5136
5279
|
await replaceExistingLeaf(port, tmp, target, rel);
|
|
5137
5280
|
return;
|
|
5138
5281
|
}
|
|
@@ -5292,6 +5435,9 @@ function createNodeFilesystemIdentityPort(hostPolicy) {
|
|
|
5292
5435
|
async rename(from, to) {
|
|
5293
5436
|
await fs2.rename(from, to);
|
|
5294
5437
|
},
|
|
5438
|
+
async copyMode(from, to) {
|
|
5439
|
+
await fs2.chmod(to, (await fs2.lstat(from)).mode & 511);
|
|
5440
|
+
},
|
|
5295
5441
|
async unlink(target) {
|
|
5296
5442
|
await fs2.unlink(target);
|
|
5297
5443
|
},
|
|
@@ -5314,6 +5460,7 @@ function blobVersion(bytes) {
|
|
|
5314
5460
|
|
|
5315
5461
|
// ../core/src/backend.ts
|
|
5316
5462
|
var publicationContexts = /* @__PURE__ */ new WeakMap();
|
|
5463
|
+
var fileMutations = /* @__PURE__ */ new WeakMap();
|
|
5317
5464
|
function firstString(...vals) {
|
|
5318
5465
|
for (const v of vals) {
|
|
5319
5466
|
if (typeof v === "string" && v.trim() !== "") return v;
|
|
@@ -5430,6 +5577,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
5430
5577
|
this.#root = path3.resolve(root);
|
|
5431
5578
|
this.#coordinator = createFilesystemMutationCoordinator(this.#port, this.#root, options.hostPolicy);
|
|
5432
5579
|
publicationContexts.set(this, { root: this.#root, port: this.#port });
|
|
5580
|
+
fileMutations.set(this, (rel, body) => this.#mutateExact(rel, body));
|
|
5433
5581
|
}
|
|
5434
5582
|
async read(id) {
|
|
5435
5583
|
return this.#readWithEdition(id, () => this.#readEdition());
|
|
@@ -5785,6 +5933,7 @@ var KIND_FIELD_TYPE_SET = new Set(KIND_FIELD_TYPES);
|
|
|
5785
5933
|
function isKindFieldType(value) {
|
|
5786
5934
|
return typeof value === "string" && KIND_FIELD_TYPE_SET.has(value);
|
|
5787
5935
|
}
|
|
5936
|
+
var UNDECLARED_FIELDS_POLICIES = ["warn", "error"];
|
|
5788
5937
|
function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
5789
5938
|
const accepted = /* @__PURE__ */ new Map();
|
|
5790
5939
|
const warnings = [...initialWarnings];
|
|
@@ -6379,6 +6528,19 @@ function parseConventionDoc(doc, options = {}) {
|
|
|
6379
6528
|
});
|
|
6380
6529
|
}
|
|
6381
6530
|
}
|
|
6531
|
+
let undeclaredFields;
|
|
6532
|
+
if (fm.undeclared_fields !== void 0) {
|
|
6533
|
+
if (UNDECLARED_FIELDS_POLICIES.includes(fm.undeclared_fields)) {
|
|
6534
|
+
undeclaredFields = fm.undeclared_fields;
|
|
6535
|
+
} else {
|
|
6536
|
+
warnings.push({
|
|
6537
|
+
code: "KIND_CONVENTION_BAD_SHAPE",
|
|
6538
|
+
message: `kind convention '${doc.id}' has an invalid 'undeclared_fields' (${describeShape(fm.undeclared_fields)}; expected warn or error); using warn.`,
|
|
6539
|
+
field: "undeclared_fields",
|
|
6540
|
+
severity: "warning"
|
|
6541
|
+
});
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6382
6544
|
const kind = {
|
|
6383
6545
|
id: doc.id,
|
|
6384
6546
|
title,
|
|
@@ -6396,6 +6558,7 @@ function parseConventionDoc(doc, options = {}) {
|
|
|
6396
6558
|
if (browseCollapsed !== void 0) kind.browseCollapsed = browseCollapsed;
|
|
6397
6559
|
if (order !== void 0) kind.order = order;
|
|
6398
6560
|
if (claim !== void 0) kind.claim = claim;
|
|
6561
|
+
if (undeclaredFields !== void 0) kind.undeclaredFields = undeclaredFields;
|
|
6399
6562
|
if (Object.hasOwn(fm, "deprecated")) {
|
|
6400
6563
|
kind.deprecated = parseKindDeprecation(fm.deprecated, kind, RESERVED_KIND_FIELD_NAMES, claimCoordinates(options.okfVersion, kind)?.fields ?? []);
|
|
6401
6564
|
warnings.push(...kind.deprecated.problems ?? []);
|
|
@@ -6472,7 +6635,7 @@ async function loadKinds(bundle) {
|
|
|
6472
6635
|
const docs = await query2(bundle, { prefix: CONVENTIONS_PREFIX, type: CONVENTION_TYPE }, {
|
|
6473
6636
|
onSkip: ({ id, reason }) => warnings.push({
|
|
6474
6637
|
code: "KIND_CONVENTION_MALFORMED",
|
|
6475
|
-
message: `skipped kind convention '${id}' with unparseable frontmatter: ${reason}`,
|
|
6638
|
+
message: isPermissionDeniedSkip({ reason }) ? `skipped kind convention '${id}' that could not be read: ${reason}` : `skipped kind convention '${id}' with unparseable frontmatter: ${reason}`,
|
|
6476
6639
|
field: id,
|
|
6477
6640
|
severity: "warning"
|
|
6478
6641
|
})
|