@ai-ecoverse/slicc-kernel 1.123.0 → 1.123.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.
package/README.md CHANGED
@@ -933,7 +933,7 @@ OPFS stores names, bytes, sizes and modification times, nothing else. Everything
933
933
 
934
934
  - mode bits (`chmod`; a `chmod` to the mode a file or directory already has writes nothing and leaves its ctime as it was, which saves a sidecar write for every installer that sets the mode a file was made with);
935
935
  - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
936
- - inode numbers that stay stable across renames;
936
+ - inode numbers that stay stable across renames. A file with no record has an inode derived from its path, and a rename keeps it on the file; the rename leaves a small record at the name it vacated with a fresh inode, so a file, symlink or directory made there later doesn't share the renamed one's ([#954](https://github.com/ai-ecoverse/slicc-kernel/issues/954)). An exclusive create doesn't count that record as a taken name, and the boot reconcile keeps it only while a live record still carries the inode it guards;
937
937
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
938
938
 
939
939
  OPFS stays authoritative: an entry is consulted only for a path that exists in OPFS (symlinks only for paths that don't), so files written straight to OPFS, for example by the BIOS installer or a page, get the defaults: directories `0755`, files `0644`, and files under `node_modules/*/bin/` `0755`. A file deleted through the OPFS API disappears at once; its leftover entry is ignored and removed when the next kernel starts. Changes go to OPFS first and then to the sidecar, so a crash in between at worst loses the metadata of that one change. Each change is a read-modify-write inside one IndexedDB transaction, so two kernels (two tabs) on the same origin don't lose each other's updates.
@@ -1031,6 +1031,7 @@ The tools in `tools/cert-review/`:
1031
1031
  `.github/workflows/cert-review-pr.yml` certifies a pull request before it merges, so `main`, and with it npm's `latest`, only gets certified changes. It reports as the `cert-review` commit status, a required check of `main`'s merge queue. Every same-repo pull request gets one (`not behaviour`, `waits for merge-ok`, the review's verdict, or the self-guard's hold). **A pull request from a fork gets none**, because fork code never reaches the pools or the key, so it can't enter the queue: only Lars merges one, after reading it.
1032
1032
 
1033
1033
  - **Which pull requests:** those that change shipped behaviour, by a conservative path filter (`behaviourFiles` in `lib.mjs`): `src/`, `build.mjs`, `package.json` and its lock, `tsconfig*.json`, `.releaserc*`, `tools/release-recover.mjs`, the workflows that build or publish (`release.yml`, `ci.yml`), and the reviewer's own code (`tools/cert-review/`, `cert-review*.yml`, `.github/runner.mjs`). **Main's code decides:** every job checks out the pull request's base and fetches only the trees under test, so the path filter, pack, suite, findings, the instructions and record all run from `main`. The merge group's lookup loads its tools from the protected branch itself (`merge_group.base_ref`), not from the group's `base_sha`, which can be the speculative commit of the group ahead and carry the reviewer changes of the pull requests queued before it; `base_sha` is only the lookup's input. A pull request that changes the reviewer is reviewed by `main`'s reviewer. The workflow file of a `pull_request` run still comes from the pull request, so a change to the reviewer is held by the **certification self-guard**, the shared composite action `slicc-shared-web/.github/actions/cert-self-guard` at a pinned sha. Its light `self-guard` job runs on every pull request event and every merge group. When the diff touches the reviewer (`.github/workflows/cert*.yml`, `tools/cert-review/**`, `.github/runner.mjs` and the guard's own paths), `cert-review` is red with `reviewer change: needs coordinator review (cert-review-approved on this head)` and no review runs. The label is the Kernel Coordinator's alone: they add `cert-review-approved` after reading the change, and the review then starts with `merge-ok`. The approval is bound to the head's arrival on GitHub, and a push removes the label. A merge group that holds an unapproved reviewer change is red too. If the guard doesn't finish, the status is an error that asks to re-run the failed jobs. **The residual:** the pull request's YAML still decides what runs, so the guard catches an accidental edit, not a deliberate rewrite of this workflow. The pull request that adds this workflow can't pass its own check, because its base has no tools yet. Any other pull request gets a green status that says `not behaviour: <files>`, so the skip is recorded.
1034
+ - **A gate's silence never reads as a pass:** every tool a workflow gates on (`pr.mjs plan`, `same` and `lookup`, `same.mjs`, `pack.mjs`, `delta.mjs`, `suite.mjs`, `compat.mjs`, `findings.mjs`, `record.mjs`, `verify.mjs`) ends with one verdict line, `<tool>: PASS <what>` or `<tool>: FAIL <why>`, from the shared `verdict()` in `lib.mjs`. `tools/cert-review/gate.sh [--either|--evidence] <tool> <command>` runs a tool, passes its output through, and exits 70 with an error when the tool's last line starting `<tool>: ` isn't a PASS or FAIL verdict, so a tool replaced by `true`, an early exit 0 or an empty run fails its step. By default the step passes only on PASS with exit 0; a PASS with a non-zero exit, or a FAIL with exit 0, fails it. `--either` keeps the tool's own exit, for tools whose FAIL is evidence that record judges later (suite, compat). `--evidence` needs only the verdict, for the NEGATIVE run on main's tree, which is expected to fail. `suite: PASS` means exit 0, no failures and every expected file has results; `compat: PASS` means a run turned up with no regressions. `prompt.mjs` writes the review prompt to stdout, which goes into a step output, so it prints no verdict; a missing prompt leaves the review without `verdict.json`, and record marks that INCOMPLETE. Because the workflow of a pull request runs main's tools, the tools gain their verdicts first, and the workflows call them through `gate.sh` in the pull request after that.
1034
1035
  - **Decide by what ships:** when a pull request's behaviour files are only `package.json` and/or `package-lock.json` (a Renovate devDependency bump, say), both sides are packed as usual, and then the `same dist` job (`same.mjs`, in code) compares the two tarballs before anything else runs. It checks the same paths, entry types (a symlink isn't a file) and modes. `package.json` must be equal once `devDependencies`, `version` and `gitHead` are removed, so `scripts` (they ship, a consumer's install runs `postinstall`, and `prepack`/`prepublishOnly` can make the published build differ from the packed one), runtime dependencies, `exports`, `bin`, `files` and `engines` must all match. Every other file must be byte-identical once each side's **exact** version string (build.mjs bakes it into `dist`) is replaced by one placeholder. A version string in one more place, or any looser match, is a difference. Source maps are compared as JSON without their `mappings`, whose columns the longer or shorter version string shifts; the code itself is compared byte for byte. If all of that matches, `cert-review` is green with `not behaviour (identical dist): <candidate sha256> = <base sha256> after normalising <v1>/<v2>`, and the suites, kernel-compat, the review and the record are skipped. `same.json` (uploaded as an artifact) names the rule and both tarballs' full sha256. Any difference, or a `same dist` job that doesn't finish, means the full review. The kernel has no runtime `dependencies`, since esbuild bundles everything into `dist`, so a devDependency that changes the product shows up as a byte difference. A test-only devDependency is checked by `ci / test`, on the pull request and on the merge group.
1035
1036
  - **When:** only for a same-repo pull request with `merge-ok`, when the label is added and on every push while it is set. Each pull request has its own concurrency group, and a newer push cancels the older review. A behaviour change without `merge-ok` gets a red status, `waits for merge-ok to be reviewed`. Other labels don't start or cancel anything. Fork code never reaches the pools or the key.
1036
1037
  - **What:** the checkout is the event's default `refs/pull/N/merge`, the pull request merged onto `main` at review time, so a review is of what would land, without pushing to the branch. Both sides are packed in the same run, on a GitHub-hosted runner, with no secrets: the candidate (the merge ref, `x.y.z-pr.N.<run>`) and the base (its first parent, `main`). Then:
@@ -107,7 +107,7 @@ var MemoryMeta = class {
107
107
  if (entry) this.entries.set(path, entry);
108
108
  else this.entries.delete(path);
109
109
  }
110
- async move(from, to, root) {
110
+ async move(from, to, root, left) {
111
111
  await this.remove([to]);
112
112
  const carried = this.entries.subtree(from).map((path) => this.entries.get(path));
113
113
  for (const entry of carried) this.entries.delete(entry.path);
@@ -116,6 +116,7 @@ var MemoryMeta = class {
116
116
  if (path !== to) this.entries.set(path, moved(entry, path));
117
117
  }
118
118
  this.entries.set(to, moved(root(carried.find((entry) => entry.path === from)), to));
119
+ if (left) this.entries.set(from, left);
119
120
  }
120
121
  async remove(paths) {
121
122
  for (const path of paths) this.entries.deleteTree(path);
@@ -240,7 +241,7 @@ var IndexedDbMeta = class {
240
241
  this.channel.close();
241
242
  this.db.close();
242
243
  }
243
- async move(from, to, root) {
244
+ async move(from, to, root, left) {
244
245
  this.settleRemoves();
245
246
  const [transaction, store] = this.transaction("readwrite");
246
247
  store.delete(to);
@@ -254,6 +255,7 @@ var IndexedDbMeta = class {
254
255
  store.put(moved(entry, to + entry.path.slice(from.length)));
255
256
  }
256
257
  store.put(moved(root(source.result), to));
258
+ if (left) store.put(left);
257
259
  };
258
260
  await settle(transaction, () => void 0);
259
261
  this.changed({ trees: [from, to] });
@@ -1104,6 +1106,14 @@ function sidecar(meta) {
1104
1106
  });
1105
1107
  return ops;
1106
1108
  }
1109
+ function occupied(entry) {
1110
+ if (!entry?.vacated) return entry ?? {};
1111
+ const { vacated: _, ...kept } = entry;
1112
+ return kept;
1113
+ }
1114
+ function vacatedAt(path) {
1115
+ return { path, ino: inodeOf(`${path}\0${crypto.randomUUID()}`), vacated: true };
1116
+ }
1107
1117
  function missing2(err) {
1108
1118
  const code = err?.code;
1109
1119
  return code === "ENOENT" || code === "ENOTDIR";
@@ -1338,7 +1348,7 @@ var OpfsFs = class {
1338
1348
  const kind = directory ? void 0 : owner?.socket ? "socket" : owner?.fifo ? "fifo" : void 0;
1339
1349
  const unique = kind === "fifo" ? { ino: inodeOf(`${path}\0${crypto.randomUUID()}`) } : {};
1340
1350
  const fields = kind ? { ...owned4, ...unique, [kind]: true } : owned4;
1341
- if (fields) await this.meta.update(path, (entry) => ({ ...entry, path, ...fields }));
1351
+ if (fields) await this.meta.update(path, (entry) => ({ ...occupied(entry), path, ...fields }));
1342
1352
  }
1343
1353
  guarded = [];
1344
1354
  protect(prefixes2, versions = false) {
@@ -2050,8 +2060,9 @@ var OpfsFs = class {
2050
2060
  await this.locked(physical, async () => {
2051
2061
  if (await this.handleExists(dir, name2)) throw fsError("EEXIST", path);
2052
2062
  const freshMeta = this.meta.fresh ? await this.meta.fresh([path]) : await this.meta.get([path]);
2053
- if (freshMeta[0]?.link !== void 0) throw fsError("EEXIST", path);
2054
- if (freshMeta[0] !== void 0) await this.meta.remove([path]);
2063
+ const found = freshMeta[0];
2064
+ if (found?.link !== void 0) throw fsError("EEXIST", path);
2065
+ if (found !== void 0 && !found.vacated) await this.meta.remove([path]);
2055
2066
  this.forget(physical);
2056
2067
  this.refuse(physical);
2057
2068
  await dir.getDirectoryHandle(name2, { create: true });
@@ -2071,8 +2082,9 @@ var OpfsFs = class {
2071
2082
  await this.locked(physical, async () => {
2072
2083
  if (await this.handleExists(dir, name2)) throw fsError("EEXIST", path);
2073
2084
  const freshMeta = this.meta.fresh ? await this.meta.fresh([path]) : await this.meta.get([path]);
2074
- if (freshMeta[0]?.link !== void 0) throw fsError("EEXIST", path);
2075
- if (freshMeta[0] !== void 0) await this.meta.remove([path]);
2085
+ const found = freshMeta[0];
2086
+ if (found?.link !== void 0) throw fsError("EEXIST", path);
2087
+ if (found !== void 0 && !found.vacated) await this.meta.remove([path]);
2076
2088
  this.forget(physical);
2077
2089
  this.refuse(physical);
2078
2090
  await dir.getFileHandle(name2, { create: true });
@@ -2176,14 +2188,15 @@ var OpfsFs = class {
2176
2188
  const moving = await this.movedOwner(source);
2177
2189
  const carry = (entry) => ({
2178
2190
  ino: inodeOf(source.path),
2179
- ...entry,
2191
+ ...occupied(entry),
2180
2192
  ...moving,
2181
2193
  path: destination,
2182
2194
  ctimeMs: now
2183
2195
  });
2196
+ const left = vacatedAt(source.path);
2184
2197
  if (source.link) {
2185
2198
  if (target?.handle) await this.rm(destination);
2186
- await this.meta.move(source.path, destination, carry);
2199
+ await this.meta.move(source.path, destination, carry, left);
2187
2200
  this.touch(Date.now(), parentOf3(source.path), parent.path);
2188
2201
  return;
2189
2202
  }
@@ -2198,9 +2211,12 @@ var OpfsFs = class {
2198
2211
  () => this.place(source.handle, dir, staging)
2199
2212
  );
2200
2213
  if (swap) await this.rm(destination, { recursive: true });
2201
- await this.meta.move(source.path, destination, carry);
2214
+ await this.meta.move(source.path, destination, carry, left);
2202
2215
  this.movePending(source.path, destination);
2203
- if (!moved2) await this.rm(source.path, { recursive: true });
2216
+ if (!moved2) {
2217
+ await this.rm(source.path, { recursive: true });
2218
+ await this.meta.update(source.path, () => left);
2219
+ }
2204
2220
  let copied = !moved2;
2205
2221
  if (swap) {
2206
2222
  const staged = await dir.getDirectoryHandle(staging);
@@ -2285,11 +2301,11 @@ var OpfsFs = class {
2285
2301
  const fields = await this.newFields(at, owner && { uid: owner.uid, gid: owner.gid }, false);
2286
2302
  await this.entering(
2287
2303
  at,
2288
- () => this.meta.update(at, () => ({
2304
+ () => this.meta.update(at, (entry) => ({
2289
2305
  path: at,
2290
2306
  link: target,
2291
2307
  dir: parent.path,
2292
- ino: inodeOf(at),
2308
+ ino: entry?.vacated && entry.ino !== void 0 ? entry.ino : inodeOf(at),
2293
2309
  mtimeMs: now,
2294
2310
  ctimeMs: now,
2295
2311
  ...fields
@@ -2357,7 +2373,10 @@ var OpfsFs = class {
2357
2373
  }
2358
2374
  async reconcile() {
2359
2375
  let removed = 0;
2360
- for (const entry of await this.meta.all()) {
2376
+ const entries = await this.meta.all();
2377
+ const carried = new Set(entries.filter((e) => !e.vacated).map((e) => e.ino));
2378
+ for (const entry of entries) {
2379
+ if (entry.vacated && carried.has(inodeOf(entry.path))) continue;
2361
2380
  const real = await this.real(entry.path);
2362
2381
  const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf3(entry.path));
2363
2382
  if (!stale) continue;
@@ -6408,7 +6427,7 @@ function satisfies(version, range2) {
6408
6427
  }
6409
6428
 
6410
6429
  // src/version.ts
6411
- var KERNEL_VERSION = true ? "1.123.0" : "0.0.0-dev";
6430
+ var KERNEL_VERSION = true ? "1.123.1" : "0.0.0-dev";
6412
6431
  var OS_RELEASE = "6.0.0-slicc";
6413
6432
  function procVersion() {
6414
6433
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC