@meantjs/cli 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,514 @@
1
- # Temporary Holding Version
1
+ # @meantjs/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Candid `.did` interfaces to [`@meantjs/schema`] runtimes, with no Rust
4
+ toolchain: the [Meant] compiler and its TypeScript generator compiled
5
+ to WebAssembly, usable as a Node CLI and as a browser library. Data in, data
6
+ out — no eval, no network, and nothing generated is ever executed.
7
+
8
+ > **0.5.0, the `latest` release, and the first under the name
9
+ > `@meantjs/cli`.** Through 0.4.0 this package was published as
10
+ > `@candid-core/cli`, with the command `candid-core-cli`. 0.5.0 is 0.4.0's
11
+ > code under new names: the command is `meant`, and the modules it writes
12
+ > import `@meantjs/schema`, the new name of `@candid-core/schema`. To upgrade
13
+ > from 0.4.0, install the new pair in place of the old one, each with
14
+ > `--save-exact`: `npm install --save-exact @meantjs/schema` and
15
+ > `npm install --save-dev --save-exact @meantjs/cli`. Replace
16
+ > `candid-core-cli` with `meant` in your scripts, and run `meant gen` again:
17
+ > each module's header line and import change and nothing else in it does,
18
+ > and the envelopes are byte-identical to 0.4.0's. Until then, `gen --check`
19
+ > reports each module 0.4.0 wrote as drifted. Run the command as
20
+ > `npx @meantjs/cli`, or as `meant` from a script in your `package.json`;
21
+ > never as a bare `npx meant`, which, where this package is not installed,
22
+ > would offer to download another author's package of that name. In a shell,
23
+ > a bare `meant` is whichever comes first on the `PATH`, which can be the Rust
24
+ > compiler's binary, with `compile` and `validate` only. The strings
25
+ > inside Contract documents keep their names, such as the
26
+ > `candid-core:interface:v1:sha256:…` identities the command prints.
27
+ > Upgrading from 0.3.0 also adds [`didToDocAttributes`](#doc-attributes),
28
+ > which reads `@name(…)` attributes in a `.did`'s doc comments, and an
29
+ > `identities` object on each entry of `gen --json`; it changes every
30
+ > envelope, whose `producer` names `meant` 0.1.0, and the module text where a
31
+ > declared `blob`, `record {}` or `variant {}` is used, which renders as
32
+ > itself. No Contract and no identity moves. Upgrading from 0.2.0 also
33
+ > changes the text of every module with a declaration or an actor (each type
34
+ > is declared under its Candid name, and each method of `Actor` carries its
35
+ > mode) and adds the `project` and `check` commands
36
+ > ([Projection and compatibility](#projection-and-compatibility)).
37
+ > Upgrading from 0.1.0 is a larger break: 0.1.0 emits modules in an older
38
+ > layout with an object-shaped principal type, refuses a whole interface
39
+ > with `ts_generation_refused` for one declaration it cannot represent, and
40
+ > returns no `omitted` list.
41
+ >
42
+ > `@candid-core/schema` 0.3 breaks the 0.2 API (principal values are canonical text, collapsing opts are boxed, every `vec nat8` is a `Uint8Array`, generated modules use a new binding layout and may omit declarations, and the `./actor`, `./transport-icp`, `./forms` and `./labels` subpaths are gone); there is no compatibility layer.
43
+ >
44
+ > Every change is recorded in the [changelog](./CHANGELOG.md), which ships
45
+ > in the tarball: since 0.4.0 under `0.5.0`, since 0.3.0 under `0.5.0` and
46
+ > `0.4.0`, since 0.2.0 under those and `0.3.0`, and since 0.1.0 under those,
47
+ > `0.2.0` and `0.2.0-beta.1`. The migration notes
48
+ > [from 0.4.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-5.html),
49
+ > [from 0.3.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-4.html),
50
+ > [from 0.2.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-3.html)
51
+ > and [from 0.1.0](https://ic-reactor.b3pay.net/candid-core/migrating-from-0-2.html)
52
+ > show what each break means for your code, with before and after code where
53
+ > the compiler shows it.
54
+
55
+ ```sh
56
+ npx @meantjs/cli gen ./service.did -o ./generated
57
+ ```
58
+
59
+ `gen` takes one or more entries (`gen <a.did> [<b.did> …]`), and two flags:
60
+ `--json` prints one machine-readable document instead of the human report,
61
+ and `--check` compares instead of writing. See [The command](#the-command).
62
+
63
+ That emits three things:
64
+
65
+ - **`service.ts`** — the generated `@meantjs/schema` module: one reviewed
66
+ type alias and one invariantly-annotated schema builder per declaration,
67
+ both exported under its Candid name (the alias declared as that name, so
68
+ compiler errors read it; the builder bound as a `$`-prefixed local), with
69
+ the `.did`'s doc comments as JSDoc, and, for a service with an actor, the
70
+ `actor` schema and the `Actor` call interface, each method of which carries
71
+ its mode (`$.WithMode<"query">`); byte-identical to what the Rust-native
72
+ generator emits;
73
+ - **`service.envelope.json`** — the one-document `ContractEnvelope`: the
74
+ canonical Contract plus its field-name table under the
75
+ `org.candid-core.field-names/v1` extension, byte-identical to
76
+ `meant compile ./service.did --envelope`, ready to hand whole to
77
+ `schemaFromContract`;
78
+ - the printed **content-addressed identities** (`contract`, and `interface`
79
+ when the service has an actor) — the same
80
+ `candid-core:contract:v1:sha256:…` addresses the Rust toolchain computes,
81
+ usable to pin an interface and detect drift. `--json` carries them as
82
+ each entry's `identities`.
83
+
84
+ A declaration the generator cannot represent does not cost the whole
85
+ module: it is left out, together with every declaration and actor method that
86
+ references it, and the run still succeeds (exit 0). The CLI prints one
87
+ `warning: omitted …` line per entry on stderr, and the module's header lists
88
+ the same entries as `// Omitted:` lines — for example
89
+ `// Omitted: type Holder (references_omitted via Bad)`. See
90
+ [Omissions](#omissions).
91
+
92
+ Imports are resolved from the entry file's directory: every `.did` beneath
93
+ it is handed to the compiler as an in-memory bundle, so relative imports
94
+ work without any filesystem access from the wasm side. Every generation runs
95
+ twice and the tool refuses to write on any byte mismatch — determinism is
96
+ enforced, not assumed.
97
+
98
+ ## The command
99
+
100
+ ```text
101
+ meant gen <service.did>... [-o <dir>] [--json] [--check]
102
+ ```
103
+
104
+ **Several entries.** Each entry generates its own module and envelope, named
105
+ from its file: `a/service.did` writes `<dir>/service.ts` and
106
+ `<dir>/service.envelope.json`, into the one `-o` directory (default: the
107
+ current directory). The output of an entry is byte-for-byte what a run of that
108
+ entry alone produces. Two decisions here are recommendations the maintainer
109
+ can overturn, each implemented in one place in `bin/cli.js`:
110
+
111
+ - *Bundle roots.* An entry's bundle is the `.did` files beneath the entry's
112
+ own directory, and the bundle limits (256 files, 1 MiB per file, 8 MiB in
113
+ all) apply to each entry's bundle separately. Entries in the same directory
114
+ share one read of it, so they are not counted twice against each other.
115
+ Inside a file, a run of more than 256 consecutive comments between two
116
+ tokens is refused with `resource_limit_exceeded` (resource
117
+ `source_nesting`), since 0.2.0; write a long header as one `/* */` block,
118
+ which counts as one comment.
119
+ - *Duplicate stems.* Two entries with the same stem (`a/service.did` and
120
+ `b/service.did`, or the same file twice) would write the same output, so the
121
+ run is refused as a usage error (exit 64) before any work. Stems compare
122
+ case-insensitively, so the output also checks out cleanly on a
123
+ case-folding filesystem. Give the entries distinct file names, or run them
124
+ into separate `-o` directories.
125
+
126
+ **Files are written only when they change.** An output that already holds
127
+ exactly the generated bytes is left alone (its modification time included),
128
+ and the human report prints `unchanged <path>` instead of `wrote <path>`.
129
+
130
+ **Exit codes.** `0` when every entry generated (a module that omits
131
+ declarations still counts, and lists them), and no `--check` drift was found;
132
+ `1` when any entry failed or any checked file drifted; `64` for a usage error,
133
+ with the usage on stderr and nothing on stdout. A failing entry does not stop
134
+ the others: every entry is attempted, and the exit code is `1` if any failed.
135
+ Without `--json`, a failed entry prints its `{ ok: false, diagnostics }`
136
+ document on stdout, exactly as a single-entry run always has (several failed
137
+ entries print several documents; use `--json` for one machine-readable
138
+ result).
139
+
140
+ **Nothing is read from stdin, and nothing is written outside `-o`.** The CLI
141
+ never prompts and never waits on input, so it is safe to run as a child process
142
+ with any stdin.
143
+
144
+ ### `--check`
145
+
146
+ Generate in memory and compare byte-for-byte with the files on disk. Nothing
147
+ is written (not even the output directory), and the run exits `1` if any
148
+ output file is missing or differs, listing each on stderr (`missing <path>` or
149
+ `drifted <path>`):
150
+
151
+ ```sh
152
+ npx @meantjs/cli gen ./service.did -o ./generated --check
153
+ ```
154
+
155
+ Use it in CI to fail when committed output is stale. A compile error is a
156
+ failed entry, not drift. Stale files for entries you no longer list are not
157
+ looked for.
158
+
159
+ ### `--json`
160
+
161
+ Prints exactly one JSON document on stdout and nothing else; stderr stays
162
+ empty. Each compiled entry carries the identities the human report prints.
163
+ Combine it with `--check`. A usage error (exit 64) prints no document.
164
+
165
+ ```json
166
+ {
167
+ "schemaVersion": 1,
168
+ "ok": true,
169
+ "check": false,
170
+ "entries": [
171
+ {
172
+ "entry": "a/service.did",
173
+ "status": "written",
174
+ "module": "generated/service.ts",
175
+ "envelope": "generated/service.envelope.json",
176
+ "identities": {
177
+ "contract_id": "candid-core:contract:v1:sha256:…",
178
+ "interface_id": "candid-core:interface:v1:sha256:…"
179
+ },
180
+ "omitted": [],
181
+ "diagnostics": []
182
+ }
183
+ ],
184
+ "drift": []
185
+ }
186
+ ```
187
+
188
+ - `schemaVersion` is `1`. It changes only when an existing field's meaning or
189
+ shape changes; adding a field does not change it. Refuse a version you do
190
+ not know.
191
+ - `ok` is true exactly when no entry is `failed` or `drifted`, and the exit
192
+ code is `0` exactly when `ok` is true.
193
+ - `check` records whether `--check` was given.
194
+ - `entries` has one object per entry, in command-line order, with `entry` (as
195
+ given on the command line), `module` and `envelope` (the paths it writes or
196
+ compares: `-o` joined with `<stem>.ts` and `<stem>.envelope.json`, present
197
+ for a failed entry too), and:
198
+ - `status`: `written` (a file was created or changed), `unchanged` (both
199
+ files already held these bytes), `drifted` (`--check` only: a file is
200
+ missing or differs; nothing was written) or `failed`;
201
+ - `identities` (new in 0.4.0): `{ contract_id, interface_id }`, equal to the envelope's
202
+ `contract.identities` and to what the human report prints, with the key
203
+ names `project --json` and `check --json` use. `interface_id` is absent
204
+ when the `.did` has no actor. Present on every entry that compiled,
205
+ `--check` included; absent on a failed entry;
206
+ - `omitted`: what the module leaves out, exactly as `didToModule` returns it
207
+ (see [Omissions](#omissions)), `[]` for a failed entry. Omissions never make
208
+ an entry fail;
209
+ - `diagnostics`: for a failed entry, the compiler's diagnostics unchanged in
210
+ shape (see [Failures](#failures)); `[]` otherwise. A failure found by the
211
+ CLI itself uses the same item shape with one of the codes
212
+ `did_file_read_error` (the entry's directory cannot be read),
213
+ `did_source_not_found` (the entry is not a `.did` file there),
214
+ `resource_limit_exceeded` (the bundle bounds), `nondeterministic_output`
215
+ (two generations disagreed) or `internal_error` (anything unexpected, such as
216
+ an output that cannot be written; it carries no `phase`).
217
+ - `drift` lists every path `--check` found missing or different, in entry
218
+ order, module before envelope; always `[]` without `--check`.
219
+
220
+ The same types ship as `CliReport`, `CliEntryReport` and `CliIdentities` in
221
+ the package's declarations. A run is deterministic: the same inputs give the same files and
222
+ the same document, byte for byte, from any working directory. There are no
223
+ timestamps, and every path in the document and in the human report (including
224
+ the messages of failures such as an unreadable or missing entry) is spelled as
225
+ you passed it, or `-o` joined with a file name, never resolved to an absolute
226
+ host path: an absolute path appears only if you passed one.
227
+
228
+ ## The library
229
+
230
+ ```js
231
+ import { didToContract, didToModule } from "@meantjs/cli";
232
+
233
+ const envelope = await didToContract("service : { ping : () -> () };");
234
+ // → the ContractEnvelope document ("contract" in envelope), or
235
+ // { ok: false, diagnostics } with the compiler's diagnostics verbatim.
236
+
237
+ const generated = await didToModule({
238
+ entry: "main.did",
239
+ files: {
240
+ "main.did": 'import "types.did";\nservice : { get : () -> (Item) query };',
241
+ "types.did": "type Item = record { id : nat };",
242
+ },
243
+ });
244
+ // → { ok: true, module, omitted } with the generated TypeScript text and
245
+ // what it left out (empty here), or { ok: false, diagnostics }.
246
+ ```
247
+
248
+ Both functions accept either a string of Candid text or
249
+ `{ entry, files: { name: text } }` for a multi-file bundle. In a browser the
250
+ wasm is fetched relative to the module automatically; call
251
+ `init(bytesOrUrl)` first to supply it yourself. Feed `didToContract`'s
252
+ result straight to `schemaFromContract` from [`@meantjs/schema`] — the
253
+ envelope carries the field names, so one document is the whole hand-off:
254
+
255
+ ```js
256
+ import { didToContract } from "@meantjs/cli";
257
+ import { schemaFromContract } from "@meantjs/schema/contract";
258
+
259
+ const built = schemaFromContract(await didToContract(didText));
260
+ ```
261
+
262
+ ## Projection and compatibility
263
+
264
+ > **New in 0.3.0.** `project`, `check`, `projectDid` and `checkCompatible`
265
+ > are not in 0.2.0, which exits 64 with its usage for either command; the
266
+ > [changelog](./CHANGELOG.md) records them under `0.3.0`.
267
+
268
+ Two commands for an interface you do not own, such as a canister's published
269
+ `.did`. Both work on files already on disk: the tool never fetches anything,
270
+ so getting the live interface is your build step's job.
271
+
272
+ ```text
273
+ meant project <in.did> --methods <a,b,...> -o <out.did> [--json]
274
+ meant check <written.did> --against <live.did> [--json]
275
+ ```
276
+
277
+ **`project`** writes a `.did` holding only the methods you name and every
278
+ declaration they reach, as one self-contained file (imports are inlined), and
279
+ prints the interface identities of the input and of the projection. Feed the
280
+ result to `gen` like any `.did`: the generated `Actor` lists only those
281
+ methods, each with its mode. The output is deterministic: the same input and
282
+ the same set of names give the same bytes, whatever order you name them in.
283
+ Declaration names, doc comments and argument names are kept; a service
284
+ class's init arguments are not, since a client never sends them. Methods are
285
+ written in name order, a service import's among the entry's. An unknown
286
+ method name, or an empty list (`--methods ""`), fails with exit 1 and writes
287
+ nothing: each distinct unknown name gets one `unknown_method` diagnostic
288
+ naming it, and the first diagnostic lists the service's methods in its
289
+ `notes`, once; an empty list gets `empty_method_list`. The unknown-method
290
+ diagnostics are bounded at 4 MiB in all, each counted as the JSON writes it
291
+ (the list of methods aside): the name at which they would pass it, and every
292
+ one after it, are counted in one `resource_limit_exceeded` diagnostic
293
+ (`projection_diagnostic_bytes`) instead of reported. A projection must
294
+ compile again, so it is at most 1 MiB, the compiler's bound on one source: one
295
+ that would be larger fails with exit 1 and `resource_limit_exceeded`
296
+ (`projection_bytes`, whose `observed` is where writing stopped) and writes
297
+ nothing. Each name, requested or reached, is
298
+ looked up by binary search among names sorted once, so no lookup scans the
299
+ service or the declarations. `-o` is required, and may not be,
300
+ or become, a source of the input: `project` reads the input as `gen` does (the
301
+ file and every `.did` beneath its directory), so an `-o` that names one of
302
+ those files by any path, symlink or hard link, or a new `.did` anywhere beneath
303
+ that directory, fails with exit 1 and `output_is_input` (`path` is the `-o`
304
+ given, `notes` the existing sources it names, empty for a new file) and writes
305
+ nothing. Write the projection outside the input's directory. An
306
+ output that cannot be read or written (a directory, a read-only parent, a full
307
+ disk) fails with exit 1 and `output_write_failed`, whose `notes` hold the
308
+ system error code, such as `EISDIR` or `EACCES`; without `--json` it is one
309
+ `cannot write …` line on stderr, as a source that cannot be read is
310
+ `cannot read …`. The file is written only when it changes, and an existing
311
+ file of another size is replaced without being read; `--json` prints
312
+ `{ ok, output, status, methods, input, projection }` instead of the report,
313
+ and `{ ok: false, diagnostics }` for any failure.
314
+
315
+ ```sh
316
+ npx @meantjs/cli project ./live/ledger.did --methods icrc1_balance_of,icrc1_transfer -o ./src/ledger.did
317
+ ```
318
+
319
+ **`check`** exits `0` when the live interface is still a Candid subtype of the
320
+ written one, and `1` otherwise. Every method of the written `.did` must exist
321
+ in the live one with the same mode, its arguments contravariant and its
322
+ results covariant; methods only the live service has are ignored, and so is
323
+ a class's init. So an added method or an added `opt` result field passes,
324
+ and a removed method, a changed mode, or a result that gained a variant arm
325
+ fails. A projection is not the only valid written side: a hand-written
326
+ subset `.did` checks the same way. The report prints both interface
327
+ identities on stdout, one line per finding on stderr, and a verdict line:
328
+
329
+ ```text
330
+ written: candid-core:interface:v1:sha256:…
331
+ live: candid-core:interface:v1:sha256:…
332
+ error: icrc1_transfer: method_incompatible at $results[0].Err.TooOld: the other variant has no such arm
333
+ incompatible: 1 error(s), 0 warning(s)
334
+ ```
335
+
336
+ Each finding names its method and a stable code: `method_missing`,
337
+ `mode_changed` and `method_incompatible` are errors; `special_opt_rule` is a
338
+ warning, for a change Candid accepts only by reading the value under an `opt`
339
+ as `null` (such as `opt nat` becoming `opt text`), so it passes but loses the
340
+ data at that path, and it is reported at every such path along which no pair
341
+ of types repeats (a changed alias under each field that uses it; a recursive
342
+ type up to where a path comes back round); `resource_limit_exceeded` fails a
343
+ method whose check reached a bound, its depth (384 pairs on one path) or its
344
+ work (1,000,000 steps, a step being one unit of work done: a pair of types
345
+ visited, a field, arm, method or value examined or passed over,
346
+ or one path segment a warning or a failure copies, or a warning
347
+ re-examines). A step reads no name: service methods are paired by ranks
348
+ computed once per check, and a field's name is read by its position. So a
349
+ step's cost does not grow with the length of the names: it does a bounded
350
+ amount of work, and its lookups in the maps of type pairs the walk keeps take
351
+ time logarithmic in their size. Past 1,000
352
+ warnings a method reports the first 1,000 and one `resource_limit_exceeded`
353
+ warning (`check_warnings`) saying the rest were dropped; its verdict stands.
354
+ The check as a whole is bounded too, so a service with many methods cannot
355
+ multiply those per-method bounds: 10,000,000 steps in all, one more per
356
+ method decided (`check_total_steps`), 10,000 `special_opt_rule` warnings in all
357
+ (`check_total_warnings`), and 4 MiB of reported diagnostics, each counted as
358
+ the JSON response writes it, keys and indentation included
359
+ (`check_output_bytes`), measured only up to the first string that passes that
360
+ bound. Methods are checked in name order. When the step or warning bound is
361
+ reached, the method at which it was reached and every method after it fail
362
+ closed: each gets one `resource_limit_exceeded` error naming the bound, with
363
+ its limit and the observed value, and nothing else. Those count against the
364
+ 4 MiB like any other diagnostic: once a method's diagnostics would pass it,
365
+ that method and every method after it are counted in one last
366
+ `resource_limit_exceeded` error naming `check_output_bytes`, which names no
367
+ method and holds the count in `unreported_methods` (without `--json`, the one
368
+ line with no method name). Either way the check exits `1`, and the response is
369
+ at most 4 MiB and 1,016 bytes, however many methods the written side has.
370
+ Below these bounds a method's findings do not depend on the other methods;
371
+ once one is reached, they do. So the walk's time grows with its steps (times that
372
+ logarithm) and the reported text's with its bytes; compiling both sides,
373
+ within the compiler's own limits, and ranking the method names are done once
374
+ per check, before the walk.
375
+ `method_incompatible` and `special_opt_rule` carry the path
376
+ into the method's type where the check failed: `$args[i]` or `$results[i]`,
377
+ then `.name` per record field or variant arm (`["name"]` when it is not an
378
+ identifier, `[id]` for a numeric label), `[*]` per `vec` element, `?` per
379
+ `opt` content, and `::args[i]`, `::results[i]` or `::name` into a `func` or
380
+ `service` type. `--json` prints the `checkCompatible` document below.
381
+
382
+ The live interface identity tells an unchanged interface from one that
383
+ changed compatibly: `project` prints the input's, so record it when you write
384
+ the projection, and compare it with what `check` prints later.
385
+
386
+ ```js
387
+ import { checkCompatible, projectDid } from "@meantjs/cli";
388
+
389
+ const projected = await projectDid(liveDid, ["icrc1_balance_of", "icrc1_transfer"]);
390
+ // → { ok: true, did, methods, input, projection } — the text, the methods it
391
+ // holds, and { contract_id, interface_id } of the input and the projection;
392
+ // or { ok: false, diagnostics }.
393
+
394
+ const report = await checkCompatible(writtenDid, liveDid);
395
+ // → { ok: true, compatible, written, live, diagnostics }, each diagnostic
396
+ // { code, severity, method, path?, message } (the one past the 4 MiB
397
+ // bound has no method, and counts the methods left in
398
+ // unreported_methods); or { ok: false, input, diagnostics } when one side
399
+ // does not compile (`input` names it).
400
+ ```
401
+
402
+ Both take Candid text or `{ entry, files }`, as `didToModule` does. The check
403
+ is one Rust implementation compiled into this package's WebAssembly, tested
404
+ against the subtype check of the `candid` crate it pins and against the one
405
+ `@meantjs/schema`'s decoder runs on a `func` or `service` reference. Where the `candid` crate accepts a pair that is not a
406
+ subtype (its coinductive memo can keep a pair proven under an assumption it
407
+ later retracted), this check refuses it, as the decoder does.
408
+
409
+ ## Doc attributes
410
+
411
+ > **New in 0.4.0.** `didToDocAttributes` and the types it returns are not in
412
+ > 0.3.0; the [changelog](./CHANGELOG.md) records them under `0.4.0`.
413
+
414
+ `didToDocAttributes(sources)` compiles the sources and reads the
415
+ `@name(key = value, …)` lines in their doc comments, as specified in
416
+ [`docs/doc-attributes.md`](https://github.com/B3Pay/Meant/blob/main/docs/doc-attributes.md):
417
+
418
+ ```candid
419
+ service : {
420
+ /// Send tokens to another principal.
421
+ /// @intent(title = "Send", template = "Send {amount} to {to}")
422
+ /// @amount(at = "amount", decimals = query(decimals))
423
+ send : (record { to : principal; amount : nat }) -> ();
424
+ }
425
+ ```
426
+
427
+ ```js
428
+ import { didToDocAttributes } from "@meantjs/cli";
429
+
430
+ const result = await didToDocAttributes(didText);
431
+ if (result.ok) {
432
+ for (const item of result.doc_attributes.items) {
433
+ // item.path is "actor.methods[0]"; item.prose is ["Send tokens to another principal."]
434
+ for (const attribute of item.attributes) console.log(attribute.name, attribute.args);
435
+ }
436
+ }
437
+ ```
438
+
439
+ Success is `{ ok: true, contract, source_info, doc_attributes }`: the
440
+ Contract and SourceInfo that `meant compile <path>` prints, and every
441
+ documented item (the actor, a declaration, a field, a method) with its prose
442
+ lines and its attributes in order. An attribute is `{ name, doc_line, args }`,
443
+ and each argument value is tagged: `text`, `number` (the digits as written,
444
+ never a float), `bool`, `ident`, `call` (`query(decimals)`), `list` or
445
+ `record`. The compiler gives no name a meaning: the tool that reads them
446
+ decides which it knows.
447
+
448
+ A line counts as an attribute only when it starts with `@`, a lowercase
449
+ name, then `(` or nothing: `@param amount the value` and `@amount (…)` stay
450
+ prose. Once a line is an attribute it must parse to its end, so a malformed
451
+ one fails the call with `doc_attribute_malformed`, and a repeated key with
452
+ `doc_attribute_duplicate_key`; each names its item's `path`. Comments never
453
+ change `contract_id` or `interface_id`.
454
+
455
+ ## Omissions
456
+
457
+ `didToModule`'s success carries `omitted`, an array of
458
+ `{ kind, name, reason, via? }` — declarations first, then methods, each
459
+ sorted by name, and empty when nothing is left out:
460
+
461
+ - `kind` is `"declaration"` or `"method"` (a method of the actor's service,
462
+ dropped from both the `actor` schema and the `Actor` type);
463
+ - `reason` is one of a closed set: `reserved_field_name` (a record field or
464
+ variant arm named like the `_N_` numeric-id rendering, which as a schema
465
+ key would read back as the wrong wire id), `ambiguous_variant_arm` (an arm
466
+ whose payload is a declared `opt` of an uninhabited type, whose generated
467
+ type could not tell it from a bare tag), `reserved_export_name` (a
468
+ declaration named `actor` or `Actor`, the module's own export names), or
469
+ `references_omitted` (the entry references another omitted declaration);
470
+ `invalid_declaration_name` completes the set for Contract documents, which
471
+ Candid source never produces;
472
+ - `via`, present only for `references_omitted`, names the omitted declaration
473
+ referenced.
474
+
475
+ Omission follows every reference, nested `func` and `service` types
476
+ included, up to the containing declaration: a record holding a
477
+ `service { f : (Bad) -> () }` is omitted whole rather than losing `f`,
478
+ because a service value's wire type is its full method table. Everything the
479
+ module still emits is exactly what it would be had the omitted declarations
480
+ never existed. `schemaFromContract` leaves out the same entries for the same
481
+ contract and lists them the same way.
482
+
483
+ ## Failures
484
+
485
+ Nothing throws for data errors and nothing half-succeeds: a compile failure
486
+ is `{ ok: false, diagnostics }` with [Meant]'s structured diagnostics
487
+ passed through verbatim (stable codes such as `did_parse_error`,
488
+ `did_file_read_error` analogues, resource bounds included), a malformed
489
+ request is `invalid_request`, and `ts_generation_refused` is reserved for a
490
+ generator refusal of an invalid contract graph — which a contract compiled
491
+ from Candid source never is, so an unrepresentable declaration is an
492
+ omission, not a failure. The CLI exits 1 when any entry failed, printing each
493
+ failing document on stdout (or all of them in the `--json` document);
494
+ usage errors exit 64 with usage on stderr, in the native binary's
495
+ convention.
496
+
497
+ ## Version pairing
498
+
499
+ This package embeds exact revisions, recorded per release in
500
+ [CHANGELOG.md](./CHANGELOG.md): the compiler crate (`meant`, published as
501
+ `candid-core` before its rename) and the
502
+ `candid-core-ts` generator (unpublishable on crates.io by design; embedding
503
+ it in this wasm artifact is an owner decision recorded on the repository's
504
+ issue tracker) are built from one repository commit, and the parity gates
505
+ prove the emitted module and contract are byte-identical to that commit's
506
+ Rust-native outputs over the golden fixtures.
507
+
508
+ The `@meantjs/schema` peer is an exact version, not a range, while the two
509
+ packages move in lockstep: each schema release is paired with a new release
510
+ of this package that names it, so a generated module always meets the runtime
511
+ it was generated for.
512
+
513
+ [Meant]: https://github.com/B3Pay/Meant
514
+ [`@meantjs/schema`]: https://www.npmjs.com/package/@meantjs/schema