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.
- package/AGENTS_FOR_CODE_GEN.md +284 -0
- package/CONTRIBUTING.md +238 -0
- package/README.md +2 -2
- package/bin/cli.js +88 -1
- package/bin/commands/auditFix.js +181 -27
- package/bin/commands/checkApi.js +14 -3
- package/bin/commands/checkStructure.js +368 -0
- package/bin/commands/checkSyntax.js +293 -0
- package/bin/commands/checkUi.js +666 -14
- package/bin/commands/create.js +14 -0
- package/bin/commands/createBundle.js +2 -2
- package/bin/commands/metrics.js +448 -0
- package/bin/commands/release.js +18 -0
- package/bin/commands/report.js +849 -0
- package/bin/commands/tools.js +76 -0
- package/bin/commands/unpack.js +39 -11
- package/bin/commands/verify.js +879 -0
- package/bin/mcp.js +280 -0
- package/bin/utils/contract.js +27 -0
- package/bin/utils/dtsProps.js +5 -1
- package/bin/utils/migrationGuide.js +146 -0
- package/bin/utils/moduleExports.js +9 -5
- package/bin/utils/templates.js +225 -18
- package/bin/utils/toolkit.js +191 -0
- package/docs/README.md +5 -1
- package/docs/arquitectura.md +1 -1
- package/docs/cli/README.md +168 -2
- package/docs/guides/README.md +6 -0
- package/docs/guides/accessibility.md +2 -1
- package/docs/guides/ai-toolkit.md +166 -0
- package/docs/guides/compatibility.md +1 -1
- package/docs/guides/migration.md +92 -0
- package/docs/guides/minification-and-protection.md +2 -1
- package/docs/guides/mobile-and-platforms.md +1 -1
- package/docs/guides/online-updates.md +2 -0
- package/docs/server/deployment.md +1 -1
- package/docs/tools/styling.md +2 -2
- package/docs/tools/theme.md +139 -4
- package/docs/widget/Accordion.md +74 -8
- package/docs/widget/AdMob.md +68 -3
- package/docs/widget/AdSense.md +67 -2
- package/docs/widget/AlertDialog.md +1 -1
- package/docs/widget/AnimatedBox.md +80 -39
- package/docs/widget/AnimatedText.md +74 -18
- package/docs/widget/Audio.md +73 -14
- package/docs/widget/Avatar.md +69 -4
- package/docs/widget/Badge.md +68 -3
- package/docs/widget/BottomSheet.md +2 -2
- package/docs/widget/Button.md +69 -3
- package/docs/widget/Card.md +69 -4
- package/docs/widget/Carousel.md +67 -2
- package/docs/widget/Chart.md +67 -2
- package/docs/widget/Checkbox.md +74 -8
- package/docs/widget/Chip.md +68 -3
- package/docs/widget/CircularBar.md +74 -15
- package/docs/widget/CircularChart.md +71 -10
- package/docs/widget/CodeViewer.md +67 -2
- package/docs/widget/Column.md +68 -3
- package/docs/widget/Container.md +68 -3
- package/docs/widget/DataTable.md +67 -2
- package/docs/widget/Divider.md +67 -2
- package/docs/widget/DraggBox.md +67 -2
- package/docs/widget/Dropdown.md +76 -10
- package/docs/widget/DroppBox.md +67 -2
- package/docs/widget/FloatingActionButton.md +68 -3
- package/docs/widget/GridView.md +67 -2
- package/docs/widget/Icon.md +71 -5
- package/docs/widget/Image.md +68 -3
- package/docs/widget/Input.md +68 -3
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +73 -13
- package/docs/widget/ListTile.md +69 -3
- package/docs/widget/ListView.md +67 -2
- package/docs/widget/Maps.md +68 -3
- package/docs/widget/Markdown.md +74 -15
- package/docs/widget/MatrixRain.md +72 -11
- package/docs/widget/Menu.md +209 -0
- package/docs/widget/Modal.md +3 -3
- package/docs/widget/Pagination.md +68 -3
- package/docs/widget/ParallaxBox.md +72 -12
- package/docs/widget/ProgressBar.md +69 -4
- package/docs/widget/QRCode.md +67 -2
- package/docs/widget/README.md +54 -50
- package/docs/widget/Radio.md +76 -10
- package/docs/widget/Rating.md +70 -6
- package/docs/widget/Row.md +68 -3
- package/docs/widget/Skeleton.md +68 -3
- package/docs/widget/Slider.md +75 -9
- package/docs/widget/SnackBar.md +2 -2
- package/docs/widget/Stack.md +68 -3
- package/docs/widget/Stepper.md +68 -3
- package/docs/widget/Switch.md +74 -8
- package/docs/widget/Text.md +71 -4
- package/docs/widget/ThemePicker.md +197 -0
- package/docs/widget/Tooltip.md +72 -7
- package/docs/widget/TreeView.md +70 -10
- package/docs/widget/Video.md +71 -10
- package/package.json +10 -3
- package/skills/fletbox-design/SKILL.md +89 -5
- package/skills/fletbox-motion/SKILL.md +1 -1
- package/skills/fletbox-rules/SKILL.md +56 -4
- package/src/core/runApp.js +12 -0
- package/src/index.d.ts +374 -101
- package/src/index.js +9 -0
- package/src/navigations/AdaptiveScaffold.js +2 -2
- package/src/navigations/BottomNavigation.js +1 -1
- package/src/navigations/Scaffold.js +18 -6
- package/src/tools/useState.js +11 -4
- package/src/utils/colors.js +275 -0
- package/src/utils/errorBoundary.js +288 -0
- package/src/utils/index.js +10 -2
- package/src/utils/stackPosition.js +39 -24
- package/src/utils/syntaxHighlight.js +4 -1
- package/src/utils/themes.js +279 -3
- package/src/widget-factory/addChildren.js +17 -10
- package/src/widget-factory/assignProps.js +18 -6
- package/src/widget-factory/createWidget.js +7 -0
- package/src/widget-factory/deprecations.js +77 -0
- package/src/widget-factory/translateProps.js +11 -1
- package/src/widget-factory/warnings.js +1 -27
- package/src/widget-factory/widgetFactory.js +59 -17
- package/src/widgets/Accordion.js +21 -7
- package/src/widgets/Badge.js +11 -2
- package/src/widgets/Button.js +15 -3
- package/src/widgets/Carousel.js +12 -4
- package/src/widgets/Chart.js +7 -2
- package/src/widgets/Checkbox.js +1 -2
- package/src/widgets/Chip.js +24 -10
- package/src/widgets/CircularBar.js +9 -3
- package/src/widgets/CircularChart.js +5 -3
- package/src/widgets/Container.js +6 -0
- package/src/widgets/Divider.js +3 -1
- package/src/widgets/DraggBox.js +9 -0
- package/src/widgets/Dropdown.js +3 -3
- package/src/widgets/DroppBox.js +5 -0
- package/src/widgets/FloatingActionButton.js +1 -1
- package/src/widgets/Icon.js +5 -1
- package/src/widgets/InstallButton.js +4 -2
- package/src/widgets/Markdown.js +171 -103
- package/src/widgets/Menu.js +340 -0
- package/src/widgets/Modal.js +7 -1
- package/src/widgets/Pagination.js +10 -5
- package/src/widgets/ProgressBar.js +5 -4
- package/src/widgets/QRCode.js +4 -0
- package/src/widgets/Radio.js +1 -2
- package/src/widgets/Rating.js +9 -0
- package/src/widgets/Slider.js +4 -1
- package/src/widgets/SnackBar.js +2 -2
- package/src/widgets/Switch.js +1 -2
- package/src/widgets/Text.js +9 -5
- package/src/widgets/ThemePicker.js +234 -0
- package/src/widgets/TreeView.js +2 -1
- package/src/widgets/index.js +2 -0
- package/docs/CONTRIBUTING.md +0 -156
package/bin/commands/create.js
CHANGED
|
@@ -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("๐ข", `
|
|
264
|
-
console.log(c("gray", "
|
|
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;
|
package/bin/commands/release.js
CHANGED
|
@@ -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");
|