@lariocpt/flawless-react 0.0.0-stage → 0.3.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.
Files changed (143) hide show
  1. package/.agents/skills/flawless-a11y/SKILL.md +113 -0
  2. package/.agents/skills/flawless-api/SKILL.md +159 -0
  3. package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
  4. package/.agents/skills/flawless-e2e/SKILL.md +155 -0
  5. package/.agents/skills/flawless-expo/SKILL.md +283 -0
  6. package/.agents/skills/flawless-findings/SKILL.md +116 -0
  7. package/.agents/skills/flawless-load/SKILL.md +112 -0
  8. package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
  9. package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
  10. package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
  11. package/.agents/skills/flawless-react/SKILL.md +403 -0
  12. package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
  13. package/.agents/skills/flawless-regen/SKILL.md +135 -0
  14. package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
  15. package/LICENSE +21 -0
  16. package/README.md +608 -2
  17. package/bin/flawless-react +4 -0
  18. package/package.json +67 -4
  19. package/scripts/postinstall.d.mts +46 -0
  20. package/scripts/postinstall.mjs +127 -0
  21. package/src/analyse/authShims.ts +220 -0
  22. package/src/analyse/bootability.ts +257 -0
  23. package/src/analyse/childBridge.ts +302 -0
  24. package/src/analyse/childrenIntoSlot.ts +66 -0
  25. package/src/analyse/compoundFamily.ts +100 -0
  26. package/src/analyse/dataExports.ts +200 -0
  27. package/src/analyse/extractChildAttrs.ts +112 -0
  28. package/src/analyse/extractDataDeps.ts +857 -0
  29. package/src/analyse/extractDisclosures.ts +186 -0
  30. package/src/analyse/extractFetchCalls.ts +81 -0
  31. package/src/analyse/extractFlows.ts +239 -0
  32. package/src/analyse/extractGuards.ts +270 -0
  33. package/src/analyse/extractHooks.ts +237 -0
  34. package/src/analyse/extractInteractions.ts +1421 -0
  35. package/src/analyse/extractLogging.ts +45 -0
  36. package/src/analyse/extractNativeA11y.ts +138 -0
  37. package/src/analyse/extractPredicates.ts +784 -0
  38. package/src/analyse/extractPropUsage.ts +162 -0
  39. package/src/analyse/extractProps.ts +1212 -0
  40. package/src/analyse/extractRenderMode.ts +165 -0
  41. package/src/analyse/extractRootTag.ts +117 -0
  42. package/src/analyse/extractState.ts +361 -0
  43. package/src/analyse/extractStaticText.ts +212 -0
  44. package/src/analyse/extractVariants.ts +186 -0
  45. package/src/analyse/harvestApiEndpoints.ts +284 -0
  46. package/src/analyse/hostProps.ts +185 -0
  47. package/src/analyse/hostTag.ts +72 -0
  48. package/src/analyse/htmlContentModel.ts +94 -0
  49. package/src/analyse/index.ts +744 -0
  50. package/src/analyse/mountPathCalls.ts +442 -0
  51. package/src/analyse/nativeContentModel.ts +129 -0
  52. package/src/analyse/nativeDialect.ts +83 -0
  53. package/src/analyse/nativeProps.ts +339 -0
  54. package/src/analyse/ownReturns.ts +31 -0
  55. package/src/analyse/parseFile.ts +919 -0
  56. package/src/analyse/pos.ts +8 -0
  57. package/src/analyse/providerWraps.ts +1977 -0
  58. package/src/analyse/pureModule.ts +88 -0
  59. package/src/analyse/reexportTable.ts +148 -0
  60. package/src/analyse/refs.ts +195 -0
  61. package/src/analyse/renderPath.ts +441 -0
  62. package/src/analyse/resolveDepReturns.ts +370 -0
  63. package/src/analyse/resolveIndexKeys.ts +98 -0
  64. package/src/analyse/resolveModule.ts +431 -0
  65. package/src/analyse/resolveTagRootTags.ts +103 -0
  66. package/src/analyse/serverActionReturns.ts +213 -0
  67. package/src/analyse/sourceCache.ts +263 -0
  68. package/src/analyse/spreadSiblings.ts +98 -0
  69. package/src/analyse/walkChildren.ts +98 -0
  70. package/src/analyse/zodSchema.ts +424 -0
  71. package/src/bin.ts +23 -0
  72. package/src/changed.ts +221 -0
  73. package/src/cli.ts +560 -0
  74. package/src/cliFlags.ts +281 -0
  75. package/src/core.ts +1229 -0
  76. package/src/docLine.ts +122 -0
  77. package/src/emitPaths.ts +51 -0
  78. package/src/expo/detect.ts +184 -0
  79. package/src/expo/refusal.ts +62 -0
  80. package/src/expo/routes.ts +145 -0
  81. package/src/index.ts +95 -0
  82. package/src/mcp/analysisTools.ts +720 -0
  83. package/src/mcp/infoTools.ts +135 -0
  84. package/src/mcp/protocol.ts +130 -0
  85. package/src/mcp/run.ts +269 -0
  86. package/src/mcp/runTools.ts +362 -0
  87. package/src/mcp/server.ts +279 -0
  88. package/src/mcp/spec.ts +86 -0
  89. package/src/mcp/support.ts +268 -0
  90. package/src/mcp/tools.ts +75 -0
  91. package/src/nextjs/apiRoutes.ts +116 -0
  92. package/src/nextjs/detect.ts +397 -0
  93. package/src/nextjs/emit.ts +106 -0
  94. package/src/nextjs/liveSpec.ts +79 -0
  95. package/src/nextjs/mswHandlers.ts +55 -0
  96. package/src/nextjs/providerWrapper.ts +603 -0
  97. package/src/nextjs/routes.ts +86 -0
  98. package/src/nextjs/swagger.ts +32 -0
  99. package/src/packageJson.ts +414 -0
  100. package/src/platform.ts +18 -0
  101. package/src/prompt.ts +132 -0
  102. package/src/reactCompiler.ts +165 -0
  103. package/src/reporters/findings.ts +156 -0
  104. package/src/reporters/report.ts +463 -0
  105. package/src/runner/runTests.ts +402 -0
  106. package/src/scaffold/adoptStyle.ts +181 -0
  107. package/src/scaffold/index.ts +253 -0
  108. package/src/scaffold/openapi.ts +183 -0
  109. package/src/scaffold/templates.ts +2379 -0
  110. package/src/scaffold-tests/index.ts +463 -0
  111. package/src/scaffold-tests/mockValues.ts +208 -0
  112. package/src/scaffold-tests/mocks.ts +794 -0
  113. package/src/scaffold-tests/nativeIndex.ts +52 -0
  114. package/src/scaffold-tests/nativeTemplates.ts +1528 -0
  115. package/src/scaffold-tests/postDispatch.ts +152 -0
  116. package/src/scaffold-tests/registry.ts +443 -0
  117. package/src/scaffold-tests/selectors.ts +493 -0
  118. package/src/scaffold-tests/selectorsNative.ts +273 -0
  119. package/src/scaffold-tests/stamp.ts +79 -0
  120. package/src/scaffold-tests/templates.ts +3578 -0
  121. package/src/skills/cli.ts +248 -0
  122. package/src/skills/compat.ts +166 -0
  123. package/src/skills/data.ts +192 -0
  124. package/src/skills/md.d.ts +7 -0
  125. package/src/skills/menu.ts +55 -0
  126. package/src/skills/spec.ts +227 -0
  127. package/src/suiteScripts.ts +110 -0
  128. package/src/tasks/a11yNative.ts +281 -0
  129. package/src/tasks/a11yScan.ts +67 -0
  130. package/src/tasks/biomeRunner.ts +301 -0
  131. package/src/tasks/biomeSweep.ts +166 -0
  132. package/src/tasks/e2eExpo.ts +436 -0
  133. package/src/tasks/e2eRoutes.ts +756 -0
  134. package/src/tasks/eslintReactRules.ts +67 -0
  135. package/src/tasks/index.ts +24 -0
  136. package/src/tasks/loadExpo.ts +273 -0
  137. package/src/tasks/loadPipes.ts +750 -0
  138. package/src/tasks/perfHarness.ts +145 -0
  139. package/src/tasks/staticPerfAnalysis.ts +510 -0
  140. package/src/tasks/types.ts +1 -0
  141. package/src/tasks/unitAndBrowser.ts +462 -0
  142. package/src/types.ts +1419 -0
  143. package/src/version.ts +12 -0
@@ -0,0 +1,67 @@
1
+ import type { RunContext, Task, TaskResult } from "../types.ts";
2
+ import { runBiomeSweep } from "./biomeSweep.ts";
3
+
4
+ /** One aggregate for the whole sweep — see A11Y_REPORT. */
5
+ export const REACT_RULES_REPORT = "react-rules.json";
6
+
7
+ export const eslintReactRules: Task = {
8
+ id: "react-rules",
9
+ label: "Functionally important React rules (Biome)",
10
+ description: "Curated bug-catching React + hooks rules via Biome",
11
+ needsTree: true,
12
+ async run(ctx: RunContext): Promise<TaskResult> {
13
+ return runBiomeSweep({
14
+ id: "react-rules",
15
+ ctx,
16
+ rules: reactRules(),
17
+ aggregateFile: REACT_RULES_REPORT,
18
+ note: "Curated React correctness rules via Biome. Local biome.json is bypassed.",
19
+ });
20
+ },
21
+ };
22
+
23
+ function reactRules(): unknown {
24
+ return {
25
+ recommended: false,
26
+ correctness: {
27
+ noChildrenProp: "error",
28
+ noConstAssign: "error",
29
+ noConstantCondition: "error",
30
+ noEmptyPattern: "error",
31
+ noInvalidUseBeforeDeclaration: "error",
32
+ noPrecisionLoss: "error",
33
+ noRenderReturnValue: "error",
34
+ noVoidElementsWithChildren: "error",
35
+ noVoidTypeReturn: "error",
36
+ useExhaustiveDependencies: "error",
37
+ useHookAtTopLevel: "error",
38
+ useIsNan: "error",
39
+ useJsxKeyInIterable: "error",
40
+ useValidForDirection: "error",
41
+ },
42
+ suspicious: {
43
+ noArrayIndexKey: "warn",
44
+ noAssignInExpressions: "error",
45
+ noAsyncPromiseExecutor: "error",
46
+ noCompareNegZero: "error",
47
+ noConfusingVoidType: "warn",
48
+ noControlCharactersInRegex: "error",
49
+ noDoubleEquals: "warn",
50
+ noDuplicateCase: "error",
51
+ noDuplicateClassMembers: "error",
52
+ noDuplicateJsxProps: "error",
53
+ noDuplicateObjectKeys: "error",
54
+ noDuplicateParameters: "error",
55
+ noEmptyBlockStatements: "warn",
56
+ noExplicitAny: "warn",
57
+ noExtraNonNullAssertion: "warn",
58
+ noFallthroughSwitchClause: "error",
59
+ noGlobalIsFinite: "error",
60
+ noGlobalIsNan: "error",
61
+ noImplicitAnyLet: "warn",
62
+ noLabelVar: "error",
63
+ noMisleadingCharacterClass: "error",
64
+ noMisleadingInstantiator: "error",
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,24 @@
1
+ import type { Task, TaskId } from "../types.ts";
2
+ import { a11yScan } from "./a11yScan.ts";
3
+ import { e2eRoutes } from "./e2eRoutes.ts";
4
+ import { eslintReactRules } from "./eslintReactRules.ts";
5
+ import { loadPipes } from "./loadPipes.ts";
6
+ import { perfHarness } from "./perfHarness.ts";
7
+ import { staticPerfAnalysis } from "./staticPerfAnalysis.ts";
8
+ import { unitAndBrowser } from "./unitAndBrowser.ts";
9
+
10
+ export const TASKS: Task[] = [
11
+ perfHarness,
12
+ unitAndBrowser,
13
+ staticPerfAnalysis,
14
+ a11yScan,
15
+ eslintReactRules,
16
+ e2eRoutes,
17
+ loadPipes,
18
+ ];
19
+
20
+ export function findTask(id: TaskId): Task | undefined {
21
+ return TASKS.find((t) => t.id === id);
22
+ }
23
+
24
+ export const ALL_TASK_IDS: TaskId[] = TASKS.map((t) => t.id);
@@ -0,0 +1,273 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { outDirRel, underOutDir } from "../emitPaths.ts";
5
+ import type { ComponentTree, ExpoContext, RunContext, TaskResult } from "../types.ts";
6
+ import { ensureParamDefaults } from "./e2eRoutes.ts";
7
+ import {
8
+ countPipes,
9
+ describeTally,
10
+ type LoadTarget,
11
+ loadEnvScript,
12
+ mergePipes,
13
+ readFilledParams,
14
+ renderPipes,
15
+ slugify,
16
+ } from "./loadPipes.ts";
17
+
18
+ /**
19
+ * API-only crust load pipes for an Expo app — B-16 wave 7, the sibling of
20
+ * loadPipes.ts (one TaskId, one engine per platform).
21
+ *
22
+ * An Expo app has no server-rendered page routes to GET, so the page-route half
23
+ * of the Next flavour drops away and the pipes are the GET endpoints the screens
24
+ * reach through the app's fetch wrapper (ComponentInfo.apiCalls, harvested one
25
+ * hop at analysis time). Everything downstream is shared verbatim — the pipe
26
+ * body, the merge-kept routes.pipes, params.json, env.sh — because none of it is
27
+ * platform-specific. Only two things differ and both are here: what a target IS
28
+ * (an api endpoint, not a page), and how the auth probe knows the session is
29
+ * dead (a 401 from the API, not a redirect to a login page).
30
+ */
31
+ export async function runExpoLoad(ctx: RunContext): Promise<TaskResult> {
32
+ const expo = ctx.expo as ExpoContext;
33
+ const loadDir = path.join(ctx.outDir, "load");
34
+ await mkdir(loadDir, { recursive: true });
35
+ const written: string[] = [];
36
+
37
+ const targets = collectExpoTargets(ctx.trees);
38
+ const rel = outDirRel(ctx.outDir, expo.root);
39
+
40
+ // A run that harvested nothing is reported as such, not as an empty success:
41
+ // 0 endpoints on an app that makes API calls means the harvest could not see
42
+ // through its client, which the reader needs to know.
43
+ if (targets.length === 0) {
44
+ return {
45
+ id: "load",
46
+ ok: true,
47
+ outputs: [],
48
+ findings: [],
49
+ summary:
50
+ "load: no GET endpoints harvested — the screens reach no api function whose body " +
51
+ "calls a fetch wrapper with a literal path (calls mediated by a store, or a non-literal URL, are not traced)",
52
+ };
53
+ }
54
+
55
+ const paramsPath = path.join(ctx.outDir, "e2e", "params.json");
56
+ const needed = [...new Set(targets.flatMap((t) => t.params))].sort();
57
+ if (await ensureParamDefaults(paramsPath, needed)) written.push(paramsPath);
58
+ const filled = await readFilledParams(paramsPath);
59
+
60
+ const pipesPath = path.join(loadDir, "routes.pipes");
61
+ let keptPipes = false;
62
+ let pipesText: string;
63
+ if (!existsSync(pipesPath)) {
64
+ pipesText = renderPipes(targets, ctx.outDir, expo.root, filled);
65
+ await Bun.write(pipesPath, pipesText);
66
+ written.push(pipesPath);
67
+ } else if (ctx.force) {
68
+ pipesText = mergePipes(
69
+ await Bun.file(pipesPath).text(),
70
+ targets,
71
+ ctx.outDir,
72
+ expo.root,
73
+ filled,
74
+ );
75
+ await Bun.write(pipesPath, pipesText);
76
+ written.push(pipesPath);
77
+ } else {
78
+ pipesText = await Bun.file(pipesPath).text();
79
+ keptPipes = true;
80
+ }
81
+
82
+ const setupPath = path.join(loadDir, "routes.setup.ts");
83
+ if (!existsSync(setupPath)) {
84
+ await Bun.write(setupPath, renderExpoLoadSetup(targets, rel));
85
+ written.push(setupPath);
86
+ }
87
+
88
+ const envPath = path.join(loadDir, "env.sh");
89
+ await Bun.write(envPath, loadEnvScript(rel));
90
+ written.push(envPath);
91
+
92
+ const readmePath = path.join(loadDir, "README.md");
93
+ if (!existsSync(readmePath) || ctx.force) {
94
+ await Bun.write(readmePath, expoLoadReadme());
95
+ written.push(readmePath);
96
+ }
97
+
98
+ const crustPresent = Bun.which("crust") !== null;
99
+ const tally = countPipes(pipesText);
100
+ return {
101
+ id: "load",
102
+ ok: true,
103
+ outputs: written,
104
+ findings: [],
105
+ summary: keptPipes
106
+ ? `kept existing load/routes.pipes (--force to merge in ${targets.length} API target(s))`
107
+ : `load pipes for ${targets.length} API endpoint(s) — ${describeTally(tally, filled, targets)}${
108
+ crustPresent ? " — run with bun run flawless:load" : " — crust not found on PATH"
109
+ }`,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * The GET endpoints the screens hit, as load targets. Mutations are excluded by
115
+ * default (load-testing a POST writes junk into the target DB); the dynamic
116
+ * segments become $LOAD_<PARAM> exactly as the Next flavour renders a page
117
+ * route's, so the shared pipe/param machinery cannot tell the two apart.
118
+ */
119
+ export function collectExpoTargets(trees: ComponentTree[]): LoadTarget[] {
120
+ const out: LoadTarget[] = [];
121
+ const seen = new Set<string>();
122
+ const usedNames = new Set<string>();
123
+ const walk = (t: ComponentTree): void => {
124
+ for (const f of t.entry.apiCalls ?? []) {
125
+ if (f.method !== "GET") continue;
126
+ let p = f.urlTemplate;
127
+ for (const hole of f.holes) p = p.replace(`:${hole}`, `$LOAD_${hole.toUpperCase()}`);
128
+ if (seen.has(p)) continue;
129
+ seen.add(p);
130
+ let name = slugify(p);
131
+ // slugify collapses every $VAR to `x`, so two distinct endpoints can
132
+ // slug to one name and would then share one last-*.json — the merge keys
133
+ // on that name and the second would never emit. First claimant keeps it.
134
+ let unique = name;
135
+ for (let n = 2; usedNames.has(unique); n++) unique = `${name}-${n}`;
136
+ name = unique;
137
+ usedNames.add(name);
138
+ out.push({ name, urlPath: p, params: f.holes });
139
+ }
140
+ for (const c of t.children) walk(c);
141
+ };
142
+ for (const tree of trees) walk(tree);
143
+ // Stable order so regeneration is byte-deterministic — endpoints are
144
+ // harvested in screen-analysis order, which is not sorted.
145
+ out.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
146
+ return out;
147
+ }
148
+
149
+ /** The parameterless GET endpoints, which the auth probe can hit without a filled id. */
150
+ function probeTargets(targets: LoadTarget[]): string[] {
151
+ return targets.filter((t) => t.params.length === 0).map((t) => t.urlPath);
152
+ }
153
+
154
+ /**
155
+ * The setup module crust imports before any pipe runs — the Expo-web variant.
156
+ *
157
+ * The Next flavour probes for a REDIRECT to a login page (crust follows
158
+ * redirects, so a dead session silently measures the login form at 200). An API
159
+ * endpoint does not redirect: an expired LOAD_AUTH gets a 401 at the SAME url,
160
+ * which the redirect check would pass. So this probes a parameterless GET
161
+ * endpoint and fails on a 401/403 — the sharper signal the redirect check could
162
+ * never see, and exactly the "an authenticated /api/me answering 401" case the
163
+ * Next setup's own comment wishes for.
164
+ *
165
+ * WRITE-ONCE, like every setup stub: a hand-written check that knows the app is
166
+ * better than this one, and no regeneration overwrites it.
167
+ */
168
+ export function renderExpoLoadSetup(targets: LoadTarget[], rel: string): string {
169
+ const probes = probeTargets(targets);
170
+ return `// Setup module for routes.pipes (Expo, API-only). test-pipes imports the
171
+ // sibling <name>.setup.ts and awaits its default export before any line runs.
172
+ //
173
+ // Why an API-specific check: the Next.js flavour probes for a redirect to a
174
+ // login page, because a dead cookie makes a page route serve /login at 200. An
175
+ // API endpoint does not redirect — an expired LOAD_AUTH answers 401 at the same
176
+ // URL — so a redirect check would pass on a dead session. This fails on the 401
177
+ // instead, before a single measurement.
178
+ //
179
+ // This file is written once and never regenerated. Replace it with a check that
180
+ // knows your app if you have a sharper one; nothing will overwrite yours.
181
+
182
+ /** Parameterless GET endpoints — hittable without a filled id, so they make honest auth probes. */
183
+ const PROBES: string[] = ${JSON.stringify(probes)};
184
+
185
+ export default async () => {
186
+ const base = process.env.LOAD_BASE;
187
+ const auth = process.env.LOAD_AUTH;
188
+ if (!base || !auth) {
189
+ throw new Error(
190
+ "routes.setup: LOAD_BASE and LOAD_AUTH are required — export them or use \`crust --env-file ${underOutDir(rel, "load/.env.load")}\`",
191
+ );
192
+ }
193
+ const sep = auth.indexOf(":");
194
+ if (sep < 0) {
195
+ throw new Error(\`routes.setup: LOAD_AUTH must be a header line, e.g. "authorization: Bearer <token>" or "cookie: session=…" — got \${auth}\`);
196
+ }
197
+ const headers = { [auth.slice(0, sep).trim()]: auth.slice(sep + 1).trim() };
198
+
199
+ // Seed LOAD_<PARAM> from params.json (the e2e task's fixture), so a dynamic
200
+ // pipe requests a real record rather than a URL with an empty segment.
201
+ const paramsFile = Bun.file("${underOutDir(rel, "e2e/params.json")}");
202
+ if (await paramsFile.exists()) {
203
+ const params = (await paramsFile.json()) as {
204
+ defaults?: Record<string, string>;
205
+ routes?: Record<string, Record<string, string>>;
206
+ };
207
+ const merged: Record<string, string> = { ...(params.defaults ?? {}) };
208
+ for (const route of Object.values(params.routes ?? {})) {
209
+ for (const [k, v] of Object.entries(route)) if (v !== "") merged[k] = v;
210
+ }
211
+ for (const [k, v] of Object.entries(merged)) {
212
+ if (v !== "") process.env[\`LOAD_\${k.toUpperCase()}\`] = v;
213
+ }
214
+ }
215
+
216
+ if (PROBES.length === 0) {
217
+ // Every endpoint is parameterised; nothing can be probed without trusting a
218
+ // fixture id, and a wrong id reads as an auth failure. The pipes' own
219
+ // \`expect 200\` is the only gate left — noted, not silently skipped.
220
+ return;
221
+ }
222
+
223
+ for (const route of PROBES) {
224
+ const res = await fetch(new URL(route, base), { headers });
225
+ if (res.status === 401 || res.status === 403) {
226
+ throw new Error(
227
+ \`routes.setup: \${route} answered \${res.status} — LOAD_AUTH does not authenticate. \` +
228
+ \`Every pipe below would measure the API's auth rejection, not the endpoint. Refresh the token/cookie.\`,
229
+ );
230
+ }
231
+ if (res.status >= 500) {
232
+ throw new Error(\`routes.setup: \${route} answered \${res.status} — the API is unhealthy; fix it before load-testing.\`);
233
+ }
234
+ }
235
+ };
236
+ `;
237
+ }
238
+
239
+ function expoLoadReadme(): string {
240
+ return `# flawless-react load pipes (Expo, API-only)
241
+
242
+ Generated crust pipelines for the GET API endpoints the app's screens reach
243
+ through its fetch wrapper (\`lib/api\`). An Expo app has no server-rendered page
244
+ routes, so — unlike the Next.js flavour — these pipes are API endpoints only.
245
+ Mutating endpoints are excluded; add them by hand against a safe target.
246
+
247
+ Run: \`bun run flawless:load\` (crust must be installed — see the repo's crust
248
+ setup or https://github.com/lariocpt/crust).
249
+
250
+ - \`LOAD_BASE\` — the API origin, e.g. \`http://localhost:3001\`. Point it at a
251
+ real API (a production build for real numbers), NOT at Metro: \`expo start
252
+ --web\` serves the UI, not the endpoints these pipes hit.
253
+ - \`LOAD_AUTH\` — a header line. On React Native the app authenticates with a
254
+ bearer token (\`authorization: Bearer <token>\`); on its web build, a cookie
255
+ (\`cookie: session=…\`). Either works — the API accepts both. \`routes.setup.ts\`
256
+ fails loudly if it no longer authenticates (a dead token answers 401, and the
257
+ pipes would otherwise measure the rejection).
258
+ - \`LOAD_<PARAM>\` — one per dynamic segment (\`LOAD_BUILDINGID\`, \`LOAD_ID\`,
259
+ \`LOAD_INVOICEID\`). \`routes.setup.ts\` seeds these from \`../e2e/params.json\`;
260
+ fill them there (the same ids the e2e task uses) or source \`env.sh\`.
261
+
262
+ Note two names for the building id: some api functions take \`id\`, others
263
+ \`buildingId\`, so params.json carries both — fill each with the same building.
264
+
265
+ Baselines: after a good run, copy \`last-<name>.json\` to \`baseline-<name>.json\`.
266
+ Subsequent runs fail when p95 exceeds 2x the baseline.
267
+
268
+ \`routes.pipes\` is yours after generation. \`--force\` merges this run's endpoints
269
+ into it rather than replacing it: a rate you tuned, a pipe you commented out and
270
+ a POST flow you added by hand all survive. This README is regenerated by
271
+ \`--force\`; put your own notes in \`routes.pipes\`, below its header.
272
+ `;
273
+ }