create-nola-lang 0.1.13 → 0.1.15

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 (54) hide show
  1. package/README.md +8 -3
  2. package/dist/add.d.ts +3 -0
  3. package/dist/add.js +2 -1
  4. package/dist/checkout.js +1 -0
  5. package/dist/{chunk-LUSVIOAN.js → chunk-UTVGG44K.js} +124 -95
  6. package/dist/examples.d.ts +4 -2
  7. package/dist/examples.js +4 -2
  8. package/dist/flow.d.ts +20 -3
  9. package/dist/flow.js +49 -17
  10. package/dist/github.d.ts +1 -1
  11. package/dist/github.js +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/launch.d.ts +2 -2
  14. package/dist/launch.js +1 -1
  15. package/dist/main.js +1 -1
  16. package/dist/node-version.d.ts +3 -2
  17. package/dist/node-version.js +4 -3
  18. package/dist/package-manager.d.ts +5 -0
  19. package/dist/package-manager.js +7 -0
  20. package/dist/providers.d.ts +1 -1
  21. package/dist/providers.js +1 -1
  22. package/dist/registry.d.ts +21 -12
  23. package/dist/registry.js +27 -5
  24. package/dist/scaffold.d.ts +36 -16
  25. package/dist/scaffold.js +96 -116
  26. package/package.json +1 -1
  27. package/skills/nola/SKILL.md +72 -17
  28. package/skills/nola/references/config.md +35 -6
  29. package/skills/nola/references/patterns.md +129 -31
  30. package/skills/nola/references/pitfalls.md +318 -56
  31. package/skills/nola/references/syntax.md +345 -121
  32. package/templates/empty/package.json +5 -5
  33. package/templates/empty/src/main.tsi +8 -0
  34. package/templates/empty/src/main.ts +0 -5
  35. package/templates/feature-extraction/README.md +0 -20
  36. package/templates/feature-extraction/nola.config.ts +0 -14
  37. package/templates/feature-extraction/nola.replay.jsonl +0 -2
  38. package/templates/feature-extraction/package.json +0 -22
  39. package/templates/feature-extraction/src/main.tsi +0 -22
  40. package/templates/feature-extraction/tsconfig.json +0 -12
  41. package/templates/function-calling/README.md +0 -21
  42. package/templates/function-calling/nola.config.ts +0 -14
  43. package/templates/function-calling/nola.replay.jsonl +0 -1
  44. package/templates/function-calling/package.json +0 -22
  45. package/templates/function-calling/src/main.tsi +0 -12
  46. package/templates/function-calling/src/tickets.ts +0 -17
  47. package/templates/function-calling/tsconfig.json +0 -12
  48. package/templates/typescript-interop/README.md +0 -15
  49. package/templates/typescript-interop/nola.config.ts +0 -14
  50. package/templates/typescript-interop/nola.replay.jsonl +0 -1
  51. package/templates/typescript-interop/package.json +0 -22
  52. package/templates/typescript-interop/src/main.ts +0 -8
  53. package/templates/typescript-interop/src/person.tsi +0 -11
  54. package/templates/typescript-interop/tsconfig.json +0 -12
package/dist/scaffold.js CHANGED
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
5
5
  import { collectExampleFromDisk, devExamplesDir, rewriteExamplePackageJson } from "./examples.js";
6
6
  import { fetchExampleFromGitHub } from "./github.js";
7
7
  import { providerById } from "./providers.js";
8
- import { templateByName, templateNames } from "./registry.js";
8
+ import { entryFile, templateByName, templateNames } from "./registry.js";
9
+ /** This package's own templates: `empty` (the one builtin), `_providers/` and `_gitignore`. */
9
10
  const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
10
11
  /** The config a live provider choice writes over the template's own (`templates/_providers/<id>.config.ts`). */
11
12
  export function providerConfigUrl(provider) {
@@ -28,39 +29,61 @@ export function vendorEnvVar(template, provider) {
28
29
  export function envExample(envVar) {
29
30
  return `# Copy to .env and fill in — \`nola run\` applies .env before evaluating nola.config.ts.\n${envVar}=\n`;
30
31
  }
31
- /** _gitignore ships underscored (npm pack strips nested .gitignore files). */
32
- const RENAMES = { _gitignore: ".gitignore" };
33
- /** The ONE recommended .gitignore — every scaffold gets it; no template keeps a copy of its own. */
32
+ /** The Bun project's bunfig.toml: the preload for `bun src/main.tsi` and for `bun test`. */
33
+ export const BUNFIG_TOML = 'preload = ["@nola-lang/bun/preload"]\n\n[test]\npreload = ["@nola-lang/bun/preload"]\n';
34
+ /** The Bun floor @nola-lang/bun needs (process.getBuiltinModule). */
35
+ export const BUN_ENGINE = ">=1.3.0";
36
+ /** A Bun project's manifest additions on top of the template's: the start script, the devDependency, engines.bun. */
37
+ export function applyBunRuntime(content, opts) {
38
+ const pkg = JSON.parse(content);
39
+ const scripts = pkg.scripts ?? {};
40
+ scripts.start = `bun ${opts.entry}`;
41
+ pkg.scripts = scripts;
42
+ const dev = pkg.devDependencies ?? {};
43
+ dev["@nola-lang/bun"] = `^${opts.version}`;
44
+ pkg.devDependencies = dev;
45
+ const engines = pkg.engines ?? {};
46
+ engines.bun = BUN_ENGINE;
47
+ pkg.engines = engines;
48
+ return `${JSON.stringify(pkg, null, 2)}\n`;
49
+ }
50
+ /** The ONE recommended .gitignore — every scaffold gets it; no template keeps a copy of its own (_gitignore ships underscored: npm pack strips nested .gitignore files). */
34
51
  const GITIGNORE_URL = new URL("../templates/_gitignore", import.meta.url);
35
52
  /**
36
- * Add the recommended `.gitignore` to an example's files. Examples are copied
37
- * verbatim from `examples/`, where the monorepo's root ignore file covers them,
38
- * so they carry none — one an example does carry wins.
53
+ * Add the recommended `.gitignore` to a template's files. Templates are
54
+ * copied as committed — examples from `examples/`, where the monorepo's root
55
+ * ignore file covers them — so they carry none; one a template does carry wins.
39
56
  */
40
57
  export async function withRecommendedGitignore(files) {
41
58
  if (!files.has(".gitignore"))
42
59
  files.set(".gitignore", await readFile(GITIGNORE_URL, "utf8"));
43
60
  return files;
44
61
  }
45
- /** Files whose __NAME__/__VERSION__, README-note and __NEXT_STEPS__ placeholders are substituted. */
46
- const SUBSTITUTED = new Set(["package.json", "README.md", "main.ts", "main.tsi"]);
47
62
  /**
48
- * The comment that opens the entry file (src/main.ts, or the `.tsi` entry
49
- * of a one-file template) — the file the scaffold lands the user on (VS Code opens it
50
- * as the active editor), so it carries the first three things to do. The VS
51
- * Code variant only ships with the editor step's .vscode files, which are
52
- * what make F5 and the extension prompt real.
63
+ * The comment that opens the entry file (the `.tsi` entry of a one-file
64
+ * template and of `empty`, else the plain-TS src/main.ts) — the file the
65
+ * scaffold lands the user on (VS Code opens it as the active editor), so it
66
+ * carries the first three things to do. The VS Code variant only ships with
67
+ * the editor step's .vscode files, which are what make F5 and the extension
68
+ * prompt real.
53
69
  */
54
- export function nextStepsComment(ide, template) {
70
+ export function nextStepsComment(ide, template, runtime = "node") {
55
71
  if (ide === "vscode") {
56
- const breakpointIn = templateByName(template)?.entry
57
- ? "on the `ask` line below"
58
- : template === "typescript-interop"
59
- ? "in src/person.tsi"
60
- : "in your .tsi file";
72
+ // `empty`'s stub has no ask yet — the breakpoint goes on the one the user writes.
73
+ const breakpointIn = template === "empty"
74
+ ? "on your first `ask` in this file"
75
+ : templateByName(template)?.entry
76
+ ? "on the `ask` line below"
77
+ : template === "typescript-interop"
78
+ ? "in src/person.tsi"
79
+ : "in your .tsi file";
80
+ // A Bun project still debugs under Node's loader: the .vscode files are valid whenever Node is present.
81
+ const runLine = runtime === "bun"
82
+ ? "// 1. Run this file: bun start (F5 runs it too, under Node's loader — .vscode/launch.json is already set up)."
83
+ : "// 1. Press F5 to run this file (.vscode/launch.json is already set up).";
61
84
  return [
62
85
  "// Next steps in VS Code:",
63
- "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
86
+ runLine,
64
87
  `// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
65
88
  '// 3. Install the recommended "Nola" extension when VS Code offers it — IntelliSense,',
66
89
  "// go to definition and diagnostics inside .tsi files.",
@@ -68,45 +91,12 @@ export function nextStepsComment(ide, template) {
68
91
  }
69
92
  return [
70
93
  "// Next steps:",
71
- "// 1. Run this file: npm start",
94
+ `// 1. Run this file: ${runtime === "bun" ? "bun start" : "npm start"}`,
72
95
  "// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/",
73
96
  ].join("\n");
74
97
  }
75
98
  /** Template files that only make sense for the offline (replay) configuration. */
76
- const OFFLINE_ONLY = new Set(["nola.replay.jsonl"]);
77
- const README_NOTES = {
78
- offline: {
79
- START_NOTE: "works offline, no API key needed",
80
- PROVIDER_NOTE: "This project runs offline: `nola.config.ts` replays answers from the committed\n" +
81
- "`nola.replay.jsonl` ledger. The ledger is keyed by the exact prompt, so once\n" +
82
- "you edit the `.tsi` file or add your own asks, switch the config to a real\n" +
83
- "model (see the comment in `nola.config.ts`): `model: \"nola\"` with a key from\n" +
84
- "`npx nola-lang key` (25 free runs), or your own provider and its key.",
85
- },
86
- nola: {
87
- START_NOTE: "uses your 25 free Nola runs (key in .env)",
88
- PROVIDER_NOTE: "`nola.config.ts` sets `model: \"nola\"` — platform-served inference with the trial key the\n" +
89
- "scaffold wrote to `.env` (git-ignored). Out of runs? `npx nola-lang account` opens\n" +
90
- "your Nola account, where prepaid balance is added, or bring your\n" +
91
- "own model (see the comment in `nola.config.ts`).",
92
- },
93
- };
94
- /** The README notes for a chosen provider: offline, the Nola trial, or a vendor and the env var its factory reads. */
95
- function readmeNotes(provider) {
96
- if (provider === "none")
97
- return README_NOTES.offline;
98
- if (provider === "nola")
99
- return README_NOTES.nola;
100
- const def = providerById(provider);
101
- if (!def?.envVar || !def.model)
102
- throw new Error(`provider "${provider}" has no vendor config`);
103
- return {
104
- START_NOTE: `set ${def.envVar} in .env first`,
105
- PROVIDER_NOTE: `\`nola.config.ts\` sets \`model: ${def.model}\` — ${def.label} serves inference with the key it reads\n` +
106
- `from \`${def.envVar}\`. Copy \`.env.example\` to \`.env\` (git-ignored; \`nola run\` applies it) and fill in\n` +
107
- `\`${def.envVar}\` before the first \`npm start\`, or switch models in \`nola.config.ts\`.`,
108
- };
109
- }
99
+ const OFFLINE_ONLY = ["nola.replay.jsonl"];
110
100
  export async function ownVersion() {
111
101
  const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
112
102
  return pkg.version;
@@ -123,9 +113,26 @@ async function prepareTarget(absRoot, force) {
123
113
  for (const entry of entries)
124
114
  await rm(join(absRoot, entry), { recursive: true, force: true });
125
115
  }
116
+ /**
117
+ * A template's files as committed: `empty` from this package's templates/
118
+ * dir, every other template from examples/ — the dev checkout's on disk,
119
+ * else GitHub at the lockstep tag. Everything is buffered before the caller
120
+ * touches the target, so a failed fetch scaffolds nothing.
121
+ */
122
+ async function collectTemplate(def, version) {
123
+ if (def.source === "builtin")
124
+ return collectExampleFromDisk(TEMPLATES_DIR, def.name);
125
+ const dev = await devExamplesDir();
126
+ return dev ? collectExampleFromDisk(dev, def.name) : fetchExampleFromGitHub(def.name, version);
127
+ }
126
128
  /**
127
129
  * Lay a template down into `targetDir`. One implementation serves both entry
128
- * points: the `create-nola-lang` bin (`npm create nola-lang`) and `nola init`.
130
+ * points — the `create-nola-lang` bin (`npm create nola-lang`) and `nola init`
131
+ * — and both sources: the files are collected as committed, then the scaffold
132
+ * adds what a project needs on top — the chosen provider's config (dropping
133
+ * the replay ledger), the project name and the lockstep ranges in
134
+ * package.json, the recommended .gitignore, the vendor key's .env.example,
135
+ * and the next-steps comment at the top of the entry file.
129
136
  */
130
137
  export async function scaffold(targetDir, opts = {}) {
131
138
  const root = targetDir;
@@ -134,69 +141,42 @@ export async function scaffold(targetDir, opts = {}) {
134
141
  const def = templateByName(template);
135
142
  if (!def)
136
143
  throw new Error(`unknown template "${template}" (valid: ${templateNames().join(", ")})`);
144
+ const version = await ownVersion();
145
+ const files = await collectTemplate(def, version);
137
146
  await prepareTarget(absRoot, opts.force ?? false);
138
147
  const name = opts.name ?? basename(absRoot);
139
- const version = await ownVersion();
140
- const envVar = vendorEnvVar(template, opts.provider ?? "none");
141
- if (def.source === "example") {
142
- const dev = await devExamplesDir();
143
- const exampleFiles = await withRecommendedGitignore(dev ? await collectExampleFromDisk(dev, template) : await fetchExampleFromGitHub(template, version));
144
- const manifest = exampleFiles.get("package.json");
145
- if (manifest)
146
- exampleFiles.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
147
- if (envVar !== undefined && !exampleFiles.has(".env.example"))
148
- exampleFiles.set(".env.example", envExample(envVar));
149
- for (const [relPath, content] of exampleFiles) {
150
- const target = join(absRoot, relPath);
151
- await mkdir(join(target, ".."), { recursive: true });
152
- await writeFile(target, content);
153
- }
154
- return { root, files: [...exampleFiles.keys()].sort() };
155
- }
156
- const templateRoot = join(TEMPLATES_DIR, template);
157
- const versionRange = `^${version}`;
158
148
  const provider = opts.provider ?? "none";
159
- const notes = readmeNotes(provider);
160
- const nextSteps = nextStepsComment(opts.ide ?? "none", template);
161
- const files = [];
162
- const copyDir = async (fromDir, relDir) => {
163
- await mkdir(join(absRoot, relDir), { recursive: true });
164
- for (const entry of await readdir(join(templateRoot, fromDir), { withFileTypes: true })) {
165
- const fromRel = join(fromDir, entry.name);
166
- if (entry.isDirectory()) {
167
- await copyDir(fromRel, join(relDir, entry.name));
168
- continue;
169
- }
170
- const toName = RENAMES[entry.name] ?? entry.name;
171
- if (provider !== "none" && OFFLINE_ONLY.has(entry.name))
172
- continue;
173
- const toRel = join(relDir, toName);
174
- // A live provider's config replaces the template's own (both builtin templates keep it at the root).
175
- const source = provider !== "none" && relDir === "." && entry.name === "nola.config.ts"
176
- ? providerConfigUrl(provider)
177
- : join(templateRoot, fromRel);
178
- let content = await readFile(source, "utf8");
179
- if (SUBSTITUTED.has(entry.name)) {
180
- content = content
181
- .replaceAll("__NAME__", name)
182
- .replaceAll("__VERSION__", versionRange)
183
- .replaceAll("__START_NOTE__", notes.START_NOTE)
184
- .replaceAll("__PROVIDER_NOTE__", notes.PROVIDER_NOTE)
185
- .replaceAll("__NEXT_STEPS__", nextSteps);
186
- }
187
- await writeFile(join(absRoot, toRel), content);
188
- files.push(toRel.replaceAll("\\", "/"));
189
- }
190
- };
191
- await copyDir(".", ".");
192
- if (!files.includes(".gitignore")) {
193
- await writeFile(join(absRoot, ".gitignore"), await readFile(GITIGNORE_URL, "utf8"));
194
- files.push(".gitignore");
149
+ const runtime = opts.runtime ?? "node";
150
+ const entry = entryFile(template);
151
+ // A live provider's config replaces the template's own; the ledger it replayed goes with it.
152
+ if (provider !== "none") {
153
+ for (const offline of OFFLINE_ONLY)
154
+ files.delete(offline);
155
+ files.set("nola.config.ts", await readFile(providerConfigUrl(provider), "utf8"));
156
+ }
157
+ const manifest = files.get("package.json");
158
+ if (manifest)
159
+ files.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
160
+ // A Bun project (spec 2026-10-14-bun-support): the preload, a `bun <entry>` start script, the devDependency, engines.bun.
161
+ if (runtime === "bun") {
162
+ files.set("bunfig.toml", BUNFIG_TOML);
163
+ const current = files.get("package.json");
164
+ if (current !== undefined)
165
+ files.set("package.json", applyBunRuntime(current, { entry, version }));
195
166
  }
196
- if (envVar !== undefined) {
197
- await writeFile(join(absRoot, ".env.example"), envExample(envVar));
198
- files.push(".env.example");
167
+ await withRecommendedGitignore(files);
168
+ const envVar = vendorEnvVar(template, provider);
169
+ if (envVar !== undefined && !files.has(".env.example"))
170
+ files.set(".env.example", envExample(envVar));
171
+ // The entry file greets the user with the next steps — it is what VS Code opens.
172
+ const program = files.get(entry);
173
+ if (program !== undefined)
174
+ files.set(entry, `${nextStepsComment(opts.ide ?? "none", template, runtime)}\n\n${program}`);
175
+ for (const [relPath, content] of files) {
176
+ const target = join(absRoot, relPath);
177
+ await mkdir(join(target, ".."), { recursive: true });
178
+ await writeFile(target, content);
199
179
  }
200
- return { root, files: files.sort() };
180
+ return { root, files: [...files.keys()].sort() };
201
181
  }
202
182
  //# sourceMappingURL=scaffold.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nola-lang",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Scaffold a new Nola project (npm create nola, or npm create nola-lang)",
5
5
  "keywords": [
6
6
  "nola",
@@ -22,38 +22,88 @@ import type { Person } from "./types.js";
22
22
  // thenable Intent<T>. `await`ing it (or `ask`) runs the inference.
23
23
  infer function extractPerson(.text: string) {
24
24
  // `ask` resolves an intent the way `await` resolves a promise.
25
- const person = ask `Extract the person described in the text`<Person>;
25
+ const person = ask `Extract the person described in the text`: Person;
26
26
  return person;
27
27
  }
28
28
  ```
29
29
 
30
- - `infer function name(...)` — declares a nola function. An optional
31
- backtick instruction goes between name and params:
32
- `` infer function name`instruction`(...) ``. `await` is legal in the body.
30
+ - `infer function name(...)` — declares a nola function. Any bare template
31
+ literal on its own line in the body is a CONTEXT STATEMENT: text every
32
+ later `ask` in the invocation sees, read at each ask. It may continue with
33
+ values: `` `Page` oncall `when the ticket is an outage.` `` (a name, a call
34
+ or a bracketed literal; anything else in parentheses — but `await`, `ask`
35
+ and `this` cannot be values: assign them to a local first). Several are
36
+ legal, anywhere in the body; one before a loop shows the current state on
37
+ every pass. Visibility is lexical, like a `const`: a statement inside a
38
+ block reaches only the asks inside those braces, and statements never
39
+ accumulate across loop passes — each ask sees every statement visible at
40
+ its position exactly once, rendered at that ask. `await` is legal in the
41
+ body.
33
42
  - `.name: T` parameters are CONTEXT parameters: their values are shown to
34
43
  the LLM. Plain (no dot) parameters are ordinary values the LLM never
35
44
  sees. `.` is only legal on infer-function parameters. Rule of thumb:
36
45
  one dot marks a value flowing INTO the model (`.name`); a template after
37
46
  `ask` is a value coming OUT.
38
- - `` ask `prompt`<T> `` — an extractor: asks the LLM for a `T`.
39
- `${...}` interpolation works inside the backticks. Anywhere that is NOT
40
- directly after `ask` — a stored intent, a call-intent argument, an
41
- object/array literal — spell it `` ..`prompt`<T> `` (a typed template
42
- there without the dots is NOLA2014); `` ask ..`prompt` `` is still legal.
47
+ - `` ask `prompt`: T `` — an extractor: asks the LLM for a `T`. The colon
48
+ is on the closing backtick's line (glued or spaced); `` ask `prompt`<T> ``
49
+ is the same construct (write `: T` unless a page shows otherwise).
50
+ `${...}` interpolation works inside the backticks. The `..` is implied
51
+ directly after `ask` and in a call's argument list — a typed template
52
+ that starts an argument, or a value nested in a plain object/array
53
+ literal there, is an extractor: `` createTicket(`a short title`: string, 2) ``.
54
+ Anywhere else — a stored intent, a ternary, a spread, `new` — spell it
55
+ `` ..`prompt`: T `` (a typed `` `x`<T> `` elsewhere is NOLA2014, a
56
+ `` `x`: T `` a syntax error); `` ask ..`prompt` `` and `` fn(..`x`: T) ``
57
+ are still legal.
43
58
  - `` ask fn`hint`(...) `` — or a plain call whose arguments contain an
44
59
  extractor — is a call intent (the LLM fills the extractor-shaped
45
60
  arguments, then the function runs). Only `` fn`hint`(...) `` carries
46
61
  instruction text.
62
+ - `` ask `use` admin `and` cfg.defaults `go`: T `` — an ASK SEQUENCE: a
63
+ bare value between texts is named data the model sees where it stands
64
+ (`${x}` stays your words); `` ask `words` fn(`slot`: T) `` is a call
65
+ intent with words; several `[lists]` union with your words between them;
66
+ the type glues to the last part. The kind follows the parts: texts and
67
+ data → extract, one call → function calling, lists → tool use; a list
68
+ beside a call is reserved (NOLA2019).
69
+ - `` ask `task` [searchFiles, fs.readFile]: T `` — a TOOL-USE ask: the
70
+ functions in brackets between the template and the type are tools the
71
+ model may call, as often as it needs, before it answers; Nola runs each
72
+ call, hands the result back and asks again, the typed answer ends the
73
+ loop. Parentheses RUN a function once (a call intent), brackets OFFER
74
+ functions. Entries are names or member chains (a call written there is
75
+ reserved, NOLA1022); the contract derives from the signature and JSDoc;
76
+ `.withMaxTurns(n)` / config `ask.maxTurns` (10) cap the turns (NOLA3020);
77
+ a tool that throws ends the ask (NOLA3021); the model must support tool
78
+ calls — openai/anthropic/google/"nola", or `mockProvider(…, { tools:
79
+ true })` (NOLA3019 otherwise).
47
80
  - `ask with <modelName> <intent>` — routes one ask through a named model
48
81
  from `nola.config.ts`. The name must be a static identifier. When the
49
82
  platform serves inference (`model: "nola"`) any unconfigured
50
83
  name is legal — it is sent to the platform as a free-form inference profile.
51
- - Prompt templates: inside ANY instruction backticks (marker, extractor,
52
- call hint) a hole that starts with a single dot — `${.member}` — reads
53
- the intent's prompt scope; a literal containing one REPLACES that
54
- intent's prompt block (`${.default}` is the built-in block, `${.next}`
55
- the rest of the prompt). Every other `${expr}` is a lexical value.
56
- See `references/syntax.md` → "Prompt templates".
84
+ - Values in an instruction — two spellings, one rule: `.name` (a contextual
85
+ parameter or `const .name` binding) is DATA, rendered as an `<input
86
+ name>` block the system turn marks as data; a value after text in a
87
+ context statement (`` `Page` oncall ``) or a `${expr}` hole in ANY
88
+ instruction backticks is part of the developer's words, spliced where it
89
+ stands (a string verbatim, a function its name, a call intent its callee
90
+ and arguments, anything else JSON) — the model FOLLOWS it, so interpolate
91
+ only text the developer controls (a team name, a count, a date) and put
92
+ anything a user or a document supplied in `.name`. A context statement is
93
+ read at EACH ask that sees it (module or body alike); an extractor's or
94
+ hint's text when the ask runs. A Nola construct (`ask`, `..`) inside a
95
+ context statement's value or hole, or in a call hint's hole, is NOLA2010
96
+ (written bare right after the text, NOLA1020); a call intent is a legal
97
+ value (a HINTED `` fn`hint`(…) `` one only in parentheses or a `${}` hole —
98
+ bare it is no call intent, and its extractor argument is NOLA2010). There
99
+ is no `${.member}` prompt scope (a dot cannot start a hole's expression).
100
+ See `references/syntax.md` → "Context statements" and "Values in an
101
+ instruction".
102
+ - The prompt itself is the developer's words in four tags — `<context
103
+ function|module>` with `<input name>` blocks, then `<task>` (`<task
104
+ call="fn">` for a call intent); the schema rides structured output, never
105
+ the prompt; `system.message` REPLACES the default system text. See
106
+ `references/syntax.md` → "The prompt".
57
107
 
58
108
  ## Where Nola diverges from TypeScript — hard rules
59
109
 
@@ -64,6 +114,11 @@ infer function extractPerson(.text: string) {
64
114
  `import { f } from "./x.tsi"`. Imports of plain TS use NodeNext style:
65
115
  `import { g } from "./y.js"` (the `.js` extension, even though the source
66
116
  file is `.ts`).
117
+ - `.tsi` runs from source through the Node loader (`nola run`, Node ≥ 22.18)
118
+ or, on Bun, through `@nola-lang/bun`'s preload — `preload =
119
+ ["@nola-lang/bun/preload"]` in `bunfig.toml`, then `bun src/main.tsi`;
120
+ `nola check` / `nola build` stay on Node (`references/config.md`, "Running
121
+ on Bun").
67
122
  - Every `export type` / `export interface` in a `.tsi` is ALSO a runtime
68
123
  value (`User.toJsonSchema()`, `User.validate(v)`, `User.parse(v)`,
69
124
  `User["~standard"]` — Standard Schema AND Standard JSON Schema, so
@@ -72,7 +127,7 @@ infer function extractPerson(.text: string) {
72
127
  with an exported type's name (NOLA2011). `./x.tsi` with no `x.tsi` on disk
73
128
  is the VIEW of `x.ts`: the same module plus those values — import plain-TS
74
129
  types that way when you need their schema. Keep one basename per module.
75
- - An extractor's `<T>` should be a named, JSON-shaped type. The schema comes
130
+ - An extractor's type should be a named, JSON-shaped type. The schema comes
76
131
  from the RESOLVED type, so unions (a `kind` key makes them discriminated),
77
132
  `Partial<T>`, `extends`, `Record<string, T>` and types from packages all
78
133
  work; `Date` is revived to a real `Date`. `Map`/`Set`/`Promise`, functions
@@ -92,5 +147,5 @@ Read these before non-trivial work:
92
147
 
93
148
  - `references/syntax.md` — full grammar and semantics of every construct
94
149
  - `references/patterns.md` — worked examples (extraction, call intents)
95
- - `references/config.md` — `nola.config.ts`, providers, project layout
150
+ - `references/config.md` — `nola.config.ts`, providers, project layout, running on Bun
96
151
  - `references/pitfalls.md` — common errors (NOLA codes) and their fixes
@@ -39,7 +39,9 @@ export default defineConfig({
39
39
  ```
40
40
 
41
41
  `openai({ model })` requires an explicit model; optional fields are `apiKey`,
42
- `apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`.
42
+ `apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`. With a custom
43
+ `fetch`, `apiKey`/`apiKeyEnv` are optional: the fetch authenticates, and without
44
+ a key no vendor auth header is sent (the same rule for every vendor factory).
43
45
 
44
46
  `typesafe()` (typesafe.ai's Jev, reads `TYPESAFE_API_KEY`, model defaults to
45
47
  `jev-latest`) is NOT a chat model: it is the DECISION model that answers the
@@ -124,8 +126,8 @@ export default defineConfig({
124
126
 
125
127
  ```tsi
126
128
  export infer function summarize(.text: string) {
127
- const draft = ask with fast `a rough summary`<string>;
128
- return ask with careful `a polished summary of: ${draft}`<string>;
129
+ const draft = ask with fast `a rough summary`: string;
130
+ return ask with careful `a polished summary of: ${draft}`: string;
129
131
  }
130
132
  ```
131
133
 
@@ -232,11 +234,11 @@ the providers package:
232
234
  "check": "nola check"
233
235
  },
234
236
  "dependencies": {
235
- "@nola-lang/providers": "^0.1.13",
236
- "@nola-lang/runtime": "^0.1.13"
237
+ "@nola-lang/providers": "^0.1.15",
238
+ "@nola-lang/runtime": "^0.1.15"
237
239
  },
238
240
  "devDependencies": {
239
- "nola-lang": "^0.1.13",
241
+ "nola-lang": "^0.1.15",
240
242
  "typescript": "^5.6.0"
241
243
  },
242
244
  "engines": { "node": ">=22.18" }
@@ -270,6 +272,33 @@ for you). Do not mix versions.
270
272
  a bare `npx nola` outside a project resolves an unrelated npm package named
271
273
  `nola`.)
272
274
 
275
+ ### Running on Bun
276
+
277
+ `bun create nola` scaffolds a Bun project: `@nola-lang/bun` in devDependencies,
278
+ `bunfig.toml` with its preload, `start: "bun src/main.tsi"`. Any Node project
279
+ becomes one by adding the same three things:
280
+
281
+ ```toml
282
+ # bunfig.toml
283
+ preload = ["@nola-lang/bun/preload"]
284
+
285
+ [test]
286
+ preload = ["@nola-lang/bun/preload"]
287
+ ```
288
+
289
+ - `bun src/main.tsi` / `bun --watch src/main.tsi` run `.tsi` from source;
290
+ `bun test` files import `.tsi` modules. Bun ≥ 1.3.
291
+ - One-step bundles and executables: a build script with
292
+ `Bun.build({ entrypoints: ["src/main.tsi"], plugins: [nola()], target: "bun",
293
+ compile: { outfile: "my-app" } })` (`import { nola } from "@nola-lang/bun"`).
294
+ - `nola build` output runs under `bun` with nothing added
295
+ (`bun dist/src/main.tsi.js`, `bun build --compile dist/src/main.tsi.js`).
296
+ - `nola check`, `nola build`, `nola console` run on Node (`bun run check`
297
+ launches them there). `nola run` is the Node loader — under Bun it is
298
+ NOLA3015; use the preload. No source map rides a Bun plugin: stack-trace
299
+ lines through `.tsi` are right at module level and in plain functions, ONE
300
+ TOO HIGH inside an `infer function` body, and columns inside an `ask` drift.
301
+
273
302
  ## tsconfig.json
274
303
 
275
304
  ```json