@reventlessdev/reventless-spec 3.0.0-alpha.168 → 3.0.0-alpha.170

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +17 -3
  3. package/package.json +5 -4
  4. package/run-rename-to-decision.mjs +3 -0
  5. package/src/AnsiStyle.res +3 -3
  6. package/src/CliArgs.res +1 -1
  7. package/src/LogPrefix.res +1 -1
  8. package/src/codemod/RenameToDecision.res +207 -0
  9. package/src/codemod/RenameToDecision.res.mjs +334 -0
  10. package/src/components/Aggregate.res +1 -1
  11. package/src/components/CapabilityManifest.res +1 -1
  12. package/src/components/ComponentKind.res +2 -2
  13. package/src/components/ComponentKind.res.mjs +1 -1
  14. package/src/components/DcbScopeInference.res +2 -2
  15. package/src/components/DcbTag.res +5 -5
  16. package/src/components/OutboundTranslationSlice.res +2 -2
  17. package/src/components/Plugin.res +2 -2
  18. package/src/components/Sensitive.res +1 -1
  19. package/src/components/Snapshot.res +5 -5
  20. package/src/components/StateChangeSlice.res +6 -5
  21. package/src/components/TaggedUnion.res +1 -1
  22. package/src/generator/Codegen.res +5 -5
  23. package/src/generator/Codegen.res.mjs +1 -1
  24. package/src/generator/Discovery.res +3 -3
  25. package/src/generator/Pairing.res +47 -18
  26. package/src/generator/Pairing.res.mjs +41 -10
  27. package/src/lifecycle/CheckLifecycleModel.res +3 -3
  28. package/src/semantic/Capabilities.res +2 -2
  29. package/src/semantic/CapabilityNeed.res +3 -3
  30. package/src/semantic/Color.res +8 -8
  31. package/src/semantic/IdentityProvider.res +1 -1
  32. package/src/semantic/Messaging.res +2 -2
  33. package/src/types/{Behavior.res → Decision.res} +10 -6
  34. package/src/types/Message.res +1 -1
  35. package/src/types/OwnerScope.res +5 -5
  36. package/src/types/ShellEnvironment.res +39 -0
  37. package/src/types/ShellEnvironment.res.mjs +39 -0
  38. package/src/types/Trait.res +1 -1
  39. /package/src/types/{Behavior.res.mjs → Decision.res.mjs} +0 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.170 (2026-10-21)
7
+
8
+ * feat!: the write-side logic module is called the decision, and a codemod moves an app over ([cb573ff](https://github.com/ReventlessDev/reventless-core/commit/cb573ff5b928c7d7f3e75988dfaa65066f8c95eb))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * <Spec>_Behavior.res, @@reventless.behavior,
13
+ Reventless.Behavior.T, StateChangeSlice.Behavior, Behavior_GWT and
14
+ thenRefused no longer exist. Run `npx reventless-rename-to-decision` at an
15
+ app's root, then rebuild; comments are not rewritten. The PPX must be the
16
+ matching release.
17
+
18
+
19
+
20
+ # 3.0.0-alpha.169 (2026-10-19)
21
+
22
+ ### Features
23
+
24
+ * **gwt:** record behaviour scenarios as they run, for suites whose source names none ([256b253](https://github.com/ReventlessDev/reventless-core/commit/256b253153ec043105c38ac742a1a3efbde48c87))
25
+ * **spec,aws,local:** tell the host shell which environment it runs in, and write local host UI files into a chosen assetsDir ([4f896d1](https://github.com/ReventlessDev/reventless-core/commit/4f896d13ae9ec0d211c33dbd9271e7197cfebc41))
26
+
27
+
6
28
  # 3.0.0-alpha.168 (2026-10-17)
7
29
 
8
30
  **Note:** Version bump only for package @reventlessdev/reventless-spec
package/README.md CHANGED
@@ -11,7 +11,7 @@ The **specification layer** of [Reventless](https://docs.reventless.dev) — a
11
11
  spec-driven, event-sourced CQRS framework written in [ReScript](https://rescript-lang.org).
12
12
  This package holds the type specifications and module-type interfaces that the rest of the
13
13
  framework is built on: the contracts application domain code implements (commands, events,
14
- behaviors, projections) and the component-kind vocabulary the whole framework shares. It
14
+ decisions, projections) and the component-kind vocabulary the whole framework shares. It
15
15
  has **no** runtime, cloud, or storage dependency, so domain code can declare its shape
16
16
  against `reventless-spec` without pulling in an implementation. Modules are exposed under
17
17
  the `Reventless` namespace.
@@ -24,8 +24,9 @@ ReScript modules, consumed by adding the package to your `rescript.json` `depend
24
24
 
25
25
  - **`Message`** — the envelope metadata carried by every event and command (`meta`,
26
26
  `event'<>`, `command'<>`), used for audit, correlation, tracing, and routing.
27
- - **`Behavior`** — the module type application code implements to define aggregate business
28
- logic: `initialState`, `evolve(state, event)`, and `decide(command, state)`.
27
+ - **`Decision`** — the module type application code implements to define an aggregate's
28
+ write-side logic (the decider pattern): `initialState`, `evolve(state, event)`, and
29
+ `decide(state, command)`. Its file is `<Entity>_Decision.res`.
29
30
  - **`Projection`** — `Source` / `Target` module types describing a read model projected
30
31
  from an aggregate's events.
31
32
  - **`Handler`** — function signatures for handling commands, events, and errors.
@@ -62,6 +63,19 @@ generate-plugin src/ # generate src/Plugin.res from the src/ folder
62
63
  Wire it into your build via a `prebuild` script so it runs before `rescript build`. The
63
64
  generated `Plugin.res` is committed to git, so CI compiles it directly.
64
65
 
66
+ **`reventless-rename-to-decision` CLI**
67
+
68
+ Moves an application from the logic module's former name, *behavior*, to *decision*:
69
+ it renames every `*_Behavior.res` to `*_Decision.res` (with `git mv` inside a work tree)
70
+ and rewrites `@@reventless.behavior`, `Behavior_GWT`, `Reventless.Behavior`,
71
+ `thenRefused` and `<Spec>_Behavior` references in `.res` files. The framework refuses the
72
+ old spellings, so a bump is this command plus a build.
73
+
74
+ ```bash
75
+ reventless-rename-to-decision --dry-run # print what would change under .
76
+ reventless-rename-to-decision # rename; skips node_modules and lib
77
+ ```
78
+
65
79
  ## Where it fits
66
80
 
67
81
  `reventless-spec` is the foundation package in the Reventless framework:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.168",
3
+ "version": "3.0.0-alpha.170",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "certify-trait": "./run-certify-trait.mjs",
11
11
  "trait-manifest": "./run-trait-manifest.mjs",
12
12
  "check-lifecycle": "./run-check-lifecycle.mjs",
13
- "prepare-accounts": "./run-prepare-accounts.mjs"
13
+ "prepare-accounts": "./run-prepare-accounts.mjs",
14
+ "reventless-rename-to-decision": "./run-rename-to-decision.mjs"
14
15
  },
15
16
  "jest": {
16
17
  "testMatch": [
@@ -26,11 +27,11 @@
26
27
  "sury": "11.0.0",
27
28
  "sury-ppx": "11.0.0",
28
29
  "yaml": "^2.8.3",
29
- "@reventlessdev/rescript-node": "2.0.0-alpha.22"
30
+ "@reventlessdev/rescript-node": "2.0.0-alpha.23"
30
31
  },
31
32
  "devDependencies": {
32
33
  "rescript": "12.3.1",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.12"
34
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.13"
34
35
  },
35
36
  "peerDependencies": {
36
37
  "rescript": "^12.3.1"
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./src/codemod/RenameToDecision.res.mjs"
3
+ await main()
package/src/AnsiStyle.res CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  This is the lowest layer in the logging stack — LogPrefix and Logger
7
7
  depend on it, so the sink decision lives here as the single source of
8
- truth. `useAnsi()` ⇒ TTY text mode (colour/bold); `isJsonSink()` ⇒
8
+ truth. `useAnsi()` ⇒ TTY text mode (color/bold); `isJsonSink()` ⇒
9
9
  structured JSON for any non-TTY collector (CloudWatch, Datadog, …). */
10
10
  @val external _isTty: option<bool> = "process.stdout.isTTY"
11
11
  @val external _logFormat: option<string> = "process.env.REVENTLESS_LOG_FORMAT"
@@ -38,7 +38,7 @@ let _format = ref(_resolveFormat())
38
38
  /** Declares what this runtime is, for the pipe case the TTY probe gets wrong.
39
39
  Call it at module init, before anything logs. An explicit
40
40
  REVENTLESS_LOG_FORMAT still wins, so a developer can always ask for the other
41
- one. A runtime that says nothing keeps the TTY behaviour. */
41
+ one. A runtime that says nothing keeps the TTY behavior. */
42
42
  let setDefaultFormat = (format: [#text | #json]) => {
43
43
  _default :=
44
44
  Some(
@@ -56,7 +56,7 @@ let reload = () => _format := _resolveFormat()
56
56
  /** True when output should be structured JSON (non-TTY sink). */
57
57
  let isJsonSink = () => _format.contents == "json"
58
58
 
59
- /** True when output should carry ANSI colour/bold (TTY text mode). */
59
+ /** True when output should carry ANSI color/bold (TTY text mode). */
60
60
  let useAnsi = () => _format.contents == "text"
61
61
 
62
62
  /** Wrap a string in ANSI bold escape codes — no-op in a JSON sink. */
package/src/CliArgs.res CHANGED
@@ -1,7 +1,7 @@
1
1
  /***
2
2
  How every command-line tool in this repository reads `process.argv`: through
3
3
  Node's `parseArgs`, with mistakes worded the way these tools word them, and one
4
- behaviour on `--help`, on a usage mistake and on a bare invocation (see [run]).
4
+ behavior on `--help`, on a usage mistake and on a bare invocation (see [run]).
5
5
  Non-strict with tokens rather than strict mode, because strict mode names the
6
6
  offending argument only inside English prose.
7
7
  */
package/src/LogPrefix.res CHANGED
@@ -164,7 +164,7 @@ let fmtPlainPrefix = (~comp=?, ()) => {
164
164
  // no trailing space between `[Plugin]` and `[Comp]`.
165
165
  //
166
166
  // Sink-aware: in a non-TTY/JSON sink it falls back to `fmtPlainPrefix` so no
167
- // ANSI colour or bold leaks into structured logs. This makes every caller that
167
+ // ANSI color or bold leaks into structured logs. This makes every caller that
168
168
  // pre-formats a prefix before reaching `Logger.emit` (EffectLogger.withComp,
169
169
  // the infra-layer `compLog`s) correct without per-call-site changes.
170
170
  let fmtComp = (~comp=?, ()) =>
@@ -0,0 +1,207 @@
1
+ // Move an application from the write-side logic module's former name, *behavior*,
2
+ // to *decision*: `X_Behavior.res` becomes `X_Decision.res`, and the spellings that
3
+ // name it in `.res` sources follow.
4
+ //
5
+ // The framework accepts none of the former spellings, so a consumer's bump is this
6
+ // command plus a build. Everything is planned before anything changes: a target
7
+ // that already exists stops the run with nothing touched.
8
+
9
+ let usage = `Usage: reventless-rename-to-decision [--dry-run] [<dir>]
10
+
11
+ <dir> where to start; the default is the current directory
12
+ --dry-run print what would change, change nothing
13
+
14
+ Renames every *_Behavior.res to *_Decision.res (git mv inside a git work tree,
15
+ with its compiled output beside it), and in every .res and .resi file:
16
+ @@reventless.behavior -> @@reventless.decision, Behavior_GWT -> Decision_GWT,
17
+ Reventless.Behavior -> Reventless.Decision, thenRefused -> thenForbidden, and
18
+ <Spec>_Behavior -> <Spec>_Decision. Skips node_modules and lib.`
19
+
20
+ type args = {dir: string, dryRun: bool}
21
+
22
+ let parseArgs = (argv: array<string>): result<args, string> =>
23
+ CliArgs.parse(~bools=["dry-run"], argv)
24
+ ->Result.flatMap(a => a->CliArgs.atMost(1))
25
+ ->Result.map(a => {
26
+ dir: a->CliArgs.positionals->Array.get(0)->Option.getOr("."),
27
+ dryRun: a->CliArgs.bool("dry-run"),
28
+ })
29
+
30
+ let cli: CliArgs.cli<args> = {bin: "reventless-rename-to-decision", usage, parse: parseArgs}
31
+
32
+ // ── The text ─────────────────────────────────────────────────────────────────
33
+
34
+ // In order: the qualified forms before the bare module names they contain.
35
+ let replacements: array<(RegExp.t, string)> = [
36
+ (/@@reventless\.behavior\b/g, "@@reventless.decision"),
37
+ (/\bBehavior_GWT\.BehaviorSpec\b/g, "Decision_GWT.DecisionSpec"),
38
+ (/\bBehavior_GWT\.Behavior\b/g, "Decision_GWT.Decision"),
39
+ (/\bBehavior_GWT\b/g, "Decision_GWT"),
40
+ (/\bReventless\.Behavior\b/g, "Reventless.Decision"),
41
+ (/\bStateChangeSlice\.Behavior\b/g, "StateChangeSlice.Decision"),
42
+ (/\bMapping_GWT\.FromBehavior\b/g, "Mapping_GWT.FromDecision"),
43
+ (/\bthenRefused\b/g, "thenForbidden"),
44
+ (/\b([A-Z][A-Za-z0-9]*)_Behavior\b/g, "$1_Decision"),
45
+ ]
46
+
47
+ // A trait's conformance binding names the decision module `Behavior`; where a
48
+ // file binds it, its uses in that file follow.
49
+ let localBinding = /^(\s*)module Behavior = /m
50
+
51
+ let rewrite = (source: string): string => {
52
+ let text =
53
+ replacements->Array.reduce(source, (text, (pattern, replacement)) =>
54
+ text->String.replaceRegExp(pattern, replacement)
55
+ )
56
+ if localBinding->RegExp.test(text) {
57
+ text
58
+ ->String.replaceRegExp(/^(\s*)module Behavior = /gm, "$1module Decision = ")
59
+ ->String.replaceRegExp(/\bBehavior\./g, "Decision.")
60
+ } else {
61
+ text
62
+ }
63
+ }
64
+
65
+ // ── The tree ─────────────────────────────────────────────────────────────────
66
+
67
+ type move = {from: string, to_: string}
68
+ type edit = {path: string, contents: string}
69
+ type plan = {moves: array<move>, edits: array<edit>}
70
+
71
+ let skippedDirs = ["node_modules", "lib", ".git"]
72
+
73
+ let isSource = (name: string) => name->String.endsWith(".res") || name->String.endsWith(".resi")
74
+
75
+ // A moved source takes its compiled output and interface along.
76
+ let companions = [".res", ".resi", ".res.mjs", ".res.js", ".res.cjs"]
77
+
78
+ let rec walk = (dir: string, files: array<string>) =>
79
+ NodeFs.readdirSync(dir, {withFileTypes: true})->Array.forEach(entry => {
80
+ let name = entry->NodeFs.direntName
81
+ let path = NodePath.join([dir, name])
82
+ if entry->NodeFs.isDirectory {
83
+ if !(skippedDirs->Array.includes(name)) {
84
+ walk(path, files)
85
+ }
86
+ } else if entry->NodeFs.isFile {
87
+ files->Array.push(path)
88
+ }
89
+ })
90
+
91
+ let byPath = (a: string, b: string) => String.compare(a, b)
92
+
93
+ /** What the run would do under `dir`, or why it cannot. */
94
+ let plan = (~dir: string): result<plan, string> => {
95
+ let files = []
96
+ walk(dir, files)
97
+ files->Array.sort(byPath)
98
+ let present = Set.fromArray(files)
99
+
100
+ let moves =
101
+ files
102
+ ->Array.filter(path => path->String.endsWith("_Behavior.res"))
103
+ ->Array.flatMap(source => {
104
+ let stem = source->String.slice(~start=0, ~end=String.length(source) - 13)
105
+ companions->Array.filterMap(suffix => {
106
+ let from = stem ++ "_Behavior" ++ suffix
107
+ present->Set.has(from) ? Some({from, to_: stem ++ "_Decision" ++ suffix}) : None
108
+ })
109
+ })
110
+
111
+ let edits = files->Array.filterMap(path =>
112
+ if path->NodePath.basename->isSource {
113
+ let contents = NodeFs.readFileSync(path)
114
+ let rewritten = rewrite(contents)
115
+ rewritten == contents ? None : Some({path, contents: rewritten})
116
+ } else {
117
+ None
118
+ }
119
+ )
120
+
121
+ switch moves->Array.find(({to_}) => present->Set.has(to_)) {
122
+ | Some({from, to_}) =>
123
+ Error(`${to_} already exists, so ${from} cannot take its name. Nothing was changed.`)
124
+ | None => Ok({moves, edits})
125
+ }
126
+ }
127
+
128
+ // ── Applying it ──────────────────────────────────────────────────────────────
129
+
130
+ let git = (~cwd: string, args: array<string>) =>
131
+ NodeChildProcess.spawnSync(
132
+ "git",
133
+ args,
134
+ {cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]},
135
+ )
136
+
137
+ let succeeded = (result: NodeChildProcess.spawnSyncResult) =>
138
+ result.status->Nullable.toOption == Some(0)
139
+
140
+ let isTracked = (path: string) =>
141
+ git(
142
+ ~cwd=NodePath.dirname(path),
143
+ ["ls-files", "--error-unmatch", "--", NodePath.basename(path)],
144
+ )->succeeded
145
+
146
+ let moveFile = ({from, to_}: move): result<unit, string> =>
147
+ if isTracked(from) {
148
+ let result = git(
149
+ ~cwd=NodePath.dirname(from),
150
+ ["mv", "--", NodePath.basename(from), NodePath.basename(to_)],
151
+ )
152
+ result->succeeded
153
+ ? Ok()
154
+ : Error(`git mv ${from}: ${result.stderr->Nullable.toOption->Option.getOr("")->String.trim}`)
155
+ } else {
156
+ try {
157
+ NodeFs.renameSync(from, to_)
158
+ Ok()
159
+ } catch {
160
+ | JsExn(e) => Error(`rename ${from}: ${e->JsExn.message->Option.getOr("failed")}`)
161
+ }
162
+ }
163
+
164
+ /** Edits are written at the old paths, then the files move, so a moved file
165
+ keeps both its history and its new contents. */
166
+ let apply = ({moves, edits}: plan): result<unit, string> => {
167
+ edits->Array.forEach(({path, contents}) => NodeFs.writeFileSync(path, contents))
168
+ moves->Array.reduce(Ok(), (done, move) => done->Result.flatMap(() => moveFile(move)))
169
+ }
170
+
171
+ let describe = ({moves, edits}: plan, ~dryRun: bool): array<string> => {
172
+ let verb = dryRun ? "would " : ""
173
+ let lines = [
174
+ ...moves->Array.map(({from, to_}) => `${verb}move ${from} -> ${to_}`),
175
+ ...edits->Array.map(({path}) => `${verb}edit ${path}`),
176
+ ]
177
+ let movedSources = moves->Array.filter(({from}) => from->String.endsWith(".res"))->Array.length
178
+ lines->Array.push(
179
+ `${dryRun ? "Would rename" : "Renamed"} ${movedSources->Int.toString} file(s) and ${dryRun
180
+ ? "edit"
181
+ : "edited"} ${edits->Array.length->Int.toString}.`,
182
+ )
183
+ lines
184
+ }
185
+
186
+ /** The whole run without a process: the lines it prints, or why it failed. */
187
+ let execute = ({dir, dryRun}: args): result<array<string>, string> =>
188
+ if !(dir->NodeFs.existsSync) {
189
+ Error(`${dir} does not exist.`)
190
+ } else {
191
+ plan(~dir)->Result.flatMap(plan =>
192
+ dryRun ? Ok(describe(plan, ~dryRun)) : apply(plan)->Result.map(() => describe(plan, ~dryRun))
193
+ )
194
+ }
195
+
196
+ let main = () =>
197
+ CliArgs.run(cli, async args =>
198
+ switch execute(args) {
199
+ | Ok(lines) => lines->Array.forEach(line => Console.log(line))
200
+ | Error(message) =>
201
+ Console.error(`reventless-rename-to-decision: ${message}`)
202
+ NodeProcess.exit(1)
203
+ }
204
+ )
205
+
206
+ // No top-level call: `../../run-rename-to-decision.mjs` invokes [main], so a test
207
+ // can import this module.
@@ -0,0 +1,334 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodepath from "node:path";
5
+ import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
6
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
8
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
10
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
11
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
12
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
13
+ import * as CliArgs$Reventless from "../CliArgs.res.mjs";
14
+ import * as Nodechild_process from "node:child_process";
15
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
16
+
17
+ let usage = `Usage: reventless-rename-to-decision [--dry-run] [<dir>]
18
+
19
+ <dir> where to start; the default is the current directory
20
+ --dry-run print what would change, change nothing
21
+
22
+ Renames every *_Behavior.res to *_Decision.res (git mv inside a git work tree,
23
+ with its compiled output beside it), and in every .res and .resi file:
24
+ @@reventless.behavior -> @@reventless.decision, Behavior_GWT -> Decision_GWT,
25
+ Reventless.Behavior -> Reventless.Decision, thenRefused -> thenForbidden, and
26
+ <Spec>_Behavior -> <Spec>_Decision. Skips node_modules and lib.`;
27
+
28
+ function parseArgs(argv) {
29
+ return Stdlib_Result.map(Stdlib_Result.flatMap(CliArgs$Reventless.parse(undefined, ["dry-run"], undefined, undefined, argv), a => CliArgs$Reventless.atMost(a, 1)), a => ({
30
+ dir: Stdlib_Option.getOr(CliArgs$Reventless.positionals(a)[0], "."),
31
+ dryRun: CliArgs$Reventless.bool(a, "dry-run")
32
+ }));
33
+ }
34
+
35
+ let cli = {
36
+ bin: "reventless-rename-to-decision",
37
+ usage: usage,
38
+ parse: parseArgs
39
+ };
40
+
41
+ let replacements = [
42
+ [
43
+ /@@reventless\.behavior\b/g,
44
+ "@@reventless.decision"
45
+ ],
46
+ [
47
+ /\bBehavior_GWT\.BehaviorSpec\b/g,
48
+ "Decision_GWT.DecisionSpec"
49
+ ],
50
+ [
51
+ /\bBehavior_GWT\.Behavior\b/g,
52
+ "Decision_GWT.Decision"
53
+ ],
54
+ [
55
+ /\bBehavior_GWT\b/g,
56
+ "Decision_GWT"
57
+ ],
58
+ [
59
+ /\bReventless\.Behavior\b/g,
60
+ "Reventless.Decision"
61
+ ],
62
+ [
63
+ /\bStateChangeSlice\.Behavior\b/g,
64
+ "StateChangeSlice.Decision"
65
+ ],
66
+ [
67
+ /\bMapping_GWT\.FromBehavior\b/g,
68
+ "Mapping_GWT.FromDecision"
69
+ ],
70
+ [
71
+ /\bthenRefused\b/g,
72
+ "thenForbidden"
73
+ ],
74
+ [
75
+ /\b([A-Z][A-Za-z0-9]*)_Behavior\b/g,
76
+ "$1_Decision"
77
+ ]
78
+ ];
79
+
80
+ let localBinding = /^(\s*)module Behavior = /m;
81
+
82
+ function rewrite(source) {
83
+ let text = Stdlib_Array.reduce(replacements, source, (text, param) => text.replace(param[0], param[1]));
84
+ if (localBinding.test(text)) {
85
+ return text.replace(/^(\s*)module Behavior = /gm, "$1module Decision = ").replace(/\bBehavior\./g, "Decision.");
86
+ } else {
87
+ return text;
88
+ }
89
+ }
90
+
91
+ let skippedDirs = [
92
+ "node_modules",
93
+ "lib",
94
+ ".git"
95
+ ];
96
+
97
+ function isSource(name) {
98
+ if (name.endsWith(".res")) {
99
+ return true;
100
+ } else {
101
+ return name.endsWith(".resi");
102
+ }
103
+ }
104
+
105
+ let companions = [
106
+ ".res",
107
+ ".resi",
108
+ ".res.mjs",
109
+ ".res.js",
110
+ ".res.cjs"
111
+ ];
112
+
113
+ function walk(dir, files) {
114
+ Nodefs.readdirSync(dir, {
115
+ withFileTypes: true
116
+ }).forEach(entry => {
117
+ let name = entry.name;
118
+ let path = Nodepath.join(dir, name);
119
+ if (entry.isDirectory()) {
120
+ if (!skippedDirs.includes(name)) {
121
+ return walk(path, files);
122
+ } else {
123
+ return;
124
+ }
125
+ } else if (entry.isFile()) {
126
+ files.push(path);
127
+ return;
128
+ } else {
129
+ return;
130
+ }
131
+ });
132
+ }
133
+
134
+ let byPath = Primitive_string.compare;
135
+
136
+ function plan(dir) {
137
+ let files = [];
138
+ walk(dir, files);
139
+ files.sort(byPath);
140
+ let present = new Set(files);
141
+ let moves = files.filter(path => path.endsWith("_Behavior.res")).flatMap(source => {
142
+ let stem = source.slice(0, source.length - 13 | 0);
143
+ return Stdlib_Array.filterMap(companions, suffix => {
144
+ let from = stem + "_Behavior" + suffix;
145
+ if (present.has(from)) {
146
+ return {
147
+ from: from,
148
+ to_: stem + "_Decision" + suffix
149
+ };
150
+ }
151
+ });
152
+ });
153
+ let edits = Stdlib_Array.filterMap(files, path => {
154
+ if (!isSource(Nodepath.basename(path))) {
155
+ return;
156
+ }
157
+ let contents = Nodefs.readFileSync(path, "utf8");
158
+ let rewritten = rewrite(contents);
159
+ if (rewritten === contents) {
160
+ return;
161
+ } else {
162
+ return {
163
+ path: path,
164
+ contents: rewritten
165
+ };
166
+ }
167
+ });
168
+ let match = moves.find(param => present.has(param.to_));
169
+ if (match !== undefined) {
170
+ return {
171
+ TAG: "Error",
172
+ _0: match.to_ + ` already exists, so ` + match.from + ` cannot take its name. Nothing was changed.`
173
+ };
174
+ } else {
175
+ return {
176
+ TAG: "Ok",
177
+ _0: {
178
+ moves: moves,
179
+ edits: edits
180
+ }
181
+ };
182
+ }
183
+ }
184
+
185
+ function git(cwd, args) {
186
+ return Nodechild_process.spawnSync("git", args, {
187
+ cwd: cwd,
188
+ encoding: "utf8",
189
+ stdio: [
190
+ "ignore",
191
+ "pipe",
192
+ "pipe"
193
+ ]
194
+ });
195
+ }
196
+
197
+ function succeeded(result) {
198
+ return Primitive_object.equal(Primitive_option.fromNullable(result.status), 0);
199
+ }
200
+
201
+ function isTracked(path) {
202
+ return succeeded(git(Nodepath.dirname(path), [
203
+ "ls-files",
204
+ "--error-unmatch",
205
+ "--",
206
+ Nodepath.basename(path)
207
+ ]));
208
+ }
209
+
210
+ function moveFile(param) {
211
+ let to_ = param.to_;
212
+ let from = param.from;
213
+ if (isTracked(from)) {
214
+ let result = git(Nodepath.dirname(from), [
215
+ "mv",
216
+ "--",
217
+ Nodepath.basename(from),
218
+ Nodepath.basename(to_)
219
+ ]);
220
+ if (succeeded(result)) {
221
+ return {
222
+ TAG: "Ok",
223
+ _0: undefined
224
+ };
225
+ } else {
226
+ return {
227
+ TAG: "Error",
228
+ _0: `git mv ` + from + `: ` + Stdlib_Option.getOr(Primitive_option.fromNullable(result.stderr), "").trim()
229
+ };
230
+ }
231
+ }
232
+ try {
233
+ Nodefs.renameSync(from, to_);
234
+ return {
235
+ TAG: "Ok",
236
+ _0: undefined
237
+ };
238
+ } catch (raw_e) {
239
+ let e = Primitive_exceptions.internalToException(raw_e);
240
+ if (e.RE_EXN_ID === "JsExn") {
241
+ return {
242
+ TAG: "Error",
243
+ _0: `rename ` + from + `: ` + Stdlib_Option.getOr(Stdlib_JsExn.message(e._1), "failed")
244
+ };
245
+ }
246
+ throw e;
247
+ }
248
+ }
249
+
250
+ function apply(param) {
251
+ param.edits.forEach(param => {
252
+ Nodefs.writeFileSync(param.path, param.contents, "utf8");
253
+ });
254
+ return Stdlib_Array.reduce(param.moves, {
255
+ TAG: "Ok",
256
+ _0: undefined
257
+ }, (done, move) => Stdlib_Result.flatMap(done, () => moveFile(move)));
258
+ }
259
+
260
+ function describe(param, dryRun) {
261
+ let edits = param.edits;
262
+ let moves = param.moves;
263
+ let verb = dryRun ? "would " : "";
264
+ let lines = Belt_Array.concatMany([
265
+ moves.map(param => verb + `move ` + param.from + ` -> ` + param.to_),
266
+ edits.map(param => verb + `edit ` + param.path)
267
+ ]);
268
+ let movedSources = moves.filter(param => param.from.endsWith(".res")).length;
269
+ lines.push((
270
+ dryRun ? "Would rename" : "Renamed"
271
+ ) + ` ` + movedSources.toString() + ` file(s) and ` + (
272
+ dryRun ? "edit" : "edited"
273
+ ) + ` ` + edits.length.toString() + `.`);
274
+ return lines;
275
+ }
276
+
277
+ function execute(param) {
278
+ let dir = param.dir;
279
+ if (!Nodefs.existsSync(dir)) {
280
+ return {
281
+ TAG: "Error",
282
+ _0: dir + ` does not exist.`
283
+ };
284
+ }
285
+ let dryRun = param.dryRun;
286
+ return Stdlib_Result.flatMap(plan(dir), plan => {
287
+ if (dryRun) {
288
+ return {
289
+ TAG: "Ok",
290
+ _0: describe(plan, dryRun)
291
+ };
292
+ } else {
293
+ return Stdlib_Result.map(apply(plan), () => describe(plan, dryRun));
294
+ }
295
+ });
296
+ }
297
+
298
+ function main() {
299
+ return CliArgs$Reventless.run(cli, undefined, undefined, async args => {
300
+ let lines = execute(args);
301
+ if (lines.TAG === "Ok") {
302
+ lines._0.forEach(line => {
303
+ console.log(line);
304
+ });
305
+ return;
306
+ }
307
+ console.error(`reventless-rename-to-decision: ` + lines._0);
308
+ process.exit(1);
309
+ });
310
+ }
311
+
312
+ export {
313
+ usage,
314
+ parseArgs,
315
+ cli,
316
+ replacements,
317
+ localBinding,
318
+ rewrite,
319
+ skippedDirs,
320
+ isSource,
321
+ companions,
322
+ walk,
323
+ byPath,
324
+ plan,
325
+ git,
326
+ succeeded,
327
+ isTracked,
328
+ moveFile,
329
+ apply,
330
+ describe,
331
+ execute,
332
+ main,
333
+ }
334
+ /* node:fs Not a pure module */