@stag-build/phonebook 0.1.3 → 0.1.4

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.
@@ -0,0 +1,208 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { realpath } from 'node:fs/promises';
4
+ import { relative, resolve } from 'node:path';
5
+ import { computeStats } from './stats.js';
6
+ import { componentsNamedIn } from './ios.js';
7
+ const run = promisify(execFile);
8
+ /**
9
+ * Narrow a finished report to the components declared in `paths`.
10
+ *
11
+ * The scan itself is never narrowed, and that distinction is the whole design.
12
+ * Which types are enums, what a project's preview helper supplies, which
13
+ * locales ship — all of it is a fact about the project, not about one file, and
14
+ * a scan that walked only the changed files would get every one of them wrong.
15
+ * So the walk stays whole and the report is filtered afterwards, which costs a
16
+ * few hundred milliseconds and buys an answer that is still correct.
17
+ *
18
+ * What it saves is the reader. A run that touched five components was handed
19
+ * five hundred gaps, of which ten were its own; the rest are a backlog nobody
20
+ * asked it about, and an agent cannot tell which is which.
21
+ */
22
+ export function scopeReport(report, paths) {
23
+ if (paths.length === 0)
24
+ return report;
25
+ const wanted = paths.map((p) => p.replace(/^\.\//, '').replace(/\/+$/, ''));
26
+ const matches = (file) => wanted.some((p) => file === p || file.startsWith(`${p}/`));
27
+ const components = report.components.filter((c) => matches(c.file));
28
+ const orphanPreviews = report.orphanPreviews.filter((p) => matches(p.file));
29
+ const changed = new Set(components.map((c) => c.name));
30
+ const previewsOfWhatChanged = previewsRendering(report, changed, matches);
31
+ const uncoveredUsesOfWhatChanged = uncoveredUses(report, changed, matches);
32
+ return {
33
+ ...report,
34
+ components,
35
+ orphanPreviews,
36
+ stats: computeStats(components, orphanPreviews),
37
+ scope: {
38
+ paths: wanted,
39
+ componentsScanned: report.components.length,
40
+ ...(previewsOfWhatChanged.length > 0 ? { previewsOfWhatChanged } : {}),
41
+ ...(uncoveredUsesOfWhatChanged.length > 0 ? { uncoveredUsesOfWhatChanged } : {}),
42
+ },
43
+ };
44
+ }
45
+ /**
46
+ * Previews outside the scope that put something inside it on screen.
47
+ *
48
+ * A component is rarely previewed only where it is declared: edit a row and the
49
+ * preview that shows it may live in the list's file, which the filter drops. The
50
+ * agent can read these and decide — they are as likely to be fine as not.
51
+ *
52
+ * Reached through the component graph rather than the preview's own text,
53
+ * because a preview usually names one view and shows a dozen. IceCubes made
54
+ * the point: changing `StatusRowDetailView` matched no preview at all, because
55
+ * the one preview that renders it says `StatusRowView(` and nothing else — the
56
+ * detail view is three levels down its body. Matching the text alone answers a
57
+ * question nobody asked, which is which previews mention a name.
58
+ */
59
+ function previewsRendering(report, changed, inScope) {
60
+ const found = [];
61
+ const previews = [...report.components.flatMap((c) => c.previews), ...report.orphanPreviews];
62
+ const known = new Set(report.components.map((c) => c.name));
63
+ // Guarded children are left to the ask list below. A preview that reaches
64
+ // something only through an `if` has not been shown to render it, and saying
65
+ // both here and there would be two answers to one question.
66
+ const uses = new Map(report.components.map((c) => {
67
+ const guarded = new Set((c.conditionalUses ?? []).map((u) => u.name));
68
+ return [c.name, (c.uses ?? []).filter((n) => !guarded.has(n))];
69
+ }));
70
+ for (const preview of previews) {
71
+ if (inScope(preview.file))
72
+ continue;
73
+ const renders = reachedFrom(componentsNamedIn(preview.annotationText ?? '', known), uses, changed);
74
+ if (renders.length === 0)
75
+ continue;
76
+ found.push({ name: preview.displayName ?? preview.name, file: preview.file, line: preview.line, renders });
77
+ }
78
+ return found;
79
+ }
80
+ /**
81
+ * Which of `targets` are reachable from `seeds` by following `uses`.
82
+ *
83
+ * A view tree, walked breadth-first. `seen` is what keeps a cycle — two views
84
+ * that render each other under different conditions, which SwiftUI allows —
85
+ * from being an infinite descent.
86
+ */
87
+ function reachedFrom(seeds, uses, targets) {
88
+ const hit = new Set();
89
+ const seen = new Set();
90
+ const queue = [...seeds];
91
+ while (queue.length > 0) {
92
+ const name = queue.shift();
93
+ if (seen.has(name))
94
+ continue;
95
+ seen.add(name);
96
+ if (targets.has(name))
97
+ hit.add(name);
98
+ queue.push(...(uses.get(name) ?? []));
99
+ }
100
+ return [...hit];
101
+ }
102
+ /**
103
+ * Views outside the scope that render something inside it and have no preview
104
+ * of their own.
105
+ *
106
+ * Two ways to end up here, and the second is the one that took two runs against
107
+ * IceCubes to see. A view with no preview at all shows the change nowhere. A
108
+ * view that renders it inside an `if` has a preview that may show nothing of it
109
+ * either — StatusRowView is previewed as a timeline row, and the detail view it
110
+ * was changed to render only appears when focused. "Has a preview" answered the
111
+ * wrong question, and answered it yes.
112
+ *
113
+ * Whether either context deserves a preview is a judgment about the product,
114
+ * which is why this is something to ask the designer rather than a gap to
115
+ * close.
116
+ *
117
+ * Direct users only, unlike the list above, and the asymmetry is the point. A
118
+ * fact can be long and still be worth reading; a question cannot. Follow the
119
+ * graph from a component every screen shows and the ask list becomes every
120
+ * screen in the app, which is not something anyone can answer.
121
+ */
122
+ function uncoveredUses(report, changed, inScope) {
123
+ const found = [];
124
+ for (const component of report.components) {
125
+ if (inScope(component.file))
126
+ continue;
127
+ const uses = (component.uses ?? []).filter((name) => changed.has(name));
128
+ if (uses.length === 0)
129
+ continue;
130
+ if (component.previews.length === 0) {
131
+ found.push({ component: component.name, file: component.file, line: component.line, uses, reason: 'no-preview' });
132
+ continue;
133
+ }
134
+ // It has a preview. That only settles the question for what it shows
135
+ // unconditionally.
136
+ const guarded = (component.conditionalUses ?? []).filter((u) => changed.has(u.name));
137
+ if (guarded.length === 0)
138
+ continue;
139
+ // The first guarded render carries the line and the condition. Listing one
140
+ // condition per child would make the question longer than the answer, and
141
+ // the reader opens the file either way.
142
+ found.push({
143
+ component: component.name,
144
+ file: component.file,
145
+ line: guarded[0].line,
146
+ uses: guarded.map((u) => u.name),
147
+ reason: 'conditional',
148
+ guard: guarded[0].guard,
149
+ previews: component.previews.map((p) => ({
150
+ name: p.displayName ?? p.name,
151
+ file: p.file,
152
+ line: p.line,
153
+ })),
154
+ });
155
+ }
156
+ return found;
157
+ }
158
+ /**
159
+ * Files with uncommitted changes, relative to the project directory.
160
+ *
161
+ * `git status --porcelain` reports staged, unstaged and untracked in one pass,
162
+ * which is what an agent's turn leaves behind: it edits and adds, it does not
163
+ * commit. Paths come back relative to the repository root, so they are rebased
164
+ * onto the project directory — the two differ whenever phonebook.config.json
165
+ * sits in a subdirectory of a larger repository.
166
+ *
167
+ * A directory that is not a repository, or a git that is not installed, is not
168
+ * an error here. It means the caller cannot scope by change and gets the whole
169
+ * project, which is the behaviour they had before asking.
170
+ */
171
+ export async function changedFiles(projectDir) {
172
+ let root;
173
+ let status;
174
+ let base;
175
+ try {
176
+ root = (await run('git', ['rev-parse', '--show-toplevel'], { cwd: projectDir })).stdout.trim();
177
+ status = (await run('git', ['status', '--porcelain'], { cwd: projectDir })).stdout;
178
+ // git answers in real paths. The caller's directory may be reached through
179
+ // a symlink — every macOS temp directory is — and comparing the two
180
+ // unresolved puts the whole repository outside itself.
181
+ base = await realpath(projectDir);
182
+ }
183
+ catch {
184
+ return [];
185
+ }
186
+ const files = new Set();
187
+ for (const line of status.split('\n')) {
188
+ if (line.trim() === '')
189
+ continue;
190
+ // "XY path", or "XY old -> new" for a rename: the new name is the one that
191
+ // still exists to be scanned.
192
+ let path = line.slice(3);
193
+ const arrow = path.indexOf(' -> ');
194
+ if (arrow !== -1)
195
+ path = path.slice(arrow + 4);
196
+ path = path.trim().replace(/^"|"$/g, '');
197
+ if (path === '')
198
+ continue;
199
+ const fromProject = relative(base, resolve(root, path));
200
+ // Outside the project directory: another module of the same repository,
201
+ // which this scan never walked and cannot report on.
202
+ if (fromProject.startsWith('..'))
203
+ continue;
204
+ files.add(fromProject);
205
+ }
206
+ return [...files];
207
+ }
208
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/scan/scope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { ScannedComponent, ScannedPreview, CoverageReport } from './types.js';
2
+ /**
3
+ * Totals over a set of components and the previews that could not be matched
4
+ * to one.
5
+ *
6
+ * Shared by both scanners and by `scopeReport`, which needs the same numbers
7
+ * over a subset. The self-preview rule is Android's — a composable annotated
8
+ * with `@Preview` is its own preview — and it costs nothing on iOS, where
9
+ * `#Preview` is always a separate top-level block and the count is therefore
10
+ * always zero.
11
+ */
12
+ export declare function computeStats(components: ScannedComponent[], orphanPreviews: ScannedPreview[]): CoverageReport['stats'];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Totals over a set of components and the previews that could not be matched
3
+ * to one.
4
+ *
5
+ * Shared by both scanners and by `scopeReport`, which needs the same numbers
6
+ * over a subset. The self-preview rule is Android's — a composable annotated
7
+ * with `@Preview` is its own preview — and it costs nothing on iOS, where
8
+ * `#Preview` is always a separate top-level block and the count is therefore
9
+ * always zero.
10
+ */
11
+ export function computeStats(components, orphanPreviews) {
12
+ const allPreviews = [...components.flatMap((c) => c.previews), ...orphanPreviews];
13
+ return {
14
+ components: components.length,
15
+ withPreview: components.filter((c) => c.previews.length > 0).length,
16
+ withDarkPreview: components.filter((c) => c.previews.some((p) => p.dark)).length,
17
+ totalPreviews: components.reduce((sum, c) => sum + c.previews.length, 0) + orphanPreviews.length,
18
+ hintCount: allPreviews.reduce((sum, p) => sum + (p.hints?.length ?? 0), 0),
19
+ gapCount: components.reduce((sum, c) => sum + (c.gaps?.length ?? 0), 0),
20
+ componentsWithGaps: components.filter((c) => (c.gaps ?? []).some((g) => g.severity === 'warning')).length,
21
+ selfPreviewed: components.filter((c) => c.previews.some((p) => p.name === c.name && p.line === c.line)).length,
22
+ };
23
+ }
24
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/scan/stats.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,UAA8B,EAC9B,cAAgC;IAEhC,MAAM,WAAW,GAAG,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAG,cAAc,CAAC,CAAC;IAClF,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,WAAW,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM;QACnE,eAAe,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QAChF,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,cAAc,CAAC,MAAM;QAChG,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1E,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;QACvE,kBAAkB,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAC1C,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CACrD,CAAC,MAAM;QACR,aAAa,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACrC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAC/D,CAAC,MAAM;KACT,CAAC;AACJ,CAAC"}
@@ -4,7 +4,8 @@
4
4
  * can find components with missing previews/states and add them as code.
5
5
  */
6
6
  import type { PreviewHint } from './hints.js';
7
- export type { PreviewHint };
7
+ import type { ComponentProperty, CoverageGap } from './gaps.js';
8
+ export type { PreviewHint, ComponentProperty, CoverageGap };
8
9
  export interface ScannedPreview {
9
10
  /** Preview function name (Android) or #Preview display name / "unnamed" (iOS) */
10
11
  name: string;
@@ -25,6 +26,22 @@ export interface ScannedPreview {
25
26
  * annotation doesn't declare. See src/scan/hints.ts. */
26
27
  hints?: PreviewHint[];
27
28
  }
29
+ /**
30
+ * A component rendered only behind a condition, and enough about the render to
31
+ * go and look at it.
32
+ *
33
+ * The condition text is the payload. "Renders it inside an if" names a shape;
34
+ * `if isFocused, !isCompact` names the state a preview would have to set, which
35
+ * is the difference between a question and a chore.
36
+ */
37
+ export interface ConditionalUse {
38
+ /** The component rendered behind the condition. */
39
+ name: string;
40
+ /** The condition as written, whitespace collapsed. */
41
+ guard: string;
42
+ /** Line of the guarded render, in the file that declares the parent. */
43
+ line: number;
44
+ }
28
45
  export interface ScannedComponent {
29
46
  /** Composable function name (Android) or View struct name (iOS) */
30
47
  name: string;
@@ -32,12 +49,78 @@ export interface ScannedComponent {
32
49
  line: number;
33
50
  /** Previews matched to this component (same file + name-prefix heuristic) */
34
51
  previews: ScannedPreview[];
52
+ /** Stored properties (iOS) or composable parameters (Android). What the
53
+ * component's states are derived from. */
54
+ properties?: ComponentProperty[];
55
+ /** Types read via `@Environment(X.self)` (iOS). An unsatisfied one traps at
56
+ * render, so a preview that omits it is broken rather than incomplete. */
57
+ environmentTypes?: string[];
58
+ /** Other discovered components this one renders in its body (iOS). What makes
59
+ * it possible to say who else is affected when a component changes. */
60
+ uses?: string[];
61
+ /**
62
+ * The subset of `uses` this component renders only inside an `if`, `switch`
63
+ * or `guard`. Its own preview may never reach these.
64
+ */
65
+ conditionalUses?: ConditionalUse[];
66
+ /** Previews this component should have and doesn't. See src/scan/gaps.ts. */
67
+ gaps?: CoverageGap[];
35
68
  }
36
69
  export interface CoverageReport {
37
70
  platform: 'android' | 'ios';
38
71
  components: ScannedComponent[];
39
72
  /** Previews that could not be matched to any discovered component */
40
73
  orphanPreviews: ScannedPreview[];
74
+ /** Locales the project ships beyond its development language. Empty when the
75
+ * project is not localized, which keeps the localization gap silent there. */
76
+ extraLocales: string[];
77
+ /** Present when the report was narrowed to a subset of the project's files.
78
+ * The scan still walked everything; only this report is filtered. */
79
+ scope?: {
80
+ paths: string[];
81
+ /** Components the scan found, before the filter. */
82
+ componentsScanned: number;
83
+ /**
84
+ * Previews outside the scope that put a component inside it on screen,
85
+ * directly or through the views they render. Editing a row changes what
86
+ * these show, and the filter would otherwise hide them. A fact to act on,
87
+ * not a defect.
88
+ */
89
+ previewsOfWhatChanged?: {
90
+ name: string;
91
+ file: string;
92
+ line: number;
93
+ /** In-scope components this preview reaches. Its own text may name none of them. */
94
+ renders: string[];
95
+ }[];
96
+ /**
97
+ * Views outside the scope that render a component inside it directly and
98
+ * have no preview of their own, so nothing shows the change in that
99
+ * context. Whether that context is worth covering is a judgment about the
100
+ * product, which is why this is a question for the designer rather than a
101
+ * gap — and why it stops at one hop where `previewsOfWhatChanged` does not.
102
+ */
103
+ uncoveredUsesOfWhatChanged?: {
104
+ component: string;
105
+ file: string;
106
+ /** The guarded render when `reason` is `conditional`, so the line points at
107
+ * the branch in question; the component's declaration otherwise. */
108
+ line: number;
109
+ /** In-scope components it renders. */
110
+ uses: string[];
111
+ /** Why nothing shows the change here. */
112
+ reason: 'no-preview' | 'conditional';
113
+ /** `conditional` only: the condition a preview would have to satisfy. */
114
+ guard?: string;
115
+ /** `conditional` only: the previews that exist and do not enter the branch.
116
+ * Naming them is what makes this answerable — the reader opens one. */
117
+ previews?: {
118
+ name: string;
119
+ file: string;
120
+ line: number;
121
+ }[];
122
+ }[];
123
+ };
41
124
  /** Totals for a quick summary */
42
125
  stats: {
43
126
  components: number;
@@ -45,6 +128,9 @@ export interface CoverageReport {
45
128
  withDarkPreview: number;
46
129
  totalPreviews: number;
47
130
  hintCount: number;
131
+ gapCount: number;
132
+ /** Components with at least one warning-severity gap. */
133
+ componentsWithGaps: number;
48
134
  /** Components whose only preview is themselves (the @Preview-on-the-composable
49
135
  * pattern for screen-level composables with default parameters). */
50
136
  selfPreviewed: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stag-build/phonebook",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Storybook-style static gallery generated from Compose @Preview / SwiftUI #Preview screenshots",
5
5
  "license": "MIT",
6
6
  "mcpName": "io.github.stag-build/phonebook",