@vibe-agent-toolkit/utils 0.2.0-rc.3 → 0.2.0-rc.5
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 +21 -13
- package/dist/crawl-timing.d.ts +43 -66
- package/dist/crawl-timing.d.ts.map +1 -1
- package/dist/crawl-timing.js +35 -79
- package/dist/crawl-timing.js.map +1 -1
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/fs-utils.d.ts +15 -6
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +15 -6
- package/dist/fs-utils.js.map +1 -1
- package/dist/git-tracker.d.ts +32 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +32 -1
- package/dist/git-tracker.js.map +1 -1
- package/dist/git.d.ts +11 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +11 -4
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +19 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -41
- package/dist/index.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/safe-exec.js +0 -1
- package/dist/safe-exec.js.map +1 -1
- package/dist/spawn-hardened.js +0 -1
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/testing.d.ts +73 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +93 -3
- package/dist/testing.js.map +1 -1
- package/dist/timing-dump.d.ts +22 -5
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +89 -23
- package/dist/timing-dump.js.map +1 -1
- package/eslint/index.cjs +9 -0
- package/eslint/index.d.cts +14 -7
- package/eslint/rules/no-process-exit-in-phase.cjs +117 -0
- package/package.json +5 -7
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -139
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -115
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
package/dist/testing.d.ts
CHANGED
|
@@ -1,8 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Fixture primitives shared by suites in more than one package.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* a
|
|
4
|
+
* Deliberately framework-free: nothing here imports `vitest`, so this module
|
|
5
|
+
* stays a plain function library rather than something that registers hooks as a
|
|
6
|
+
* side effect of being imported. Each suite owns its own `beforeEach`/
|
|
7
|
+
* `afterEach` and calls these from inside them — two lines, which is below
|
|
8
|
+
* anything worth sharing, while the part that is genuinely identical (mkdtemp,
|
|
9
|
+
* write the literal, remove the tree) lives here once.
|
|
10
|
+
*
|
|
11
|
+
* It earned a home in `utils` the ordinary way: two packages needed it, not one
|
|
12
|
+
* package speculating that a second might.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ **Do not import `vitest` here to share the hook wrapper itself.**
|
|
15
|
+
* `subpath-purity.test.ts` pins this entry at an EMPTY third-party set, and that
|
|
16
|
+
* pin is load-bearing: `./testing` is a published subpath, so a test framework
|
|
17
|
+
* reached from it becomes a runtime requirement for every adopter who imports
|
|
18
|
+
* it. {@link replantableCorpus} exists precisely so the per-test wrapper each
|
|
19
|
+
* suite still writes is three lines rather than twelve.
|
|
6
20
|
*/
|
|
7
21
|
export * from './test-helpers.js';
|
|
22
|
+
/** A planted fixture tree and the means to remove it. */
|
|
23
|
+
export interface TempCorpus {
|
|
24
|
+
/** Absolute path to the tree's root. */
|
|
25
|
+
root: string;
|
|
26
|
+
/** Remove the tree. Safe to call when it is already gone. */
|
|
27
|
+
cleanup: () => void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write a literal corpus into a fresh temp directory.
|
|
31
|
+
*
|
|
32
|
+
* 🪤 Fixture files must NOT be byte-identical to each other when the suite is
|
|
33
|
+
* about content-addressed behaviour: VAT's blobs are keyed on their bytes plus
|
|
34
|
+
* the parser kind, so two files with the same content collapse into ONE blob and
|
|
35
|
+
* assertions then describe whichever path sorted first. Give each fixture a
|
|
36
|
+
* distinguishing marker line.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ The root is minted per call, so a suite that plants per test must call this
|
|
39
|
+
* per test — a root captured once and reused across tests survives its own
|
|
40
|
+
* `cleanup`.
|
|
41
|
+
*
|
|
42
|
+
* @param prefix - `mkdtemp` prefix, so a leaked directory names its own suite
|
|
43
|
+
* @param corpus - Fixture name to file content; written verbatim as UTF-8.
|
|
44
|
+
* Names may include forward-slash subpaths only if their parents already exist
|
|
45
|
+
* @returns The tree's root and its teardown
|
|
46
|
+
*/
|
|
47
|
+
export declare function createTempCorpus(prefix: string, corpus: Readonly<Record<string, string>>): TempCorpus;
|
|
48
|
+
/** A corpus that can be planted and cleared repeatedly — one tree per test. */
|
|
49
|
+
export interface ReplantableCorpus {
|
|
50
|
+
/** Plant a fresh tree. Drive from the suite's `beforeEach`. */
|
|
51
|
+
plant: () => void;
|
|
52
|
+
/** Remove the current tree, if any. Drive from the suite's `afterEach`. */
|
|
53
|
+
clear: () => void;
|
|
54
|
+
/** The root minted by the most recent {@link plant}. */
|
|
55
|
+
root: () => string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Hold a per-test corpus root, so a suite's hook wrapper is three lines.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ **The root comes back through a GETTER, never as a value.** A per-test
|
|
61
|
+
* fixture is reminted for every test, so a root captured at registration time is
|
|
62
|
+
* `undefined` in the first test and stale in every one after it. That mistake is
|
|
63
|
+
* the reason this holder exists rather than each suite keeping its own `let`.
|
|
64
|
+
*
|
|
65
|
+
* ⭐ Framework-free on purpose: it takes no hooks and registers none, so the
|
|
66
|
+
* suite still owns its own `beforeEach`/`afterEach` and this module keeps the
|
|
67
|
+
* empty third-party set its purity pin asserts. See the module docstring.
|
|
68
|
+
*
|
|
69
|
+
* 🪤 `root()` before the first `plant()` THROWS by name. The shape it replaced
|
|
70
|
+
* read an uninitialised `let` and raised `Cannot read properties of undefined`
|
|
71
|
+
* from inside the fixture, which names neither the suite nor the missing hook.
|
|
72
|
+
*
|
|
73
|
+
* @param prefix - `mkdtemp` prefix, so a leaked directory names its own suite
|
|
74
|
+
* @param corpus - Fixture name to file content, as {@link createTempCorpus} takes it
|
|
75
|
+
* @returns Plant/clear/root, to be driven from the caller's own hooks
|
|
76
|
+
*/
|
|
77
|
+
export declare function replantableCorpus(prefix: string, corpus: Readonly<Record<string, string>>): ReplantableCorpus;
|
|
8
78
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,cAAc,mBAAmB,CAAC;AAElC,yDAAyD;AACzD,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACvC,UAAU,CAOZ;AAED,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,+DAA+D;IAC/D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,wDAAwD;IACxD,IAAI,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACvC,iBAAiB,CAyBnB"}
|
package/dist/testing.js
CHANGED
|
@@ -1,8 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Fixture primitives shared by suites in more than one package.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* a
|
|
4
|
+
* Deliberately framework-free: nothing here imports `vitest`, so this module
|
|
5
|
+
* stays a plain function library rather than something that registers hooks as a
|
|
6
|
+
* side effect of being imported. Each suite owns its own `beforeEach`/
|
|
7
|
+
* `afterEach` and calls these from inside them — two lines, which is below
|
|
8
|
+
* anything worth sharing, while the part that is genuinely identical (mkdtemp,
|
|
9
|
+
* write the literal, remove the tree) lives here once.
|
|
10
|
+
*
|
|
11
|
+
* It earned a home in `utils` the ordinary way: two packages needed it, not one
|
|
12
|
+
* package speculating that a second might.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ **Do not import `vitest` here to share the hook wrapper itself.**
|
|
15
|
+
* `subpath-purity.test.ts` pins this entry at an EMPTY third-party set, and that
|
|
16
|
+
* pin is load-bearing: `./testing` is a published subpath, so a test framework
|
|
17
|
+
* reached from it becomes a runtime requirement for every adopter who imports
|
|
18
|
+
* it. {@link replantableCorpus} exists precisely so the per-test wrapper each
|
|
19
|
+
* suite still writes is three lines rather than twelve.
|
|
6
20
|
*/
|
|
21
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { normalizedTmpdir, safePath } from './path-utils.js';
|
|
23
|
+
// The rest of the testing surface. ⚠️ This re-export is the ENTIRETY of what
|
|
24
|
+
// `./testing` used to be, and dropping it silently removed `detachGitEnv`,
|
|
25
|
+
// `setupSyncTempDirSuite` and every sibling from a published subpath. A module
|
|
26
|
+
// that is both a definition site and a barrel loses the barrel first.
|
|
7
27
|
export * from './test-helpers.js';
|
|
28
|
+
/**
|
|
29
|
+
* Write a literal corpus into a fresh temp directory.
|
|
30
|
+
*
|
|
31
|
+
* 🪤 Fixture files must NOT be byte-identical to each other when the suite is
|
|
32
|
+
* about content-addressed behaviour: VAT's blobs are keyed on their bytes plus
|
|
33
|
+
* the parser kind, so two files with the same content collapse into ONE blob and
|
|
34
|
+
* assertions then describe whichever path sorted first. Give each fixture a
|
|
35
|
+
* distinguishing marker line.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ The root is minted per call, so a suite that plants per test must call this
|
|
38
|
+
* per test — a root captured once and reused across tests survives its own
|
|
39
|
+
* `cleanup`.
|
|
40
|
+
*
|
|
41
|
+
* @param prefix - `mkdtemp` prefix, so a leaked directory names its own suite
|
|
42
|
+
* @param corpus - Fixture name to file content; written verbatim as UTF-8.
|
|
43
|
+
* Names may include forward-slash subpaths only if their parents already exist
|
|
44
|
+
* @returns The tree's root and its teardown
|
|
45
|
+
*/
|
|
46
|
+
export function createTempCorpus(prefix, corpus) {
|
|
47
|
+
const root = mkdtempSync(safePath.join(normalizedTmpdir(), prefix));
|
|
48
|
+
for (const [name, content] of Object.entries(corpus)) {
|
|
49
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- a fixture name from the caller's own literal corpus, under this call's fresh mkdtemp root
|
|
50
|
+
writeFileSync(safePath.join(root, name), content, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Hold a per-test corpus root, so a suite's hook wrapper is three lines.
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ **The root comes back through a GETTER, never as a value.** A per-test
|
|
58
|
+
* fixture is reminted for every test, so a root captured at registration time is
|
|
59
|
+
* `undefined` in the first test and stale in every one after it. That mistake is
|
|
60
|
+
* the reason this holder exists rather than each suite keeping its own `let`.
|
|
61
|
+
*
|
|
62
|
+
* ⭐ Framework-free on purpose: it takes no hooks and registers none, so the
|
|
63
|
+
* suite still owns its own `beforeEach`/`afterEach` and this module keeps the
|
|
64
|
+
* empty third-party set its purity pin asserts. See the module docstring.
|
|
65
|
+
*
|
|
66
|
+
* 🪤 `root()` before the first `plant()` THROWS by name. The shape it replaced
|
|
67
|
+
* read an uninitialised `let` and raised `Cannot read properties of undefined`
|
|
68
|
+
* from inside the fixture, which names neither the suite nor the missing hook.
|
|
69
|
+
*
|
|
70
|
+
* @param prefix - `mkdtemp` prefix, so a leaked directory names its own suite
|
|
71
|
+
* @param corpus - Fixture name to file content, as {@link createTempCorpus} takes it
|
|
72
|
+
* @returns Plant/clear/root, to be driven from the caller's own hooks
|
|
73
|
+
*/
|
|
74
|
+
export function replantableCorpus(prefix, corpus) {
|
|
75
|
+
let planted;
|
|
76
|
+
return {
|
|
77
|
+
plant: () => {
|
|
78
|
+
// 🪤 Removes any tree still standing FIRST. Nested `describe` blocks each
|
|
79
|
+
// get their own `beforeEach` and vitest runs outer-then-inner before a
|
|
80
|
+
// single `afterEach`, so a double plant is ordinary rather than exotic.
|
|
81
|
+
// Overwriting the handle without this would leak the first tree for the
|
|
82
|
+
// process's lifetime, silently and only on the suites that nest.
|
|
83
|
+
planted?.cleanup();
|
|
84
|
+
planted = createTempCorpus(prefix, corpus);
|
|
85
|
+
},
|
|
86
|
+
clear: () => {
|
|
87
|
+
planted?.cleanup();
|
|
88
|
+
planted = undefined;
|
|
89
|
+
},
|
|
90
|
+
root: () => {
|
|
91
|
+
if (planted === undefined) {
|
|
92
|
+
throw new Error(`replantableCorpus('${prefix}'): root() before plant() — the suite is missing its beforeEach`);
|
|
93
|
+
}
|
|
94
|
+
return planted.root;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
8
98
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,6EAA6E;AAC7E,2EAA2E;AAC3E,+EAA+E;AAC/E,sEAAsE;AACtE,cAAc,mBAAmB,CAAC;AAUlC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAc,EACd,MAAwC;IAExC,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACrD,gKAAgK;QAChK,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AACjF,CAAC;AAYD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAc,EACd,MAAwC;IAExC,IAAI,OAA+B,CAAC;IACpC,OAAO;QACL,KAAK,EAAE,GAAG,EAAE;YACV,0EAA0E;YAC1E,uEAAuE;YACvE,wEAAwE;YACxE,wEAAwE;YACxE,iEAAiE;YACjE,OAAO,EAAE,OAAO,EAAE,CAAC;YACnB,OAAO,GAAG,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QACD,KAAK,EAAE,GAAG,EAAE;YACV,OAAO,EAAE,OAAO,EAAE,CAAC;YACnB,OAAO,GAAG,SAAS,CAAC;QACtB,CAAC;QACD,IAAI,EAAE,GAAG,EAAE;YACT,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,IAAI,KAAK,CACb,sBAAsB,MAAM,iEAAiE,CAC9F,CAAC;YACJ,CAAC;YACD,OAAO,OAAO,CAAC,IAAI,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/timing-dump.d.ts
CHANGED
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
* 2. A dump failure is written to **stderr and never thrown**. These run from
|
|
19
19
|
* an `exit` listener, where a throw changes the process's exit behaviour, and
|
|
20
20
|
* they must never touch stdout, which carries vat's report.
|
|
21
|
-
* 3. A pid can file more than one dump
|
|
22
|
-
* once per phase and pids are
|
|
23
|
-
*
|
|
21
|
+
* 3. A pid can file more than one dump, and the name is CLAIMED rather than
|
|
22
|
+
* checked. `vat validate` spawns the vat binary once per phase and pids are
|
|
23
|
+
* reused, so `<stem>-<pid>.json` genuinely collides; worse, worker threads
|
|
24
|
+
* SHARE their parent's pid, so a thread pool's whole cohort competes for one
|
|
25
|
+
* `<stem>-<pid>` sequence at the same instant. The name gains a counter
|
|
26
|
+
* rather than overwriting, and the counter is settled by
|
|
27
|
+
* {@link EXCLUSIVE_CREATE} so the OS — not a check-then-write gap — decides
|
|
28
|
+
* who won it.
|
|
24
29
|
* 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
|
|
25
30
|
* lifetime figure for the process and never a duration of the measured work;
|
|
26
31
|
* its value is the RATIO, which tells a reader whether the wall-timed
|
|
@@ -59,6 +64,16 @@ export interface TimingProcess {
|
|
|
59
64
|
/** System CPU consumed by the process, across all its threads. */
|
|
60
65
|
cpuSystemMs: number;
|
|
61
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Ceiling on the pid-collision search. A directory holding this many dumps for
|
|
69
|
+
* one pid is a runaway, not a collision, and giving up on a reported line is a
|
|
70
|
+
* better outcome than spinning.
|
|
71
|
+
*
|
|
72
|
+
* Exhausting it does NOT fall back to overwriting the last slot. That was the
|
|
73
|
+
* old behaviour, and it destroyed a dump for the same reason the check-then-
|
|
74
|
+
* write gap did — silently, and precisely when the directory was busiest.
|
|
75
|
+
*/
|
|
76
|
+
export declare const MAX_DUMP_COLLISIONS = 1000;
|
|
62
77
|
/**
|
|
63
78
|
* Reduce a raw env value to a directory or `null`.
|
|
64
79
|
*
|
|
@@ -107,13 +122,15 @@ export declare function readTimingProcess(): TimingProcess;
|
|
|
107
122
|
* Write one seam's dump, if the seam is on.
|
|
108
123
|
*
|
|
109
124
|
* The body is built lazily, inside this call, so a disabled seam never pays to
|
|
110
|
-
* snapshot accumulators nobody will read
|
|
125
|
+
* snapshot accumulators nobody will read — and a `build` that throws is reported
|
|
126
|
+
* like any other failure, because this runs from an `exit` listener where a
|
|
127
|
+
* throw would change the process's exit behaviour.
|
|
111
128
|
*
|
|
112
129
|
* @param noun - What the seam is called, for any failure line
|
|
113
130
|
* @param directory - Where to write, or `null` when the seam is off
|
|
114
131
|
* @param basename - Basename stem for the file
|
|
115
132
|
* @param build - Produces the dump body
|
|
116
|
-
* @returns The path written, or `null` when the seam is off or
|
|
133
|
+
* @returns The path written, or `null` when the seam is off or nothing was written
|
|
117
134
|
*/
|
|
118
135
|
export declare function writeTimingDump(noun: string, directory: string | null, basename: string, build: () => unknown): string | null;
|
|
119
136
|
//# sourceMappingURL=timing-dump.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing-dump.d.ts","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"timing-dump.d.ts","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAOH;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;CACrB;AAQD;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AA6BxC;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAG1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAM3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,CAOjD;AA0DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,OAAO,GACnB,MAAM,GAAG,IAAI,CAoBf"}
|
package/dist/timing-dump.js
CHANGED
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
* 2. A dump failure is written to **stderr and never thrown**. These run from
|
|
19
19
|
* an `exit` listener, where a throw changes the process's exit behaviour, and
|
|
20
20
|
* they must never touch stdout, which carries vat's report.
|
|
21
|
-
* 3. A pid can file more than one dump
|
|
22
|
-
* once per phase and pids are
|
|
23
|
-
*
|
|
21
|
+
* 3. A pid can file more than one dump, and the name is CLAIMED rather than
|
|
22
|
+
* checked. `vat validate` spawns the vat binary once per phase and pids are
|
|
23
|
+
* reused, so `<stem>-<pid>.json` genuinely collides; worse, worker threads
|
|
24
|
+
* SHARE their parent's pid, so a thread pool's whole cohort competes for one
|
|
25
|
+
* `<stem>-<pid>` sequence at the same instant. The name gains a counter
|
|
26
|
+
* rather than overwriting, and the counter is settled by
|
|
27
|
+
* {@link EXCLUSIVE_CREATE} so the OS — not a check-then-write gap — decides
|
|
28
|
+
* who won it.
|
|
24
29
|
* 4. The process's own wall and CPU time is read ONCE, at dump time. It is a
|
|
25
30
|
* lifetime figure for the process and never a duration of the measured work;
|
|
26
31
|
* its value is the RATIO, which tells a reader whether the wall-timed
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
* thing a reader has to be able to trust identically. What each seam keeps for
|
|
43
48
|
* itself is its accumulator shape, its dump body and the noun it is called by.
|
|
44
49
|
*/
|
|
45
|
-
import {
|
|
50
|
+
import { writeFileSync } from 'node:fs';
|
|
46
51
|
import { safePath } from './path-core.js';
|
|
47
52
|
import { mkdirSyncReal } from './path-utils.js';
|
|
48
53
|
/** `process.cpuUsage()` reports microseconds; a dump reports milliseconds. */
|
|
@@ -51,10 +56,27 @@ const MICROSECONDS_PER_MS = 1000;
|
|
|
51
56
|
const MS_PER_SECOND = 1000;
|
|
52
57
|
/**
|
|
53
58
|
* Ceiling on the pid-collision search. A directory holding this many dumps for
|
|
54
|
-
* one pid is a runaway, not a collision
|
|
55
|
-
* outcome than spinning.
|
|
59
|
+
* one pid is a runaway, not a collision, and giving up on a reported line is a
|
|
60
|
+
* better outcome than spinning.
|
|
61
|
+
*
|
|
62
|
+
* Exhausting it does NOT fall back to overwriting the last slot. That was the
|
|
63
|
+
* old behaviour, and it destroyed a dump for the same reason the check-then-
|
|
64
|
+
* write gap did — silently, and precisely when the directory was busiest.
|
|
56
65
|
*/
|
|
57
|
-
const MAX_DUMP_COLLISIONS = 1000;
|
|
66
|
+
export const MAX_DUMP_COLLISIONS = 1000;
|
|
67
|
+
/**
|
|
68
|
+
* The `writeFileSync` flag that makes claiming a name atomic.
|
|
69
|
+
*
|
|
70
|
+
* `wx` is create-exclusive: the OS fails the call with `EEXIST` when the path
|
|
71
|
+
* already exists, so exactly one caller wins each name no matter how many are
|
|
72
|
+
* racing for it. Asking `existsSync` first and writing second is two operations
|
|
73
|
+
* with a gap in between, and threads sharing a pid land in that gap routinely —
|
|
74
|
+
* a measured 8-worker parse pool lost 6 of 9 dumps to it, reporting 44 of 172
|
|
75
|
+
* documents as if that were the whole run.
|
|
76
|
+
*/
|
|
77
|
+
const EXCLUSIVE_CREATE = 'wx';
|
|
78
|
+
/** How the failure line names the ceiling case, so it reads unlike an OS error. */
|
|
79
|
+
const CEILING_DETAIL = `every name up to the ${String(MAX_DUMP_COLLISIONS)}-collision ceiling was already claimed`;
|
|
58
80
|
/**
|
|
59
81
|
* Reduce a raw env value to a directory or `null`.
|
|
60
82
|
*
|
|
@@ -119,46 +141,90 @@ export function readTimingProcess() {
|
|
|
119
141
|
};
|
|
120
142
|
}
|
|
121
143
|
/**
|
|
122
|
-
*
|
|
144
|
+
* The nth candidate name in one pid's dump sequence.
|
|
145
|
+
*
|
|
146
|
+
* @param directory - Directory dumps are written to
|
|
147
|
+
* @param stem - Basename and pid, already joined
|
|
148
|
+
* @param collision - 0 for the unsuffixed name, then the collision counter
|
|
149
|
+
* @returns The candidate path
|
|
150
|
+
*/
|
|
151
|
+
function timingDumpCandidate(directory, stem, collision) {
|
|
152
|
+
const suffix = collision === 0 ? '' : `-${String(collision)}`;
|
|
153
|
+
return safePath.join(directory, `${stem}${suffix}.json`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a caught write failure means somebody else already holds the name.
|
|
157
|
+
*
|
|
158
|
+
* @param error - Whatever `writeFileSync` threw
|
|
159
|
+
* @returns `true` only for `EEXIST`, which is a lost race and not a fault
|
|
160
|
+
*/
|
|
161
|
+
function isNameAlreadyTaken(error) {
|
|
162
|
+
return error instanceof Error && 'code' in error && error.code === 'EEXIST';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Claim a name by creating it, and fill it in the same operation.
|
|
166
|
+
*
|
|
167
|
+
* Choosing the name and writing it CANNOT be two steps: whatever separates them
|
|
168
|
+
* is a window in which another writer takes the name that was just declared
|
|
169
|
+
* free, and the loser's dump disappears with no error raised anywhere. So each
|
|
170
|
+
* candidate is written create-exclusively ({@link EXCLUSIVE_CREATE}) and an
|
|
171
|
+
* `EEXIST` — the OS saying somebody else got there — advances to the next
|
|
172
|
+
* counter rather than overwriting.
|
|
123
173
|
*
|
|
124
174
|
* @param directory - Directory dumps are written to
|
|
125
175
|
* @param basename - Basename stem; the pid and any collision counter follow
|
|
126
|
-
* @
|
|
176
|
+
* @param contents - The already-serialized dump body
|
|
177
|
+
* @returns Which name was claimed, or why none could be
|
|
127
178
|
*/
|
|
128
|
-
function
|
|
179
|
+
function claimTimingDump(directory, basename, contents) {
|
|
129
180
|
const stem = `${basename}-${String(process.pid)}`;
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
181
|
+
for (let collision = 0; collision <= MAX_DUMP_COLLISIONS; collision += 1) {
|
|
182
|
+
const candidate = timingDumpCandidate(directory, stem, collision);
|
|
183
|
+
try {
|
|
184
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- operator-supplied diagnostic directory from a VAT_*_TIMING variable
|
|
185
|
+
writeFileSync(candidate, contents, { encoding: 'utf-8', flag: EXCLUSIVE_CREATE });
|
|
186
|
+
return { outcome: 'written', path: candidate };
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (!isNameAlreadyTaken(error))
|
|
190
|
+
return { outcome: 'failed', path: candidate, error };
|
|
191
|
+
}
|
|
135
192
|
}
|
|
136
|
-
return
|
|
193
|
+
return {
|
|
194
|
+
outcome: 'exhausted',
|
|
195
|
+
path: timingDumpCandidate(directory, stem, MAX_DUMP_COLLISIONS),
|
|
196
|
+
};
|
|
137
197
|
}
|
|
138
198
|
/**
|
|
139
199
|
* Write one seam's dump, if the seam is on.
|
|
140
200
|
*
|
|
141
201
|
* The body is built lazily, inside this call, so a disabled seam never pays to
|
|
142
|
-
* snapshot accumulators nobody will read
|
|
202
|
+
* snapshot accumulators nobody will read — and a `build` that throws is reported
|
|
203
|
+
* like any other failure, because this runs from an `exit` listener where a
|
|
204
|
+
* throw would change the process's exit behaviour.
|
|
143
205
|
*
|
|
144
206
|
* @param noun - What the seam is called, for any failure line
|
|
145
207
|
* @param directory - Where to write, or `null` when the seam is off
|
|
146
208
|
* @param basename - Basename stem for the file
|
|
147
209
|
* @param build - Produces the dump body
|
|
148
|
-
* @returns The path written, or `null` when the seam is off or
|
|
210
|
+
* @returns The path written, or `null` when the seam is off or nothing was written
|
|
149
211
|
*/
|
|
150
212
|
export function writeTimingDump(noun, directory, basename, build) {
|
|
151
213
|
if (directory === null)
|
|
152
214
|
return null;
|
|
153
|
-
|
|
215
|
+
let claim;
|
|
154
216
|
try {
|
|
155
|
-
|
|
156
|
-
writeFileSync(target, `${JSON.stringify(build(), null, 2)}\n`, 'utf-8');
|
|
217
|
+
claim = claimTimingDump(directory, basename, `${JSON.stringify(build(), null, 2)}\n`);
|
|
157
218
|
}
|
|
158
219
|
catch (error) {
|
|
159
|
-
|
|
220
|
+
// Only `build()` and its serialization can reach here; every filesystem
|
|
221
|
+
// failure is already an outcome rather than a throw.
|
|
222
|
+
reportTimingDumpFailure(noun, directory, error);
|
|
160
223
|
return null;
|
|
161
224
|
}
|
|
162
|
-
|
|
225
|
+
if (claim.outcome === 'written')
|
|
226
|
+
return claim.path;
|
|
227
|
+
reportTimingDumpFailure(noun, claim.path, claim.outcome === 'exhausted' ? CEILING_DETAIL : claim.error);
|
|
228
|
+
return null;
|
|
163
229
|
}
|
|
164
230
|
//# sourceMappingURL=timing-dump.js.map
|
package/dist/timing-dump.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"timing-dump.js","sourceRoot":"","sources":["../src/timing-dump.ts"],"names":[],"mappings":"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"}
|
package/eslint/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ const rules = {
|
|
|
54
54
|
'no-self-package-import': require('./rules/no-self-package-import.cjs'),
|
|
55
55
|
'require-justified-skip': require('./rules/require-justified-skip.cjs'),
|
|
56
56
|
'no-bare-symlink-in-tests': require('./rules/no-bare-symlink-in-tests.cjs'),
|
|
57
|
+
'no-process-exit-in-phase': require('./rules/no-process-exit-in-phase.cjs'),
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -136,6 +137,14 @@ const RECOMMENDED_EXCLUDE = new Set([
|
|
|
136
137
|
// ride in `recommended`; that is a public-API change and has not been made.
|
|
137
138
|
// VAT enables the whole rule explicitly, scoped to its own conventions.
|
|
138
139
|
'no-bare-symlink-in-tests',
|
|
140
|
+
// Excluded because it keys on a NAMING CONVENTION that is VAT's, not a
|
|
141
|
+
// portable fact — the same reason `no-unsafe-root-join` is excluded above. The
|
|
142
|
+
// hazard it guards is real and general (an in-process orchestrator whose step
|
|
143
|
+
// calls `process.exit()` silently skips every later step), but the marker is
|
|
144
|
+
// the `…Phase` suffix, and an adopter with an unrelated `computeRenderPhase()`
|
|
145
|
+
// that legitimately exits would get a finding they cannot act on. It ships in
|
|
146
|
+
// `rules` and VAT enables it explicitly, scoped to its own orchestrators.
|
|
147
|
+
'no-process-exit-in-phase',
|
|
139
148
|
]);
|
|
140
149
|
|
|
141
150
|
/**
|
package/eslint/index.d.cts
CHANGED
|
@@ -47,13 +47,20 @@ declare namespace plugin {
|
|
|
47
47
|
rules: Record<string, RuleModule>;
|
|
48
48
|
configs: {
|
|
49
49
|
/**
|
|
50
|
-
* The cross-platform safety core: 18 of the
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
50
|
+
* The cross-platform safety core: 18 of the 25 rules, 15 `error` / 3 `warn`.
|
|
51
|
+
*
|
|
52
|
+
* Seven are excluded, for four reasons. `no-test-scoped-functions`,
|
|
53
|
+
* `require-justified-skip` and `no-bare-symlink-in-tests` are positions on
|
|
54
|
+
* TEST STYLE rather than portability facts. `no-unsafe-root-join` and
|
|
55
|
+
* `no-process-exit-in-phase` key on NAMING rather than on the property they
|
|
56
|
+
* care about (taint, and an orchestrated call site). `no-raw-text-decode`
|
|
57
|
+
* names a decoding SEAM that only exists in the consuming repo. And
|
|
58
|
+
* `no-self-package-import` REQUIRES an option this config cannot supply.
|
|
59
|
+
*
|
|
60
|
+
* All seven still ship in `rules` and are enabled by naming them — which is
|
|
61
|
+
* what this repo's own `eslint.config.js` does. The count above is asserted
|
|
62
|
+
* by `packages/utils/test/eslint/rules.test.ts`, so it cannot drift
|
|
63
|
+
* unnoticed the way it did when this comment said "four".
|
|
57
64
|
*/
|
|
58
65
|
recommended: FlatConfig;
|
|
59
66
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: no-process-exit-in-phase
|
|
3
|
+
*
|
|
4
|
+
* Forbid `process.exit()` inside a phase entry point — a function whose name
|
|
5
|
+
* ends in `Phase`.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this rule exists
|
|
8
|
+
*
|
|
9
|
+
* `vat validate`, `vat verify` and `vat build` used to run each phase as a
|
|
10
|
+
* SEPARATE CHILD PROCESS. A `process.exit()` inside a phase was then perfectly
|
|
11
|
+
* safe: it ended that child, the parent read the exit code, and the run carried
|
|
12
|
+
* on to the next phase. Nothing about the code said so — the safety came
|
|
13
|
+
* entirely from the process boundary.
|
|
14
|
+
*
|
|
15
|
+
* That boundary is gone. Phases now run in the orchestrator's own process, so a
|
|
16
|
+
* surviving `process.exit()` ends THE WHOLE RUN: every later phase is silently
|
|
17
|
+
* skipped, the parent's aggregation never happens, and the process exits 0-or-1
|
|
18
|
+
* having done half the work with nothing in the document to say so. It is the
|
|
19
|
+
* worst shape of failure this codebase has — a confident, well-formed, wrong
|
|
20
|
+
* answer — and it cannot be caught by a type, because `process.exit()`
|
|
21
|
+
* typechecks anywhere.
|
|
22
|
+
*
|
|
23
|
+
* A phase must RETURN `{ document, exitCode }` and let its caller decide whether
|
|
24
|
+
* to print and exit (a command-line run) or to fold the result into the run (an
|
|
25
|
+
* orchestrated one).
|
|
26
|
+
*
|
|
27
|
+
* ## Why the name is the marker
|
|
28
|
+
*
|
|
29
|
+
* The `…Phase` suffix is the convention every phase entry point already follows
|
|
30
|
+
* (`runResourcesValidatePhase`, `runSkillsBuildPhase`, …). Keying the rule to it
|
|
31
|
+
* means a new phase is protected the moment it is named like one, with no list
|
|
32
|
+
* to keep in sync — a list is the thing that goes stale silently, which is the
|
|
33
|
+
* defect class this rule exists to prevent in the first place.
|
|
34
|
+
*
|
|
35
|
+
* The thin Commander wrappers that CALL these functions keep their
|
|
36
|
+
* `process.exit()`: deciding how the process ends is exactly their job, and they
|
|
37
|
+
* are not named `…Phase`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Does this call expression read as `process.exit(...)`? */
|
|
41
|
+
function isProcessExitCall(node) {
|
|
42
|
+
const { callee } = node;
|
|
43
|
+
return (
|
|
44
|
+
callee.type === 'MemberExpression' &&
|
|
45
|
+
!callee.computed &&
|
|
46
|
+
callee.object.type === 'Identifier' &&
|
|
47
|
+
callee.object.name === 'process' &&
|
|
48
|
+
callee.property.type === 'Identifier' &&
|
|
49
|
+
callee.property.name === 'exit'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The declared name of a function-ish node, however it was declared.
|
|
55
|
+
*
|
|
56
|
+
* Covers the three spellings a phase entry point can legitimately take: a
|
|
57
|
+
* function declaration, a `const x = function () {}`, and a `const x = () => {}`.
|
|
58
|
+
* A rule that only understood declarations would be silently inert against an
|
|
59
|
+
* arrow const — the same blind spot `no-test-scoped-functions` documents.
|
|
60
|
+
*/
|
|
61
|
+
function functionName(node) {
|
|
62
|
+
if (node.id && node.id.type === 'Identifier') return node.id.name;
|
|
63
|
+
|
|
64
|
+
const { parent } = node;
|
|
65
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
66
|
+
return parent.id.name;
|
|
67
|
+
}
|
|
68
|
+
if (parent && parent.type === 'Property' && parent.key.type === 'Identifier') {
|
|
69
|
+
return parent.key.name;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FUNCTION_TYPES = new Set([
|
|
75
|
+
'FunctionDeclaration',
|
|
76
|
+
'FunctionExpression',
|
|
77
|
+
'ArrowFunctionExpression',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** The nearest enclosing function named like a phase entry point, if any. */
|
|
81
|
+
function enclosingPhaseName(node) {
|
|
82
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
83
|
+
if (!FUNCTION_TYPES.has(current.type)) continue;
|
|
84
|
+
const name = functionName(current);
|
|
85
|
+
if (name !== undefined && name.endsWith('Phase')) return name;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
meta: {
|
|
92
|
+
type: 'problem',
|
|
93
|
+
docs: {
|
|
94
|
+
description:
|
|
95
|
+
'Forbid process.exit() inside a phase entry point, where it would end the whole orchestrated run',
|
|
96
|
+
category: 'Agentic Code Safety',
|
|
97
|
+
recommended: true,
|
|
98
|
+
},
|
|
99
|
+
fixable: null,
|
|
100
|
+
schema: [],
|
|
101
|
+
messages: {
|
|
102
|
+
exitInPhase:
|
|
103
|
+
"'{{name}}' is a phase entry point, so process.exit() here ends the ENTIRE run — every later phase is skipped and the orchestrator never aggregates. Return { document, exitCode } instead and let the caller decide how the process ends.",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
create(context) {
|
|
108
|
+
return {
|
|
109
|
+
CallExpression(node) {
|
|
110
|
+
if (!isProcessExitCall(node)) return;
|
|
111
|
+
const name = enclosingPhaseName(node);
|
|
112
|
+
if (name === undefined) return;
|
|
113
|
+
context.report({ node, messageId: 'exitInPhase', data: { name } });
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|