Skip to content

Commit edaa2da

Browse files
Copilotpmcelhaney
andauthored
Switch to named function exports; last path segment is function name, rest is file path
Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/0a6bcdff-fa2e-41bf-a254-313af4803268 Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com>
1 parent 207669e commit edaa2da

File tree

3 files changed

+47
-45
lines changed

3 files changed

+47
-45
lines changed

.github/issue-proposals/apply-approach-1-function-injection.md

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,29 @@ milestone:
1111

1212
## Summary
1313

14-
Implement `.apply` as a plain TypeScript module that exports a single default function. When the command is run, Counterfact dynamically imports the file, calls the function, and passes the live REPL environment as arguments.
14+
Implement `.apply` as a plain TypeScript module that exports one or more named functions. When the command is run, Counterfact dynamically imports the file, looks up the named export matching the last path segment, calls that function, and passes the live REPL environment as its argument.
1515

16-
This is the simplest possible design: no new abstractions, no DSL, and no framework. A script is just a function.
16+
This is the simplest possible design: no new abstractions, no DSL, and no framework. A script is just a named function.
1717

1818
---
1919

2020
## Design
2121

2222
### Script format
2323

24-
An apply script is a TypeScript file with a default export:
24+
An apply script is a TypeScript file with one or more named function exports:
2525

2626
```ts
2727
// repl/sold-pets.ts
2828
import type { ApplyContext } from "counterfact";
2929

30-
export default ($: ApplyContext) => {
30+
export function soldPets($: ApplyContext) {
3131
$.context.petService.reset();
3232
$.context.petService.addPet({ id: 1, status: "sold" });
3333
$.context.petService.addPet({ id: 2, status: "available" });
3434

3535
$.routes.getSoldPets = $.route("/pet/findByStatus").method("get").query({ status: "sold" });
36-
};
36+
}
3737
```
3838

3939
### The `ApplyContext` type
@@ -53,23 +53,20 @@ export interface ApplyContext {
5353

5454
### Invocation
5555

56+
The argument to `.apply` is a slash-separated path. The last segment is the **function name** to call; everything before it is the **file path** (resolved relative to `<basePath>/repl/`, with `index.ts` as the default file):
57+
5658
```
57-
.apply sold-pets
58-
.apply path/to/sold-pets.ts
59+
.apply foo # repl/index.ts → foo($)
60+
.apply foo/bar # repl/foo.ts → bar($)
61+
.apply foo/bar/baz # repl/foo/bar.ts → baz($)
5962
```
6063

61-
**Resolution order for named scenarios:**
62-
63-
1. `<basePath>/repl/<name>.ts`
64-
2. `<basePath>/repl/<name>/index.ts`
65-
3. `<basePath>/<name>.ts` (direct path)
66-
6764
### Feedback output
6865

6966
After execution, Counterfact compares the environment state before and after the script runs and prints a diff summary:
7067

7168
```
72-
Applied sold-pets
69+
Applied sold-pets/soldPets
7370
7471
Routes added:
7572
getSoldPets
@@ -82,9 +79,9 @@ Context diffs are not automatically tracked in this approach — the script auth
8279
## Implementation sketch
8380

8481
1. Add `.apply` as a dot-command in `src/repl/repl.ts`.
85-
2. Resolve the file path using the ordered lookup above.
82+
2. Split the argument on `/`: the last segment is the function name; the rest form the file path.
8683
3. Dynamically import the resolved module (using `tsx` or the existing transpiler if the file is TypeScript).
87-
4. Call the exported function with the live environment objects.
84+
4. Look up the named export matching the function name and call it with the live environment objects.
8885
5. Snapshot `routes` before/after and print the diff.
8986

9087
---
@@ -103,9 +100,10 @@ Context diffs are not automatically tracked in this approach — the script auth
103100

104101
## Acceptance criteria
105102

106-
- [ ] `.apply <name>` resolves and executes a TypeScript file from the configured `repl/` directory
107-
- [ ] `.apply <path>` resolves and executes a TypeScript file at the given relative path
108-
- [ ] The script receives `$` with `{ context, loadContext, routes, route }` as properties
103+
- [ ] `.apply foo` resolves `repl/index.ts` and calls the exported `foo` function
104+
- [ ] `.apply foo/bar` resolves `repl/foo.ts` and calls the exported `bar` function
105+
- [ ] `.apply foo/bar/baz` resolves `repl/foo/bar.ts` and calls the exported `baz` function
106+
- [ ] The function receives `$` with `{ context, loadContext, routes, route }` as properties
109107
- [ ] Routes injected by the script are available in the REPL after the command runs
110108
- [ ] The REPL prints a summary of routes added and removed after each apply
111109
- [ ] A meaningful error is shown when the file cannot be found or the export is not a function

.github/issue-proposals/apply-approach-2-scenario-class-lifecycle.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@ milestone:
1111

1212
## Summary
1313

14-
Implement `.apply` using a `Scenario` class interface with explicit `setup()` and `teardown()` lifecycle hooks, plus optional `dependencies` declaration. This approach adds structure for scenarios that need cleanup and enables dependency-ordered composition.
14+
Implement `.apply` using a `Scenario` class interface with explicit `setup()` and `teardown()` lifecycle hooks, plus optional `dependencies` declaration. Classes are exported by name — the last segment of the `.apply` path selects the named export, and Counterfact instantiates it automatically. This approach adds structure for scenarios that need cleanup and enables dependency-ordered composition.
1515

1616
---
1717

1818
## Design
1919

2020
### Script format
2121

22-
A scenario is a class that implements the `Scenario` interface:
22+
A scenario is a named class export that implements the `Scenario` interface:
2323

2424
```ts
2525
// repl/sold-pets.ts
2626
import type { Scenario, ApplyContext } from "counterfact";
2727

28-
export default class SoldPetsScenario implements Scenario {
28+
export class soldPets implements Scenario {
2929
static dependencies = ["base"];
3030

3131
setup($: ApplyContext): void {
@@ -62,20 +62,23 @@ export interface Scenario {
6262
6363
### Invocation
6464

65+
The same path/name convention as Approach 1 applies. The last segment selects the named export; everything before it is the file path:
66+
6567
```
66-
.apply sold-pets # apply a named scenario
67-
.unapply sold-pets # tear down if teardown() is defined
68-
.apply base sold-pets # apply multiple scenarios in order
68+
.apply foo # repl/index.ts → new foo().setup($)
69+
.apply foo/bar # repl/foo.ts → new bar().setup($)
70+
.unapply foo/bar # repl/foo.ts → new bar().teardown($)
71+
.apply base soldPets # apply multiple scenarios in order
6972
```
7073

7174
### Dependency resolution
7275

7376
When a scenario declares `static dependencies`, Counterfact automatically applies each dependency in order before applying the requested scenario. If a dependency is already applied (tracked by name), it is skipped.
7477

7578
```
76-
⬣> .apply sold-pets
77-
# Counterfact first applies "base" (dependency), then "sold-pets"
78-
Applied base → sold-pets
79+
⬣> .apply sold-pets/soldPets
80+
# Counterfact first applies "base" (dependency), then "soldPets"
81+
Applied base → soldPets
7982
8083
Routes added:
8184
getSoldPets
@@ -122,10 +125,11 @@ Applied scenarios:
122125

123126
## Acceptance criteria
124127

125-
- [ ] `.apply <name>` resolves, instantiates, and calls `setup()` on the scenario class
128+
- [ ] `.apply foo/bar` resolves `repl/foo.ts`, instantiates `bar`, and calls `setup($)`
129+
- [ ] `.apply foo` resolves `repl/index.ts`, instantiates `foo`, and calls `setup($)`
126130
- [ ] Static `dependencies` are applied automatically before the target scenario
127131
- [ ] A dependency that is already applied is not re-applied
128-
- [ ] `.unapply <name>` calls `teardown()` on the applied scenario instance
132+
- [ ] `.unapply foo/bar` calls `teardown($)` on the stored `bar` instance
129133
- [ ] The REPL tracks and displays the stack of currently applied scenarios
130134
- [ ] Scenarios that do not declare `teardown()` are still valid and usable
131135
- [ ] A meaningful error is shown when a dependency cycle is detected

.github/issue-proposals/apply-approach-3-proxy-based-tracking.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,18 @@ This approach maximises ergonomics for script authors: a script looks like ordin
2121

2222
### Script format
2323

24-
A script is a plain TypeScript module with a default export function. The script receives the live environment objects and mutates them directly, with no special wrapper required:
24+
A script is a plain TypeScript module that exports one or more named functions. The script receives the live environment objects and mutates them directly, with no special wrapper required:
2525

2626
```ts
2727
// repl/sold-pets.ts
28-
export default ($) => {
28+
export function soldPets($) {
2929
$.context.petService.reset();
3030

3131
$.context.petService.addPet({ id: 1, status: "sold" });
3232
$.context.petService.addPet({ id: 2, status: "available" });
3333

3434
$.routes.getSoldPets = $.route("/pet/findByStatus").method("get").query({ status: "sold" });
35-
};
35+
}
3636
```
3737

3838
This is identical in surface syntax to Approach 1. The difference is internal: `$.context` and `$.routes` passed to the script are **transparent reactive proxies** of the live objects.
@@ -44,7 +44,8 @@ Before calling the script, Counterfact wraps each environment object in a `Proxy
4444
```ts
4545
const tracked = trackChanges(liveContext);
4646
const trackedRoutes = trackChanges(liveRoutes);
47-
await script({ context: tracked.proxy, routes: trackedRoutes.proxy, route: createRouteFunction(...) });
47+
const fn = module[functionName]; // named export resolved from path
48+
await fn({ context: tracked.proxy, routes: trackedRoutes.proxy, route: createRouteFunction(...) });
4849
const report = { context: tracked.changes(), routes: trackedRoutes.changes() };
4950
```
5051

@@ -64,7 +65,7 @@ interface ChangeRecord {
6465
After the script runs, Counterfact automatically prints a diff derived from the recorded changes:
6566

6667
```
67-
Applied sold-pets
68+
Applied sold-pets/soldPets
6869
6970
Context changes:
7071
petService.pets: [] → [{id:1,status:"sold"},{id:2,status:"available"}]
@@ -77,25 +78,24 @@ No manual annotation is required from the script author.
7778

7879
### Invocation
7980

81+
The same path/name convention as Approach 1 applies:
82+
8083
```
81-
.apply sold-pets
82-
.apply path/to/sold-pets.ts
84+
.apply foo # repl/index.ts → foo($) with proxy-wrapped $
85+
.apply foo/bar # repl/foo.ts → bar($) with proxy-wrapped $
86+
.apply foo/bar/baz # repl/foo/bar.ts → baz($) with proxy-wrapped $
8387
```
8488

85-
Resolution order is the same as Approach 1:
86-
87-
1. `<basePath>/repl/<name>.ts`
88-
2. `<basePath>/repl/<name>/index.ts`
89-
3. `<basePath>/<name>.ts`
89+
Resolution is the same as Approach 1; the difference is purely in what is passed to the function.
9090

9191
---
9292

9393
## Implementation sketch
9494

9595
1. Add `.apply` as a dot-command in `src/repl/repl.ts`.
96-
2. Resolve the file path.
96+
2. Split the argument on `/`: the last segment is the function name; the rest form the file path.
9797
3. Create proxy wrappers around `context` and `routes`.
98-
4. Dynamically import and call the script with the proxy-wrapped arguments.
98+
4. Dynamically import the module, look up the named export, and call it with the proxy-wrapped arguments.
9999
5. Collect the accumulated change records from the proxies.
100100
6. Print the formatted diff.
101101

@@ -139,7 +139,7 @@ Deep (nested) change tracking can be implemented by returning a recursive proxy
139139

140140
## Acceptance criteria
141141

142-
- [ ] `.apply <name>` resolves and executes a TypeScript file, passing proxy-wrapped environment objects
142+
- [ ] `.apply foo/bar` resolves `repl/foo.ts`, calls `bar($)` with proxy-wrapped environment objects
143143
- [ ] All top-level property assignments to `context` are captured in the change log
144144
- [ ] Nested property mutations (e.g. `context.petService.reset()`) are captured where feasible
145145
- [ ] `$.routes.name = builder` assignments and `delete $.routes.name` are captured and reported

0 commit comments

Comments
 (0)