create-minlang-app 0.3.0 → 0.4.1

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/bin.mjs CHANGED
@@ -65,7 +65,7 @@ Next steps:
65
65
  pnpm --dir app install
66
66
  make dev # http://localhost:3111
67
67
 
68
- Your whole app lives in ${name}.ml (it starts as a task tracker — replace it).
68
+ Your whole app lives in ${name}.ml (it starts as a MinLang welcome tour — replace it).
69
69
  Agent instructions: ${name}/AGENTS.md
70
70
  Language rules: https://github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md
71
71
  Deploys: push to GitHub with org secrets VERCEL_TOKEN/VERCEL_ORG_ID set;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-minlang-app",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Scaffold a MinLang web app: write one .ml file, compile with ml1, deploy.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,6 +12,17 @@ To change behavior, edit `__APP_NAME__.ml`, then run `make compile`.
12
12
  Never edit files under `app/generated/` — CI recompiles with `--check`
13
13
  and fails the build if they drift from the `.ml` source.
14
14
 
15
+ For UI changes, sketch the intended screens in `ui/design/screens.mlui` first.
16
+ Then translate that sketch into `screen` blocks in `__APP_NAME__.ml`, compile,
17
+ and compare the generated `app/generated/ui/wire/*.mlui` projections. The
18
+ `ui/design` sketch is hand-authored; generated wireframes are not.
19
+
20
+ When product behavior cannot yet fit the language (remote services, uploads,
21
+ rich local UI, AI structured outputs, full app shell/test generation), read
22
+ `MINLANG_LANGUAGE_SUPPORT.md`. `make compile` refreshes
23
+ `app/generated/MINLANG_LANGUAGE_SUPPORT.md`; agents should write any audit
24
+ results to `MINLANG_LANGUAGE_SUPPORT_RESULTS.md`, not into generated output.
25
+
15
26
  ## Language authority (fetch before writing MinLang)
16
27
 
17
28
  Download the single-file language bundle and read it before producing or
@@ -27,6 +38,17 @@ detector list, and the end-to-end web-app authoring guide. Highlights:
27
38
  - Generate -> validate -> fix -> revalidate until zero violations, then
28
39
  output. `ml1 validate` is the same gate CI runs.
29
40
 
41
+ ## Language version & updates
42
+
43
+ This repo is pinned to a MinLang language version in `minlang.json`
44
+ (`languageBundleVersion`, written by `ml1 init`). Run `make update` (or
45
+ `ml1 update`) to pull a newer compiler + language. When the language has
46
+ moved on, update writes `MINLANG_MIGRATION.md` — a checklist of
47
+ old-form -> new-form recommendations. Read it, apply the changes to
48
+ `__APP_NAME__.ml`, run `make compile && make test` until green, then delete
49
+ it. `ml1 update` advances the pin for additive upgrades and never edits your
50
+ `.ml` itself.
51
+
30
52
  ## Commands
31
53
 
32
54
  | Task | Command |
@@ -34,11 +56,12 @@ detector list, and the end-to-end web-app authoring guide. Highlights:
34
56
  | One-time setup | `corepack enable && corepack prepare pnpm@10.33.0 --activate` |
35
57
  | Validate the program | `make validate` |
36
58
  | Compile to `app/generated/` | `make compile` |
59
+ | Refresh language-support prompt without compiling | `ml1 support __APP_NAME__.ml` |
37
60
  | Install shell deps | `pnpm --dir app install` |
38
61
  | Unit + generated test triads | `make test` |
39
62
  | Lint custom skins (`app/skins/`) | `make lint-skins` (also runs in `make test`) |
40
63
  | Dev server (http://localhost:3111) | `make dev` |
41
- | Update toolchain + deps | `make update` |
64
+ | Update toolchain, language pin + deps | `make update` (writes MINLANG_MIGRATION.md if the language moved on) |
42
65
  | Production build | `make build` |
43
66
  | Browser tests (a11y, perf) | `make test-e2e` (installs the browser on first run) |
44
67
  | Screen previews (`screen-previews/*.png`, all screens, mobile + desktop) | `make preview` |
@@ -0,0 +1,74 @@
1
+ # MinLang language support brief for `__APP_NAME__`
2
+
3
+ This file is for coding agents and maintainers. It is not app code, and it is
4
+ not proof that the capabilities below exist in the current language. It explains
5
+ what MinLang must gain before this project can delete custom web shell, API,
6
+ upload, provider, and test wiring.
7
+
8
+ ## Current status
9
+
10
+ - Current web support covers persisted entities, constraints, pure actions,
11
+ queries, declarative screens, static themes/assets, generated reducers,
12
+ server action boundaries, screen schemas, wireframes, and generated unit tests
13
+ from MinLang test triads.
14
+ - Remote provider calls, uploads, local client state, rich forms, structured AI
15
+ outputs, and full app-shell/test generation need a future bundle and compiler
16
+ work before app authors can rely on them.
17
+
18
+ ## Roadmap checklist
19
+
20
+ ### Declarative secrets, config, and services
21
+
22
+ MinLang needs first-class declarations for provider secrets, environment-backed
23
+ config, base URLs, auth headers, endpoint schemas, generated `.env.example`,
24
+ and safe missing-secret errors.
25
+
26
+ ### Server-only remote effects and action pipelines
27
+
28
+ Provider calls, retries, downloads, fallback chains, and persistence-after-effect
29
+ need an explicit server-only boundary before they can be generated safely.
30
+
31
+ ### File inputs, uploads, and generated image assets
32
+
33
+ Browser file inputs, multipart transport, server-side blobs, previews, MIME
34
+ allowlists, size limits, URL fields, hex colors, and image transparency metadata
35
+ need typed language support.
36
+
37
+ ### AI prompt templates and structured outputs
38
+
39
+ Prompt construction, vision inputs, provider model selection, JSON schemas,
40
+ normalization, fallback chains, and traceable prompt persistence need language
41
+ and validator ownership.
42
+
43
+ ### Rich screens, local state, forms, and events
44
+
45
+ Full product UI needs local screen state, form controls, event handlers,
46
+ conditional result panels, root-route bindings, and richer accessibility
47
+ metadata.
48
+
49
+ ### Structured theme, layout, and component variants
50
+
51
+ Product-grade web styling needs validated stack/grid/card/panel/result-grid
52
+ primitives and tokenized variants for buttons, tabs, fields, previews, alerts,
53
+ and swatches.
54
+
55
+ ### Complete app shell and generated test suites
56
+
57
+ The web target should eventually generate package/config/env examples, seed
58
+ adapter binding, unit and pipeline tests, UI flows, accessibility, keyboard,
59
+ performance, and E2E checks from MinLang declarations.
60
+
61
+ ## Agent prompt
62
+
63
+ Read this file together with the current MinLang bundle and the app source.
64
+ Then write findings to `MINLANG_LANGUAGE_SUPPORT_RESULTS.md` with:
65
+
66
+ 1. The product behavior still implemented outside MinLang.
67
+ 2. The language/runtime/codegen feature that would absorb each behavior.
68
+ 3. The validation rule or detector needed to keep it deterministic and safe.
69
+ 4. The generated files that should disappear once the feature exists.
70
+ 5. The tests required before deleting handwritten code.
71
+
72
+ Do not edit a published language bundle in place. New syntax or enforcement
73
+ rules require a new full versioned bundle, a migration ledger entry, parser and
74
+ validator support, codegen support, docs, and deterministic tests.
@@ -3,6 +3,12 @@
3
3
  A MinLang web app. The whole application lives in `__APP_NAME__.ml`;
4
4
  `app/generated/` is compiler output (committed, never edited); `app/` holds
5
5
  the thin Next.js shell consuming the published `@minlang/*` runtime.
6
+ Sketch screen changes first in `ui/design/screens.mlui`, then encode them in
7
+ the `.ml` `screen` blocks and compare against `app/generated/ui/wire/*.mlui`
8
+ after compile.
9
+ Read `MINLANG_LANGUAGE_SUPPORT.md` when the app needs behavior the current
10
+ bundle cannot express yet; generated compiles also refresh a copy under
11
+ `app/generated/`.
6
12
 
7
13
  ```bash
8
14
  corepack enable && corepack prepare pnpm@10.33.0 --activate
@@ -1,240 +1,174 @@
1
- entity Workspace {
1
+ // Welcome to MinLang — the starter app `ml1 init` / `npm create minlang-app`
2
+ // scaffolds. The WHOLE app is this one file: entities (data shape),
3
+ // constraints (the only place rules live), actions (pure create/set/delete),
4
+ // queries (read-only view-models), screens (declarative UI + copy), and
5
+ // tests (the success / failure / no-change triad). Edit this file, run
6
+ // `make compile`, and the generated app updates. Delete the demo below and
7
+ // model your own domain when you're ready.
8
+
9
+ // --- Navigation state -------------------------------------------------------
10
+ // A single Tour row drives which screen is active (see the `when` selectors).
11
+ entity Tour {
2
12
  id: string req
3
- view: enum(board, projects)
13
+ page: enum(welcome, files, enhance, resources)
4
14
  }
5
15
 
6
- entity Project {
16
+ // --- A tiny live demo: a guestbook note -------------------------------------
17
+ // Shows the core loop — a form creates a row, a constraint guards it, a board
18
+ // lists it, and tests prove all three.
19
+ entity Note {
7
20
  id: string req
8
- name: string
21
+ greeting: string
9
22
  }
10
23
 
11
- entity Task {
24
+ // --- Seeded content shown as tables (rows live in app/seed.ts) --------------
25
+ entity FileGuide {
12
26
  id: string req
13
- project: ref(Project)
14
- title: string
15
- status: enum(todo, doing, done)
16
- assignee: string
17
- created_at: string
18
- actor_id: string
27
+ file: string
28
+ purpose: string
19
29
  }
20
30
 
21
- constraint TitleRequired {
22
- on: Task
23
- validate: NOT(self.title == '')
24
- message: 'title is required'
25
- }
26
-
27
- constraint AssigneeRequiredWhenDoing {
28
- on: Task
29
- validate: NOT(self.status == 'doing' AND self.assignee == '')
30
- message: 'assignee is required when a task is doing'
31
- }
32
-
33
- constraint ProjectExists {
34
- on: Task
35
- validate: count(Project as p, p == self.project) == 1
36
- message: 'project does not exist'
37
- }
38
-
39
- constraint ProjectNameRequired {
40
- on: Project
41
- validate: NOT(self.name == '')
42
- message: 'project name is required'
43
- }
44
-
45
- constraint UniqueProjectName {
46
- on: Project
47
- validate: count(Project as p, p.name == name) == 0
48
- message: 'a project with this name already exists'
49
- }
50
-
51
- action CreateProject(id: string, name: string, actor_id: string) {
52
- on: Project
53
- create(Project, { id: id, name: name })
54
- }
55
-
56
- action CreateTask(id: string, project: ref(Project), title: string, status: enum(todo, doing, done), assignee: string, created_at: string, actor_id: string) {
57
- on: Task
58
- create(Task, { id: id, project: project, title: title, status: status, assignee: assignee, created_at: created_at, actor_id: actor_id })
31
+ entity BuildStep {
32
+ id: string req
33
+ step: string
34
+ detail: string
59
35
  }
60
36
 
61
- action MoveTask(id: string, status: enum(todo, doing, done), actor_id: string) {
62
- on: Task
63
- set(status, status)
37
+ entity Resource {
38
+ id: string req
39
+ topic: string
40
+ link: string
64
41
  }
65
42
 
66
- action RenameTask(id: string, title: string, actor_id: string) {
67
- on: Task
68
- set(title, title)
43
+ // --- Rules live in constraints, never in actions ----------------------------
44
+ constraint GreetingRequired {
45
+ on: Note
46
+ validate: NOT(self.greeting == '')
47
+ message: 'a greeting is required'
69
48
  }
70
49
 
71
- action OpenProjects(workspace: ref(Workspace), actor_id: string) {
72
- on: Workspace
73
- set(view, 'projects')
50
+ // --- Actions are pure mutation (create / set / delete) ----------------------
51
+ action AddNote(id: string, greeting: string, actor_id: string) {
52
+ on: Note
53
+ create(Note, { id: id, greeting: greeting })
74
54
  }
75
55
 
76
- action OpenBoard(workspace: ref(Workspace), actor_id: string) {
77
- on: Workspace
78
- set(view, 'board')
56
+ action ShowWelcome(tour: ref(Tour), actor_id: string) {
57
+ on: Tour
58
+ set(page, 'welcome')
79
59
  }
80
60
 
81
- query TaskBoard(workspace: ref(Workspace)) -> list<Task> {
82
- from Task
61
+ action ShowFiles(tour: ref(Tour), actor_id: string) {
62
+ on: Tour
63
+ set(page, 'files')
83
64
  }
84
65
 
85
- query ProjectList(workspace: ref(Workspace)) -> list<Project> {
86
- from Project
66
+ action ShowEnhance(tour: ref(Tour), actor_id: string) {
67
+ on: Tour
68
+ set(page, 'enhance')
87
69
  }
88
70
 
89
- query TaskDetail(task: ref(Task)) -> Task {
90
- from Task
71
+ action ShowResources(tour: ref(Tour), actor_id: string) {
72
+ on: Tour
73
+ set(page, 'resources')
91
74
  }
92
75
 
93
- screen Board {
94
- when workspace.view == 'board'
95
- title "Task board"
96
- hint "Create tasks, move them between statuses, and rename them."
97
- board TaskBoard
98
- primary { label "Add task" action CreateTask }
99
- button { label "Move task" action MoveTask }
100
- button { label "Rename task" action RenameTask }
101
- button { label "Projects" action OpenProjects }
76
+ // --- Queries are read-only view-models --------------------------------------
77
+ query NoteList(tour: ref(Tour)) -> list<Note> {
78
+ from Note
102
79
  }
103
80
 
104
- screen Projects {
105
- when workspace.view == 'projects'
106
- title "Projects"
107
- body "Every task belongs to a project."
108
- board ProjectList
109
- primary { label "Add project" action CreateProject }
110
- button { label "Back to board" action OpenBoard }
81
+ query FileList(tour: ref(Tour)) -> list<FileGuide> {
82
+ from FileGuide
111
83
  }
112
84
 
113
- test CreateTaskSucceeds {
114
- setup: [
115
- create(Workspace, { id: "w1", view: "board" }),
116
- create(Project, { id: "p1", name: "General" })
117
- ]
118
- when: CreateTask on Task { id: "t1", project: "p1", title: "Ship the compiler", status: "todo", assignee: "" }
119
- then: [
120
- entity_count(Task) == 1,
121
- entity_has(Task.1, title == "Ship the compiler")
122
- ]
123
- }
124
-
125
- test TitleRequiredRejectsEmpty {
126
- setup: [
127
- create(Workspace, { id: "w1", view: "board" }),
128
- create(Project, { id: "p1", name: "General" })
129
- ]
130
- when: CreateTask on Task { id: "t1", project: "p1", title: "", status: "todo", assignee: "" }
131
- then: [
132
- error_raised('title is required'),
133
- entity_count(Task) == 0
134
- ]
85
+ query StepList(tour: ref(Tour)) -> list<BuildStep> {
86
+ from BuildStep
135
87
  }
136
88
 
137
- test AssigneeRequiredWhenDoingRejects {
138
- setup: [
139
- create(Workspace, { id: "w1", view: "board" }),
140
- create(Project, { id: "p1", name: "General" })
141
- ]
142
- when: CreateTask on Task { id: "t1", project: "p1", title: "Ship the compiler", status: "doing", assignee: "" }
143
- then: [
144
- error_raised('assignee is required when a task is doing'),
145
- entity_count(Task) == 0
146
- ]
89
+ query ResourceList(tour: ref(Tour)) -> list<Resource> {
90
+ from Resource
147
91
  }
148
92
 
149
- test ProjectExistsRejectsUnknown {
150
- setup: [
151
- create(Workspace, { id: "w1", view: "board" })
152
- ]
153
- when: CreateTask on Task { id: "t1", project: "ghost", title: "Ship the compiler", status: "todo", assignee: "" }
154
- then: [
155
- error_raised('project does not exist'),
156
- entity_count(Task) == 0
157
- ]
93
+ // --- Screens: declarative flow + copy (all text lives here) -----------------
94
+ screen Welcome {
95
+ when tour.page == 'welcome'
96
+ title "Welcome to MinLang"
97
+ body "The whole app is one .ml file. Edit it, run make compile, and this UI updates."
98
+ hint "Try the demo: add a note below, then take the tour."
99
+ board NoteList
100
+ primary { label "Say hello" action AddNote }
101
+ button { label "What's in this project" action ShowFiles }
102
+ button { label "Learn more" action ShowResources }
158
103
  }
159
104
 
160
- test MoveTaskSucceeds {
161
- setup: [
162
- create(Workspace, { id: "w1", view: "board" }),
163
- create(Project, { id: "p1", name: "General" }),
164
- create(Task, { id: "t1", project: "p1", title: "Ship the compiler", status: "todo", assignee: "casey", created_at: "2026-01-01T00:00:00Z", actor_id: "a1" })
165
- ]
166
- when: MoveTask on Task { id: "t1", status: "doing" }
167
- then: [
168
- entity_has(Task.1, status == "doing"),
169
- entity_count(Task) == 1
170
- ]
105
+ screen Files {
106
+ when tour.page == 'files'
107
+ title "What's in this project"
108
+ body "Everything is generated from your .ml source. Edit it, never app/generated."
109
+ board FileList
110
+ primary { label "How to enhance it" action ShowEnhance }
111
+ button { label "Back to start" action ShowWelcome }
171
112
  }
172
113
 
173
- test MoveTaskRejectsDoingWithoutAssignee {
174
- setup: [
175
- create(Workspace, { id: "w1", view: "board" }),
176
- create(Project, { id: "p1", name: "General" }),
177
- create(Task, { id: "t1", project: "p1", title: "Ship the compiler", status: "todo", assignee: "", created_at: "2026-01-01T00:00:00Z", actor_id: "a1" })
178
- ]
179
- when: MoveTask on Task { id: "t1", status: "doing" }
180
- then: [
181
- error_raised('assignee is required when a task is doing'),
182
- entity_has(Task.1, status == "todo")
183
- ]
114
+ screen Enhance {
115
+ when tour.page == 'enhance'
116
+ title "Make it yours"
117
+ body "Describe your own domain in the .ml, then compile and test."
118
+ board StepList
119
+ primary { label "Resources" action ShowResources }
120
+ button { label "Back to start" action ShowWelcome }
184
121
  }
185
122
 
186
- test RenameTaskSucceeds {
187
- setup: [
188
- create(Workspace, { id: "w1", view: "board" }),
189
- create(Project, { id: "p1", name: "General" }),
190
- create(Task, { id: "t1", project: "p1", title: "Old name", status: "todo", assignee: "", created_at: "2026-01-01T00:00:00Z", actor_id: "a1" })
191
- ]
192
- when: RenameTask on Task { id: "t1", title: "New name" }
193
- then: [
194
- entity_has(Task.1, title == "New name")
195
- ]
123
+ screen Resources {
124
+ when tour.page == 'resources'
125
+ title "Learn more"
126
+ body "The language bundle has the full rules and the web authoring guide."
127
+ hint "Fetch it: github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md"
128
+ board ResourceList
129
+ primary { label "Back to start" action ShowWelcome }
130
+ button { label "What's in this project" action ShowFiles }
196
131
  }
197
132
 
198
- test CreateProjectSucceeds {
133
+ // --- Tests: the success / failure / no-change triad -------------------------
134
+ test AddNoteSucceeds {
199
135
  setup: [
200
- create(Workspace, { id: "w1", view: "board" })
136
+ create(Tour, { id: "t1", page: "welcome" })
201
137
  ]
202
- when: CreateProject on Project { id: "p1", name: "Platform" }
138
+ when: AddNote on Note { id: "n1", greeting: "Hello, MinLang" }
203
139
  then: [
204
- entity_count(Project) == 1,
205
- entity_has(Project.1, name == "Platform")
140
+ entity_count(Note) == 1,
141
+ entity_has(Note.1, greeting == "Hello, MinLang")
206
142
  ]
207
143
  }
208
144
 
209
- test ProjectNameRequiredRejectsEmpty {
145
+ test GreetingRequiredRejectsEmpty {
210
146
  setup: [
211
- create(Workspace, { id: "w1", view: "board" })
147
+ create(Tour, { id: "t1", page: "welcome" })
212
148
  ]
213
- when: CreateProject on Project { id: "p1", name: "" }
149
+ when: AddNote on Note { id: "n1", greeting: "" }
214
150
  then: [
215
- error_raised('project name is required'),
216
- entity_count(Project) == 0
151
+ error_raised('a greeting is required'),
152
+ entity_count(Note) == 0
217
153
  ]
218
154
  }
219
155
 
220
- test UniqueProjectNameRejectsDuplicate {
156
+ test ShowFilesSwitchesPage {
221
157
  setup: [
222
- create(Workspace, { id: "w1", view: "board" }),
223
- create(Project, { id: "p0", name: "General" })
158
+ create(Tour, { id: "t1", page: "welcome" })
224
159
  ]
225
- when: CreateProject on Project { id: "p1", name: "General" }
160
+ when: ShowFiles on Tour { tour: "t1" }
226
161
  then: [
227
- error_raised('a project with this name already exists'),
228
- entity_count(Project) == 1
162
+ entity_has(Tour.1, page == "files")
229
163
  ]
230
164
  }
231
165
 
232
- test OpenProjectsSwitchesView {
166
+ test ShowWelcomeReturnsHome {
233
167
  setup: [
234
- create(Workspace, { id: "w1", view: "board" })
168
+ create(Tour, { id: "t1", page: "files" })
235
169
  ]
236
- when: OpenProjects on Workspace { workspace: "w1" }
170
+ when: ShowWelcome on Tour { tour: "t1" }
237
171
  then: [
238
- entity_has(Workspace.1, view == "projects")
172
+ entity_has(Tour.1, page == "welcome")
239
173
  ]
240
174
  }
@@ -0,0 +1,36 @@
1
+ // Core journeys: land on the welcome screen, add a greeting through the demo
2
+ // form, see the rejection path, and confirm the tour screens render their
3
+ // seeded tables. Deterministic: every run starts a fresh server with the
4
+ // same seed.
5
+
6
+ import { expect, test } from "@playwright/test";
7
+
8
+ test.describe.configure({ mode: "serial" });
9
+
10
+ test("welcome screen: add a greeting -> table row", async ({ page }) => {
11
+ await page.goto("/");
12
+ await expect(page.getByRole("heading", { name: "Welcome to MinLang" })).toBeVisible();
13
+
14
+ const form = page.getByRole("form", { name: "Add note" });
15
+ await form.getByLabel("Greeting").fill("Hi there");
16
+ await form.getByRole("button", { name: "Say hello" }).click();
17
+
18
+ await expect(page.getByRole("row").filter({ hasText: "Hi there" })).toBeVisible();
19
+ });
20
+
21
+ test("validation rejection shows the exact constraint message", async ({ page }) => {
22
+ await page.goto("/");
23
+ const form = page.getByRole("form", { name: "Add note" });
24
+ await form.getByRole("button", { name: "Say hello" }).click();
25
+ await expect(form.getByRole("alert")).toHaveText("a greeting is required");
26
+ });
27
+
28
+ test("tour screens render their seeded content tables", async ({ page }) => {
29
+ await page.goto("/screens/files");
30
+ await expect(page.getByRole("heading", { name: "What's in this project" })).toBeVisible();
31
+ await expect(page.getByRole("row").filter({ hasText: "app/generated/" })).toBeVisible();
32
+
33
+ await page.goto("/screens/resources");
34
+ await expect(page.getByRole("heading", { name: "Learn more" })).toBeVisible();
35
+ await expect(page.getByRole("row").filter({ hasText: "Language rules" })).toBeVisible();
36
+ });
@@ -1,5 +1,5 @@
1
1
  // WCAG 2.1 AA gate: axe-core scans per screen (no serious/critical
2
- // violations) and a keyboard-only create journey.
2
+ // violations) and a keyboard-only "add a greeting" journey.
3
3
 
4
4
  import AxeBuilder from "@axe-core/playwright";
5
5
  import { expect, test } from "@playwright/test";
@@ -14,31 +14,27 @@ const scan = async (page: import("@playwright/test").Page): Promise<void> => {
14
14
  expect(serious, JSON.stringify(serious, null, 2)).toEqual([]);
15
15
  };
16
16
 
17
- test("board screen is axe-clean (WCAG 2.1 AA)", async ({ page }) => {
17
+ test("welcome screen is axe-clean (WCAG 2.1 AA)", async ({ page }) => {
18
18
  await page.goto("/");
19
- await expect(page.getByRole("heading", { name: "Task board" })).toBeVisible();
19
+ await expect(page.getByRole("heading", { name: "Welcome to MinLang" })).toBeVisible();
20
20
  await scan(page);
21
21
  });
22
22
 
23
- test("projects screen is axe-clean (WCAG 2.1 AA)", async ({ page }) => {
24
- await page.goto("/screens/projects");
25
- await expect(page.getByRole("heading", { name: "Projects" })).toBeVisible();
23
+ test("files screen is axe-clean (WCAG 2.1 AA)", async ({ page }) => {
24
+ await page.goto("/screens/files");
25
+ await expect(page.getByRole("heading", { name: "What's in this project" })).toBeVisible();
26
26
  await scan(page);
27
27
  });
28
28
 
29
- test("keyboard-only journey: create a task without the mouse", async ({ page }) => {
29
+ test("keyboard-only journey: add a greeting without the mouse", async ({ page }) => {
30
30
  await page.goto("/");
31
- const form = page.getByRole("form", { name: "Create task" });
32
- await form.getByLabel("Project").focus();
33
- await page.keyboard.press("Tab"); // -> Title
34
- await page.keyboard.type("Keyboard task");
35
- await page.keyboard.press("Tab"); // -> Status (select)
36
- await page.keyboard.press("Tab"); // -> Assignee
37
- await page.keyboard.type("sam");
31
+ const form = page.getByRole("form", { name: "Add note" });
32
+ await form.getByLabel("Greeting").focus();
33
+ await page.keyboard.type("Keyboard hello");
38
34
  await page.keyboard.press("Tab"); // -> submit
39
- await expect(form.getByRole("button", { name: "Add task" })).toBeFocused();
35
+ await expect(form.getByRole("button", { name: "Say hello" })).toBeFocused();
40
36
  await page.keyboard.press("Enter");
41
37
  await expect(
42
- page.getByRole("row").filter({ hasText: "Keyboard task" }),
38
+ page.getByRole("row").filter({ hasText: "Keyboard hello" }),
43
39
  ).toBeVisible();
44
40
  });
@@ -1,8 +1,8 @@
1
- // App wiring: bind the data adapter (in-memory default) with the
2
- // deterministic demo seed. Imported for its side effect by app/layout.tsx so
1
+ // App wiring: bind the data adapter (in-memory default) with the welcome
2
+ // app's starting content. Imported for its side effect by app/layout.tsx so
3
3
  // it runs once in the route-server bundle before any render or action.
4
- // WEB_DATA_ADAPTER=memory is the supported adapter for plan completion; a
5
- // real adapter (USE_REAL_SERVICES=true) would bind here instead.
4
+ // The tables on the Files / Enhance / Resources screens are seeded here; the
5
+ // notes list starts empty so the first thing you do is add one.
6
6
 
7
7
  import { createInMemoryAdapter } from "@minlang/runtime-web";
8
8
  import { bindServerAdapter } from "@minlang/runtime-web/server";
@@ -11,7 +11,24 @@ import { emptyAppState } from "./generated/state";
11
11
  bindServerAdapter(
12
12
  createInMemoryAdapter({
13
13
  ...emptyAppState,
14
- workspaces: [{ id: "w1", view: "board" }],
15
- projects: [{ id: "p1", name: "General" }],
14
+ tours: [{ id: "t1", page: "welcome" }],
15
+ fileGuides: [
16
+ { id: "f1", file: "*.ml", purpose: "The whole app: entities, rules, actions, screens, tests." },
17
+ { id: "f2", file: "app/generated/", purpose: "Compiler output. Committed, never hand-edited." },
18
+ { id: "f3", file: "app/", purpose: "Thin Next.js shell over the @minlang runtime." },
19
+ { id: "f4", file: "minlang.json", purpose: "Pins the MinLang language + compiler versions." },
20
+ { id: "f5", file: "AGENTS.md", purpose: "Instructions for AI coding agents working here." },
21
+ ],
22
+ buildSteps: [
23
+ { id: "s1", step: "Edit welcome.ml", detail: "Model your domain: entities, constraints, actions, screens." },
24
+ { id: "s2", step: "make compile", detail: "Regenerate app/generated from the .ml source." },
25
+ { id: "s3", step: "make test", detail: "Run the generated success / failure / no-change triads." },
26
+ { id: "s4", step: "make dev", detail: "Open http://localhost:3111 and use your app." },
27
+ ],
28
+ resources: [
29
+ { id: "r1", topic: "Language rules", link: "github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md" },
30
+ { id: "r2", topic: "Run it", link: "make dev, then http://localhost:3111" },
31
+ { id: "r3", topic: "Enhance it", link: "Edit welcome.ml, then make compile && make test" },
32
+ ],
16
33
  }),
17
34
  );
@@ -0,0 +1,77 @@
1
+ // BDD: the welcome demo's domain flows through dispatch + the in-memory
2
+ // adapter — add a greeting, see the rejection path keep state unchanged,
3
+ // navigate between screens, and replay a fixed trace deterministically.
4
+
5
+ import { createInMemoryAdapter, dispatch } from "@minlang/runtime-web";
6
+ import type { DataAdapter, DispatchDeps } from "@minlang/runtime-web";
7
+ import { describe, expect, test } from "vitest";
8
+ import { applyAddNote, applyShowFiles } from "../generated/actions";
9
+ import type { AddNoteInput } from "../generated/actions";
10
+ import { runNoteList } from "../generated/queries";
11
+ import { emptyAppState } from "../generated/state";
12
+ import type { AppState } from "../generated/state";
13
+
14
+ const seed: AppState = {
15
+ ...emptyAppState,
16
+ tours: [{ id: "t1", page: "welcome" }],
17
+ };
18
+
19
+ const fixedDeps = (adapter: DataAdapter<AppState>): DispatchDeps<AppState> => ({
20
+ adapter,
21
+ clock: { currentTime: () => "2026-01-01T00:00:00Z" },
22
+ rng: { draw: () => 7 },
23
+ identity: { currentActorId: () => "test-actor" },
24
+ logger: { info: () => undefined, warn: () => undefined, error: () => undefined },
25
+ correlationId: "c-test",
26
+ emptyState: emptyAppState,
27
+ newId: () => "id-fixed",
28
+ });
29
+
30
+ const noteInput = (overrides: Partial<AddNoteInput>): AddNoteInput => ({
31
+ id: "n1",
32
+ greeting: "Hello, MinLang",
33
+ actor_id: "test-actor",
34
+ ...overrides,
35
+ });
36
+
37
+ describe("welcome demo flows", () => {
38
+ test("adding a greeting persists it to the notes list", async () => {
39
+ const adapter = createInMemoryAdapter<AppState>(seed);
40
+ const deps = fixedDeps(adapter);
41
+ expect((await dispatch(applyAddNote, noteInput({}), deps)).ok).toBe(true);
42
+ const state = (await adapter.load()) ?? emptyAppState;
43
+ expect(runNoteList(state, { tour: "t1" }).map((n) => n.greeting)).toEqual(["Hello, MinLang"]);
44
+ });
45
+
46
+ test("an empty greeting is rejected with the exact constraint message", async () => {
47
+ const adapter = createInMemoryAdapter<AppState>(seed);
48
+ const outcome = await dispatch(applyAddNote, noteInput({ greeting: "" }), fixedDeps(adapter));
49
+ expect(outcome.ok).toBe(false);
50
+ if (!outcome.ok) {
51
+ expect(outcome.error.message).toBe("a greeting is required");
52
+ }
53
+ expect(((await adapter.load()) ?? emptyAppState).notes).toHaveLength(0);
54
+ });
55
+
56
+ test("navigation switches the active page", async () => {
57
+ const adapter = createInMemoryAdapter<AppState>(seed);
58
+ const deps = fixedDeps(adapter);
59
+ expect((await dispatch(applyShowFiles, { tour: "t1", actor_id: "a" }, deps)).ok).toBe(true);
60
+ const state = (await adapter.load()) ?? emptyAppState;
61
+ expect(state.tours[0]?.page).toBe("files");
62
+ });
63
+
64
+ test("replay: a fixed trace yields a deep-equal final state across runs", async () => {
65
+ const run = async (): Promise<AppState> => {
66
+ const adapter = createInMemoryAdapter<AppState>(seed);
67
+ const deps = fixedDeps(adapter);
68
+ await dispatch(applyAddNote, noteInput({ id: "n1", greeting: "One" }), deps);
69
+ await dispatch(applyAddNote, noteInput({ id: "n2", greeting: "Two" }), deps);
70
+ await dispatch(applyShowFiles, { tour: "t1", actor_id: "test-actor" }, deps);
71
+ return (await adapter.load()) ?? emptyAppState;
72
+ };
73
+ const [first, second] = [await run(), await run()];
74
+ expect(first).toEqual(second);
75
+ expect(runNoteList(first, { tour: "t1" }).map((n) => n.greeting)).toEqual(["One", "Two"]);
76
+ });
77
+ });
@@ -0,0 +1,4 @@
1
+ **/generated/** linguist-generated=true
2
+ **/generated/ui/wire/*.mlui linguist-generated=false linguist-detectable=false
3
+ minlang-core/ linguist-vendored=true
4
+ *.mlui text eol=lf
@@ -0,0 +1,74 @@
1
+ Source ASCII UI design for examples/welcome/welcome.ml.
2
+ Author this first, then encode it as MinLang `screen` blocks and compare with
3
+ web/generated/ui/wire/*.mlui after `make compile-web EXAMPLE=welcome`.
4
+
5
+ ── Welcome ───────────────────────────────────────────────────────────────────
6
+ ┌──────────────────────────────────────────────────────────────────────────────┐
7
+ │ Welcome to MinLang │
8
+ ├──────────────────────────────────────────────────────────────────────────────┤
9
+ │ The whole app is one .ml file. Edit it, run make compile, and this UI │
10
+ │ updates. │
11
+ │ │
12
+ │ Try the demo: add a note below, then take the tour. │
13
+ │ │
14
+ │ Notes │
15
+ │ ┌──────────────────────────────────────────────────────────────────────────┐ │
16
+ │ │ Greeting │ │
17
+ │ ├──────────────────────────────────────────────────────────────────────────┤ │
18
+ │ │ Hi from MinLang │ │
19
+ │ └──────────────────────────────────────────────────────────────────────────┘ │
20
+ │ │
21
+ │ [ Say hello ] │
22
+ │ [ What's in this project ] [ Learn more ] │
23
+ └──────────────────────────────────────────────────────────────────────────────┘
24
+
25
+ ── Files ─────────────────────────────────────────────────────────────────────
26
+ ┌──────────────────────────────────────────────────────────────────────────────┐
27
+ │ What's in this project │
28
+ ├──────────────────────────────────────────────────────────────────────────────┤
29
+ │ Everything is generated from your .ml source. Edit it, never app/generated. │
30
+ │ │
31
+ │ ┌───────────────────────┬──────────────────────────────────────────────────┐ │
32
+ │ │ File │ Purpose │ │
33
+ │ ├───────────────────────┼──────────────────────────────────────────────────┤ │
34
+ │ │ *.ml │ Entities, constraints, actions, screens, tests │ │
35
+ │ │ app/generated/ │ Compiler output; never hand-edited │ │
36
+ │ │ AGENTS.md │ Instructions for AI coding agents │ │
37
+ │ └───────────────────────┴──────────────────────────────────────────────────┘ │
38
+ │ │
39
+ │ [ How to enhance it ] [ Back to start ] │
40
+ └──────────────────────────────────────────────────────────────────────────────┘
41
+
42
+ ── Enhance ───────────────────────────────────────────────────────────────────
43
+ ┌──────────────────────────────────────────────────────────────────────────────┐
44
+ │ Make it yours │
45
+ ├──────────────────────────────────────────────────────────────────────────────┤
46
+ │ Describe your own domain in the .ml, then compile and test. │
47
+ │ │
48
+ │ ┌────────────────────┬─────────────────────────────────────────────────────┐ │
49
+ │ │ Step │ Detail │ │
50
+ │ ├────────────────────┼─────────────────────────────────────────────────────┤ │
51
+ │ │ Edit *.ml │ Model entities, constraints, actions, screens │ │
52
+ │ │ make compile │ Regenerate app/generated │ │
53
+ │ │ make test │ Run generated triads and integration tests │ │
54
+ │ └────────────────────┴─────────────────────────────────────────────────────┘ │
55
+ │ │
56
+ │ [ Resources ] [ Back to start ] │
57
+ └──────────────────────────────────────────────────────────────────────────────┘
58
+
59
+ ── Resources ─────────────────────────────────────────────────────────────────
60
+ ┌──────────────────────────────────────────────────────────────────────────────┐
61
+ │ Learn more │
62
+ ├──────────────────────────────────────────────────────────────────────────────┤
63
+ │ The language bundle has the full rules and the web authoring guide. │
64
+ │ │
65
+ │ ┌────────────────────┬─────────────────────────────────────────────────────┐ │
66
+ │ │ Topic │ Link │ │
67
+ │ ├────────────────────┼─────────────────────────────────────────────────────┤ │
68
+ │ │ Language rules │ releases/latest/download/minlang-language-bundle.md │ │
69
+ │ │ Run it │ make dev │ │
70
+ │ │ Enhance it │ edit *.ml, then make compile && make test │ │
71
+ │ └────────────────────┴─────────────────────────────────────────────────────┘ │
72
+ │ │
73
+ │ [ Back to start ] [ What's in this project ] │
74
+ └──────────────────────────────────────────────────────────────────────────────┘
@@ -1,68 +0,0 @@
1
- // Core journeys: create a task, see the rejection path, move and rename it.
2
- // Deterministic: every run starts a fresh server with the same seed.
3
-
4
- import { expect, test } from "@playwright/test";
5
-
6
- test.describe.configure({ mode: "serial" });
7
-
8
- test("create task journey: form -> table row", async ({ page }) => {
9
- await page.goto("/");
10
- await expect(page.getByRole("heading", { name: "Task board" })).toBeVisible();
11
- await expect(page.getByRole("note")).toContainText("No task yet");
12
-
13
- const form = page.getByRole("form", { name: "Create task" });
14
- await form.getByLabel("Title").fill("Ship the web target");
15
- await form.getByLabel("Status").selectOption("todo");
16
- await form.getByLabel("Assignee").fill("casey");
17
- await form.getByRole("button", { name: "Add task" }).click();
18
-
19
- const table = page.getByRole("table", { name: "Task board" });
20
- await expect(table).toBeVisible();
21
- await expect(table.getByRole("row").filter({ hasText: "Ship the web target" })).toBeVisible();
22
- });
23
-
24
- test("validation rejection shows the exact constraint message", async ({ page }) => {
25
- await page.goto("/");
26
- const form = page.getByRole("form", { name: "Create task" });
27
- await form.getByLabel("Status").selectOption("doing");
28
- await form.getByLabel("Title").fill("Doing without assignee");
29
- await form.getByRole("button", { name: "Add task" }).click();
30
- await expect(form.getByRole("alert")).toHaveText("assignee is required when a task is doing");
31
- });
32
-
33
- test("move task journey: status changes in the table", async ({ page }) => {
34
- await page.goto("/");
35
- const create = page.getByRole("form", { name: "Create task" });
36
- await create.getByLabel("Title").fill("Move me");
37
- await create.getByLabel("Assignee").fill("alex");
38
- await create.getByRole("button", { name: "Add task" }).click();
39
- await expect(page.getByRole("table", { name: "Task board" })).toBeVisible();
40
-
41
- const move = page.getByRole("form", { name: "Move task" });
42
- await move.getByLabel("Id").selectOption({ label: "Move me" });
43
- await move.getByLabel("Status").selectOption("doing");
44
- await move.getByRole("button", { name: "Move task" }).click();
45
- const row = page.getByRole("row").filter({ hasText: "Move me" });
46
- await expect(row).toContainText("doing");
47
- });
48
-
49
- test("rename task journey + projects navigation", async ({ page }) => {
50
- await page.goto("/");
51
- const create = page.getByRole("form", { name: "Create task" });
52
- await create.getByLabel("Title").fill("Old title");
53
- await create.getByRole("button", { name: "Add task" }).click();
54
- await expect(page.getByRole("table", { name: "Task board" })).toBeVisible();
55
-
56
- const rename = page.getByRole("form", { name: "Rename task" });
57
- await rename.getByLabel("Id").selectOption({ label: "Old title" });
58
- await rename.getByLabel("Title").fill("New title");
59
- await rename.getByRole("button", { name: "Rename task" }).click();
60
- await expect(page.getByRole("row").filter({ hasText: "New title" })).toBeVisible();
61
-
62
- await page.goto("/screens/projects");
63
- await expect(page.getByRole("heading", { name: "Projects" })).toBeVisible();
64
- const project = page.getByRole("form", { name: "Create project" });
65
- await project.getByLabel("Name").fill("Platform");
66
- await project.getByRole("button", { name: "Add project" }).click();
67
- await expect(page.getByRole("row").filter({ hasText: "Platform" })).toBeVisible();
68
- });
@@ -1,111 +0,0 @@
1
- // BDD: end-to-end domain flows through dispatch + the in-memory adapter —
2
- // create → move → rename journeys, rejection leaves state unchanged, and a
3
- // deterministic replay over a fixed input trace.
4
-
5
- import { createInMemoryAdapter, dispatch } from "@minlang/runtime-web";
6
- import type { DataAdapter, DispatchDeps } from "@minlang/runtime-web";
7
- import { describe, expect, test } from "vitest";
8
- import { applyCreateTask, applyMoveTask, applyRenameTask } from "../generated/actions";
9
- import type { CreateTaskInput } from "../generated/actions";
10
- import { runTaskBoard } from "../generated/queries";
11
- import { emptyAppState } from "../generated/state";
12
- import type { AppState } from "../generated/state";
13
-
14
- const seed: AppState = {
15
- ...emptyAppState,
16
- workspaces: [{ id: "w1", view: "board" }],
17
- projects: [{ id: "p1", name: "General" }],
18
- };
19
-
20
- const fixedDeps = (adapter: DataAdapter<AppState>): DispatchDeps<AppState> => ({
21
- adapter,
22
- clock: { currentTime: () => "2026-01-01T00:00:00Z" },
23
- rng: { draw: () => 7 },
24
- identity: { currentActorId: () => "test-actor" },
25
- logger: { info: () => undefined, warn: () => undefined, error: () => undefined },
26
- correlationId: "c-test",
27
- emptyState: emptyAppState,
28
- newId: () => "id-fixed",
29
- });
30
-
31
- const taskInput = (overrides: Partial<CreateTaskInput>): CreateTaskInput => ({
32
- id: "t1",
33
- project: "p1",
34
- title: "Ship the web target",
35
- status: "todo",
36
- assignee: "",
37
- created_at: "2026-01-01T00:00:00Z",
38
- actor_id: "test-actor",
39
- ...overrides,
40
- });
41
-
42
- describe("task-tracker domain flows", () => {
43
- test("create → move → rename journey persists each step", async () => {
44
- const adapter = createInMemoryAdapter<AppState>(seed);
45
- const deps = fixedDeps(adapter);
46
- expect((await dispatch(applyCreateTask, taskInput({}), deps)).ok).toBe(true);
47
- expect(
48
- (await dispatch(applyMoveTask, { id: "t1", status: "doing", actor_id: "a" }, deps)).ok,
49
- ).toBe(false); // assignee required when doing
50
- expect(
51
- (
52
- await dispatch(
53
- applyRenameTask,
54
- { id: "t1", title: "Ship it", actor_id: "a" },
55
- deps,
56
- )
57
- ).ok,
58
- ).toBe(true);
59
- const state = (await adapter.load()) ?? emptyAppState;
60
- expect(runTaskBoard(state, { workspace: "w1" }).map((t) => t.title)).toEqual(["Ship it"]);
61
- });
62
-
63
- test("rejection returns the exact constraint message and persists nothing", async () => {
64
- const adapter = createInMemoryAdapter<AppState>(seed);
65
- const outcome = await dispatch(
66
- applyCreateTask,
67
- taskInput({ title: "" }),
68
- fixedDeps(adapter),
69
- );
70
- expect(outcome.ok).toBe(false);
71
- if (!outcome.ok) {
72
- expect(outcome.error.message).toBe("title is required");
73
- }
74
- expect(((await adapter.load()) ?? emptyAppState).tasks).toHaveLength(0);
75
- });
76
-
77
- test("unknown project is rejected by the count join", async () => {
78
- const adapter = createInMemoryAdapter<AppState>(seed);
79
- const outcome = await dispatch(
80
- applyCreateTask,
81
- taskInput({ project: "ghost" }),
82
- fixedDeps(adapter),
83
- );
84
- expect(outcome.ok).toBe(false);
85
- if (!outcome.ok) {
86
- expect(outcome.error.message).toBe("project does not exist");
87
- }
88
- });
89
-
90
- test("replay: a fixed trace yields a deep-equal final state across runs", async () => {
91
- const run = async (): Promise<AppState> => {
92
- const adapter = createInMemoryAdapter<AppState>(seed);
93
- const deps = fixedDeps(adapter);
94
- await dispatch(applyCreateTask, taskInput({ id: "t1", title: "One" }), deps);
95
- await dispatch(applyCreateTask, taskInput({ id: "t2", title: "Two" }), deps);
96
- await dispatch(
97
- applyMoveTask,
98
- { id: "t2", status: "done", actor_id: "test-actor" },
99
- deps,
100
- );
101
- await dispatch(applyRenameTask, { id: "t1", title: "Won", actor_id: "test-actor" }, deps);
102
- return (await adapter.load()) ?? emptyAppState;
103
- };
104
- const [first, second] = [await run(), await run()];
105
- expect(first).toEqual(second);
106
- expect(runTaskBoard(first, { workspace: "w1" }).map((t) => [t.title, t.status])).toEqual([
107
- ["Won", "todo"],
108
- ["Two", "done"],
109
- ]);
110
- });
111
- });