@intentic/constants 1.242.0 → 1.243.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,17 +13,20 @@ The ports, paths and image references the daemon, the CLIs and the desktop app a
13
13
  values, and the install-script table. Isomorphic, imported by browser code, so nothing here may touch `node:fs`.
14
14
  - [src/node.mjs](src/node.mjs): `repoRoot()` and `packageRoot()`, behind the `@intentic/constants/node`
15
15
  subpath. Node-only, and hand-written JavaScript rather than compiled TypeScript.
16
- - [src/assertion-measure.mjs](src/assertion-measure.mjs), [src/contract-shrink.mjs](src/contract-shrink.mjs) and
17
- [src/control-bytes.mjs](src/control-bytes.mjs): the three judgments the repository's checkout gates
18
- (`_tools/checks/`) and the daemon both make, kept as one copy each. Hand-written JavaScript for the same
19
- reason `node.mjs` is: a gate that runs before `pnpm install` imports them by relative path, and the daemon
20
- imports them as subpaths of this package.
16
+ - [src/assertion-measure.mjs](src/assertion-measure.mjs), [src/contract-shrink.mjs](src/contract-shrink.mjs),
17
+ [src/control-bytes.mjs](src/control-bytes.mjs) and [src/mirror-roots.mjs](src/mirror-roots.mjs): the four
18
+ judgments the repository's checkout gates (`_tools/checks/`) and the daemon both make, kept as one copy each.
19
+ Hand-written JavaScript for the same reason `node.mjs` is: a gate that runs before `pnpm install` imports them
20
+ by relative path, and the daemon imports them as subpaths of this package.
21
21
 
22
22
  ## How it fits
23
23
 
24
24
  The bottom of the dependency graph: it imports nothing and almost everything imports it. That is also what
25
25
  makes it the home of the few pure judgments a pre-install script and the daemon have to share: anything else
26
- they could both import would need an install to resolve. A port number that lives
26
+ they could both import would need an install to resolve. `mirror-roots.mjs` is the clearest case of that: the
27
+ set of directories an isolated turn overlays is the daemon's business (`agents/isolation.ts` mounts them), and
28
+ whether a build script may `rm -rf` one of them is a checkout gate's business, and the two answers have to be
29
+ the same answer or a name added to one is a directory the other stops protecting. A port number that lives
27
30
  in two files is a port number that will eventually be two different numbers, which is the entire argument for
28
31
  this package existing. The same argument covers the directory layouts: `/work`, `/history`, `.intentic`,
29
32
  `/opt/intentic`: which were previously typed out by hand across dozens of files with nothing linking the copies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/constants",
3
- "version": "1.242.0",
3
+ "version": "1.243.0",
4
4
  "description": "Shared constants for the intentic packages, ports, paths, and image references the daemon, CLIs and desktop app all agree on",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,9 @@
19
19
  "src/contract-shrink.mjs",
20
20
  "src/contract-shrink.d.mts",
21
21
  "src/control-bytes.mjs",
22
- "src/control-bytes.d.mts"
22
+ "src/control-bytes.d.mts",
23
+ "src/mirror-roots.mjs",
24
+ "src/mirror-roots.d.mts"
23
25
  ],
24
26
  "exports": {
25
27
  ".": {
@@ -48,6 +50,10 @@
48
50
  "./control-bytes": {
49
51
  "types": "./src/control-bytes.d.mts",
50
52
  "default": "./src/control-bytes.mjs"
53
+ },
54
+ "./mirror-roots": {
55
+ "types": "./src/mirror-roots.d.mts",
56
+ "default": "./src/mirror-roots.mjs"
51
57
  }
52
58
  },
53
59
  "devDependencies": {
@@ -0,0 +1,3 @@
1
+ // Types for mirror-roots.mjs, the overlay-mount invariant the checkout gate and the daemon's turn isolation share.
2
+ export const MIRRORED_DIRS: ReadonlySet<string>;
3
+ export function replacedMirrorRoots(command: string): string[];
@@ -0,0 +1,142 @@
1
+ /* THE DIRECTORIES AN ISOLATED TURN MOUNTS OVER, and the one thing the main checkout must never do to them.
2
+ *
3
+ * A worktree holds TRACKED files only, so the two trees a package's dependents resolve THROUGH — its installed
4
+ * tree (`node_modules`) and its build output (`dist`, `generated`) — cannot be checked out and have to come
5
+ * from the main tree. The daemon supplies them as overlayfs mounts, one per directory, the MAIN checkout's copy
6
+ * as the lowerdir and a per-conversation upper layer for whatever the turn writes
7
+ * (_sandbox/sandbox/src/agents/isolation.ts, which imports this set rather than keeping its own).
8
+ *
9
+ * AN OVERLAY RESOLVES ITS LOWERDIR ONCE, AT MOUNT TIME, and holds that dentry for the life of the mount.
10
+ * Rewriting the FILES inside it is harmless, and that is the whole reason mirroring a build directory works at
11
+ * all: measured on this image (ext4 lower, kernel 6.18), unlinking every file in the lower root and writing new
12
+ * ones is picked up by the merged view immediately, entry for entry. REPLACING THE DIRECTORY ITSELF is not.
13
+ * `rm -rf dist` followed by a `mkdir` gives that path a new inode; the mount keeps pointing at the old one, and
14
+ * the merged directory then reads as COMPLETELY EMPTY — not even the entries in the turn's own upper layer,
15
+ * though `stat` on any of those upper files still succeeds, which is what makes the symptom so hard to read.
16
+ * `mount -o remount` does not repair it. Only umount/mount does, and nothing inside the turn can do either: the
17
+ * mount root is the one lower directory a turn cannot shadow with a write of its own.
18
+ *
19
+ * That is not a hazard someone imagined. It is what `_platform/prisma`'s build script did: `rm -rf ./generated
20
+ * ./dist ./.cache` ahead of `prisma generate`. Run on the main tree by `turbo run build` — the push gate's third
21
+ * tier, the image-tree prep, an owner typing `pnpm build` — it replaced the lowerdir of every live agent
22
+ * worktree's `_platform/prisma/generated` overlay at once. Each of those turns was then holding a directory with
23
+ * a freshly generated `client.ts` in it that `readdir` reported as empty, so the `"include": ["./generated/**"]`
24
+ * glob in that package's tsconfig matched nothing and the declarations emit died with
25
+ *
26
+ * _platform/prisma/client.ts(1,15): error TS6307: File '.../generated/client.ts' is not listed within the
27
+ * file list of project '.../_platform/prisma/tsconfig.json'
28
+ *
29
+ * on the turn-ending check of every conversation, whatever the turn had actually changed. A gate that is red for
30
+ * a reason no turn caused is the failure mode docs/ci-failure-audit.md exists to hunt, and it teaches everyone
31
+ * reading it that a red check is background noise.
32
+ *
33
+ * WHY THE RULE IS ABOUT THE MOUNT ROOT AND NOT ABOUT EVERY DIRECTORY UNDER IT. `prisma generate` does the same
34
+ * remove-and-recreate to `generated/models` and `generated/internal` on every run, and no rule here could stop
35
+ * it — that is a third-party generator's business. It does not have to be stopped: a turn's own generate rmdirs
36
+ * those same subdirectories through the MERGED view first, which leaves an opaque upper directory the stale
37
+ * lower can no longer reach. Only the mount root has no such repair, which is exactly where this rule sits.
38
+ *
39
+ * So: EMPTY A MIRRORED DIRECTORY, NEVER REPLACE IT. _tools/scripts/clean-outputs.mjs is what does that, and
40
+ * _tools/checks/mirror-roots.mjs refuses the shape wherever a shell command in this repository spells it.
41
+ *
42
+ * Hand-written JavaScript rather than compiled TypeScript for the reason node.mjs gives: the checkout gate that
43
+ * enforces this imports it by relative path from a clone that has never installed, and the daemon imports the
44
+ * same file as `@intentic/constants/mirror-roots`. */
45
+
46
+ // The directory NAMES a turn overlays, discovered by name wherever they appear in the tree (isolation.ts walks
47
+ // for them). Caches are deliberately absent, and that absence is load-bearing: see the MIRRORED_DIRS comment in
48
+ // isolation.ts for why a mirrored `.cache` would hand a turn the main checkout's idea of what its dist was
49
+ // built from. Nothing mounts a `.cache`, so it is free to be removed outright, and the build scripts fixed for
50
+ // this still do exactly that to theirs.
51
+ export const MIRRORED_DIRS = new Set(["node_modules", "dist", "generated"]);
52
+
53
+ // A path's last segment, with quotes and trailing slashes taken off. `"$PKG/dist"` and `./generated/` both name
54
+ // a mirror root; `node_modules/.pnpm/onnxruntime-web@*` does not, and neither does `dist/*`, which removes the
55
+ // CONTENTS and leaves the inode alone.
56
+ const lastSegment = (token) => {
57
+ const bare = token.replace(/^['"]|['"]$/g, "").replace(/\/+$/, "");
58
+ return bare.slice(bare.lastIndexOf("/") + 1);
59
+ };
60
+
61
+ /* One shell word at a time, quotes kept so `lastSegment` can strip them and a quoted `'{}'` still reads as the
62
+ * find placeholder it is. Not a shell parser and not trying to be: what this has to recognize is a removal
63
+ * someone WROTE, and every one of those in this repository is a plain sequence of words. */
64
+ const tokenize = (segment) => segment.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
65
+
66
+ /* The verbs that can remove a directory, and the words that may stand in front of one. `docker rm -f <name>`
67
+ * removes a container and `find … -exec rm -rf {} +` removes files, so the preceding word is what tells them
68
+ * apart: a removal is a COMMAND here, or the thing an exec/sudo/xargs runs, never an argument to something
69
+ * else. (It costs nothing to be wrong about `docker rm` anyway — it is never recursive — but a check that
70
+ * reports a container by name would be read as noise, and a noisy gate gets switched off.) */
71
+ const REMOVERS = new Set(["rm", "rmdir", "rimraf"]);
72
+ const RUNNERS = new Set(["exec", "-exec", "-execdir", "sudo", "xargs", "then", "do", "else", "{", "(", "npx", "pnpm", "bunx", "yarn"]);
73
+ // `-rf`, `-fr`, `-Rf`, `-r`, `--recursive`. A non-recursive `rm` cannot take a directory at all, so it can
74
+ // never be the operation this is about: `rm -f dist.zip` is fine and must stay unreported. (It is also what
75
+ // keeps `pnpm rm <package>`, an uninstall, out of this: nothing there is recursive.)
76
+ const RECURSIVE = /^(?:--recursive$|-[a-zA-Z]*[rR])/;
77
+ // Where a `find -exec` command ends. Everything after it belongs to the find again.
78
+ const EXEC_END = new Set([";", "\\;", "+"]);
79
+ const PLACEHOLDER = /^['"]?\{\}['"]?$/;
80
+ // The find predicates that NAME what will be removed, and the one that makes a find safe: `-mindepth 1` never
81
+ // yields the directory it started from, which is precisely how you empty a tree without replacing its root.
82
+ const NAME_PREDICATES = new Set(["-name", "-iname", "-path", "-wholename", "-ipath"]);
83
+
84
+ /* WHICH MIRROR ROOTS A SHELL COMMAND WOULD REPLACE, as the operands were written, so a report can quote them.
85
+ *
86
+ * Two shapes, because those are the two ways this repository has ever spelled it:
87
+ * · a literal removal — `rm -rf ./generated ./dist ./.cache`, `rm -rf "$PKG/dist"`
88
+ * · a find that removes what it names — `find . \( -name 'node_modules' -o -name 'dist' \) -prune -exec rm
89
+ * -rf '{}' +`, where the removal's own operand is a placeholder and the find's predicates say what it hits.
90
+ *
91
+ * Split on the separators that end one command, so `a && rm -rf dist` is two commands and a `find … -exec rm …`
92
+ * stays one: only inside a single command do a find's predicates describe that removal's operands.
93
+ *
94
+ * A FIND IS READ WHOLE, AND THAT ROUNDS TOWARDS REFUSING. `find . -name node_modules -prune -o -name dist
95
+ * -prune -exec rm -rf {} +` removes only the second name; the first is pruned past. Telling them apart means
96
+ * implementing find's expression grammar — `-o`, `-a`, `-prune` and their precedence — for a distinction that
97
+ * changes nothing about the answer, since the safe rewrite is the same either way and neither name may be
98
+ * REMOVED by a command running in the checkout. So every `-name` in a removing find is reported, and the fix
99
+ * for a false one is the fix for a true one. */
100
+ export const replacedMirrorRoots = (command) => {
101
+ const found = [];
102
+ for (const segment of command.split(/\|\||&&|[;|\n]/)) {
103
+ const tokens = tokenize(segment);
104
+ // `-mindepth 1` (or deeper) makes every removal in this command an emptying rather than a replacement.
105
+ const shallowest = tokens.indexOf("-mindepth");
106
+ if (shallowest !== -1 && Number(tokens[shallowest + 1]) >= 1) {
107
+ continue;
108
+ }
109
+ const named = tokens.flatMap((token, at) => (NAME_PREDICATES.has(token) && tokens[at + 1] !== undefined ? [tokens[at + 1]] : []));
110
+ // `find -delete` removes what the predicates name, with no `rm` anywhere in the line to notice.
111
+ if (tokens.includes("-delete")) {
112
+ found.push(...named.filter((token) => MIRRORED_DIRS.has(lastSegment(token))));
113
+ }
114
+ for (const [at, token] of tokens.entries()) {
115
+ const before = tokens[at - 1];
116
+ if (!REMOVERS.has(token) || (before !== undefined && !RUNNERS.has(before))) {
117
+ continue;
118
+ }
119
+ const operands = [];
120
+ let recursive = token !== "rm";
121
+ for (const word of tokens.slice(at + 1)) {
122
+ if (EXEC_END.has(word)) {
123
+ break;
124
+ }
125
+ if (word.startsWith("-")) {
126
+ recursive ||= RECURSIVE.test(word);
127
+ continue;
128
+ }
129
+ operands.push(word);
130
+ }
131
+ if (!recursive) {
132
+ continue;
133
+ }
134
+ for (const operand of operands) {
135
+ // A `{}` is the find's placeholder: what it stands for is whatever the predicates named.
136
+ const targets = PLACEHOLDER.test(operand) ? named : [operand];
137
+ found.push(...targets.filter((target) => MIRRORED_DIRS.has(lastSegment(target))));
138
+ }
139
+ }
140
+ }
141
+ return [...new Set(found)];
142
+ };