flet-box 1.1.2 โ†’ 1.1.3

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 (154) hide show
  1. package/AGENTS_FOR_CODE_GEN.md +284 -0
  2. package/CONTRIBUTING.md +238 -0
  3. package/README.md +2 -2
  4. package/bin/cli.js +88 -1
  5. package/bin/commands/auditFix.js +181 -27
  6. package/bin/commands/checkApi.js +14 -3
  7. package/bin/commands/checkStructure.js +368 -0
  8. package/bin/commands/checkSyntax.js +293 -0
  9. package/bin/commands/checkUi.js +666 -14
  10. package/bin/commands/create.js +14 -0
  11. package/bin/commands/createBundle.js +2 -2
  12. package/bin/commands/metrics.js +448 -0
  13. package/bin/commands/release.js +18 -0
  14. package/bin/commands/report.js +849 -0
  15. package/bin/commands/tools.js +76 -0
  16. package/bin/commands/unpack.js +39 -11
  17. package/bin/commands/verify.js +879 -0
  18. package/bin/mcp.js +280 -0
  19. package/bin/utils/contract.js +27 -0
  20. package/bin/utils/dtsProps.js +5 -1
  21. package/bin/utils/migrationGuide.js +146 -0
  22. package/bin/utils/moduleExports.js +9 -5
  23. package/bin/utils/templates.js +225 -18
  24. package/bin/utils/toolkit.js +191 -0
  25. package/docs/README.md +5 -1
  26. package/docs/arquitectura.md +1 -1
  27. package/docs/cli/README.md +168 -2
  28. package/docs/guides/README.md +6 -0
  29. package/docs/guides/accessibility.md +2 -1
  30. package/docs/guides/ai-toolkit.md +166 -0
  31. package/docs/guides/compatibility.md +1 -1
  32. package/docs/guides/migration.md +92 -0
  33. package/docs/guides/minification-and-protection.md +2 -1
  34. package/docs/guides/mobile-and-platforms.md +1 -1
  35. package/docs/guides/online-updates.md +2 -0
  36. package/docs/server/deployment.md +1 -1
  37. package/docs/tools/styling.md +2 -2
  38. package/docs/tools/theme.md +139 -4
  39. package/docs/widget/Accordion.md +74 -8
  40. package/docs/widget/AdMob.md +68 -3
  41. package/docs/widget/AdSense.md +67 -2
  42. package/docs/widget/AlertDialog.md +1 -1
  43. package/docs/widget/AnimatedBox.md +80 -39
  44. package/docs/widget/AnimatedText.md +74 -18
  45. package/docs/widget/Audio.md +73 -14
  46. package/docs/widget/Avatar.md +69 -4
  47. package/docs/widget/Badge.md +68 -3
  48. package/docs/widget/BottomSheet.md +2 -2
  49. package/docs/widget/Button.md +69 -3
  50. package/docs/widget/Card.md +69 -4
  51. package/docs/widget/Carousel.md +67 -2
  52. package/docs/widget/Chart.md +67 -2
  53. package/docs/widget/Checkbox.md +74 -8
  54. package/docs/widget/Chip.md +68 -3
  55. package/docs/widget/CircularBar.md +74 -15
  56. package/docs/widget/CircularChart.md +71 -10
  57. package/docs/widget/CodeViewer.md +67 -2
  58. package/docs/widget/Column.md +68 -3
  59. package/docs/widget/Container.md +68 -3
  60. package/docs/widget/DataTable.md +67 -2
  61. package/docs/widget/Divider.md +67 -2
  62. package/docs/widget/DraggBox.md +67 -2
  63. package/docs/widget/Dropdown.md +76 -10
  64. package/docs/widget/DroppBox.md +67 -2
  65. package/docs/widget/FloatingActionButton.md +68 -3
  66. package/docs/widget/GridView.md +67 -2
  67. package/docs/widget/Icon.md +71 -5
  68. package/docs/widget/Image.md +68 -3
  69. package/docs/widget/Input.md +68 -3
  70. package/docs/widget/Inspector.md +2 -2
  71. package/docs/widget/InstallButton.md +73 -13
  72. package/docs/widget/ListTile.md +69 -3
  73. package/docs/widget/ListView.md +67 -2
  74. package/docs/widget/Maps.md +68 -3
  75. package/docs/widget/Markdown.md +74 -15
  76. package/docs/widget/MatrixRain.md +72 -11
  77. package/docs/widget/Menu.md +209 -0
  78. package/docs/widget/Modal.md +3 -3
  79. package/docs/widget/Pagination.md +68 -3
  80. package/docs/widget/ParallaxBox.md +72 -12
  81. package/docs/widget/ProgressBar.md +69 -4
  82. package/docs/widget/QRCode.md +67 -2
  83. package/docs/widget/README.md +54 -50
  84. package/docs/widget/Radio.md +76 -10
  85. package/docs/widget/Rating.md +70 -6
  86. package/docs/widget/Row.md +68 -3
  87. package/docs/widget/Skeleton.md +68 -3
  88. package/docs/widget/Slider.md +75 -9
  89. package/docs/widget/SnackBar.md +2 -2
  90. package/docs/widget/Stack.md +68 -3
  91. package/docs/widget/Stepper.md +68 -3
  92. package/docs/widget/Switch.md +74 -8
  93. package/docs/widget/Text.md +71 -4
  94. package/docs/widget/ThemePicker.md +197 -0
  95. package/docs/widget/Tooltip.md +72 -7
  96. package/docs/widget/TreeView.md +70 -10
  97. package/docs/widget/Video.md +71 -10
  98. package/package.json +10 -3
  99. package/skills/fletbox-design/SKILL.md +89 -5
  100. package/skills/fletbox-motion/SKILL.md +1 -1
  101. package/skills/fletbox-rules/SKILL.md +56 -4
  102. package/src/core/runApp.js +12 -0
  103. package/src/index.d.ts +374 -101
  104. package/src/index.js +9 -0
  105. package/src/navigations/AdaptiveScaffold.js +2 -2
  106. package/src/navigations/BottomNavigation.js +1 -1
  107. package/src/navigations/Scaffold.js +18 -6
  108. package/src/tools/useState.js +11 -4
  109. package/src/utils/colors.js +275 -0
  110. package/src/utils/errorBoundary.js +288 -0
  111. package/src/utils/index.js +10 -2
  112. package/src/utils/stackPosition.js +39 -24
  113. package/src/utils/syntaxHighlight.js +4 -1
  114. package/src/utils/themes.js +279 -3
  115. package/src/widget-factory/addChildren.js +17 -10
  116. package/src/widget-factory/assignProps.js +18 -6
  117. package/src/widget-factory/createWidget.js +7 -0
  118. package/src/widget-factory/deprecations.js +77 -0
  119. package/src/widget-factory/translateProps.js +11 -1
  120. package/src/widget-factory/warnings.js +1 -27
  121. package/src/widget-factory/widgetFactory.js +59 -17
  122. package/src/widgets/Accordion.js +21 -7
  123. package/src/widgets/Badge.js +11 -2
  124. package/src/widgets/Button.js +15 -3
  125. package/src/widgets/Carousel.js +12 -4
  126. package/src/widgets/Chart.js +7 -2
  127. package/src/widgets/Checkbox.js +1 -2
  128. package/src/widgets/Chip.js +24 -10
  129. package/src/widgets/CircularBar.js +9 -3
  130. package/src/widgets/CircularChart.js +5 -3
  131. package/src/widgets/Container.js +6 -0
  132. package/src/widgets/Divider.js +3 -1
  133. package/src/widgets/DraggBox.js +9 -0
  134. package/src/widgets/Dropdown.js +3 -3
  135. package/src/widgets/DroppBox.js +5 -0
  136. package/src/widgets/FloatingActionButton.js +1 -1
  137. package/src/widgets/Icon.js +5 -1
  138. package/src/widgets/InstallButton.js +4 -2
  139. package/src/widgets/Markdown.js +171 -103
  140. package/src/widgets/Menu.js +340 -0
  141. package/src/widgets/Modal.js +7 -1
  142. package/src/widgets/Pagination.js +10 -5
  143. package/src/widgets/ProgressBar.js +5 -4
  144. package/src/widgets/QRCode.js +4 -0
  145. package/src/widgets/Radio.js +1 -2
  146. package/src/widgets/Rating.js +9 -0
  147. package/src/widgets/Slider.js +4 -1
  148. package/src/widgets/SnackBar.js +2 -2
  149. package/src/widgets/Switch.js +1 -2
  150. package/src/widgets/Text.js +9 -5
  151. package/src/widgets/ThemePicker.js +234 -0
  152. package/src/widgets/TreeView.js +2 -1
  153. package/src/widgets/index.js +2 -0
  154. package/docs/CONTRIBUTING.md +0 -156
@@ -20,6 +20,7 @@ import { parseGithubRepo } from "../utils/bundleConfig.js";
20
20
  import { checkApi } from "../commands/checkApi.js";
21
21
  import { unpack } from "../commands/unpack.js";
22
22
  import { skillNames, readSkill } from "../utils/skills.js";
23
+ import { CONTRACT_FILE, readContract } from "../utils/contract.js";
23
24
  import { c, banner, gradient } from "../utils/colors.js";
24
25
 
25
26
  /**
@@ -223,6 +224,7 @@ export const createProject = async (projectName, rawArgs = []) => {
223
224
  // AI agent contract: rules, memory and the generated API cheatsheet.
224
225
  ".fletbox/api",
225
226
  ".fletbox/memory/notes",
227
+ ".fletbox/metrics",
226
228
  ".claude",
227
229
  ".claude/skills",
228
230
  ".qoder/skills",
@@ -449,6 +451,12 @@ export const createProject = async (projectName, rawArgs = []) => {
449
451
  templates.cursorRules(),
450
452
  );
451
453
 
454
+ // The code-generation contract: one file at the package root, copied here
455
+ // byte-for-byte. AGENTS.md is this app's rules; this one is what an agent
456
+ // that *writes* code in the app must obey โ€” the golden rule, the
457
+ // prohibitions, the ten design rules and the verification protocol.
458
+ createFile(path.join(projectPath, CONTRACT_FILE), readContract());
459
+
452
460
  // The agent skills: `skills/` at the package root is the only source, one
453
461
  // kebab-case folder per subject (rules, design, motion, memory). The app gets
454
462
  // the same text twice over: once as `skills/` at its own root โ€” the folder any
@@ -496,6 +504,12 @@ export const createProject = async (projectName, rawArgs = []) => {
496
504
  path.join(projectPath, ".fletbox/memory/notes/current-status.md"),
497
505
  templates.memoryStatusMd(finalName, template),
498
506
  );
507
+ // The metrics log: one row per verify run, so the north metric can say how
508
+ // much of the generated code passed the gate with no correction at all.
509
+ createFile(
510
+ path.join(projectPath, ".fletbox/metrics/generations.md"),
511
+ templates.metricsLogMd(),
512
+ );
499
513
  createFile(
500
514
  path.join(projectPath, "scripts/check-app.mjs"),
501
515
  templates.checkAppScript(finalName),
@@ -260,8 +260,8 @@ export const createBundle = async (targetArg) => {
260
260
  // Visual reference only: createBundle NEVER bumps the version โ€” only
261
261
  // `flet-box release` does. Print what each release flag would produce so the
262
262
  // next version is visible at build time.
263
- console.log(section("๐Ÿ”ข", `Versiรณn del bundle: ${appVersion}`, "#34d399"));
264
- console.log(c("gray", " Quรฉ harรญa cada release:"));
263
+ console.log(section("๐Ÿ”ข", `Bundle version: ${appVersion}`, "#34d399"));
264
+ console.log(c("gray", " What each release would do:"));
265
265
  for (const [flag, type] of [
266
266
  ["--patch", "patch"],
267
267
  ["--minor", "minor"],
@@ -0,0 +1,448 @@
1
+ /**
2
+ * @file bin/commands/metrics.js
3
+ * @description The north metric โ€” how much generated code passes the gate with
4
+ * no correction at all.
5
+ *
6
+ * The rule this file exists to enforce is one line long: *do not optimise before
7
+ * measuring.* So every `flet-box verify` run appends a row to a log (see
8
+ * `recordGeneration` in `verify.js`), and this command reads those rows and
9
+ * answers two questions:
10
+ *
11
+ * 1. What share of the generated code passed the gate with no correction at
12
+ * all? A **generation** is a run of failing rows closed by the first green
13
+ * one โ€” the rows before it are the corrections that were needed. The north
14
+ * metric is the share of generations whose *first* run was green.
15
+ * 2. Which failures keep coming back? Those are the rules that have not been
16
+ * written yet. Each one is printed with the rule that should own it, and a
17
+ * code that recurs with no owner behind it **fails** this command: that is
18
+ * the loop closing.
19
+ * 3. Was a failure the gate's own fault? A code can be recorded in the log's
20
+ * "Gate defects" table with the window of runs it accounts for. Those runs
21
+ * are the framework's debt, not a correction this project needed, so they
22
+ * count as passes and never feed the rule loop. A run after the window's
23
+ * `Fixed` day counts again: a defect that comes back cannot hide behind an
24
+ * old row.
25
+ *
26
+ * Node core only: this file ships inside every app `flet-box create` builds.
27
+ */
28
+
29
+ import fs from "node:fs";
30
+ import path from "node:path";
31
+ import { CODE_RULE } from "./checkUi.js";
32
+ import { c } from "../utils/colors.js";
33
+
34
+ /** How many times a code has to come back before it counts as a rule to write. */
35
+ export const RECURRING = 3;
36
+
37
+ /**
38
+ * Where a code should be taught, by the gate that reports it.
39
+ *
40
+ * A code without an owner is a rule that was never written: the gate proves the
41
+ * mistake happens, and nothing tells the next agent why it matters.
42
+ */
43
+ export const OWNERS = {
44
+ CIRCULAR_IMPORT: "skills/fletbox-rules/SKILL.md R7 โ€” one module, one direction",
45
+ SOURCE_OUTSIDE_ROOT: "skills/fletbox-rules/SKILL.md R7 โ€” a module lives in src/",
46
+ HAND_WRITTEN_DOM: "skills/fletbox-rules/SKILL.md R7 โ€” the engine is the only renderer",
47
+ UNKNOWN_EXPORT: "skills/fletbox-rules/SKILL.md R1 โ€” FletBox only",
48
+ WRONG_STACK: "skills/fletbox-rules/SKILL.md R1 โ€” FletBox only",
49
+ UNKNOWN_PROP: "skills/fletbox-rules/SKILL.md R2 โ€” never invent an API",
50
+ POSITIONAL_ARG: "skills/fletbox-rules/SKILL.md R1 โ€” a widget is a call with a props object",
51
+ MODULE_NOT_FOUND: "skills/fletbox-rules/SKILL.md R2 โ€” look the module up",
52
+ MISSING_EXPORT: "skills/fletbox-rules/SKILL.md R2 โ€” look the module up",
53
+ DEEP_IMPORT: "skills/fletbox-rules/SKILL.md R2 โ€” import from the barrel",
54
+ UNDOCUMENTED_PROP: "src/index.d.ts โ€” the declaration is the contract",
55
+ INVENTED_PROP: "src/index.d.ts โ€” the declaration is the contract",
56
+ DEAD_PROP: "src/index.d.ts โ€” the declaration or the widget",
57
+ GATE_FAILED: "the gate itself โ€” a missing package, not a mistake in the code",
58
+ SYNTAX_ERROR: "the parser itself โ€” the finding carries the line, the column and the hint",
59
+ TEST_SUITE_FAILED: "the project's own suite โ€” run it alone, its failing line names the rule",
60
+ TYPE_ERROR: "skills/fletbox-rules/SKILL.md R2 โ€” the declaration is the contract",
61
+ };
62
+
63
+ /**
64
+ * A code the gate itself got wrong, over a window of runs.
65
+ *
66
+ * The marker lives in the log, not in this file: it describes a moment in one
67
+ * project's history, and a table that hard-coded "MISSING_EXPORT is never the
68
+ * app's fault" would silence the real mistake the next time it happens.
69
+ *
70
+ * @typedef {object} GateDefect
71
+ * @property {string} code
72
+ * @property {string} seen First calendar day (YYYY-MM-DD) it accounts for.
73
+ * @property {string|null} fixed Last day it accounts for; null while it is open.
74
+ * @property {string} where Why the gate was wrong, and where the fix lives.
75
+ */
76
+
77
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
78
+ const CODE = /^[A-Z][A-Z0-9_]*$/;
79
+
80
+ /**
81
+ * Split a markdown table row into trimmed cells, or null when the line is not
82
+ * one. The log holds two tables and prose; both readers ignore the same lines.
83
+ *
84
+ * @param {string} line
85
+ * @returns {string[]|null}
86
+ */
87
+ const cellsOf = (line) => {
88
+ const trimmed = line.trim();
89
+ if (!trimmed.startsWith("|")) return null;
90
+ return trimmed
91
+ .slice(1, trimmed.endsWith("|") ? -1 : undefined)
92
+ .split("|")
93
+ .map((cell) => cell.trim());
94
+ };
95
+
96
+ /**
97
+ * Read the "Gate defects" table out of a log's text.
98
+ *
99
+ * A run row starts with a timestamp and a registry row starts with a code, so
100
+ * the two tables share a file without either reader guessing.
101
+ *
102
+ * @param {string} text
103
+ * @returns {GateDefect[]}
104
+ */
105
+ const parseDefectsFrom = (text) => {
106
+ const defects = [];
107
+ for (const line of text.split("\n")) {
108
+ const cells = cellsOf(line);
109
+ if (!cells || cells.length < 2) continue;
110
+ if (!CODE.test(cells[0]) || !DATE.test(cells[1])) continue;
111
+ defects.push({
112
+ code: cells[0],
113
+ seen: cells[1],
114
+ fixed: DATE.test(cells[2] || "") ? cells[2] : null,
115
+ where: cells.slice(3).join(" | ").trim(),
116
+ });
117
+ }
118
+ return defects;
119
+ };
120
+
121
+ /**
122
+ * Read the gate-defect records of a log file.
123
+ *
124
+ * @param {string} file
125
+ * @returns {GateDefect[]}
126
+ */
127
+ export const parseDefects = (file) => parseDefectsFrom(fs.readFileSync(file, "utf8"));
128
+
129
+ /**
130
+ * Whether a code was a gate defect on the day a run happened.
131
+ *
132
+ * @param {GateDefect[]} defects
133
+ * @param {string} code
134
+ * @param {string} when ISO timestamp of the run.
135
+ * @returns {boolean}
136
+ */
137
+ export const gateDefect = (defects, code, when) => {
138
+ const day = when.slice(0, 10);
139
+ return defects.some(
140
+ (defect) =>
141
+ defect.code === code && day >= defect.seen && (!defect.fixed || day <= defect.fixed),
142
+ );
143
+ };
144
+
145
+ /**
146
+ * Split a row's codes into the gate's and this project's.
147
+ *
148
+ * A row that failed only because of a defect is exonerated: the correction it
149
+ * needed belonged to the framework, and holding it against the code under test
150
+ * is the metric lying about that code.
151
+ *
152
+ * @param {object} row
153
+ * @param {GateDefect[]} defects
154
+ * @returns {object}
155
+ */
156
+ const classifyRow = (row, defects) => {
157
+ const defectCodes = row.codes.filter((code) => gateDefect(defects, code, row.when));
158
+ const appCodes = row.codes.filter((code) => !defectCodes.includes(code));
159
+ return {
160
+ ...row,
161
+ defectCodes,
162
+ appCodes,
163
+ exonerated: !row.ok && defectCodes.length > 0 && appCodes.length === 0,
164
+ };
165
+ };
166
+
167
+ /**
168
+ * The rule that owns a code.
169
+ *
170
+ * @param {string} code
171
+ * @returns {string|null} null when no rule owns it yet
172
+ */
173
+ export const owner = (code) => {
174
+ if (OWNERS[code]) return OWNERS[code];
175
+ const rule = CODE_RULE[code];
176
+ return rule ? `skills/fletbox-design/SKILL.md rule ${rule}` : null;
177
+ };
178
+
179
+ /**
180
+ * Where a project's metrics log lives.
181
+ *
182
+ * A generated app keeps it under `.fletbox/`, next to the memory journal; the
183
+ * framework keeps it in `metrics/`. A project with no log is not measured โ€”
184
+ * that is the whole point of the file existing.
185
+ *
186
+ * @param {string} root
187
+ * @returns {string|null}
188
+ */
189
+ export const metricsLog = (root) => {
190
+ const candidates = [
191
+ path.join(root, ".fletbox", "metrics", "generations.md"),
192
+ path.join(root, "metrics", "generations.md"),
193
+ ];
194
+ return candidates.find((file) => fs.existsSync(file)) || null;
195
+ };
196
+
197
+ /**
198
+ * Read the rows of a metrics log.
199
+ *
200
+ * `codes` keeps everything the row reported; `defectCodes`, `appCodes` and
201
+ * `exonerated` come from the log's "Gate defects" table, so a caller can still
202
+ * see what happened without the metric charging it to the project.
203
+ *
204
+ * @param {string} file
205
+ * @returns {Array<{when: string, project: string, ok: boolean, gates: string, codes: string[], defectCodes: string[], appCodes: string[], exonerated: boolean}>}
206
+ */
207
+ export const parseLog = (file) => {
208
+ const text = fs.readFileSync(file, "utf8");
209
+ const defects = parseDefectsFrom(text);
210
+ const rows = [];
211
+ for (const line of text.split("\n")) {
212
+ const cells = cellsOf(line);
213
+ if (!cells || cells.length < 5) continue;
214
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(cells[0])) continue;
215
+ rows.push(
216
+ classifyRow(
217
+ {
218
+ when: cells[0],
219
+ project: cells[1],
220
+ ok: cells[2] === "โœ“",
221
+ gates: cells[3],
222
+ codes: cells[4] === "โ€”" ? [] : cells[4].split(",").map((code) => code.trim()),
223
+ },
224
+ defects,
225
+ ),
226
+ );
227
+ }
228
+ return rows;
229
+ };
230
+
231
+ /**
232
+ * Group the rows into generations: a run of failing rows closed by the first
233
+ * green one. A green row that follows a green row is its own generation.
234
+ *
235
+ * A run the gate itself failed is left out of the arithmetic: it measured
236
+ * nothing about the code under test, so counting it as a pass would move the
237
+ * number without the code getting better, and counting it as a correction
238
+ * would blame the project for the framework's defect.
239
+ *
240
+ * @param {Array<object>} rows
241
+ * @returns {Array<{project: string, firstTry: boolean, corrections: number, unfinished?: boolean}>}
242
+ */
243
+ export const groupGenerations = (rows) => {
244
+ const byProject = new Map();
245
+ for (const row of rows) {
246
+ if (row.exonerated) continue;
247
+ if (!byProject.has(row.project)) byProject.set(row.project, []);
248
+ byProject.get(row.project).push(row);
249
+ }
250
+ const generations = [];
251
+ for (const [project, list] of byProject) {
252
+ let open = null;
253
+ for (const row of list) {
254
+ if (row.ok) {
255
+ generations.push({
256
+ project,
257
+ firstTry: !open,
258
+ corrections: open ? open.length : 0,
259
+ });
260
+ open = null;
261
+ } else {
262
+ open = open || [];
263
+ open.push(row);
264
+ }
265
+ }
266
+ if (open) {
267
+ generations.push({ project, firstTry: false, corrections: open.length, unfinished: true });
268
+ }
269
+ }
270
+ return generations;
271
+ };
272
+
273
+ /**
274
+ * Count how often each code was reported against the project โ€” the occurrences
275
+ * the log's "Gate defects" table does not cover.
276
+ *
277
+ * @param {Array<object>} rows
278
+ * @returns {Map<string, number>}
279
+ */
280
+ export const rankCodes = (rows) => {
281
+ const counts = new Map();
282
+ for (const row of rows) {
283
+ for (const code of row.appCodes ?? row.codes) {
284
+ counts.set(code, (counts.get(code) || 0) + 1);
285
+ }
286
+ }
287
+ return counts;
288
+ };
289
+
290
+ /**
291
+ * Count how often each gate defect accounted for a run.
292
+ *
293
+ * @param {Array<object>} rows
294
+ * @returns {Map<string, number>}
295
+ */
296
+ export const rankDefects = (rows) => {
297
+ const counts = new Map();
298
+ for (const row of rows) {
299
+ for (const code of row.defectCodes ?? []) {
300
+ counts.set(code, (counts.get(code) || 0) + 1);
301
+ }
302
+ }
303
+ return counts;
304
+ };
305
+
306
+ /**
307
+ * The `flet-box metrics` command.
308
+ *
309
+ * @param {string[]} argv Flags after the command name.
310
+ * @param {{print?: (line: string) => void, root?: string}} [io]
311
+ * @returns {Promise<{exitCode: number, north: number, generations: number}>}
312
+ */
313
+ export const metrics = async (argv = [], io = {}) => {
314
+ const print = io.print ?? ((line) => console.log(line));
315
+ const rootIndex = argv.indexOf("--root");
316
+ const root = path.resolve(
317
+ rootIndex >= 0 && argv[rootIndex + 1] ? argv[rootIndex + 1] : io.root || process.cwd(),
318
+ );
319
+ const logIndex = argv.indexOf("--log");
320
+ const file = logIndex >= 0 && argv[logIndex + 1]
321
+ ? path.resolve(argv[logIndex + 1])
322
+ : metricsLog(root);
323
+
324
+ if (!file || !fs.existsSync(file)) {
325
+ print(
326
+ c(
327
+ "yellow",
328
+ `๐Ÿ“ˆ No metrics log yet โ€” ${path.join(root, ".fletbox", "metrics", "generations.md")} does not exist.`,
329
+ ),
330
+ );
331
+ print(c("gray", " Every `flet-box verify` run writes a row into it; run the gates once."));
332
+ return { exitCode: 0, north: 0, generations: 0 };
333
+ }
334
+
335
+ const rows = parseLog(file);
336
+ const generations = groupGenerations(rows);
337
+ const firstTry = generations.filter((generation) => generation.firstTry).length;
338
+ const north = generations.length ? firstTry / generations.length : 0;
339
+ const counts = rankCodes(rows);
340
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
341
+ const defects = parseDefects(file);
342
+ const defectCounts = rankDefects(rows);
343
+ const exonerated = rows.filter((row) => row.exonerated).length;
344
+
345
+ print(c("brightCyan", `๐Ÿ“ˆ ${path.relative(root, file) || file}`));
346
+ print(
347
+ c(
348
+ "gray",
349
+ generations.length
350
+ ? ` north metric โ€” ${firstTry}/${generations.length} generation(s) passed with no correction (${(north * 100).toFixed(0)}%)`
351
+ : " north metric โ€” no generation to measure yet",
352
+ ),
353
+ );
354
+ print(c("gray", ` ${rows.length} run(s) logged`));
355
+ if (exonerated) {
356
+ print(
357
+ c(
358
+ "gray",
359
+ ` ${exonerated} run(s) failed only because of a gate defect โ€” left out of the metric, not charged as a correction`,
360
+ ),
361
+ );
362
+ }
363
+
364
+ // The framework's debt, kept next to the number it distorts. A fixed defect
365
+ // stays on the record; an open one says the metric is measuring a broken gate.
366
+ if (defects.length) {
367
+ print("");
368
+ print(c("gray", " gate defects โ€” a code the gate reported, not the code it checked:"));
369
+ for (const defect of defects) {
370
+ const count = defectCounts.get(defect.code) || 0;
371
+ const status = defect.fixed ? `fixed ${defect.fixed}` : `โš  OPEN since ${defect.seen}`;
372
+ print(
373
+ c(
374
+ defect.fixed ? "gray" : "yellow",
375
+ ` ${String(count).padStart(3)}ร— ${defect.code.padEnd(22)} ${status}${
376
+ defect.where ? ` ยท ${defect.where}` : ""
377
+ }`,
378
+ ),
379
+ );
380
+ }
381
+ const open = defects.filter((defect) => !defect.fixed);
382
+ if (open.length) {
383
+ print(
384
+ c(
385
+ "yellow",
386
+ ` โš  ${open.length} gate defect(s) still open โ€” the metric is being read against a gate that is wrong.`,
387
+ ),
388
+ );
389
+ }
390
+ }
391
+
392
+ if (ranked.length) {
393
+ print("");
394
+ print(c("gray", " the failures that come back, and the rule each one should own:"));
395
+ for (const [code, count] of ranked.slice(0, 3)) {
396
+ const where = owner(code);
397
+ print(
398
+ c(
399
+ "gray",
400
+ ` ${String(count).padStart(3)}ร— ${code.padEnd(22)} ${where || "โ€” no rule owns this one yet"}`,
401
+ ),
402
+ );
403
+ }
404
+ // A code that comes back with a rule behind it is a different problem: the
405
+ // rule exists and the mistake still happens, so the rule is not landing.
406
+ // That is worth naming โ€” it is the difference between "write the rule" and
407
+ // "make the rule impossible to miss".
408
+ const stubborn = ranked.filter(
409
+ ([code, count]) => count >= RECURRING && owner(code),
410
+ );
411
+ if (stubborn.length) {
412
+ print("");
413
+ for (const [code] of stubborn) {
414
+ print(
415
+ c(
416
+ "yellow",
417
+ ` โš  ${code} came back ${RECURRING} times with a rule behind it โ€” the rule is not landing.`,
418
+ ),
419
+ );
420
+ }
421
+ }
422
+ }
423
+
424
+ const unowned = ranked.filter(([code, count]) => count >= RECURRING && !owner(code));
425
+ print("");
426
+ if (unowned.length) {
427
+ print(
428
+ c(
429
+ "red",
430
+ `โŒ ${unowned.length} code(s) came back ${RECURRING} times or more and no rule owns them: ${unowned
431
+ .map(([code]) => code)
432
+ .join(", ")}`,
433
+ ),
434
+ );
435
+ print(
436
+ c(
437
+ "gray",
438
+ " Write the rule where it belongs (the design skill, the rules skill, or the gate), then run again.",
439
+ ),
440
+ );
441
+ return { exitCode: 1, north, generations: generations.length };
442
+ }
443
+
444
+ print(c("green", "โœ… every recurring failure has a rule behind it."));
445
+ return { exitCode: 0, north, generations: generations.length };
446
+ };
447
+
448
+ export default metrics;
@@ -29,6 +29,7 @@ import { c, banner, gradient } from "../utils/colors.js";
29
29
  import { createBundle } from "./createBundle.js";
30
30
  import { resolveUpdateUrl } from "../utils/bundleConfig.js";
31
31
  import { INITIAL_VERSION, incrementVersion, isValidVersion } from "../utils/version.js";
32
+ import { isFrameworkProject, missingMigration } from "../utils/migrationGuide.js";
32
33
 
33
34
  /**
34
35
  * Runs a git command in `cwd`, returning trimmed stdout. Throws on failure.
@@ -109,6 +110,23 @@ export const release = async (argv = []) => {
109
110
  const current = isValidVersion(pkg.version) ? pkg.version : INITIAL_VERSION;
110
111
  const next = incrementVersion(current, type);
111
112
 
113
+ // ---- Guard: a major needs its migration guide ---------------------------
114
+ // A major is the release where an app has to change something, so the guide
115
+ // has to say what before the version exists. The guide belongs to the
116
+ // framework, so the guard never fires inside somebody's app.
117
+ if (type === "major" && isFrameworkProject(projectRoot)) {
118
+ const missing = missingMigration(next, projectRoot);
119
+ if (missing) {
120
+ console.error(c("red", `โŒ The migration guide does not cover ${next}`));
121
+ console.log(c("gray", ` ${missing.reason}.`));
122
+ console.log(
123
+ c("gray", " Add a section to docs/guides/migration.md that says what this major"),
124
+ );
125
+ console.log(c("gray", " changes, one row per renamed or removed prop, then release again."));
126
+ process.exit(1);
127
+ }
128
+ }
129
+
112
130
  pkg.version = next;
113
131
  // Preserve the file's existing 2-space style + trailing newline.
114
132
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");