@nodegx/export 0.0.0-stage → 0.2.6

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 (53) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +76 -2
  3. package/dist/analyze/appState.d.ts +303 -0
  4. package/dist/analyze/plan.d.ts +3904 -0
  5. package/dist/analyze/querySettings.d.ts +75 -0
  6. package/dist/analyze/reach.d.ts +41 -0
  7. package/dist/analyze/scriptUses.d.ts +29 -0
  8. package/dist/catalog.d.ts +129 -0
  9. package/dist/cli/args.d.ts +100 -0
  10. package/dist/cli/exitCodes.d.ts +125 -0
  11. package/dist/cli/run.d.ts +30 -0
  12. package/dist/cli.inputs.json +203 -0
  13. package/dist/cli.mjs +62547 -0
  14. package/dist/cli.mjs.map +7 -0
  15. package/dist/emit/animateLib.d.ts +43 -0
  16. package/dist/emit/component.d.ts +166 -0
  17. package/dist/emit/dateLib.d.ts +45 -0
  18. package/dist/emit/emitApp.d.ts +38 -0
  19. package/dist/emit/idLib.d.ts +68 -0
  20. package/dist/emit/kits.d.ts +91 -0
  21. package/dist/emit/mediaLib.d.ts +75 -0
  22. package/dist/emit/naming.d.ts +76 -0
  23. package/dist/emit/parseLib.d.ts +108 -0
  24. package/dist/emit/preflight.d.ts +174 -0
  25. package/dist/emit/readme.d.ts +51 -0
  26. package/dist/emit/report.d.ts +309 -0
  27. package/dist/emit/scaffold.d.ts +83 -0
  28. package/dist/emit/statesLib.d.ts +52 -0
  29. package/dist/emit/style.d.ts +181 -0
  30. package/dist/emit/utilLib.d.ts +78 -0
  31. package/dist/errorMessage.d.ts +28 -0
  32. package/dist/index.cjs +61649 -0
  33. package/dist/index.cjs.map +7 -0
  34. package/dist/index.d.ts +37 -0
  35. package/dist/index.mjs +61570 -0
  36. package/dist/index.mjs.map +7 -0
  37. package/dist/ir/types.d.ts +567 -0
  38. package/dist/ledger.cjs +971 -0
  39. package/dist/ledger.cjs.map +7 -0
  40. package/dist/ledger.d.ts +75 -0
  41. package/dist/ledger.mjs +948 -0
  42. package/dist/ledger.mjs.map +7 -0
  43. package/dist/node-catalog.json +65370 -0
  44. package/dist/parse/kitSource.d.ts +135 -0
  45. package/dist/parse/parseModules.d.ts +42 -0
  46. package/dist/parse/parseProject.d.ts +13 -0
  47. package/dist/react19/VERSION +2 -0
  48. package/dist/react19/react-dom.production.min.js +51 -0
  49. package/dist/react19/react.production.min.js +21 -0
  50. package/dist/scriptFiles.d.ts +30 -0
  51. package/dist/structurePorts.d.ts +70 -0
  52. package/dist/write/writeExport.d.ts +67 -0
  53. package/package.json +53 -4
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `src/lib/parse.ts` — `Parse CSV`, `To CSV`, `Parse XML` and `Parse Feed`, emitted into the app (EXP-011 §75).
3
+ *
4
+ * The same shape as {@link ./utilLib.ts}: constant text, shipped only where a component calls into it, and a
5
+ * **separate** module from `util.ts` on that file's own rule (a project that formats a string should not ship
6
+ * a CSV tokeniser, and an XML parser even less). The four are `util-call`s in the plan — each node is a
7
+ * function of its inputs and holds nothing else — with one difference from the three small utilities:
8
+ * every one of them has SEVERAL value outputs, so the call carries the output port's name as its trailing
9
+ * literal argument (`DATE_NODES`' selector, one family over) and the helper answers that port. A one-entry
10
+ * memo inside each helper keeps a page that reads three outputs of one node to one parse.
11
+ *
12
+ * A transcription of three runtime modules and four node files:
13
+ *
14
+ * - `noodl-runtime/src/csv.ts` — `stripBOM`, the contiguity-checking `tokenise`, `parseCSV` (the
15
+ * unterminated-quote sentence verbatim), `rowsToRecords`, `unionOfKeys`, `quoteCell`, `toCSV`, `rowsToCSV`.
16
+ * - `data/parsecsv.ts` / `data/tocsv.ts` — the abstain (`textSupplied` / `itemsSupplied`: nothing arrived ⇒
17
+ * no Items, Count 0, no Error — Failure Contract §2), the setters' fallbacks (`delimiter || ','`,
18
+ * `!!hasHeader`, a `stringlist` Columns split on commas and trimmed), rows-of-cells written cell for cell.
19
+ * - `noodl-runtime/src/xml.ts` — `scanForHostileConstructs` (the two refusals, sentences verbatim) and
20
+ * `parseXML` over **`fast-xml-parser` at the interpreter's own pin, 5.11.2**, with the interpreter's own
21
+ * options. 🔴 The one third-party dependency an emitted lib has ever declared: FED-001 R1 took the library
22
+ * over a hand-rolled scanner for the runtime (entity expansion is a denial-of-service hole, not a wrong
23
+ * answer), and a transcription of an XML parser would be a second parser that drifts — so
24
+ * {@link PARSE_DEPENDENCIES} is added to the exported `package.json` exactly when a component calls
25
+ * `parseXml` or `parseFeed`.
26
+ * - `noodl-runtime/src/feed.ts` — the whole module: RSS/Atom/RDF detection (RDF first, its `<item>` a
27
+ * SIBLING of `<channel>`), the readers, `toISODate`'s retry, the Atom link family, images, enclosures,
28
+ * tags, authors, the identity ladder and FNV-1a `hashId`. 🔴 **The ladder's separator is a NUL byte.**
29
+ * `feed.ts:365` holds a literal U+0000 between the quotes, which every editor draws as a space; the
30
+ * transcription writes `'\u0000'` because FED-002's unique index keys on the id and a space would mint a
31
+ * different id for every item on the hash rung. Found by the §A differential, not by reading.
32
+ *
33
+ * ⚠️ **The one divergence, and where it is reported.** On a FAILED parse the interpreter keeps the previous
34
+ * successful answer on its value outputs beside the Error (`items` is "replaced only on a successful
35
+ * parse"); a pure call has no previous answer, so Items is absent and Count is 0 beside the Error. The
36
+ * planner files a note at every parse node whose text arrives by wire — a literal cannot have a previous
37
+ * parse. On a FIRST failure the two agree exactly.
38
+ *
39
+ * `Changed` and `Failure` are recomputation pulses and defer with the date family's sentence
40
+ * ({@link PARSE_SIGNAL_SENTENCES}); a wire off one is not a chain the export can hang anything on.
41
+ */
42
+ /** Where the module lands in the exported app. */
43
+ export declare const PARSE_LIB_PATH = "src/lib/parse.ts";
44
+ /** The exported helpers, one per node. Sorted — the import list is sorted too. */
45
+ export declare const PARSE_HELPERS: readonly ["parseCsv", "parseFeed", "parseXml", "toCsv"];
46
+ export type ParseHelper = (typeof PARSE_HELPERS)[number];
47
+ export declare function isParseHelper(fn: string): fn is ParseHelper;
48
+ /**
49
+ * The npm dependency the exported app takes on when XML is parsed — the interpreter's own pin
50
+ * (`noodl-runtime/package.json`; `xml.ts`'s module comment says why 5.x and what it costs). Exact, not a caret,
51
+ * so the exported app and the interpreter read one parser. 4.5.7 until 2026-10-26: P112-OWN008-AUDIT (its
52
+ * advisory reached every exported app's own `npm audit`) and BETA-S11-ENTITYCAP (it refused a feed past a
53
+ * thousand `&amp;`s).
54
+ */
55
+ export declare const PARSE_DEPENDENCIES: Readonly<Record<string, string>>;
56
+ /** Which helpers need {@link PARSE_DEPENDENCIES}. `parseCsv` and `toCsv` need nothing. */
57
+ export declare const PARSE_HELPERS_NEEDING_DEPENDENCY: ReadonlySet<string>;
58
+ /**
59
+ * Per output port: the static TypeScript type the helper's overload answers (`exprTsType`'s vocabulary —
60
+ * `unknown` wherever the value can be `undefined`, for `dateToString`'s reason: a single-placeholder String
61
+ * Format collapses a `string`-typed expression to the bare value, and that would print the word
62
+ * "undefined" where the interpreter substitutes `''`), and whether it can be `undefined` at all
63
+ * (`maybeUndefinedExpr`'s table, one family over).
64
+ *
65
+ * 🔴 Must agree with the overloads in {@link parseLibSource}. `items` is `any[]` on both CSV and Feed —
66
+ * the CSV rows' shape depends on Has Header, and the feed rows are typed inside the module only, so the
67
+ * emitted component never has to name a type it does not import. {@link FEED_ITEM_FIELDS} is what lets a
68
+ * repeater over a feed drop a mapped field the rows do not carry, by name.
69
+ */
70
+ export declare const PARSE_OUTPUTS: Readonly<Record<ParseHelper, Readonly<Record<string, {
71
+ tsType: string;
72
+ maybeUndefined: boolean;
73
+ }>>>>;
74
+ /** The fields every feed item carries (`FeedItem` in the emitted module; `parsefeed.ts`'s Items description). */
75
+ export declare const FEED_ITEM_FIELDS: readonly string[];
76
+ /**
77
+ * The selector a `util-call` into this module carries: its trailing literal argument. `undefined` for a
78
+ * call that is not a parse helper, or whose last argument is not a string literal — neither can be built by
79
+ * the planner, so a lookup that answers `undefined` is a programming error upstream, not an authored shape.
80
+ */
81
+ export declare function parseSelectorOf(expr: {
82
+ fn: string;
83
+ args: ReadonlyArray<{
84
+ kind: string;
85
+ value?: unknown;
86
+ }>;
87
+ }): string | undefined;
88
+ /** `exprTsType` for a parse call — `unknown` for a selector the table does not know, never a throw. */
89
+ export declare function parseOutputTsType(fn: ParseHelper, selector: string | undefined): string;
90
+ /** `maybeUndefinedExpr` for a parse call — `true` for a selector the table does not know: the guarded answer. */
91
+ export declare function parseOutputMayBeUndefined(fn: ParseHelper, selector: string | undefined): boolean;
92
+ /**
93
+ * The deferral sentence for a signal output consumed as a wire — the date family's `DATE_SIGNAL_OUTPUTS`,
94
+ * one family over, and for the same reason: `Changed` and `Failure` announce a recomputation, and a
95
+ * recomputation in the emitted component is a render, not an event a chain can hang off. `Failure` is a
96
+ * value this slice renders as an absent one (the Error output) rather than a pulse it can fire.
97
+ */
98
+ export declare const PARSE_SIGNAL_SENTENCES: Readonly<Record<string, string>>;
99
+ /**
100
+ * The module's source.
101
+ *
102
+ * ⚠️ A plain string array rather than a template literal, for `dateLib.ts`'s stated reason: the emitted body
103
+ * needs no interpolation, and keeping it out of an interpolation context means a future edit cannot
104
+ * accidentally interpolate the generator's own scope into the exported app. Generated from a real `.ts`
105
+ * file that was typechecked under the exported app's own strict options and graded against the runtime
106
+ * modules by `tests/the-parsers.test.ts` §A; edit the strings and the test grades the edit.
107
+ */
108
+ export declare function parseLibSource(): string;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The pre-flight summary — EXP-004's *before you export* surface.
3
+ *
4
+ * ## What this is for
5
+ *
6
+ * `EXPORT-REPORT.md` (see {@link module:emit/report}) is the account an author reads *after* the
7
+ * export, inside the app they now own. EXP-004 asks for one more moment: **before** they pick a
8
+ * directory and write sixty files, tell them what they are about to get, so the decision to
9
+ * proceed is made with accurate expectations rather than with the word "export".
10
+ *
11
+ * Same facts, earlier moment, and much shorter — this is a thing someone reads in a dialog and
12
+ * then clicks a button, not a document they study.
13
+ *
14
+ * ## 🔴 It is exact, and it is deliberately not an estimate
15
+ *
16
+ * EXP-004's own wording is *"pre-flight **estimate**"*, and its testing plan asks that the
17
+ * estimate "match the actual outcome within a reasonable margin". **That premise does not survive
18
+ * contact with this pipeline, and the honest thing is to say so rather than to build a
19
+ * deliberately worse predictor so that a margin exists to measure.**
20
+ *
21
+ * `emitApp` is pure: it reads nothing, writes nothing, and returns the files as strings. Writing
22
+ * them to disk is the caller's separate act (`write/writeExport.ts`, which both front doors call).
23
+ * So the cost of knowing the real answer is the cost of running the generator — about a tenth of a
24
+ * second on every project in the corpus — and there is no reason whatever to approximate a number
25
+ * you can simply compute.
26
+ * A margin here would be a defect, not a tolerance.
27
+ *
28
+ * ⚠️ **The cheap alternative was measured before this was written, because it is the one a future
29
+ * session will reach for.** The obvious "estimate" is to stop after `planProject` and skip
30
+ * emission — that is where most refusals are decided (§20.1), and it looks like the same answer
31
+ * for less work. Over the seven fixtures it is **not** the same answer: the plan layer files 28
32
+ * refusals where the export files 32. The missing four are all decided during emission — three
33
+ * `parameter … has no style/content mapping` on `puppy-test-3` and one retired kit parameter on
34
+ * `kits` — and they are invisible to any analysis that stops before the generator runs.
35
+ *
36
+ * 🔴 **That divergence happens to be invisible at component level today, and that is a fact about
37
+ * the corpus rather than a property of the layers.** No fixture currently has a component whose
38
+ * *only* refusals are emit-layer ones, so a plan-layer pre-flight would still sort every component
39
+ * onto the correct side of "clean". Nothing makes that hold: a component with one unmapped
40
+ * `padding` and no dropped wires would be called clean and export dirty. The control pair in
41
+ * `tests/preflight.test.ts` asserts the two layers **disagree** on the totals, which is what keeps
42
+ * this paragraph honest — a cheap-layer rewrite reddens a row instead of quietly under-reporting.
43
+ *
44
+ * ## The framing rules are `report.ts`'s, and they are the actual work
45
+ *
46
+ * - **Lead with what worked.** Counts and whole components first; a project with nothing to report
47
+ * never renders a list of problems, because there are none to render.
48
+ * - **Be specific, not statistical.** No percentages and no confidence score. Components are named
49
+ * and their refusals counted.
50
+ * - **Never let unverified code look verified.** Nothing has been run here either — less so, in
51
+ * fact, since at this point nothing has even been written.
52
+ *
53
+ * @module emit/preflight
54
+ */
55
+ import { Catalog } from '../catalog';
56
+ import { ExportIR, RefusedNode } from '../ir/types';
57
+ import { EmittedApp } from './emitApp';
58
+ import { BackendMode, ExportCascade } from './report';
59
+ /** A component that will emit a file, but with refusals recorded against it. */
60
+ export interface PreflightAttention {
61
+ path: string;
62
+ file: string;
63
+ /** How many nodes, wires or parameters the export will leave out of this component. */
64
+ refusals: number;
65
+ unreachable: boolean;
66
+ /**
67
+ * EXP-013 AC2 — the refused nodes themselves, by type and label, with the root that starves each
68
+ * silenced one. The same rows `EXPORT-REPORT.md` prints under the component.
69
+ */
70
+ nodes: RefusedNode[];
71
+ }
72
+ /** A component that will emit no file at all. */
73
+ export interface PreflightNoFile {
74
+ path: string;
75
+ reason: string;
76
+ /**
77
+ * `scaffolded` is done — the app shell covers it. `deferred` is not. They are opposite news and
78
+ * the discriminator is carried from the two sites in `plan.ts` that know, never read back out of
79
+ * the sentence.
80
+ */
81
+ kind: 'scaffolded' | 'deferred';
82
+ /** EXP-013 AC2. The nodes the plan refused in a component that emits no file. */
83
+ nodes: RefusedNode[];
84
+ }
85
+ export interface PreflightSummary {
86
+ projectName: string;
87
+ /**
88
+ * EXP-013 AC3 — the refusals split into the nodes the export has no rule for and the nodes only
89
+ * refused because of them, with the roots named. `cascade.silenced + cascade.unsilenced` is the
90
+ * number of refused nodes; {@link refusals} below is the number of *lines* (nodes, wires,
91
+ * parameters, whole components) and stays the length of the report's list.
92
+ */
93
+ cascade: ExportCascade;
94
+ /**
95
+ * EXP-013 AC4 — the plain sentence that leads when a cascade costs a pathway (a backend verb, a
96
+ * navigation, an `On App Error`), or `null` when the loss is nodes rather than a pathway. The
97
+ * modal's button changes with it.
98
+ */
99
+ verdict: string | null;
100
+ /**
101
+ * Files the export will generate, `EXPORT-REPORT.md` included — the number the author will see
102
+ * in the folder afterwards.
103
+ */
104
+ generatedFiles: number;
105
+ /**
106
+ * Assets that travel byte-for-byte beside them: a kit's script, an icon set's `.woff2`,
107
+ * Inter's four `.ttf` (EXP-010 AC5).
108
+ *
109
+ * 🔴 **Counted separately and never folded into {@link generatedFiles}, because they are a
110
+ * separate channel that has already been dropped once.** `emitApp` returns `files` and `copies`
111
+ * as two lists, and §19.6 records the one shipped runner writing only the first — an author's
112
+ * fonts silently absent from their repo, every gate green. A pre-flight that promised
113
+ * "33 files" and delivered 45 would be describing the same defect from the other end.
114
+ */
115
+ copiedAssets: number;
116
+ /** Pages and components that will emit a file. */
117
+ pages: number;
118
+ components: number;
119
+ /** Paths of the components that will translate with nothing left over, in emission order. */
120
+ whole: string[];
121
+ /** The rest, worst first. */
122
+ attention: PreflightAttention[];
123
+ noFile: PreflightNoFile[];
124
+ /**
125
+ * Every item the export will list as not translated: each component-level refusal, each
126
+ * component that emits **no file at all**, each kit that contributes no nodes, and each line
127
+ * about the project as a whole. This is the length of the list `EXPORT-REPORT.md` will print.
128
+ *
129
+ * 🔴 **A component that emits no file is in this number, and the first draft left it out.** It
130
+ * counted only the refusals recorded *inside* generated components, so `puppy-test-3` announced
131
+ * "18" and then printed nineteen bullets — the extra being `Components/BenchLogicProbe`, which
132
+ * is not a node dropped from a file but a whole component with no file. That is the largest
133
+ * thing an export can leave out, and it was the one thing the headline number could not see.
134
+ */
135
+ refusals: number;
136
+ /** Components with no generated file that are *not* handled by the app shell — counted above. */
137
+ noFileDeferred: number;
138
+ backend: BackendMode;
139
+ backendEndpoint: string | null;
140
+ /** `src/api/http.ts` — real generated calls to the addresses the author typed, never stubs. */
141
+ httpModule: boolean;
142
+ /** EXP-011 §41. `src/api/functions.ts` — one function per `Cloud Function` node. */
143
+ functionsModule: boolean;
144
+ /** Kits that will ship their files but contribute none of their nodes. */
145
+ moduleFailures: number;
146
+ /** Refusals about the project rather than any one component (cloud functions, chiefly). */
147
+ projectRefusals: number;
148
+ }
149
+ /**
150
+ * The summary, from an export that has already been generated in memory.
151
+ *
152
+ * 🔴 **Everything here is read off `EmittedApp`, and nothing is recomputed.** The pre-flight and
153
+ * the report are two renderings of one set of facts; the moment this function starts deciding for
154
+ * itself whether a component counts as whole, the surface an author reads *before* exporting and
155
+ * the one they read *after* can disagree about the same project — which is the failure EXP-004's
156
+ * risk table calls "the report drifting out of sync with what the generators actually do",
157
+ * arriving through a door the risk table did not think to name.
158
+ */
159
+ export declare function summarizePreflight(app: EmittedApp): PreflightSummary;
160
+ /**
161
+ * Generate the app in memory and summarise it, writing nothing.
162
+ *
163
+ * This is the whole of the "pre" in pre-flight: the export runs, and its output is discarded
164
+ * rather than committed to a directory the author has not yet chosen.
165
+ */
166
+ export declare function preflight(ir: ExportIR, catalog: Catalog): PreflightSummary;
167
+ /**
168
+ * The pre-flight as text an author reads and then decides on.
169
+ *
170
+ * Pure, and deliberately much shorter than `renderReport` — this is read standing up. It names
171
+ * what will be built, names every component that will come out incomplete, and says plainly that
172
+ * the full reasons travel with the app rather than repeating them here.
173
+ */
174
+ export declare function renderPreflight(s: PreflightSummary): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `README.md` — the front door of the exported repository (EXP-004).
3
+ *
4
+ * ## Why this is a second file and not a section of the report
5
+ *
6
+ * They are reached by different routes and answer different questions.
7
+ *
8
+ * `EXPORT-REPORT.md` is the **record**: every node, wire and parameter the export refused, in the
9
+ * exporter's own words. It is what the `TODO(export)` markers in the code point at, and it is
10
+ * complete by construction — a reader arrives at it already looking for one specific thing.
11
+ *
12
+ * This file is what someone opens **first**, usually without having been sent anywhere. It has to
13
+ * answer *what is this*, *how do I run it*, *what is missing*, *what do I do about it* and *can I
14
+ * edit it* — and then get out of the way. It states no refusal individually; it says how many
15
+ * there are and sends the reader next door.
16
+ *
17
+ * ## 🔴 It is emitted for every export, and it did not used to be
18
+ *
19
+ * Until EXP-004 the README was written inside `apiModules`, under `backend !== undefined &&
20
+ * hasApi` — so it arrived only for a project with a NodeGX backend *and* something that queried
21
+ * it. Six of the seven corpus fixtures got no README at all, which is the file a developer opens
22
+ * first being absent from the export in the ordinary case. It is now emitted beside the report,
23
+ * from the same data, unconditionally. The backend section is what became conditional.
24
+ *
25
+ * ## What it shares with the report, and what it must not
26
+ *
27
+ * The ordered next steps are **the same list**, from {@link nextSteps} — one decision, two
28
+ * readers, the pattern `backendMode` already establishes here. What it does not share is the
29
+ * itemised refusals: two full copies of that list would be two things to keep in step, and the
30
+ * one that drifted would be the one nobody regenerated.
31
+ *
32
+ * 🔴 **No clock, no absolute path, no random id**, for the reason `report.ts` gives at length: the
33
+ * generators are byte-stable, and a date line would turn every golden in the package into noise.
34
+ *
35
+ * @module emit/readme
36
+ */
37
+ import { ExportReportData } from './report';
38
+ export declare const README_PATH = "README.md";
39
+ /** Just the two fields the README prints; the IR record carries more, and none of it belongs here. */
40
+ export interface ReadmeBackend {
41
+ endpoint: string;
42
+ appId: string;
43
+ }
44
+ /**
45
+ * The README, as Markdown.
46
+ *
47
+ * Pure. `backend` is read only when {@link backendMode} says `connected` — a project can declare
48
+ * cloud services and never query them, and an env-var section for a `src/api/client.ts` that was
49
+ * never emitted would be instructions for configuring nothing.
50
+ */
51
+ export declare function renderReadme(data: ExportReportData, backend: ReadmeBackend | null): string;
@@ -0,0 +1,309 @@
1
+ /**
2
+ * `EXPORT-REPORT.md` — the report the exported app carries with it (EXP-004).
3
+ *
4
+ * ## Why this file exists
5
+ *
6
+ * Two generated `TODO(export)` markers have ended with *"See the export report"* since EXP-002,
7
+ * and there was no report. Everything the export dropped or refused went to `EmittedApp.notes`,
8
+ * which the runner printed to **stderr** — so an author who ran the export and opened
9
+ * the repo saw markers pointing at a document that did not exist, and a scrollback they no longer
10
+ * had. This is that document, written into the app beside the code it is about.
11
+ *
12
+ * ## The framing rules, which are the actual work
13
+ *
14
+ * EXP-004's own words, and each one is visible in the output below:
15
+ *
16
+ * - **Lead with what worked.** A report opening on a list of problems reads as failure even when
17
+ * the export went well. The counts and the whole-component list come first, and a project with
18
+ * nothing to report never renders an "attention" section at all.
19
+ * - **Be specific, not statistical.** No percentages and no confidence score. Every refusal is a
20
+ * named component, a named node or wire, and the exporter's own sentence for why.
21
+ * - **Never let unverified code look verified.** ⚠️ This is the one that constrains wording
22
+ * hardest, and the honest statement today is *narrower* than EXP-004 anticipated: EXP-003's AI
23
+ * translation and trace verification do not exist yet, so nothing in a generated app has been
24
+ * run, replayed or compared against anything. The report says that in those words. When
25
+ * EXP-003 lands, this section gains a verdict per translation — it must not gain the word
26
+ * "verified" before then.
27
+ *
28
+ * ## 🔴 No timestamp, and that is not an oversight
29
+ *
30
+ * The generators are deterministic (EXP-002-DETERMINISTIC-GENERATORS): the same project emits the
31
+ * same bytes, which is what makes the goldens a regression net and what lets a user diff two
32
+ * exports. A date line would change every byte-comparison in the suite and turn "nothing changed"
33
+ * into noise. The report carries no clock, no random id and no absolute path.
34
+ *
35
+ * ## Where the structure comes from
36
+ *
37
+ * 🔴 **Grouping is by what the caller already knew, never by reading the prose back.** `emitApp`
38
+ * pushes a component's notes inside the loop that owns `plan`, so scope is a fact at the push
39
+ * site; `skipKind` discriminates the router shell from a logic-only component because the two
40
+ * sentences would otherwise have to be told apart by matching their words. A report that grouped
41
+ * by parsing `notes` would mis-file the first time anyone reworded a refusal — and rewording
42
+ * refusals is the ordinary business of this package.
43
+ *
44
+ * @module emit/report
45
+ */
46
+ import { RefusedNode } from '../ir/types';
47
+ export declare const REPORT_PATH = "EXPORT-REPORT.md";
48
+ /** One component, as the report talks about it. */
49
+ export interface ReportComponent {
50
+ /** "Pages/Home", "Components/PuppyCard". */
51
+ path: string;
52
+ role: 'page' | 'component';
53
+ /** The file it emitted, or `null` when it emitted none. */
54
+ file: string | null;
55
+ /**
56
+ * Everything dropped or deferred inside it, in emission order, with the exporter's own
57
+ * reason. Already stripped of the `"<path>: "` prefix the emitter writes onto each one.
58
+ */
59
+ notes: string[];
60
+ /**
61
+ * No route reaches this component. Reported, never acted on — it changes how the notes above
62
+ * read (a refusal in unreachable code is not a gap in the export's reach), so it is stated
63
+ * beside them rather than filtering them out.
64
+ */
65
+ unreachable: boolean;
66
+ /** Set when nothing was emitted for this component at all. */
67
+ skipped?: {
68
+ kind: 'scaffolded' | 'deferred';
69
+ reason: string;
70
+ };
71
+ /**
72
+ * Authored scripts this export did not translate, on a component that emitted no file
73
+ * (EXP-011 §27.6).
74
+ *
75
+ * 🔴 **Only ever populated when {@link file} is `null`.** A component with a module carries the
76
+ * identical record as a code comment beside the node, and printing it here as well would make
77
+ * the report a second copy of something the repo already holds — the shape that is a duplicate
78
+ * before it is a check. `emitApp` fills this in its skip branch and nowhere else, so which of
79
+ * the two carriers holds a script is decided by whether a file exists, not by a filter.
80
+ */
81
+ preservedScripts?: Array<{
82
+ nodeId: string;
83
+ typeName: string;
84
+ label?: string;
85
+ source: string;
86
+ }>;
87
+ /**
88
+ * EXP-013. The nodes the plan refused in this component, each with the refused node that starves
89
+ * it when the refusal is a cascade. Optional so a report built by hand before EXP-013 still
90
+ * types; `emitApp` always sets it.
91
+ */
92
+ refusals?: RefusedNode[];
93
+ }
94
+ export interface ExportReportData {
95
+ projectName: string;
96
+ /** Every emitted file, so the report can count without being handed the contents. */
97
+ files: string[];
98
+ components: ReportComponent[];
99
+ /** `noodl_modules` folders that could not contribute their nodes; their files still ship. */
100
+ modules: string[];
101
+ /** Lines that are about the project rather than any one component. */
102
+ project: string[];
103
+ /** The backend endpoint the api modules talk to, or `null` when the project declares none. */
104
+ backendEndpoint: string | null;
105
+ /**
106
+ * Whether the project asks anything of a backend — a collection query, a mutation, or a session
107
+ * call.
108
+ *
109
+ * 🔴 **Not "were any api files emitted".** `src/api/http.ts` is emitted for an `HTTP Request`,
110
+ * which talks to whatever address the author typed and is real code in a project with no backend
111
+ * at all. Keying the stub sentence on "any api file" told a project with a working fetch and no
112
+ * backend that its reads answer empty — see {@link httpModule}.
113
+ */
114
+ usesBackend: boolean;
115
+ /** Whether `src/api/http.ts` was emitted — generated `HTTP Request` calls, never stubs. */
116
+ httpModule: boolean;
117
+ /** EXP-011 §41. Whether `src/api/functions.ts` was emitted — one function per `Cloud Function` node. Optional so a report built by hand before §41 still types. */
118
+ functionsModule?: boolean;
119
+ /**
120
+ * HLS-003 — stored parameters this export read the editor's way rather than the file's.
121
+ *
122
+ * Absent when the project has no node that exercises the seam, which is the common case and is
123
+ * why this is optional rather than a zeroed record. 🔴 **Absent means "nothing here exercises
124
+ * it", never "the seam is closed"** — a project with no wired control signal on one of the
125
+ * fifteen NDA-017 families reads absent however open the seam is.
126
+ */
127
+ settled?: {
128
+ parameters: number;
129
+ nodes: number;
130
+ /** Component paths, sorted — where an author would go to look. */
131
+ components: string[];
132
+ };
133
+ /**
134
+ * STY-004 AC6 — the named styles this export resolved into CSS.
135
+ *
136
+ * 🔴 **Silence was the defect.** A variant and a text style on an ordinary leaf node were both
137
+ * dropped, and `variant` appeared **0 times in a 1,059-line report** — so the author had no
138
+ * signal at all, in a report that itemises dropped wires. A link that IS carried must therefore
139
+ * say so out loud; an export that prints nothing here is claiming the project has no named
140
+ * styles, which is a checkable claim rather than an absence.
141
+ *
142
+ * Absent when the project declares no styles — every fixture in this package and all seven
143
+ * shipped templates.
144
+ */
145
+ styles?: StylesReport;
146
+ }
147
+ /** STY-004. What {@link ExportReportData.styles} counts. Every field is "resolved into CSS", not "present in the file". */
148
+ export interface StylesReport {
149
+ /** `Look name (NodeType)` → how many nodes wore it. Sorted by name. */
150
+ looks: Array<{
151
+ name: string;
152
+ typename: string;
153
+ nodes: number;
154
+ }>;
155
+ /** Text style name → how many nodes resolved it. Sorted. */
156
+ textStyles: Array<{
157
+ name: string;
158
+ nodes: number;
159
+ }>;
160
+ /** Colour style names the project defines, sorted. Resolution is per parameter and not counted here. */
161
+ colors: string[];
162
+ /**
163
+ * Looks that carry `stateParameters` — hover, pressed, disabled.
164
+ *
165
+ * 🔴 **Carried in the file, NOT drawn by this export.** Naming them is the honest half of AC6:
166
+ * the states half of the Look model is a later task, and an author whose Look defines a hover
167
+ * needs to be told it did not ship rather than discover it in a browser.
168
+ */
169
+ looksWithUnemittedStates: string[];
170
+ }
171
+ /**
172
+ * Which of the three states the generated `src/api/` is in.
173
+ *
174
+ * 🔴 **One decision, two readers.** The report prints a sentence from it and the pre-flight
175
+ * summary (EXP-004's before-you-export surface) prints a different one, and the discriminator
176
+ * itself — *does the project ask anything of a backend, and did it name one* — must not exist
177
+ * twice. A second copy would be right until the day `usesBackend` grew a third case, and the two
178
+ * surfaces would then disagree about the same project in the same export.
179
+ */
180
+ export type BackendMode = 'connected' | 'stubbed' | 'absent';
181
+ export declare function backendMode(data: Pick<ExportReportData, 'usesBackend' | 'backendEndpoint'>): BackendMode;
182
+ /** `"Pages/Home: wire x dropped"` → `"wire x dropped"`, when the prefix is this component's. */
183
+ export declare function stripScope(path: string, note: string): string;
184
+ /**
185
+ * The ordered steps as Markdown, shared by the report and the README so the two cannot format the
186
+ * same facts differently. Three-space continuation indent is what keeps the detail and locations
187
+ * inside their numbered item rather than starting a new block after it.
188
+ */
189
+ /** A refused node the way a person names it: their label first, the picker's type name beside it. */
190
+ export declare function describeNode(node: Pick<RefusedNode, 'displayName' | 'label' | 'nodeId'>): string;
191
+ /**
192
+ * The reason as a person reads it. The planner's catch-all is `logic node (<typeName>)` — the
193
+ * exporter talking to itself, and the sentence `sweepUnreportedDeferrals` already refuses to
194
+ * stutter back at the author. Every other reason is a gate's own sentence and is kept verbatim.
195
+ */
196
+ export declare function plainReason(node: Pick<RefusedNode, 'type' | 'reason'>): string;
197
+ /** One root refusal and everything it silences, across the whole export. */
198
+ export interface CascadeRoot {
199
+ /** The component the root sits in. */
200
+ path: string;
201
+ node: RefusedNode;
202
+ /** Every node whose `causedBy` resolves to this root, in report order. */
203
+ silences: Array<{
204
+ path: string;
205
+ node: RefusedNode;
206
+ }>;
207
+ }
208
+ /**
209
+ * EXP-013 AC3/AC4 — the refusals of an export, split into the ones the export has no rule for
210
+ * and the ones that are only refused because of them.
211
+ */
212
+ export interface ExportCascade {
213
+ /** Refused nodes with no `causedBy`, that at least one other node names as its cause. Worst first. */
214
+ roots: CascadeRoot[];
215
+ /** Refused nodes with no `causedBy` — the roots above plus every independent refusal. */
216
+ unsilenced: number;
217
+ /** Refused nodes with a `causedBy` — the sum of every root's `silences`. */
218
+ silenced: number;
219
+ /**
220
+ * Nodes whose loss is a pathway rather than a feature (see `isPathwayType`) and that are lost to
221
+ * a cascade — as a silenced node, or as a root that is itself a pathway (an `On App Error` the
222
+ * export has no rule for takes the error pathway with it whether or not anything hangs off it).
223
+ */
224
+ pathway: Array<{
225
+ path: string;
226
+ node: RefusedNode;
227
+ }>;
228
+ }
229
+ /**
230
+ * The cascade, from the report data and nothing else.
231
+ *
232
+ * 🔴 **`silenced` + `unsilenced` is the number of refused nodes, and `silenced` is the sum of the
233
+ * roots' lists — asserted rather than assumed** (`tests/cascade.test.ts`), because the headline the
234
+ * modal prints is *"N the export has no rule for, M more silenced by them"*, and the cheap mistake
235
+ * is to count a silenced node on both sides. A node with two roots is listed under both and counted
236
+ * once.
237
+ */
238
+ export declare function cascadeOf(data: Pick<ExportReportData, 'components'>): ExportCascade;
239
+ /**
240
+ * EXP-013 AC4 — the sentence that leads when a cascade costs a pathway, or `null`.
241
+ *
242
+ * Plain, and the same words on the modal and in the report: *this export would be missing a
243
+ * pathway, not a node*. It names the roots, because the roots are what a person can act on.
244
+ */
245
+ export declare function pathwayVerdict(cascade: ExportCascade): string | null;
246
+ /** The lines under a component that name the nodes the plan refused, roots and their cascades first. */
247
+ export declare function refusedNodeLines(refusals: RefusedNode[] | undefined, indent?: string): string[];
248
+ export declare function renderSteps(steps: NextStep[]): string[];
249
+ /**
250
+ * The report, as Markdown.
251
+ *
252
+ * Pure — it is handed everything it says and reads nothing, so the suite can drive it on data
253
+ * that no fixture on disk produces (a project with no gaps at all, which is the section order's
254
+ * hardest case and the one a corpus is least likely to contain).
255
+ */
256
+ export declare function renderReport(data: ExportReportData): string;
257
+ /**
258
+ * One thing the author has to do, and where.
259
+ *
260
+ * `where` is *locations*, not always files: a component the export refused entirely has no file to
261
+ * name, so the component's own path is what a reader can act on. The renderers do not know the
262
+ * difference and do not need to — both print it as a code span.
263
+ */
264
+ export interface NextStep {
265
+ /** Imperative, one sentence, ending in a full stop. */
266
+ title: string;
267
+ /** What is broken until it is done. This is the sentence that justifies the position. */
268
+ detail: string;
269
+ /** Files or component paths, in emission order. May be empty. */
270
+ where: string[];
271
+ }
272
+ /**
273
+ * EXP-004's *"actionable next steps ordered by priority"*, computed once and rendered twice.
274
+ *
275
+ * ## 🔴 The ordering criterion, written down so it can be argued with
276
+ *
277
+ * **How much of the running app is missing without it, most first.** Not difficulty, not the
278
+ * number of refusals, and deliberately not the order the report prints its sections in — the
279
+ * report is organised for *looking something up*, and this list is organised for *doing the work*.
280
+ *
281
+ * 1. **Components with no file at all.** The most absent thing the export produces: a route can
282
+ * lead to a component that was never written.
283
+ * 2. **A stubbed `src/api/`.** Every read answers empty and every write throws, across the whole
284
+ * app rather than one screen.
285
+ * 3. **Refusals in components a route reaches.** Screens that render, missing behaviour.
286
+ * 4. **Refusals in components no route reaches.** The same work, but nothing runs it today — which
287
+ * is the whole of why it sits below 3 rather than beside it.
288
+ * 5. **Kits that shipped files but contributed no nodes.**
289
+ * 6. **Anything the export said about the project as a whole.**
290
+ * 7. **Test what was generated.** Always present, always last, and the only step that is not about
291
+ * something missing.
292
+ *
293
+ * ⚠️ **1 above 2 is a judgement and not a measurement.** A missing component takes out one screen;
294
+ * a stubbed backend takes out every data path in the app. They are ranked the way they are because
295
+ * "nothing was emitted" is strictly more absent than "a stub exists, and it names the node it came
296
+ * from" — but an author whose app is one page and six queries would reasonably reverse them. Both
297
+ * lines say what they cost, so the reading does not depend on the rank alone.
298
+ *
299
+ * ## What is *not* in the order
300
+ *
301
+ * Within a group, components stay in **emission order** — the order they appear in the report's
302
+ * own sections, so a reader can scan straight down from a step to its detail. Sorting them by
303
+ * refusal count was considered and rejected: it would break that correspondence to express a
304
+ * ranking that the counts printed beside them already express.
305
+ *
306
+ * Pure, and driven directly by the suite for the four shapes the corpus does not contain
307
+ * (a stubbed backend, project-wide notes, a clean export, and every group at once).
308
+ */
309
+ export declare function nextSteps(data: ExportReportData): NextStep[];