@intentic/constants 1.242.0 → 1.244.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 +9 -6
- package/package.json +8 -2
- package/src/mirror-roots.d.mts +3 -0
- package/src/mirror-roots.mjs +142 -0
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)
|
|
17
|
-
[src/control-bytes.mjs](src/control-bytes.mjs)
|
|
18
|
-
(`_tools/checks/`) and the daemon both make, kept as one copy each.
|
|
19
|
-
reason `node.mjs` is: a gate that runs before `pnpm install` imports them
|
|
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.
|
|
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.
|
|
3
|
+
"version": "1.244.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,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
|
+
};
|