@estiva-app/ui 0.22.0 → 0.23.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 (82) hide show
  1. package/README.md +21 -0
  2. package/dist/Banner.d.ts +20 -2
  3. package/dist/Banner.d.ts.map +1 -1
  4. package/dist/ContainerHeader.d.ts +25 -0
  5. package/dist/ContainerHeader.d.ts.map +1 -0
  6. package/dist/EmptyState.d.ts +10 -1
  7. package/dist/EmptyState.d.ts.map +1 -1
  8. package/dist/gates/app-checks.d.ts.map +1 -1
  9. package/dist/gates/{chunk-ZGJ2J5NU.js → chunk-EA33NP5B.js} +275 -11
  10. package/dist/gates/chunk-EA33NP5B.js.map +7 -0
  11. package/dist/gates/{chunk-AUXD4GCY.js → chunk-GTQZEHPC.js} +15 -1
  12. package/dist/gates/chunk-GTQZEHPC.js.map +7 -0
  13. package/dist/gates/cli.js +1 -1
  14. package/dist/gates/create-app.d.ts +2 -0
  15. package/dist/gates/create-app.d.ts.map +1 -1
  16. package/dist/gates/create-app.js +1 -1
  17. package/dist/gates/index.js +25 -3
  18. package/dist/gates/index.js.map +2 -2
  19. package/dist/gates/status.d.ts +2 -0
  20. package/dist/gates/status.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +69 -48
  24. package/dist/index.js.map +4 -4
  25. package/dist/registry/app-DQI6WLHR.js +10 -0
  26. package/dist/registry/app.d.ts +21 -0
  27. package/dist/registry/app.d.ts.map +1 -0
  28. package/dist/registry/build-LGFCCOLR.js +27 -0
  29. package/dist/registry/build-LGFCCOLR.js.map +7 -0
  30. package/dist/registry/build.d.ts +68 -2
  31. package/dist/registry/build.d.ts.map +1 -1
  32. package/dist/registry/{chunk-MRSBS5OP.js → chunk-E4JNV7PC.js} +68 -12
  33. package/dist/registry/chunk-E4JNV7PC.js.map +7 -0
  34. package/dist/registry/{chunk-QDYGB3QN.js → chunk-NJN4MQAM.js} +60 -15
  35. package/dist/registry/chunk-NJN4MQAM.js.map +7 -0
  36. package/dist/registry/{chunk-IJNCYVH4.js → chunk-W2B2G7OE.js} +34 -9
  37. package/dist/registry/chunk-W2B2G7OE.js.map +7 -0
  38. package/dist/registry/chunk-WMFF3MPP.js +648 -0
  39. package/dist/registry/chunk-WMFF3MPP.js.map +7 -0
  40. package/dist/registry/cli.js +81 -26
  41. package/dist/registry/cli.js.map +3 -3
  42. package/dist/registry/find.d.ts +12 -1
  43. package/dist/registry/find.d.ts.map +1 -1
  44. package/dist/registry/index.d.ts +5 -2
  45. package/dist/registry/index.d.ts.map +1 -1
  46. package/dist/registry/index.js +11 -3
  47. package/dist/registry/schema.d.ts +83 -6
  48. package/dist/registry/schema.d.ts.map +1 -1
  49. package/package.json +2 -2
  50. package/registry.json +233 -88
  51. package/src/Banner.mdx +17 -4
  52. package/src/Banner.stories.tsx +22 -0
  53. package/src/Banner.test.tsx +35 -0
  54. package/src/Banner.tsx +34 -9
  55. package/src/ContainerHeader.mdx +56 -0
  56. package/src/ContainerHeader.stories.tsx +62 -0
  57. package/src/ContainerHeader.test.tsx +47 -0
  58. package/src/ContainerHeader.tsx +45 -0
  59. package/src/EmptyState.mdx +12 -0
  60. package/src/EmptyState.stories.tsx +6 -1
  61. package/src/EmptyState.test.tsx +18 -1
  62. package/src/EmptyState.tsx +14 -1
  63. package/src/gates/app-checks.ts +23 -1
  64. package/src/gates/create-app.test.ts +77 -6
  65. package/src/gates/create-app.ts +288 -15
  66. package/src/gates/status.ts +16 -0
  67. package/src/index.ts +1 -0
  68. package/src/registry/app.test.ts +562 -0
  69. package/src/registry/app.ts +854 -0
  70. package/src/registry/build.ts +114 -25
  71. package/src/registry/cli.ts +105 -42
  72. package/src/registry/find.ts +45 -8
  73. package/src/registry/index.ts +8 -1
  74. package/src/registry/registry.test.ts +5 -5
  75. package/src/registry/schema.ts +163 -12
  76. package/dist/gates/chunk-AUXD4GCY.js.map +0 -7
  77. package/dist/gates/chunk-ZGJ2J5NU.js.map +0 -7
  78. package/dist/registry/build-GOVLABI6.js +0 -13
  79. package/dist/registry/chunk-IJNCYVH4.js.map +0 -7
  80. package/dist/registry/chunk-MRSBS5OP.js.map +0 -7
  81. package/dist/registry/chunk-QDYGB3QN.js.map +0 -7
  82. /package/dist/registry/{build-GOVLABI6.js.map → app-DQI6WLHR.js.map} +0 -0
@@ -45,16 +45,16 @@ describe('the registry builds', () => {
45
45
  expect(registry.entries.map((one) => one.name).sort()).toEqual(values.map((one) => one.name).sort())
46
46
  })
47
47
 
48
- it('counts 81 names over 53 files: 74 components, 6 helpers and 1 hook', () => {
48
+ it('counts 82 names over 54 files: 75 components, 6 helpers and 1 hook (ContainerHeader joined in UIG-13)', () => {
49
49
  // The reconciliation GATES.md §24 explains. If this changes, that changes.
50
50
  const kinds = registry.entries.reduce<Record<string, number>>((all, one) => ({ ...all, [one.kind]: (all[one.kind] ?? 0) + 1 }), {})
51
- expect(kinds).toEqual({ component: 74, helper: 6, hook: 1 })
52
- expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(53)
51
+ expect(kinds).toEqual({ component: 75, helper: 6, hook: 1 })
52
+ expect(new Set(registry.entries.map((one) => one.sourceFile)).size).toBe(54)
53
53
  })
54
54
 
55
55
  it('gives every entry a purpose, from its own page or from the comment above it', () => {
56
56
  expect(registry.entries.filter((one) => one.purpose.trim() === '')).toEqual([])
57
- expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(53)
57
+ expect(registry.entries.filter((one) => one.purposeFrom === 'page').length).toBe(54)
58
58
  expect(registry.entries.filter((one) => one.purposeFrom === 'comment').length).toBe(28)
59
59
  })
60
60
 
@@ -278,7 +278,7 @@ describe('the ids point at something', () => {
278
278
  describe('a second parser agrees', async () => {
279
279
  const docgen = await import('react-docgen').catch(() => null)
280
280
 
281
- it.skipIf(!docgen)('finds the same props on all 74 components', async () => {
281
+ it.skipIf(!docgen)('finds the same props on all 75 components', async () => {
282
282
  if (!docgen) return
283
283
  const resolver = new docgen.builtinResolvers.FindExportedDefinitionsResolver({ limit: 0 })
284
284
  const byName = new Map(registry.entries.map((one) => [one.name, one]))
@@ -17,7 +17,7 @@
17
17
  * reader that knows version N and is handed N+1 must be able to say so rather
18
18
  * than silently read a field that moved.
19
19
  */
20
- export const SCHEMA_VERSION = 1
20
+ export const SCHEMA_VERSION = 2
21
21
 
22
22
  /** What a name is, which decides how a reader offers it. */
23
23
  export type EntryKind =
@@ -34,6 +34,67 @@ export type PurposeSource =
34
34
  | 'page'
35
35
  /** The doc comment above the export — the name is documented on a sibling's page. */
36
36
  | 'comment'
37
+ /** An app's file that holds this one part: the comment at the top of the file (UIG-13). */
38
+ | 'file'
39
+ /** An app's pass-on: the part is the package's, and says so (UIG-13). */
40
+ | 'package'
41
+
42
+ /**
43
+ * Which of four kinds an app's part is (UIG-13), or that nothing uses it.
44
+ *
45
+ * Worked out from the code, never from the folder a file sits in: Peek's
46
+ * `components/ui` holds one-line pass-ons beside 187-line components.
47
+ */
48
+ export type EntryClass =
49
+ /** Its file only hands a package part on, so the app keeps its own import path. */
50
+ | 're-export'
51
+ /** Used in two or more places in the app. Gets a usage page (UIG-17, UIG-18). */
52
+ | 'reusable'
53
+ /** Used in one place. Needs only its purpose line. */
54
+ | 'one-off'
55
+ /** Reusable, and everything it uses is already in the package: it could move there. Katerina rules. */
56
+ | 'promote-candidate'
57
+ /** No file of the app uses it: only its stories or tests, or nothing. Listed, never deleted by the catalogue. */
58
+ | 'unused'
59
+
60
+ /**
61
+ * What the catalogue knows about a part that lives in an app (UIG-13). `null`
62
+ * on the package's own entries.
63
+ */
64
+ export interface AppFacts {
65
+ class: EntryClass
66
+ /** Why it has that class, in words: the evidence, or the written reason. */
67
+ reason: string
68
+ /**
69
+ * `true` when the class is written beside the part (`@registry <class>: <reason>`
70
+ * in its comment) rather than worked out — "general, though one screen uses it yet".
71
+ */
72
+ written: boolean
73
+ /** A pass-on's part, as `Name` for the package's and `Name from <package>` for another's. `null` otherwise. */
74
+ handsOn: string | null
75
+ /**
76
+ * The app's files that use it, relative to the app: every file that imports it,
77
+ * and its own file when something else there draws it. Stories and tests are not
78
+ * uses — a part only they reach is unused.
79
+ */
80
+ usedIn: string[]
81
+ /**
82
+ * What ties it to this app: each import that is neither in the package's own
83
+ * dependencies nor an app file that is itself untied. Empty means it could
84
+ * live in the package as it is.
85
+ */
86
+ tiedTo: string[]
87
+ /** The package part with the same name, when there is one: the reader should see both. */
88
+ packageNamesake: string | null
89
+ /** Imported as `import X from …` rather than `import { X } from …`. */
90
+ defaultExport: boolean
91
+ }
92
+
93
+ /** A file in an app's target set that holds no part, and why, in words. */
94
+ export interface FileWithoutPart {
95
+ file: string
96
+ reason: string
97
+ }
37
98
 
38
99
  export type EntryStatus = 'stable' | 'migrating' | 'deprecated'
39
100
 
@@ -74,7 +135,7 @@ export interface EntryBehaviour {
74
135
  export interface RegistryEntry {
75
136
  /** The exported name, exactly as `index.ts` exports it. */
76
137
  name: string
77
- /** Which library it came from. One repo per registry until UIG-13 merges them. */
138
+ /** Which library it came from: `estiva-ui`, or the app that owns it. */
78
139
  repo: string
79
140
  kind: EntryKind
80
141
  /** What a caller actually types. */
@@ -114,6 +175,8 @@ export interface RegistryEntry {
114
175
  storyId: string | null
115
176
  /** The `.mdx` path, relative to the repo, or `null`. */
116
177
  docPage: string | null
178
+ /** An app's part: its class and the evidence for it (UIG-13). `null` on the package's own entries. */
179
+ app: AppFacts | null
117
180
  }
118
181
 
119
182
  /** A name that is exported and deliberately not an entry. Empty today; the count still has to reconcile. */
@@ -125,14 +188,28 @@ export interface RegistryExclusion {
125
188
  export interface Registry {
126
189
  schemaVersion: number
127
190
  builtFrom: {
191
+ /**
192
+ * `package`: the package's catalogue, committed and shipped. `app`: one app's,
193
+ * built fresh from its code each time and never committed — Peek and Ship are
194
+ * private, the package is public (Katerina, 18 September 2026).
195
+ */
196
+ kind: 'package' | 'app'
128
197
  repo: string
129
- /** The npm package the entries are imported from. */
198
+ /** The package the entries are imported from, or the app's own name. */
130
199
  package: string
131
200
  packageVersion: string
132
- /** Value exports counted in `index.ts`. `entries + excluded` must equal it. */
201
+ /**
202
+ * The package: value exports counted in `index.ts`. An app: parts found in its
203
+ * files. `entries + excluded` must equal it.
204
+ */
133
205
  exports: number
134
- /** Type-only exports, which are not entries. Recorded so the count reconciles. */
206
+ /** Type-only exports, which are not entries. Recorded so the count reconciles. `0` for an app. */
135
207
  typeExports: number
208
+ /**
209
+ * An app only: its `.tsx` files outside stories and tests. Each one either holds
210
+ * an entry or is in `filesWithoutParts`. `null` for the package.
211
+ */
212
+ files: number | null
136
213
  }
137
214
  /**
138
215
  * How to turn an id into a link. There is no Storybook on the internet yet
@@ -146,11 +223,14 @@ export interface Registry {
146
223
  }
147
224
  entries: RegistryEntry[]
148
225
  excluded: RegistryExclusion[]
226
+ /** An app's `.tsx` files that hold no part, each with its reason. Empty for the package. */
227
+ filesWithoutParts: FileWithoutPart[]
149
228
  }
150
229
 
151
230
  const KINDS: EntryKind[] = ['component', 'hook', 'helper']
152
231
  const STATUSES: EntryStatus[] = ['stable', 'migrating', 'deprecated']
153
- const SOURCES: PurposeSource[] = ['page', 'comment']
232
+ const SOURCES: PurposeSource[] = ['page', 'comment', 'file', 'package']
233
+ export const CLASSES: EntryClass[] = ['re-export', 'reusable', 'one-off', 'promote-candidate', 'unused']
154
234
 
155
235
  const isString = (v: unknown): v is string => typeof v === 'string'
156
236
  const isFilledString = (v: unknown): v is string => isString(v) && v.trim().length > 0
@@ -171,13 +251,17 @@ export function validateRegistry(value: unknown): string[] {
171
251
  if (registry.schemaVersion !== SCHEMA_VERSION) fail(`schemaVersion is ${String(registry.schemaVersion)}, expected ${SCHEMA_VERSION}`)
172
252
 
173
253
  const built = registry.builtFrom
254
+ let isApp = false
174
255
  if (typeof built !== 'object' || built === null) fail('builtFrom is missing')
175
256
  else {
257
+ if (built.kind !== 'package' && built.kind !== 'app') fail(`builtFrom.kind is ${String(built.kind)}, expected package or app`)
258
+ isApp = built.kind === 'app'
176
259
  if (!isFilledString(built.repo)) fail('builtFrom.repo is missing')
177
260
  if (!isFilledString(built.package)) fail('builtFrom.package is missing')
178
261
  if (!isFilledString(built.packageVersion)) fail('builtFrom.packageVersion is missing')
179
262
  if (typeof built.exports !== 'number') fail('builtFrom.exports is not a number')
180
263
  if (typeof built.typeExports !== 'number') fail('builtFrom.typeExports is not a number')
264
+ if (isApp ? typeof built.files !== 'number' : built.files !== null) fail(`builtFrom.files is ${String(built.files)}: a number for an app, null for the package`)
181
265
  }
182
266
 
183
267
  const book = registry.storybook
@@ -192,11 +276,17 @@ export function validateRegistry(value: unknown): string[] {
192
276
 
193
277
  const entries = registry.entries
194
278
  const excluded = registry.excluded
279
+ const withoutParts = registry.filesWithoutParts
195
280
  if (!Array.isArray(entries)) return [...problems, 'entries is not an array']
196
281
  if (!Array.isArray(excluded)) return [...problems, 'excluded is not an array']
197
- if (entries.length === 0) fail('entries is empty')
282
+ if (!Array.isArray(withoutParts)) return [...problems, 'filesWithoutParts is not an array']
283
+ // An app with no part of its own yet is still an app: only the package must have entries.
284
+ if (entries.length === 0 && !isApp) fail('entries is empty')
198
285
 
286
+ // The package's names are unique; an app may have two parts of one name in two
287
+ // files, and the catalogue says which is which rather than refusing the app.
199
288
  const seen = new Set<string>()
289
+ const seenNames = new Set<string>()
200
290
  for (const [i, raw] of entries.entries()) {
201
291
  const at = (field: string) => `entries[${i}] (${isString((raw as RegistryEntry)?.name) ? (raw as RegistryEntry).name : '?'}).${field}`
202
292
  if (typeof raw !== 'object' || raw === null) {
@@ -204,9 +294,13 @@ export function validateRegistry(value: unknown): string[] {
204
294
  continue
205
295
  }
206
296
  const entry = raw as Partial<RegistryEntry>
297
+ const key = isApp ? `${String(entry.sourceFile)}#${String(entry.name)}` : String(entry.name)
207
298
  if (!isFilledString(entry.name)) fail(at('name') + ' is missing')
208
- else if (seen.has(entry.name)) fail(`${entry.name} appears twice`)
209
- else seen.add(entry.name)
299
+ else if (seen.has(key)) fail(`${entry.name} appears twice${isApp ? ` in ${String(entry.sourceFile)}` : ''}`)
300
+ else {
301
+ seen.add(key)
302
+ seenNames.add(entry.name)
303
+ }
210
304
 
211
305
  if (!isFilledString(entry.repo)) fail(at('repo') + ' is missing')
212
306
  if (!isFilledString(entry.importPath)) fail(at('importPath') + ' is missing')
@@ -244,16 +338,26 @@ export function validateRegistry(value: unknown): string[] {
244
338
  // nothing may have nowhere to be looked at. A component with no story is
245
339
  // already an error of UIG-5's component-has-a-story; this stops the
246
340
  // catalogue quietly carrying one.
247
- if (entry.kind === 'component' && (entry.docsId === null || entry.storyId === null)) {
341
+ //
342
+ // The package only. An app's parts are described before they are drawn:
343
+ // absence of a story says nothing about whether a part is reused, and UIG-19
344
+ // is where a reusable one must have one.
345
+ if (!isApp && entry.kind === 'component' && (entry.docsId === null || entry.storyId === null)) {
248
346
  fail(at('docsId') + ' is null, and only a helper or a hook may have no page or story')
249
347
  }
348
+
349
+ if (!isApp) {
350
+ if (entry.app !== null) fail(at('app') + ' is set on a package entry')
351
+ } else {
352
+ problems.push(...validateAppFacts(entry, at))
353
+ }
250
354
  }
251
355
 
252
356
  for (const [i, raw] of excluded.entries()) {
253
357
  const gap = raw as Partial<RegistryExclusion>
254
358
  if (!isFilledString(gap?.name)) fail(`excluded[${i}].name is missing`)
255
359
  if (!isFilledString(gap?.reason)) fail(`excluded[${i}].reason is missing`)
256
- if (isString(gap?.name) && seen.has(gap.name)) fail(`${gap.name} is both an entry and excluded`)
360
+ if (!isApp && isString(gap?.name) && seenNames.has(gap.name)) fail(`${gap.name} is both an entry and excluded`)
257
361
  }
258
362
 
259
363
  // Completeness (the ticket's acceptance): every value export is either an
@@ -261,8 +365,55 @@ export function validateRegistry(value: unknown): string[] {
261
365
  // failure this check exists to make impossible.
262
366
  const counted = entries.length + excluded.length
263
367
  if (typeof built === 'object' && built !== null && typeof built.exports === 'number' && counted !== built.exports) {
264
- fail(`${entries.length} entries + ${excluded.length} excluded = ${counted}, but index.ts exports ${built.exports} values`)
368
+ fail(`${entries.length} entries + ${excluded.length} excluded = ${counted}, but ${isApp ? 'the app has' : 'index.ts exports'} ${built.exports} ${isApp ? 'parts' : 'values'}`)
265
369
  }
266
370
 
371
+ // And an app's files: every one either holds a part or says why it does not.
372
+ const fileProblems: string[] = []
373
+ const withParts = new Set(entries.map((entry) => (entry as RegistryEntry).sourceFile))
374
+ for (const [i, raw] of withoutParts.entries()) {
375
+ const gap = raw as Partial<FileWithoutPart>
376
+ if (!isFilledString(gap?.file)) fileProblems.push(`filesWithoutParts[${i}].file is missing`)
377
+ else if (withParts.has(gap.file)) fileProblems.push(`${gap.file} holds a part and is listed as holding none`)
378
+ if (!isFilledString(gap?.reason)) fileProblems.push(`filesWithoutParts[${i}].reason is missing`)
379
+ }
380
+ if (!isApp && withoutParts.length) fileProblems.push('filesWithoutParts is for an app; the package lists its exports instead')
381
+ if (isApp && typeof built === 'object' && built !== null && typeof built.files === 'number' && withParts.size + withoutParts.length !== built.files) {
382
+ fileProblems.push(`${withParts.size} files hold a part + ${withoutParts.length} hold none = ${withParts.size + withoutParts.length}, but the app has ${built.files} files`)
383
+ }
384
+ problems.push(...fileProblems)
385
+
386
+ return problems
387
+ }
388
+
389
+ /**
390
+ * An app entry's class must be what its own evidence says, unless it is written
391
+ * beside the part with a reason. A pass-on is a fact about the file, so it can
392
+ * never be written, and only a pass-on hands a part on.
393
+ */
394
+ function validateAppFacts(entry: Partial<RegistryEntry>, at: (field: string) => string): string[] {
395
+ const problems: string[] = []
396
+ const fail = (message: string) => problems.push(message)
397
+ const facts = entry.app
398
+ if (typeof facts !== 'object' || facts === null) return [at('app') + ' is missing on an app entry']
399
+ if (!CLASSES.includes(facts.class)) fail(at('app.class') + ` is ${String(facts.class)}`)
400
+ if (!isFilledString(facts.reason)) fail(at('app.reason') + ' is empty')
401
+ if (typeof facts.written !== 'boolean') fail(at('app.written') + ' is not true/false')
402
+ if (typeof facts.defaultExport !== 'boolean') fail(at('app.defaultExport') + ' is not true/false')
403
+ if (!Array.isArray(facts.usedIn) || !facts.usedIn.every(isFilledString)) fail(at('app.usedIn') + ' is not a list of files')
404
+ if (!Array.isArray(facts.tiedTo) || !facts.tiedTo.every(isFilledString)) fail(at('app.tiedTo') + ' is not a list')
405
+ if (!(facts.handsOn === null || isFilledString(facts.handsOn))) fail(at('app.handsOn') + ' is neither a name nor null')
406
+ if (!(facts.packageNamesake === null || isFilledString(facts.packageNamesake))) fail(at('app.packageNamesake') + ' is neither a name nor null')
407
+ if (problems.length) return problems
408
+
409
+ const passOn = facts.class === 're-export'
410
+ if (passOn !== (facts.handsOn !== null)) fail(at('app.handsOn') + (passOn ? ' is missing on a pass-on' : ' is set on a part that is not a pass-on'))
411
+ if (passOn !== (entry.purposeFrom === 'package')) fail(at('purposeFrom') + (passOn ? ' must be package on a pass-on' : ' is package on a part that is not a pass-on'))
412
+ if (passOn && facts.written) fail(at('app.written') + ': a pass-on is read from the file, never written')
413
+ if (!facts.written && !passOn) {
414
+ const uses = facts.usedIn.length
415
+ const expected: EntryClass = uses === 0 ? 'unused' : uses === 1 ? 'one-off' : facts.tiedTo.length ? 'reusable' : 'promote-candidate'
416
+ if (facts.class !== expected) fail(at('app.class') + ` is ${facts.class}, but its evidence (${uses} uses, ${facts.tiedTo.length} ties) says ${expected}`)
417
+ }
267
418
  return problems
268
419
  }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/gates/count.ts", "../../src/gates/hook.ts", "../../src/gates/status.ts"],
4
- "sourcesContent": ["/// <reference types=\"node\" />\n/**\n * Write `.gates-count.json`: what the gate finds, per rule \u2014 errors, warnings\n * and escapes (seam S3 in docs/GATES.md \u00A716). Written once (\u00A723): until UIG-10\n * Peek, Ship and this package each carried the same script, differing only in\n * the repo's name and which rules it seeded.\n *\n * Runs after the gate lint (npm's `postlint:rules`), so CI runs it too. The\n * file is committed: nothing compares it yet, and the later ratchet needs the\n * history to start at the first commit. It lints with the gate's own config,\n * with escapes reported, so an escape is counted rather than silent. The file\n * is rewritten only when a count changes, so running the lint does not leave a\n * changed file behind every time.\n *\n * It fails when an `eslint-disable` switched a gate rule off: that is not an\n * escape, and the escape count cannot see it.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { join, relative } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type { ESLint as ESLintClass } from 'eslint'\nimport { APP_RULE_IDS, countGates, PACKAGE_RULE_IDS, type GateCount } from '../eslint/index'\n\nexport interface CountOptions {\n /** The folder the gate lints from, where `.gates-count.json` is written. */\n root?: string\n /** The name written into the file: `peek`, `ship`, `estiva-ui`, or the app's. */\n repo: string\n /** `app` seeds the app rules; `package` this package's inward set (UIG-5). */\n audience?: 'app' | 'package'\n /** The gate's config file, relative to `root`. */\n config?: string\n}\n\nexport interface CountResult extends GateCount {\n changed: boolean\n /** What to print, line by line. */\n lines: string[]\n /** What to print to stderr when a rule was switched off; empty when none was. */\n failures: string[]\n}\n\n/** ESLint from where the gate lints, never from beside this file: an app's own install. */\nasync function eslintFrom(root: string): Promise<typeof ESLintClass> {\n const resolved = createRequire(join(root, 'package.json')).resolve('eslint')\n return ((await import(pathToFileURL(resolved).href)) as { ESLint: typeof ESLintClass }).ESLint\n}\n\nexport async function writeGateCount({ root = process.cwd(), repo, audience = 'app', config = 'eslint.gates.config.js' }: CountOptions): Promise<CountResult> {\n const out = join(root, '.gates-count.json')\n const ESLint = await eslintFrom(root)\n const eslint = new ESLint({\n cwd: root,\n overrideConfigFile: join(root, config),\n overrideConfig: { settings: { estiva: { reportEscapes: true } } },\n })\n // The set the gate runs, so a rule with nothing to report still has its row.\n const { rules, disabled } = countGates(await eslint.lintFiles(['.']), audience === 'package' ? PACKAGE_RULE_IDS : APP_RULE_IDS)\n\n const previous = existsSync(out) ? (JSON.parse(readFileSync(out, 'utf8')) as { rules?: unknown }) : null\n const changed = JSON.stringify(previous?.rules) !== JSON.stringify(rules)\n if (changed) {\n const count = { schemaVersion: 1, repo, generatedAt: new Date().toISOString(), rules }\n writeFileSync(out, `${JSON.stringify(count, null, 2)}\\n`)\n }\n const lines = [changed ? '.gates-count.json: written' : '.gates-count.json: unchanged']\n for (const [id, c] of Object.entries(rules)) lines.push(` ${id}: ${c.errors} errors, ${c.warnings} warnings, ${c.escapes} escapes`)\n\n const failures: string[] = []\n if (disabled.length > 0) {\n failures.push('', 'An eslint-disable switched a gate rule off. Write the reason above the element instead: // @estiva-escape: <reason>')\n for (const d of disabled) failures.push(` ${relative(root, d.filePath)}:${d.line} ${d.ruleId}`)\n }\n return { rules, disabled, changed, lines, failures }\n}\n", "/// <reference types=\"node\" />\n/**\n * The editor gate: before Claude writes a `.ts` or `.tsx` under `src/`, lint the\n * text it is about to write with the gate, and refuse the write on an error.\n * Written once (docs/GATES.md \u00A723): until UIG-10 Peek, Ship and this package\n * each carried their own copy of this script.\n *\n * A `PreToolUse` hook in the repository's committed `.claude/settings.json`\n * runs it. Claude Code documents exit 2 as blocking the tool call, with stderr\n * handed back to Claude; exit 0 raises no objection. It reads\n * `$CLAUDE_PROJECT_DIR`, the project root where the session started \u2014 a session\n * started in a folder above or below the repository gets no hook (UIG-3,\n * UIG-4 findings).\n *\n * It runs the gate's own config and reimplements nothing: a hook and a lint\n * that could disagree would be worse than neither. Tests are not checked\n * (Katerina, 13 September); stories are. This package's inward gate reads `.tsx`\n * only, as its lint does.\n *\n * Input, from Claude Code on stdin: `tool_name` and `tool_input` \u2014 `file_path`\n * and `content` for Write; `file_path`, `old_string`, `new_string` and\n * `replace_all` for Edit. An Edit carries no whole file, so the edit is applied\n * to the file on disk here. Anything it cannot read passes, and the tool itself\n * reports its own failure.\n */\nimport { existsSync, readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type { ESLint as ESLintClass } from 'eslint'\nimport { countGates } from '../eslint/index'\n\nexport interface HookOptions {\n /** The repository's top folder. Defaults to `$CLAUDE_PROJECT_DIR`, then the tool call's `cwd`. */\n root?: string\n /** The folder that holds the app, relative to `root`: `web` in Ship, `.` elsewhere. */\n app?: string\n /** `package` gates `.tsx` only, as this package's inward lint does. */\n audience?: 'app' | 'package'\n /** The hook's input, when not read from stdin (tests). */\n input?: unknown\n}\n\nexport interface HookResult {\n /** 0 lets the write through; 2 refuses it. */\n code: 0 | 2\n /** What goes to stderr when refused. */\n message: string\n}\n\ninterface ToolCall {\n tool_name?: string\n cwd?: string\n tool_input?: { file_path?: unknown; content?: unknown; old_string?: unknown; new_string?: unknown; replace_all?: unknown }\n}\n\nconst PASS: HookResult = { code: 0, message: '' }\n\nexport async function runHook({ root, app = '.', audience = 'app', input }: HookOptions = {}): Promise<HookResult> {\n let call: ToolCall\n try {\n call = (input ?? JSON.parse(readFileSync(0, 'utf8'))) as ToolCall\n } catch {\n return PASS\n }\n const args = call.tool_input ?? {}\n if (typeof args.file_path !== 'string') return PASS\n\n const top = root ?? process.env.CLAUDE_PROJECT_DIR ?? call.cwd ?? process.cwd()\n const appDir = resolve(top, app)\n const file = isAbsolute(args.file_path) ? args.file_path : resolve(call.cwd ?? top, args.file_path)\n const rel = relative(appDir, file).split(sep).join('/')\n const source = audience === 'package' ? /^src\\/.+\\.tsx$/ : /^src\\/.+\\.tsx?$/\n if (rel.startsWith('../') || isAbsolute(rel) || !source.test(rel) || /\\.test\\.tsx?$/.test(rel) || /\\.d\\.ts$/.test(rel)) return PASS\n\n const text = proposed(call.tool_name, args, file)\n if (text === null) return PASS\n\n // ESLint from the app's own install: in Ship the top folder has none.\n const resolved = createRequire(join(appDir, 'package.json')).resolve('eslint')\n const { ESLint } = (await import(pathToFileURL(resolved).href)) as { ESLint: typeof ESLintClass }\n const config = join(appDir, 'eslint.gates.config.js')\n const eslint = new ESLint({ cwd: appDir, overrideConfigFile: config })\n const results = await eslint.lintText(text, { filePath: file })\n // A rule's errors only. Code that does not parse yet (one edit of several) is\n // the typecheck's to judge; the gate looks again at the edit that completes it.\n const errors = results.flatMap((r) => r.messages.filter((m) => m.severity === 2 && m.ruleId))\n const { disabled } = countGates(results)\n if (errors.length === 0 && disabled.length === 0) return PASS\n\n const shown = (p: string) => relative(top, p).split(sep).join('/')\n const lines = [`${shown(file)} was not written: the UI Guardrails refuse it (${shown(config)}).`]\n for (const m of errors) lines.push(` ${m.line}:${m.column} ${m.message}${m.ruleId ? ` (${m.ruleId})` : ''}`)\n const keep = audience === 'package' ? 'Keep it only' : 'Keep the element only'\n for (const d of disabled) lines.push(` ${d.line} an eslint-disable switches ${d.ruleId} off. ${keep} with its reason on the line above: // @estiva-escape: <reason>`)\n return { code: 2, message: `${lines.join('\\n')}\\n` }\n}\n\n/** The file as it will be after this tool call, or null when that cannot be known. */\nfunction proposed(tool: string | undefined, args: NonNullable<ToolCall['tool_input']>, file: string): string | null {\n if (tool === 'Write') return typeof args.content === 'string' ? args.content : null\n if (tool !== 'Edit' || typeof args.old_string !== 'string' || typeof args.new_string !== 'string') return null\n const { old_string: from, new_string: to } = args\n const current = existsSync(file) ? readFileSync(file, 'utf8') : ''\n // A checkout on Windows has CRLF on disk; an edit is written with LF.\n for (const text of [current, current.replace(/\\r\\n/g, '\\n')]) {\n if (!text.includes(from)) continue\n return args.replace_all === true ? text.split(from).join(to) : text.replace(from, () => to)\n }\n return null\n}\n", "/// <reference types=\"node\" />\n/**\n * gates:status \u2014 where the UI Guardrails project stands, read from the code.\n *\n * estiva-gates status one row per ticket\n * estiva-gates status --detail every check under every row\n * estiva-gates status --json machine output, read by estiva-ui's run\n * estiva-gates status --app web the app sits in `web/`, as Ship's does\n *\n * Every row is decided by checks on real files, a real lint run or a real\n * GitHub setting. Nothing here reads a list that someone ticks by hand.\n *\n * **One copy, in the package** (docs/GATES.md \u00A723). Until UIG-10 this engine was\n * `scripts/gates-status.mjs`, the same file copied by hand into estiva-ui, Peek\n * and Ship, with estiva-ui warning when the copies drifted. Now it ships here,\n * and a repo runs it with `estiva-gates status`. A repo still carrying its own\n * copy is named as one when estiva-ui runs it; UIG-32 moves Peek and Ship off\n * theirs.\n *\n * What differs per repo is `scripts/gates-checks.mjs`, beside the app: its\n * checks, or the checks every app runs (`appChecks`) plus its own. It sits with\n * the app because it imports the package \u2014 `web/scripts/` in Ship, whose app,\n * and whose install, are in `web/` (UIG-32).\n *\n * The rows and the reasons for each check are in estiva-ui docs/GATES.md \u00A715\n * and \u00A717.\n */\nimport { execFileSync } from 'node:child_process'\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { join, relative, resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type { ESLint as ESLintClass } from 'eslint'\n\nexport type CheckResult = { result: 'pass' | 'fail' | 'part' | 'unknown'; detail: string }\ntype Maybe<T> = T | Promise<T>\n\nexport interface LintProbe {\n /** The folder ESLint runs from, relative to the repo: `web` in Ship. */\n cwd?: string\n /** The config file, relative to `cwd`. */\n config: string\n /** The path the text is linted as, relative to `cwd`. Nothing is written. */\n file: string\n code: string\n expect: 'error' | 'warning' | 'none'\n /** Words the expected message must contain. */\n mentions?: string\n}\n\n/** The helpers a checks file is written with. */\nexport interface GateHelpers {\n PASS(detail: string): CheckResult\n FAIL(detail: string): CheckResult\n PART(detail: string): CheckResult\n UNKNOWN(detail: string): CheckResult\n exists(rel: string): boolean\n read(rel: string): string\n listFiles(rel: string, test: (name: string, path: string) => boolean): string[]\n file(rel: string): CheckResult\n committed(rel: string): CheckResult\n contains(rel: string, needle: string | RegExp, label?: string): CheckResult\n lacks(rel: string, needle: string | RegExp, label?: string): CheckResult\n script(pkgRel: string, name: string): CheckResult\n loads(rel: string): Promise<CheckResult>\n ci(script: string | RegExp): CheckResult\n ciJob(job: string, script: string): CheckResult\n hook(settingsRel: string, needle: string): CheckResult\n json(rel: string, test?: (data: unknown) => boolean, label?: string): CheckResult\n lint(probe: LintProbe): Promise<CheckResult>\n protectedBranch(pattern: RegExp): CheckResult\n gh(args: string[], label: string): CheckResult\n share(files: string[], test: (file: string) => boolean, label: string): CheckResult\n}\n\nexport interface GateCheck {\n what: string\n run: () => Maybe<CheckResult>\n aggregate?: boolean\n}\nexport interface GateTicket {\n ref: string\n title?: string\n owner: boolean\n checks: GateCheck[]\n}\nexport interface TicketListEntry {\n ref: string\n owner: string\n title: string\n parts?: string[]\n aggregate?: boolean\n}\nexport interface GateSpec {\n repo: string\n tickets: GateTicket[]\n /** estiva-ui only: every ticket, and the repos it joins in. */\n all?: TicketListEntry[]\n /** estiva-ui only: the repos beside it. `app` is the folder its app sits in: `web` in Ship. */\n siblings?: { name: string; path: string; env: string; app?: string }[]\n}\n\ninterface Row {\n ref: string\n title?: string\n owner?: boolean\n ownerRepo?: string\n checks: (CheckResult & { repo: string; what: string })[]\n status?: string\n aggregate?: boolean\n}\ninterface Report {\n schema: 1\n repo: string\n branch: string | null\n commit: string | null\n engine: string\n rows: Row[]\n}\n\nconst { version } = createRequire(import.meta.url)('../../package.json') as { version: string }\n/** What a report says ran it. A copy of the old script reports a hash instead. */\nexport const ENGINE = `@estiva-app/ui@${version}`\n\nconst PASS = (detail: string): CheckResult => ({ result: 'pass', detail })\nconst FAIL = (detail: string): CheckResult => ({ result: 'fail', detail })\nconst PART = (detail: string): CheckResult => ({ result: 'part', detail })\nconst UNKNOWN = (detail: string): CheckResult => ({ result: 'unknown', detail })\nconst said = (e: unknown) => {\n const err = e as { stdout?: unknown; stderr?: unknown; message?: string; code?: string }\n return { text: `${err.stdout ?? ''}${err.stderr ?? ''}`, message: String(err.message ?? e), code: err.code }\n}\n\n/** The helpers, bound to one repository. */\nexport function helpers(ROOT: string): GateHelpers {\n const abs = (rel: string) => resolve(ROOT, rel)\n const exists = (rel: string) => existsSync(abs(rel))\n const read = (rel: string) => readFileSync(abs(rel), 'utf8')\n const git = (...a: string[]) => {\n try {\n return execFileSync('git', a, { cwd: ROOT, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()\n } catch {\n return null\n }\n }\n const listFiles = (rel: string, test: (name: string, path: string) => boolean) => {\n const out: string[] = []\n const walk = (dir: string) => {\n if (!existsSync(dir)) return\n for (const name of readdirSync(dir)) {\n if (['node_modules', '.git', 'dist', 'storybook-static', '.verify-shots'].includes(name)) continue\n const p = join(dir, name)\n if (statSync(p).isDirectory()) walk(p)\n else if (test(name, p)) out.push(relative(ROOT, p).replace(/\\\\/g, '/'))\n }\n }\n walk(abs(rel))\n return out\n }\n const escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n return {\n PASS, FAIL, PART, UNKNOWN, exists, read, listFiles,\n\n file(rel) {\n return exists(rel) ? PASS(`${rel} exists`) : FAIL(`${rel} does not exist`)\n },\n\n committed(rel) {\n if (!exists(rel)) return FAIL(`${rel} does not exist`)\n return git('ls-files', '--error-unmatch', rel) ? PASS(`${rel} is committed`) : FAIL(`${rel} exists but is not committed`)\n },\n\n contains(rel, needle, label) {\n if (!exists(rel)) return FAIL(`${rel} does not exist`)\n const hit = typeof needle === 'string' ? read(rel).includes(needle) : needle.test(read(rel))\n return hit ? PASS(label ?? `${rel} has ${needle}`) : FAIL(label ? `not yet: ${label}` : `${rel} has no ${needle}`)\n },\n\n lacks(rel, needle, label) {\n if (!exists(rel)) return FAIL(`${rel} does not exist`)\n const hit = typeof needle === 'string' ? read(rel).includes(needle) : needle.test(read(rel))\n return hit ? FAIL(`${rel} still has ${needle}`) : PASS(label ?? `${rel} has no ${needle}`)\n },\n\n script(pkgRel, name) {\n if (!exists(pkgRel)) return FAIL(`${pkgRel} does not exist`)\n const cmd = (JSON.parse(read(pkgRel)) as { scripts?: Record<string, string> }).scripts?.[name]\n return cmd ? PASS(`${pkgRel} runs \"${name}\": ${cmd}`) : FAIL(`${pkgRel} has no \"${name}\" script`)\n },\n\n async loads(rel) {\n if (!exists(rel)) return FAIL(`${rel} does not exist`)\n try {\n const mod = (await import(pathToFileURL(abs(rel)).href)) as { default?: unknown }\n return mod.default ? PASS(`${rel} loads`) : FAIL(`${rel} has no default export`)\n } catch (e) {\n return FAIL(`${rel} does not load: ${said(e).message.split('\\n')[0]}`)\n }\n },\n\n /** A workflow step that runs `npm run <script>`; a regex matches the script name. */\n ci(script) {\n const files = listFiles('.github/workflows', (n) => /\\.ya?ml$/.test(n))\n const name = typeof script === 'string' ? escape(script) : script.source\n const step = new RegExp(`npm run (${name})(?![\\\\w:-])`)\n for (const f of files) {\n const m = read(f).match(step)\n if (m) return PASS(`${f} runs npm run ${m[1]}`)\n }\n return FAIL(`no workflow runs npm run ${script}`)\n },\n\n /**\n * A workflow job, by its id, with a step that runs `npm run <script>`. Branch\n * protection requires a whole job by its name, so a required check needs the\n * job, not the step somewhere (UIG-6). A job is a key two spaces in under\n * `jobs:`, and ends at the next line that is not indented further; a comment\n * line does not count.\n */\n ciJob(job, script) {\n const files = listFiles('.github/workflows', (n) => /\\.ya?ml$/.test(n))\n const header = new RegExp(`^ ${escape(job)}:\\\\s*(#.*)?$`)\n const step = new RegExp(`^(?!\\\\s*#).*npm run ${escape(script)}(?![\\\\w:-])`, 'm')\n for (const f of files) {\n const lines = read(f).split(/\\r?\\n/)\n const from = lines.findIndex((l) => /^jobs:\\s*$/.test(l))\n const start = from === -1 ? -1 : lines.findIndex((l, i) => i > from && header.test(l))\n if (start === -1) continue\n const end = lines.findIndex((l, i) => i > start && /^ {0,2}\\S/.test(l))\n const body = lines.slice(start + 1, end === -1 ? undefined : end).join('\\n')\n return step.test(body) ? PASS(`${f}: the job ${job} runs npm run ${script}`) : FAIL(`${f}: the job ${job} does not run npm run ${script}`)\n }\n return FAIL(`no workflow has a job ${job}`)\n },\n\n hook(settingsRel, needle) {\n if (!exists(settingsRel)) return FAIL(`${settingsRel} does not exist`)\n let s: { hooks?: { PreToolUse?: { hooks?: { command?: string }[] }[] } }\n try {\n s = JSON.parse(read(settingsRel))\n } catch {\n return FAIL(`${settingsRel} is not valid JSON`)\n }\n const commands = (s.hooks?.PreToolUse ?? []).flatMap((m) => (m.hooks ?? []).map((x) => x.command ?? ''))\n const hit = commands.find((c) => c.includes(needle))\n if (hit) return PASS(`${settingsRel} has a PreToolUse hook running ${needle}`)\n return FAIL(commands.length ? `${settingsRel} has PreToolUse hooks, none running ${needle}` : `${settingsRel} has no PreToolUse hook`)\n },\n\n json(rel, test = () => true, label) {\n if (!exists(rel)) return FAIL(`${rel} does not exist`)\n let data: unknown\n try {\n data = JSON.parse(read(rel))\n } catch {\n return FAIL(`${rel} is not valid JSON`)\n }\n if (!git('ls-files', '--error-unmatch', rel)) return FAIL(`${rel} is not committed`)\n return test(data) ? PASS(label ?? `${rel} is committed and parses`) : FAIL(`${rel} parses, but ${label ?? 'fails its test'}`)\n },\n\n /** Lints a line of code that is never written to disk, the way the hook will. */\n async lint({ cwd = '.', config, file, code, expect, mentions }) {\n const cfg = join(cwd, config)\n if (!exists(cfg)) return FAIL(`${cfg} does not exist`)\n let ESLint: typeof ESLintClass\n try {\n const req = createRequire(join(abs(cwd), 'package.json'))\n ;({ ESLint } = (await import(pathToFileURL(req.resolve('eslint')).href)) as { ESLint: typeof ESLintClass })\n } catch (e) {\n return UNKNOWN(`could not load eslint from ${cwd}: ${said(e).message.split('\\n')[0]}`)\n }\n let results: ESLintClass.LintResult[]\n try {\n const eslint = new ESLint({ cwd: abs(cwd), overrideConfigFile: config })\n results = await eslint.lintText(code, { filePath: join(abs(cwd), file), warnIgnored: true })\n } catch (e) {\n return UNKNOWN(`${cfg} could not lint ${file}: ${said(e).message.split('\\n')[0]}`)\n }\n const messages = results.flatMap((r) => r.messages)\n const ignored = messages.some((m) => /ignored/i.test(m.message) && !m.ruleId)\n const fatal = messages.find((m) => m.fatal && !/ignored/i.test(m.message))\n if (fatal) return UNKNOWN(`${file} did not parse: ${fatal.message}`)\n const want = expect === 'error' ? 2 : expect === 'warning' ? 1 : 0\n if (want === 0) {\n const any = messages.filter((m) => m.ruleId && m.severity === 2)\n return any.length ? FAIL(`${file} gets an error it should not: ${any[0].message}`) : PASS(`${file}: no error${ignored ? ' (not linted)' : ''}`)\n }\n if (ignored) return FAIL(`${cfg} does not lint ${file}`)\n const hit = messages.find((m) => m.ruleId && m.severity === want && (!mentions || m.message.includes(mentions)))\n const kind = want === 2 ? 'error' : 'warning'\n return hit ? PASS(`${file} gets ${want === 2 ? 'an' : 'a'} ${kind}: ${hit.message}`) : FAIL(`${file} gets no ${kind}${mentions ? ` naming ${mentions}` : ''}`)\n },\n\n /**\n * A GitHub setting, not a file, so it is asked of GitHub: the checks a merge\n * into the default branch must pass, from the rulesets in force on it\n * (`rules/branches/<branch>`, which anyone who can read the repository may\n * see) and from classic protection as `branches/<branch>` reports it. Not\n * `branches/<branch>/protection`: GitHub answers \"Not Found\" there to anyone\n * who is not an admin, and it knows nothing of rulesets (UIG-6).\n */\n protectedBranch(pattern) {\n const url = git('remote', 'get-url', 'origin')\n const slug = url?.match(/github\\.com[:/](.+?)(?:\\.git)?$/)?.[1]\n if (!slug) return UNKNOWN('no GitHub remote')\n const branch = (git('symbolic-ref', '--short', 'refs/remotes/origin/HEAD') ?? 'origin/main').replace(/^origin\\//, '')\n const ask = (path: string) => JSON.parse(execFileSync('gh', ['api', path], { stdio: ['ignore', 'pipe', 'pipe'], timeout: 20000 }).toString())\n let rules: { type: string; parameters?: { required_status_checks?: { context: string }[] } }[]\n let classic: { contexts?: string[]; checks?: { context: string }[] } | undefined\n try {\n rules = ask(`repos/${slug}/rules/branches/${branch}`)\n classic = ask(`repos/${slug}/branches/${branch}`).protection?.required_status_checks\n } catch (e) {\n const s = said(e)\n return UNKNOWN(`could not ask GitHub (${s.code === 'ENOENT' ? 'gh is not installed' : s.text.split('\\n')[0] || s.message})`)\n }\n const names = [\n ...rules.filter((r) => r.type === 'required_status_checks').flatMap((r) => r.parameters?.required_status_checks ?? []).map((c) => c.context),\n ...(classic?.contexts ?? []),\n ...(classic?.checks ?? []).map((c) => c.context),\n ]\n const hit = names.find((n) => pattern.test(n))\n if (hit) return PASS(`${slug} ${branch} requires \"${hit}\"`)\n return FAIL(names.length ? `${slug} ${branch} requires ${names.map((n) => `\"${n}\"`).join(', ')}, none matching ${pattern}` : `${slug} ${branch} requires no check to merge`)\n },\n\n gh(args, label) {\n try {\n execFileSync('gh', args, { stdio: ['ignore', 'pipe', 'pipe'], timeout: 20000 })\n return PASS(label)\n } catch (e) {\n const s = said(e)\n if (/could not resolve|not found|HTTP 404/i.test(s.text)) return FAIL(`not yet: ${label}`)\n return UNKNOWN(`could not ask GitHub (${s.code === 'ENOENT' ? 'gh is not installed' : s.text.split('\\n')[0] || s.message})`)\n }\n },\n\n /** How many of `files` pass `test`: all is a pass, some is a part. */\n share(files, test, label) {\n if (!files.length) return FAIL(`no files to check for: ${label}`)\n const ok = files.filter(test).length\n const detail = `${ok} of ${files.length}: ${label}`\n return ok === files.length ? PASS(detail) : ok ? PART(detail) : FAIL(detail)\n },\n }\n}\n\n// \u2500\u2500 running the checks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst STATUS: Record<string, string> = { done: '\u2705', started: '\uD83D\uDEA7', none: '\u2B1C', unknown: '\u2754' }\n\nfunction statusOf(checks: CheckResult[]) {\n const n = (r: CheckResult['result']) => checks.filter((c) => c.result === r).length\n const pass = n('pass'), part = n('part'), fail = n('fail'), unknown = n('unknown')\n if (!checks.length) return 'unknown'\n if (pass === checks.length) return 'done'\n if (pass + part > 0) return fail === 0 && unknown > 0 ? 'unknown' : 'started'\n return fail > 0 ? 'none' : 'unknown'\n}\n\nasync function runRepo(ROOT: string, app: string, h: GateHelpers): Promise<{ spec: GateSpec; report: Report }> {\n // The checks file sits with the app, because it imports `appChecks` from the\n // package: in Ship that install is `web/node_modules`, not the repo's top\n // folder (UIG-32). Everything it names is still read from the repo's top.\n const checksFile = join(ROOT, app, 'scripts', 'gates-checks.mjs')\n const define = ((await import(pathToFileURL(checksFile).href)) as { default: (h: GateHelpers) => GateSpec }).default\n const spec = define(h)\n const git = (...a: string[]) => {\n try {\n return execFileSync('git', a, { cwd: ROOT, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()\n } catch {\n return null\n }\n }\n const rows: Row[] = []\n for (const t of spec.tickets) {\n const checks: Row['checks'] = []\n for (const c of t.checks ?? []) {\n if (c.aggregate) continue\n let r: CheckResult\n try {\n r = await c.run()\n } catch (e) {\n r = UNKNOWN(`the check threw: ${said(e).message}`)\n }\n checks.push({ repo: spec.repo, what: c.what, ...r })\n }\n rows.push({ ref: t.ref, title: t.title, owner: t.owner, checks })\n }\n return { spec, report: { schema: 1, repo: spec.repo, branch: git('rev-parse', '--abbrev-ref', 'HEAD'), commit: git('rev-parse', '--short', 'HEAD'), engine: ENGINE, rows } }\n}\n\n/**\n * Another repository's report. A repo on the package's engine runs it from its\n * own install; a repo still carrying `scripts/gates-status.mjs` runs that copy.\n *\n * `app` is the folder that repo's app sits in \u2014 `web` in Ship \u2014 where its\n * install and its checks file are. The report is still of the whole repo.\n */\nfunction runSibling(dir: string, app: string, shown: string): { report?: Report; error?: string } {\n const copy = join(dir, 'scripts', 'gates-status.mjs')\n const installed = createRequire(join(dir, app, 'package.json'))\n let script = copy\n const args = ['--json']\n if (!existsSync(copy)) {\n try {\n script = join(installed.resolve('@estiva-app/ui/package.json'), '..', 'dist', 'gates', 'cli.js')\n args.unshift('status')\n if (app !== '.') args.push('--app', app)\n } catch {\n return { error: `${shown} has neither its own gates-status.mjs nor @estiva-app/ui installed${app === '.' ? '' : ` in ${app}`}` }\n }\n if (!existsSync(script)) return { error: `${shown} has an @estiva-app/ui with no gates engine: install 0.21.0 or later` }\n }\n try {\n const out = execFileSync(process.execPath, [script, ...args], { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], timeout: 300000, maxBuffer: 16 * 1024 * 1024 })\n return { report: JSON.parse(out.toString()) as Report }\n } catch (e) {\n const err = e as { stderr?: unknown; message?: string }\n return { error: `its gates:status failed: ${String(err.stderr ?? err.message).split('\\n').filter(Boolean).slice(-1)[0]}` }\n }\n}\n\nfunction pad(s: string, n: number) {\n const len = [...s].length\n return len >= n ? s : s + ' '.repeat(n - len)\n}\n\nfunction printRows(rows: Row[], { showOwner, detail }: { showOwner: boolean; detail: boolean }) {\n const lines: string[] = []\n const width = Math.max(...rows.map((r) => [...(r.title ?? '')].length))\n for (const r of rows) {\n const pass = r.checks.filter((c) => c.result === 'pass').length\n const count = r.checks.length ? `${pass} of ${r.checks.length}` : 'no checks here'\n lines.push(`${STATUS[r.status ?? 'unknown']} ${pad(r.ref, 7)} ${pad(r.title ?? '', width)} ${showOwner ? pad(r.ownerRepo ?? '', 10) : ''}${count}`)\n if (detail || r.status === 'started' || r.status === 'unknown') {\n for (const c of r.checks) {\n const mark = { pass: '\u2713', part: '\u00BD', fail: '\u2717', unknown: '?' }[c.result]\n lines.push(` ${mark} ${showOwner ? `${pad(c.repo, 10)} ` : ''}${c.what} \u2014 ${c.detail}`)\n }\n }\n }\n return lines.join('\\n')\n}\n\nfunction summary(rows: Row[]) {\n const n = (s: string) => rows.filter((r) => r.status === s).length\n return `${STATUS.done} ${n('done')} done \u00B7 ${STATUS.started} ${n('started')} started \u00B7 ${STATUS.none} ${n('none')} not started \u00B7 ${STATUS.unknown} ${n('unknown')} could not check`\n}\n\nexport interface StatusOptions {\n /** The repository to report on. */\n root?: string\n /** The folder that holds the app, relative to `root`: `web` in Ship, `.` elsewhere. */\n app?: string\n json?: boolean\n detail?: boolean\n}\n\n/** Print where the project stands. Returns what was printed, or the JSON report. */\nexport async function runStatus({ root = process.cwd(), app = '.', json = false, detail = false }: StatusOptions = {}): Promise<string> {\n const ROOT = resolve(root)\n const h = helpers(ROOT)\n const shown = (dir: string) => (relative(ROOT, dir) || dir).replace(/\\\\/g, '/')\n const { spec, report } = await runRepo(ROOT, app, h)\n\n if (json) return JSON.stringify(report)\n\n const out: string[] = []\n out.push('UI Guardrails \u00B7 gates:status', 'Read from the code. Nothing here is a hand-ticked list.', '')\n out.push(`${pad(spec.repo, 10)} ${report.branch} @ ${report.commit}`)\n\n if (!spec.all) {\n // A sibling repo on its own: its own rows, then its parts of rows owned elsewhere.\n const own = report.rows.filter((r) => r.owner).map((r) => ({ ...r, ownerRepo: spec.repo, status: statusOf(r.checks) }))\n const parts = report.rows.filter((r) => !r.owner).map((r) => ({ ...r, ownerRepo: '', status: statusOf(r.checks) }))\n out.push('', `Tickets ${spec.repo} owns (${own.length}):`, printRows(own, { showOwner: false, detail }))\n out.push('', `Parts of tickets another repo owns, checked here (${parts.length}):`, printRows(parts, { showOwner: false, detail }))\n out.push('', 'For every ticket across the repos, run npm run gates:status in estiva-ui.')\n return out.join('\\n')\n }\n\n // estiva-ui: gather the sibling checkouts and join everything into one row per ticket.\n const reports: Report[] = [report]\n const found: { name: string; note: string; missing?: boolean }[] = []\n for (const s of spec.siblings ?? []) {\n const dir = resolve(ROOT, process.env[s.env] ?? s.path)\n if (!existsSync(join(dir, 'package.json'))) {\n found.push({ name: s.name, note: `not found at ${shown(dir)} (set ${s.env} to point at it)`, missing: true })\n continue\n }\n const r = runSibling(dir, s.app ?? '.', shown(dir))\n if (!r.report) {\n found.push({ name: s.name, note: r.error ?? 'no report', missing: true })\n continue\n }\n const copy = r.report.engine !== ENGINE ? (r.report.engine.startsWith('@estiva-app/ui@') ? ` \u00B7 on ${r.report.engine}'s engine` : ' \u00B7 runs its own copy of the status engine; UIG-32 moves it onto the package') : ''\n found.push({ name: s.name, note: `${r.report.branch} @ ${r.report.commit}, found at ${shown(dir)}${copy}` })\n reports.push(r.report)\n }\n for (const f of found) out.push(`${pad(f.name, 10)} ${f.note}`)\n\n const problems: string[] = []\n const known = new Set(spec.all.map((t) => t.ref))\n for (const rep of reports) {\n for (const r of rep.rows.filter((r) => !known.has(r.ref))) problems.push(`${rep.repo} reports ${r.ref}, which is not one of the tickets`)\n }\n const rows: Row[] = spec.all.map((t) => {\n const pieces = reports.flatMap((rep) => rep.rows.filter((r) => r.ref === t.ref).map((r) => ({ rep, r })))\n const claims = pieces.filter((p) => p.r.owner).map((p) => p.rep.repo)\n const ownerMissing = found.some((f) => f.name === t.owner && f.missing)\n if (!ownerMissing && (claims.length !== 1 || claims[0] !== t.owner)) {\n problems.push(`${t.ref}: should be owned by ${t.owner}, is claimed by ${claims.join(' and ') || 'no repo'}`)\n }\n for (const p of pieces.filter((p) => !p.r.owner && !(t.parts ?? []).includes(p.rep.repo))) {\n problems.push(`${t.ref}: ${p.rep.repo} checks a part of it, but the ticket list does not name ${p.rep.repo}`)\n }\n const checks = pieces.flatMap((p) => p.r.checks)\n for (const f of found.filter((f) => f.missing)) {\n if ((t.parts ?? []).includes(f.name) || t.owner === f.name) checks.push({ repo: f.name, what: `${f.name}'s part`, result: 'unknown', detail: f.note })\n }\n return { ref: t.ref, title: t.title, ownerRepo: t.owner, checks, aggregate: t.aggregate }\n })\n for (const row of rows) row.status = statusOf(row.checks)\n for (const row of rows.filter((r) => r.aggregate)) {\n const others = rows.filter((r) => r !== row)\n const done = others.filter((r) => r.status === 'done').length\n row.checks.push({ repo: 'all', what: 'every other ticket is done', ...(done === others.length ? PASS(`${done} of ${others.length} done`) : FAIL(`${done} of ${others.length} done`)) })\n row.status = statusOf(row.checks)\n }\n\n out.push('', printRows(rows, { showOwner: true, detail }), '', summary(rows))\n const owners = Object.entries(rows.reduce<Record<string, number>>((a, r) => ({ ...a, [r.ownerRepo ?? '']: (a[r.ownerRepo ?? ''] ?? 0) + 1 }), {}))\n .map(([k, v]) => `${k} ${v}`)\n .join(', ')\n out.push(`${rows.length} tickets. Owned by ${owners}.`)\n const unchecked = found.filter((f) => f.missing && spec.all!.some((t) => t.owner === f.name)).map((f) => f.name)\n if (problems.length) out.push(`\u26A0\uFE0F Ownership does not add up:\\n ${problems.join('\\n ')}`)\n else if (unchecked.length) out.push(`\u2754 Ownership not fully checked: ${unchecked.join(' and ')} could not be read.`)\n else out.push('\u2705 Each ticket is owned by exactly one repo, and every repo agrees.')\n if (!detail) out.push('Add -- --detail to see every check.')\n return out.join('\\n')\n}\n"],
5
- "mappings": "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",
6
- "names": ["existsSync", "readFileSync", "createRequire", "join", "relative", "pathToFileURL", "countGates", "existsSync", "readFileSync", "createRequire", "join", "relative", "resolve", "pathToFileURL", "PASS", "r", "p", "f"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/gates/create-app.ts"],
4
- "sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `create-estiva-app <name>` \u2014 a new Estiva app that runs, with every gate on\n * (UIG-10).\n *\n * npx -p @estiva-app/ui create-estiva-app leaf [--title Leaf] [--theme light]\n *\n * The command is `create-app-cli.ts`; this file is what it runs, and what a test reads.\n *\n * **It reads only this package** (Katerina's ruling of 17 September, docs/GATES.md\n * \u00A723). Never Peek, never Ship: they are private, and reading them would carry\n * their history into every new app. What a new app needs to run a gate is this\n * package's own \u2014 the rules, the token and gate configs, the hook, the count,\n * gates:status and the checks every app runs \u2014 so the app gets short files that\n * import them, and a gate added later arrives with a version bump.\n *\n * What only a new app has is written here: its first page in the sidebar frame\n * (ruled the same day), its one theme, sign-in the way Ship signs in, one test,\n * one story, the CI workflow with the job `gate`, and its README and CLAUDE.md.\n *\n * Tool versions are this package's own (its devDependencies build and test the\n * same tools). The two it does not use itself \u2014 `@estiva-app/identity` and\n * `eslint-plugin-react-hooks` \u2014 are asked of the npm registry when the app is\n * made. The lockfile is the app's first `npm install`; make it on Linux.\n */\nimport { execFileSync } from 'node:child_process'\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { APP_RULE_IDS } from '../eslint/index'\n\ninterface PackageJson {\n version: string\n devDependencies: Record<string, string>\n}\n\nexport interface CreateAppOptions {\n /** The folder and npm name: lowercase letters, digits and dashes. */\n name: string\n /** What the app is called on screen. */\n title?: string\n /** One of this package's themes. */\n theme?: string\n /** The folder the app's folder is made in. */\n parent?: string\n /** The `@estiva-app/ui` dependency, when not this package's own version (a tarball, for a proof). */\n ui?: string\n /** Versions of the packages this package does not itself use; asked of npm when absent. */\n versions?: Record<string, string>\n}\n\nconst here = dirname(fileURLToPath(import.meta.url))\nconst packageRoot = resolve(here, '..', '..')\n\n/** The package's themes, read from its tokens.css: `light` is the block with no attribute. */\nexport function themes(): string[] {\n const css = readFileSync(join(packageRoot, 'tokens.css'), 'utf8')\n return ['light', ...new Set([...css.matchAll(/data-theme='([\\w-]+)'/g)].map((m) => m[1]))]\n}\n\n/** Every file a new app starts with, by path. Nothing is written. */\nexport function appFiles({ name, title = name, theme = 'light', ui, versions = {} }: CreateAppOptions): Record<string, string> {\n if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new Error(`\"${name}\" is not a name an app can have: lowercase letters, digits and dashes, starting with a letter`)\n const known = themes()\n if (!known.includes(theme)) throw new Error(`\"${theme}\" is not one of the package's themes: ${known.join(', ')}`)\n\n const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson\n const own = (dep: string) => {\n const range = pkg.devDependencies[dep] ?? versions[dep]\n if (!range) throw new Error(`no version for ${dep}`)\n return range\n }\n const deps = (names: string[]) => Object.fromEntries(names.map((n) => [n, own(n)]))\n\n const packageJson = {\n name,\n private: true,\n version: '0.0.0',\n type: 'module',\n scripts: {\n dev: 'vite',\n build: 'tsc -b && vite build',\n preview: 'vite preview',\n typecheck: 'tsc -b',\n lint: 'eslint .',\n 'lint:tokens': 'eslint --config eslint.tokens.config.js .',\n 'lint:rules': 'eslint --config eslint.gates.config.js .',\n 'postlint:rules': `estiva-gates count --repo ${name}`,\n 'gates:status': 'estiva-gates status',\n test: 'vitest run',\n storybook: 'storybook dev -p 6006',\n 'build-storybook': 'storybook build',\n },\n dependencies: {\n '@estiva-app/identity': own('@estiva-app/identity'),\n '@estiva-app/ui': ui ?? `^${pkg.version}`,\n ...deps(['@tabler/icons-react', 'react', 'react-dom']),\n },\n devDependencies: deps([\n '@eslint/js', '@storybook/addon-docs', '@storybook/react-vite', '@testing-library/dom', '@testing-library/react',\n '@types/node', '@types/react', '@types/react-dom', '@vitejs/plugin-react', 'autoprefixer', 'eslint',\n 'eslint-plugin-better-tailwindcss', 'eslint-plugin-react-hooks', 'globals', 'jsdom', 'postcss', 'storybook',\n 'tailwindcss', 'typescript', 'typescript-eslint', 'vite', 'vitest',\n ]),\n }\n\n const count = { schemaVersion: 1, repo: name, generatedAt: new Date().toISOString(), rules: Object.fromEntries(APP_RULE_IDS.map((id) => [id, { errors: 0, warnings: 0, escapes: 0 }])) }\n const themeAttr = ` data-theme=\"${theme}\"`\n const tsBase = {\n target: 'ES2023',\n module: 'ESNext',\n skipLibCheck: true,\n moduleResolution: 'bundler',\n allowImportingTsExtensions: true,\n verbatimModuleSyntax: true,\n moduleDetection: 'force',\n noEmit: true,\n strict: true,\n noUnusedLocals: false,\n noUnusedParameters: false,\n erasableSyntaxOnly: true,\n noFallthroughCasesInSwitch: true,\n noUncheckedSideEffectImports: true,\n }\n const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\\n`\n\n return {\n 'package.json': json(packageJson),\n\n '.gitignore': ['node_modules', 'dist', 'storybook-static', '*.local', '*.log', '*.tsbuildinfo', '.DS_Store', ''].join('\\n'),\n\n '.env.example': `# Estiva ID, for signing in. Copy this file to .env.local and fill both in.\n#\n# Left empty, the app offers no sign-in at all and runs anonymous: it never\n# reaches the real Estiva ID by accident.\n#\n# The app must be registered with that Estiva ID first, as its own app, with\n# this redirect address: the origin the app runs on, and a slash\n# (http://localhost:5173/). The real Estiva ID registers a new app on the\n# server; a local one (http://localhost:8787) in its database.\nVITE_ESTIVA_ID_ORIGIN=\nVITE_ESTIVA_ID_CLIENT_ID=${name}\n`,\n\n 'index.html': `<!doctype html>\n<html lang=\"en\"${themeAttr}>\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>${title}</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n`,\n\n 'tsconfig.json': json({ files: [], references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }] }),\n 'tsconfig.app.json': json({\n compilerOptions: { tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo', ...tsBase, useDefineForClassFields: true, lib: ['ES2023', 'DOM', 'DOM.Iterable'], types: ['vite/client'], jsx: 'react-jsx', paths: { '@/*': ['./src/*'] } },\n include: ['src', '.storybook'],\n }),\n 'tsconfig.node.json': json({\n compilerOptions: { tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo', ...tsBase, lib: ['ES2023'], types: ['node'] },\n include: ['vite.config.ts'],\n }),\n\n 'vite.config.ts': `import { fileURLToPath } from 'node:url'\nimport react from '@vitejs/plugin-react'\nimport { defineConfig } from 'vitest/config'\n\n// \\`react\\` is deduped because @estiva-app/ui declares it a peer: two copies in one\n// tree is the \"invalid hook call\" crash, and it appears at runtime.\nexport default defineConfig({\n plugins: [react()],\n resolve: {\n alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },\n dedupe: ['react', 'react-dom'],\n },\n test: {\n environment: 'jsdom',\n },\n})\n`,\n\n 'tailwind.config.js': `import estiva, { estivaContent } from '@estiva-app/ui/tailwind-preset'\n\n/**\n * The package's preset, and nothing of the app's own: a token is added to the\n * package, in every theme, never here.\n *\n * \\`estivaContent\\` is the package's own files. Tailwind does not merge \\`content\\`\n * from a preset, so without it every class used only by a package part is\n * purged, and the app builds clean while rendering at the wrong size.\n *\n * @type {import('tailwindcss').Config}\n */\nexport default {\n presets: [estiva],\n content: [...estivaContent, './index.html', './src/**/*.{ts,tsx}', './.storybook/**/*.{ts,tsx}'],\n}\n`,\n 'postcss.config.js': `export default {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n`,\n\n 'eslint.config.js': `import js from '@eslint/js'\nimport { gateLint, TOKEN_LINT_IGNORES, tokenLint, tokenValues } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport { defineConfig, globalIgnores } from 'eslint/config'\nimport globals from 'globals'\nimport tseslint from 'typescript-eslint'\n\n/**\n * Everything, in one lint (\\`npm run lint\\`, a CI step): TypeScript's and React's\n * recommended rules, the token contract and the UI Guardrails' rules. A new app\n * has no backlog, so all of it is a gate from the first commit.\n *\n * The token contract and the gate are the package's (\\`@estiva-app/ui/gates\\`),\n * imported, never copied: a rule written later arrives with a version bump.\n */\nexport default defineConfig([\n globalIgnores(TOKEN_LINT_IGNORES),\n {\n files: ['**/*.{ts,tsx}'],\n extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs.flat.recommended],\n languageOptions: { globals: globals.browser },\n },\n tokenLint(),\n tokenValues(),\n gateLint(),\n])\n`,\n 'eslint.tokens.config.js': `import { TOKEN_LINT_IGNORES, tokenLint, tokenValues } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport { defineConfig, globalIgnores } from 'eslint/config'\n\n// The token contract on its own (\\`npm run lint:tokens\\`). React's hooks plugin is\n// registered with its rules off, so its directives in the source do not break it.\nexport default defineConfig([\n globalIgnores(TOKEN_LINT_IGNORES),\n { plugins: { 'react-hooks': reactHooks }, linterOptions: { reportUnusedDisableDirectives: 'off' } },\n tokenLint(),\n tokenValues(),\n])\n`,\n 'eslint.gates.config.js': `import { gateConfig } from '@estiva-app/ui/gates'\nimport reactHooks from 'eslint-plugin-react-hooks'\n\n/**\n * The UI Guardrails' rules on their own \u2014 \\`npm run lint:rules\\`, CI's job \\`gate\\`\n * (GitHub requires it on main: never rename it), and what the editor hook in\n * \\`.claude/settings.json\\` lints a proposed write with. The package's gate,\n * imported. React's hooks plugin is named so its directives do not break it.\n *\n * A place that keeps something the gate refuses says why, on the line above:\n * \\`// @estiva-escape: <reason>\\`. Never \\`eslint-disable\\`: the count refuses it.\n */\nexport default gateConfig({ quiet: { 'react-hooks': reactHooks } })\n`,\n\n '.claude/settings.json': json({\n hooks: {\n PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ type: 'command', command: 'node \"$CLAUDE_PROJECT_DIR/node_modules/@estiva-app/ui/dist/gates/cli.js\" hook' }] }],\n },\n }),\n\n 'scripts/gates-checks.mjs': `import { appChecks } from '@estiva-app/ui/gates'\n\n/**\n * What gates:status checks in ${name}: the gate checks every app runs, from the\n * package (\\`appChecks\\`), and nothing of its own yet. A check about this app's\n * own code goes beside them.\n */\nexport default function define(h) {\n return { repo: '${name}', tickets: appChecks(h, { page: 'src/pages/HomePage.tsx' }) }\n}\n`,\n '.gates-count.json': json(count),\n 'docs/GATES-DEBT.md': `# What ${title} owes the gates\n\nNothing. ${title} was made with every gate on, at zero.\n\nIt should stay that way. A place that keeps something the gate refuses says why on\nthe line above it, \\`// @estiva-escape: <reason>\\`, and the count lists it; a whole\nfile that cannot pass yet goes here, with its reason.\n`,\n\n '.github/workflows/deploy.yml': `name: deploy\n\n# Every pull request and every push to main runs the checks. There is no deploy\n# job yet: a new app has nowhere to go until it has a home. When it does, the job\n# goes here, and it needs \\`check\\` and \\`gate\\` first.\n\non:\n push:\n branches: [main]\n pull_request:\n\nconcurrency:\n group: deploy-${name}-\\${{ github.ref }}\n cancel-in-progress: true\n\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n - name: Typecheck the app\n run: npx tsc -b tsconfig.app.json\n - name: Typecheck the settings files\n run: npx tsc -b tsconfig.node.json\n - name: Lint \u2014 everything, the token contract included\n run: npm run lint\n - name: Test\n run: npm test\n - name: Build\n run: npm run build\n # A class only a package part uses reaches the stylesheet only if\n # tailwind.config.js spreads estivaContent. When it does not, the build still\n # succeeds and parts render at the wrong size, so this is where it is caught.\n - name: The package's classes survive Tailwind's purge\n run: |\n if ! grep -qr \"max-h-72\" dist/assets/*.css; then\n echo \"::error::a class used only by @estiva-app/ui is missing from the built CSS \u2014 check estivaContent is spread into tailwind.config.js\"\n exit 1\n fi\n\n # The UI Guardrails' rules, on their own: a raw element, behaviour a part owns\n # written by hand, a part restyled. \\`npm run lint:rules\\` also writes\n # .gates-count.json and fails on an eslint-disable that switches a rule off.\n #\n # A job of its own, because GitHub can require only a whole job, by its name:\n # the rule on main requires \\`gate\\`. Renaming it leaves every pull request\n # waiting for a check that never reports.\n gate:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n - name: Gate lint\n run: npm run lint:rules\n`,\n\n 'src/index.css': `@import '@estiva-app/ui/tokens.css';\n@import '@estiva-app/ui/base.css';\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml,\nbody,\n#root {\n height: 100%;\n}\n`,\n 'src/vite-env.d.ts': `/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n readonly VITE_ESTIVA_ID_ORIGIN?: string\n readonly VITE_ESTIVA_ID_CLIENT_ID?: string\n}\n`,\n 'src/config.ts': `/** What the app is called on screen. */\nexport const APP_TITLE = ${JSON.stringify(title)}\n\n/**\n * Estiva ID, or \\`null\\` when this build offers no sign-in at all. Empty is a real\n * mode, not a broken one: the app runs anonymous, and a local build can never\n * reach the real Estiva ID by accident. See .env.example.\n */\nexport const ID_CONFIG: { base: string; clientId: string } | null =\n import.meta.env.VITE_ESTIVA_ID_ORIGIN && import.meta.env.VITE_ESTIVA_ID_CLIENT_ID\n ? { base: import.meta.env.VITE_ESTIVA_ID_ORIGIN.replace(/\\\\/+$/, ''), clientId: import.meta.env.VITE_ESTIVA_ID_CLIENT_ID }\n : null\n`,\n 'src/auth/estivaId.ts': `import { createEstivaId, type ShellReason, type StoredToken } from '@estiva-app/identity'\nimport { ID_CONFIG } from '../config'\n\n/**\n * Signing in with Estiva ID, the way Ship does: \\`@estiva-app/identity\\` holds the\n * flow, this file the app's choices.\n *\n * - The redirect is the origin and a slash, never the current path: Estiva ID\n * compares it exactly against the one address registered for the app.\n * - The session lives in localStorage, so it outlives a tab; a sign-in's\n * single-use credentials in sessionStorage, per tab.\n */\nconst client = ID_CONFIG\n ? createEstivaId({\n base: ID_CONFIG.base,\n clientId: ID_CONFIG.clientId,\n redirectUri: () => \\`\\${window.location.origin}/\\`,\n storage: () => (typeof localStorage === 'undefined' ? null : localStorage),\n pendingStore: () => (typeof sessionStorage === 'undefined' ? null : sessionStorage),\n keyPrefix: '${name}.estiva-id',\n navigate: (url) => window.location.assign(url),\n })\n : null\n\nexport const signInAvailable = client !== null\n\nexport function currentToken(): StoredToken | null {\n return client?.validToken(Date.now() + 60_000) ?? null\n}\n\nexport function startRenewal(): void {\n client?.scheduleRenewal()\n}\n\nexport async function beginSignIn(options: { silent?: boolean } = {}): Promise<void> {\n await client?.beginSignIn(\\`\\${window.location.pathname}\\${window.location.search}\\`, options)\n}\n\nexport function beginSignOut(): void {\n client?.beginSignOut()\n}\n\nexport async function completeSignIn(): Promise<{ token: StoredToken; returnTo: string }> {\n if (!client) throw new Error('this build offers no sign-in')\n return client.completeSignIn(window.location.search)\n}\n\nexport function stashShellReason(reason: ShellReason): void {\n client?.stashShellReason(reason)\n}\n\nexport function takeShellReason() {\n return client?.takeShellReason() ?? null\n}\n\nexport function callbackParams(): { code?: string; error?: string } | null {\n const params = new URLSearchParams(window.location.search)\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n return code || error ? { code, error } : null\n}\n\nexport function clearQuery(): void {\n window.history.replaceState({}, '', window.location.pathname)\n}\n\nexport function guardAttemptedAt(): number | null {\n if (typeof sessionStorage === 'undefined') return null\n const raw = sessionStorage.getItem('estiva.authShell.silentAttempt')\n const at = raw ? Number(raw) : NaN\n return Number.isFinite(at) ? at : null\n}\n\n/**\n * Who Estiva ID says this is. Asked of its directory with the token, so the\n * answer also proves Estiva ID accepted the sign-in.\n */\nexport async function whoAmI(): Promise<{ name?: string; email?: string } | null> {\n const token = currentToken()\n if (!ID_CONFIG || !token) return null\n const response = await fetch(\\`\\${ID_CONFIG.base}/directory/\\${token.pubkey}\\`, { headers: { Authorization: \\`Bearer \\${token.accessToken}\\` } })\n if (!response.ok) return null\n const entry = (await response.json()) as { displayName?: string; email?: string }\n return { name: entry.displayName, email: entry.email }\n}\n`,\n 'src/auth/boot.ts': `import { decideBoot, type ShellReason, type ShellState } from '@estiva-app/identity'\nimport {\n beginSignIn,\n callbackParams,\n clearQuery,\n completeSignIn,\n currentToken,\n guardAttemptedAt,\n signInAvailable,\n startRenewal,\n stashShellReason,\n takeShellReason,\n} from './estivaId'\n\nexport type BootResult = { kind: 'app' } | { kind: 'shell'; state: ShellState } | { kind: 'leaving' }\n\n/**\n * Settle who this load belongs to, before the app renders. The decision is\n * \\`decideBoot\\` from \\`@estiva-app/identity\\`; this is the reading and acting\n * around it. It never throws: a blank page is worse than an honest shell.\n */\nexport async function bootAuth(): Promise<BootResult> {\n if (!signInAvailable) return { kind: 'app' }\n\n const action = decideBoot({\n callback: callbackParams(),\n hasValidToken: currentToken() !== null,\n guardAttemptedAt: guardAttemptedAt(),\n enteredThisPageLoad: false,\n now: Date.now(),\n })\n\n switch (action.do) {\n case 'enter':\n startRenewal()\n return { kind: 'app' }\n case 'complete_callback':\n try {\n await completeSignIn()\n clearQuery()\n startRenewal()\n return { kind: 'app' }\n } catch (cause) {\n clearQuery()\n stashShellReason('exchange_failed')\n return shell('exchange_failed', cause)\n }\n case 'probe_silently':\n try {\n await beginSignIn({ silent: true })\n return { kind: 'leaving' }\n } catch {\n return shell('network')\n }\n case 'prompt_passkey':\n return { kind: 'shell', state: { phase: 'authenticating', reason: takeShellReason() ?? action.reason } }\n case 'fail':\n return shell(action.reason)\n }\n}\n\nfunction shell(reason: ShellReason, cause?: unknown): BootResult {\n if (cause) console.warn('[auth] entry failed:', cause)\n return { kind: 'shell', state: { phase: 'failed', reason } }\n}\n\nexport async function enterFromShell(): Promise<void> {\n await beginSignIn()\n}\n`,\n 'src/auth/AuthShell.tsx': `import { CONTINUE_LABEL, isRecoverable, RETRY_LABEL, SHELL_COPY, type ShellState } from '@estiva-app/identity'\nimport { Button } from '@estiva-app/ui'\nimport { APP_TITLE } from '../config'\n\nexport interface AuthShellProps {\n state: ShellState\n onContinue: () => void\n}\n\n/**\n * What shows while nobody is signed in: the app's name, one line and at most one\n * button. The words are \\`@estiva-app/identity\\`'s, so every app says the same.\n */\nexport function AuthShell({ state, onContinue }: AuthShellProps) {\n const waiting = state.phase === 'checking' || state.phase === 'entering' || state.phase === 'ready'\n return (\n <div className=\"fixed inset-0 z-50 flex flex-col items-center justify-center gap-4 bg-bg-surface px-6 text-center\">\n <h1 className=\"text-h3 text-text-primary\">{APP_TITLE}</h1>\n {!waiting && (\n <>\n <p className=\"max-w-prose text-body-2 text-text-secondary\">{SHELL_COPY[state.reason]}</p>\n {isRecoverable(state.reason) && <Button onClick={onContinue}>{state.phase === 'authenticating' ? CONTINUE_LABEL : RETRY_LABEL}</Button>}\n </>\n )}\n </div>\n )\n}\n`,\n 'src/main.tsx': `import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport { AuthShell } from './auth/AuthShell'\nimport { bootAuth, enterFromShell } from './auth/boot'\n\n// Settle who this load belongs to first, then render: the app itself is imported\n// only once sign-in is settled, so nothing in it evaluates as the wrong person.\nvoid (async () => {\n const root = createRoot(document.getElementById('root')!)\n const boot = await bootAuth()\n if (boot.kind === 'leaving') return\n if (boot.kind === 'shell') {\n root.render(\n <StrictMode>\n <AuthShell state={boot.state} onContinue={() => void enterFromShell()} />\n </StrictMode>,\n )\n return\n }\n const { App } = await import('./App')\n root.render(\n <StrictMode>\n <App />\n </StrictMode>,\n )\n})()\n`,\n 'src/App.tsx': `import { AppShell, IdentityMenu, NavItem, Sidebar, type Identity } from '@estiva-app/ui'\nimport { IconHome } from '@tabler/icons-react'\nimport { useEffect, useState } from 'react'\nimport { beginSignOut, currentToken, whoAmI } from './auth/estivaId'\nimport { APP_TITLE, ID_CONFIG } from './config'\nimport { HomePage } from './pages/HomePage'\n\n/** The frame: the package's AppShell with a sidebar, and the one page. */\nexport function App() {\n const signedIn = currentToken() !== null\n const [me, setMe] = useState<Identity>({})\n\n useEffect(() => {\n if (!signedIn) return\n let live = true\n void whoAmI().then((who) => {\n if (live && who) setMe(who)\n })\n return () => {\n live = false\n }\n }, [signedIn])\n\n return (\n <AppShell\n logo={APP_TITLE}\n identity={<IdentityMenu me={me} signedIn={signedIn} idBase={ID_CONFIG?.base} onSignOut={signedIn ? beginSignOut : undefined} />}\n nav={\n <Sidebar>\n <NavItem href=\"/\" label=\"Home\" icon={<IconHome size={16} stroke={1.5} />} active />\n </Sidebar>\n }\n >\n <HomePage />\n </AppShell>\n )\n}\n`,\n 'src/pages/HomePage.tsx': `import { EmptyState } from '@estiva-app/ui'\n\n/** The first page. What it becomes is this app's own work. */\nexport function HomePage() {\n return (\n <div className=\"flex justify-center px-6 py-16\">\n <EmptyState message=\"Nothing here yet.\" />\n </div>\n )\n}\n`,\n 'src/pages/HomePage.stories.tsx': `import type { Meta, StoryObj } from '@storybook/react-vite'\nimport { HomePage } from './HomePage'\n\nconst meta = {\n title: 'Pages/Home',\n component: HomePage,\n parameters: { layout: 'fullscreen' },\n} satisfies Meta<typeof HomePage>\n\nexport default meta\ntype Story = StoryObj<typeof meta>\n\nexport const Empty: Story = {}\n`,\n 'src/App.test.tsx': `import { render, screen } from '@testing-library/react'\nimport { describe, expect, it } from 'vitest'\nimport { App } from './App'\nimport { APP_TITLE } from './config'\n\ndescribe('${title}', () => {\n it('opens in its frame, anonymous in a build with no sign-in', () => {\n render(<App />)\n expect(screen.getAllByText(APP_TITLE).length).toBeGreaterThan(0)\n expect(screen.getByText('Nothing here yet.')).toBeTruthy()\n expect(screen.getByRole('link', { name: 'Home' })).toBeTruthy()\n })\n})\n`,\n\n '.storybook/main.ts': `import type { StorybookConfig } from '@storybook/react-vite'\n\nconst config: StorybookConfig = {\n framework: '@storybook/react-vite',\n stories: ['../src/**/*.stories.@(ts|tsx)'],\n addons: ['@storybook/addon-docs'],\n}\n\nexport default config\n`,\n '.storybook/preview.tsx': `import type { Preview } from '@storybook/react-vite'\nimport '../src/index.css'\n\n// The app's one theme, selected the way the app selects it: on <html>.\ndocument.documentElement.dataset.theme = ${JSON.stringify(theme)}\n\nconst preview: Preview = {\n parameters: { layout: 'centered' },\n}\n\nexport default preview\n`,\n\n 'README.md': `# ${title}\n\nAn Estiva app, made with \\`create-estiva-app\\` from \\`@estiva-app/ui\\`. It starts\nwith the package's sidebar frame, one theme (\\`${theme}\\`), sign-in with Estiva ID,\nand every UI Guardrails gate on, at zero.\n\n## Run it\n\n\\`\\`\\`sh\nnpm install\nnpm run dev\n\\`\\`\\`\n\nWith no settings it runs **anonymous**: no sign-in is offered, and nothing reaches\nthe real Estiva ID. To sign in, copy \\`.env.example\\` to \\`.env.local\\` and fill it in.\nThe app must first be registered with that Estiva ID as its own app.\n\n## The checks\n\n| command | what |\n|---|---|\n| \\`npm run typecheck\\` | TypeScript, the app and its settings files |\n| \\`npm run lint\\` | everything: TypeScript's and React's rules, the token contract, the gate |\n| \\`npm run lint:rules\\` | the gate alone, and \\`.gates-count.json\\` \u2014 CI's job \\`gate\\` |\n| \\`npm test\\` | the tests |\n| \\`npm run build\\` | the build |\n| \\`npm run gates:status\\` | which gates are on, read from the code |\n| \\`npm run storybook\\` | the stories |\n\nThe gates are the package's, imported rather than copied, so a rule written later\narrives with an ordinary version bump. A Claude session started in this folder is\nstopped before it writes code the gate refuses (\\`.claude/settings.json\\`).\n\n## Once it is on GitHub\n\nRequire the check \\`gate\\` before anything merges into \\`main\\`: a ruleset on \\`main\\`\n(Settings \u2192 Rules \u2192 Rulesets) with \"Require status checks to pass\", the check\n\\`gate\\`, and no one allowed to bypass it. Until then \\`npm run gates:status\\` shows\nUIG-6 as not done.\n`,\n 'CLAUDE.md': `# ${title}, for Claude Code\n\n${title} is an Estiva app. Its parts, tokens and gates come from \\`@estiva-app/ui\\`.\n\n**Use the package's parts, never a raw element or a hand-built look.** The gate\nrefuses a raw control, behaviour a part owns written by hand, and a part restyled\nthrough \\`className\\`, and names what to use instead. It runs before you write\n(the hook in \\`.claude/settings.json\\`, for a session started in this folder), in\n\\`npm run lint:rules\\` and in CI. A session started elsewhere: run\n\\`npm run lint:rules\\` after changing \\`src/\\`, and fix what it reports. Keep something\nonly with its reason on the line above, \\`// @estiva-escape: <reason>\\`, never with\n\\`eslint-disable\\`.\n\n**Tokens only.** Colours, type, corners and shadows come from the package's preset.\n\n**The count starts at zero and stays there** (\\`.gates-count.json\\`, \\`docs/GATES-DEBT.md\\`).\n\nWhat each gate is and how it is wired: the package's README,\n\\`node_modules/@estiva-app/ui/README.md\\`.\n`,\n }\n}\n\n/** The versions of what this package does not use itself, from the npm registry. */\nexport function askNpm(names: string[]): Record<string, string> {\n const npm = process.env.npm_execpath\n const out: Record<string, string> = {}\n for (const name of names) {\n const args = ['view', name, 'version']\n const version = (npm ? execFileSync(process.execPath, [npm, ...args]) : execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { shell: process.platform === 'win32' }))\n .toString()\n .trim()\n if (!/^\\d+\\.\\d+\\.\\d+/.test(version)) throw new Error(`npm did not say which version of ${name} is current: \"${version}\"`)\n out[name] = `^${version}`\n }\n return out\n}\n\n/** Write the app. Refuses a folder that already exists. Returns the folder. */\nexport function createApp(options: CreateAppOptions): string {\n const dir = resolve(options.parent ?? process.cwd(), options.name)\n if (existsSync(dir)) throw new Error(`${dir} already exists: create-estiva-app never writes into a folder that is there`)\n const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')) as PackageJson\n const missing = ['@estiva-app/identity', 'eslint-plugin-react-hooks'].filter((n) => !pkg.devDependencies[n] && !options.versions?.[n])\n const versions = { ...(missing.length ? askNpm(missing) : {}), ...options.versions }\n const files = appFiles({ ...options, versions })\n for (const [rel, text] of Object.entries(files)) {\n const path = join(dir, rel)\n mkdirSync(dirname(path), { recursive: true })\n writeFileSync(path, text)\n }\n return dir\n}\n"],
5
- "mappings": ";;;AAyBA,SAAS,oBAAoB;AAC7B,SAAS,YAAY,WAAW,cAAc,qBAAqB;AACnE,SAAS,SAAS,MAAM,eAAe;AACvC,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AAsB7B,IAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,IAAM,cAAc,QAAQ,MAAM,MAAM,IAAI;AAGrC,SAAS,SAAmB;AACjC,QAAM,MAAM,aAAa,KAAK,aAAa,YAAY,GAAG,MAAM;AAChE,SAAO,CAAC,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,SAAS,wBAAwB,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AAC3F;AAGO,SAAS,SAAS,EAAE,MAAM,QAAQ,MAAM,QAAQ,SAAS,IAAI,WAAW,CAAC,EAAE,GAA6C;AAC7H,MAAI,CAAC,oBAAoB,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,IAAI,IAAI,+FAA+F;AAC5J,QAAM,QAAQ,OAAO;AACrB,MAAI,CAAC,MAAM,SAAS,KAAK,EAAG,OAAM,IAAI,MAAM,IAAI,KAAK,yCAAyC,MAAM,KAAK,IAAI,CAAC,EAAE;AAEhH,QAAM,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,cAAc,GAAG,MAAM,CAAC;AAC9E,QAAM,MAAM,CAAC,QAAgB;AAC3B,UAAM,QAAQ,IAAI,gBAAgB,GAAG,KAAK,SAAS,GAAG;AACtD,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,kBAAkB,GAAG,EAAE;AACnD,WAAO;AAAA,EACT;AACA,QAAM,OAAO,CAAC,UAAoB,OAAO,YAAY,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAElF,QAAM,cAAc;AAAA,IAClB;AAAA,IACA,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN,SAAS;AAAA,MACP,KAAK;AAAA,MACL,OAAO;AAAA,MACP,SAAS;AAAA,MACT,WAAW;AAAA,MACX,MAAM;AAAA,MACN,eAAe;AAAA,MACf,cAAc;AAAA,MACd,kBAAkB,6BAA6B,IAAI;AAAA,MACnD,gBAAgB;AAAA,MAChB,MAAM;AAAA,MACN,WAAW;AAAA,MACX,mBAAmB;AAAA,IACrB;AAAA,IACA,cAAc;AAAA,MACZ,wBAAwB,IAAI,sBAAsB;AAAA,MAClD,kBAAkB,MAAM,IAAI,IAAI,OAAO;AAAA,MACvC,GAAG,KAAK,CAAC,uBAAuB,SAAS,WAAW,CAAC;AAAA,IACvD;AAAA,IACA,iBAAiB,KAAK;AAAA,MACpB;AAAA,MAAc;AAAA,MAAyB;AAAA,MAAyB;AAAA,MAAwB;AAAA,MACxF;AAAA,MAAe;AAAA,MAAgB;AAAA,MAAoB;AAAA,MAAwB;AAAA,MAAgB;AAAA,MAC3F;AAAA,MAAoC;AAAA,MAA6B;AAAA,MAAW;AAAA,MAAS;AAAA,MAAW;AAAA,MAChG;AAAA,MAAe;AAAA,MAAc;AAAA,MAAqB;AAAA,MAAQ;AAAA,IAC5D,CAAC;AAAA,EACH;AAEA,QAAM,QAAQ,EAAE,eAAe,GAAG,MAAM,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,OAAO,OAAO,YAAY,aAAa,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE;AACvL,QAAM,YAAY,gBAAgB,KAAK;AACvC,QAAM,SAAS;AAAA,IACb,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,kBAAkB;AAAA,IAClB,4BAA4B;AAAA,IAC5B,sBAAsB;AAAA,IACtB,iBAAiB;AAAA,IACjB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,IACpB,oBAAoB;AAAA,IACpB,4BAA4B;AAAA,IAC5B,8BAA8B;AAAA,EAChC;AACA,QAAM,OAAO,CAAC,UAAmB,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA;AAElE,SAAO;AAAA,IACL,gBAAgB,KAAK,WAAW;AAAA,IAEhC,cAAc,CAAC,gBAAgB,QAAQ,oBAAoB,WAAW,SAAS,iBAAiB,aAAa,EAAE,EAAE,KAAK,IAAI;AAAA,IAE1H,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAUO,IAAI;AAAA;AAAA,IAG3B,cAAc;AAAA,iBACD,SAAS;AAAA;AAAA;AAAA;AAAA,aAIb,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASd,iBAAiB,KAAK,EAAE,OAAO,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,sBAAsB,GAAG,EAAE,MAAM,uBAAuB,CAAC,EAAE,CAAC;AAAA,IACpH,qBAAqB,KAAK;AAAA,MACxB,iBAAiB,EAAE,iBAAiB,gDAAgD,GAAG,QAAQ,yBAAyB,MAAM,KAAK,CAAC,UAAU,OAAO,cAAc,GAAG,OAAO,CAAC,aAAa,GAAG,KAAK,aAAa,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE;AAAA,MAC9O,SAAS,CAAC,OAAO,YAAY;AAAA,IAC/B,CAAC;AAAA,IACD,sBAAsB,KAAK;AAAA,MACzB,iBAAiB,EAAE,iBAAiB,iDAAiD,GAAG,QAAQ,KAAK,CAAC,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE;AAAA,MACjI,SAAS,CAAC,gBAAgB;AAAA,IAC5B,CAAC;AAAA,IAED,kBAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAkBlB,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAiBtB,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQrB,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA2BpB,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAa3B,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAe1B,yBAAyB,KAAK;AAAA,MAC5B,OAAO;AAAA,QACL,YAAY,CAAC,EAAE,SAAS,cAAc,OAAO,CAAC,EAAE,MAAM,WAAW,SAAS,gFAAgF,CAAC,EAAE,CAAC;AAAA,MAChK;AAAA,IACF,CAAC;AAAA,IAED,4BAA4B;AAAA;AAAA;AAAA,iCAGC,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,oBAKjB,IAAI;AAAA;AAAA;AAAA,IAGpB,qBAAqB,KAAK,KAAK;AAAA,IAC/B,sBAAsB,UAAU,KAAK;AAAA;AAAA,WAE9B,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOZ,gCAAgC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAYlB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAqDlB,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAajB,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOrB,iBAAiB;AAAA,2BACM,KAAK,UAAU,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAY5C,wBAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAmBR,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAmEpB,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAsEpB,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA4B1B,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IA4BhB,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAsCf,0BAA0B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAW1B,kCAAkC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAclC,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,YAKZ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUb,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUtB,0BAA0B;AAAA;AAAA;AAAA;AAAA,2CAIa,KAAK,UAAU,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAS5D,aAAa,KAAK,KAAK;AAAA;AAAA;AAAA,iDAGsB,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAqClD,aAAa,KAAK,KAAK;AAAA;AAAA,EAEzB,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBL;AACF;AAGO,SAAS,OAAO,OAAyC;AAC9D,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,MAA8B,CAAC;AACrC,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,CAAC,QAAQ,MAAM,SAAS;AACrC,UAAM,WAAW,MAAM,aAAa,QAAQ,UAAU,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,aAAa,QAAQ,aAAa,UAAU,YAAY,OAAO,MAAM,EAAE,OAAO,QAAQ,aAAa,QAAQ,CAAC,GACjL,SAAS,EACT,KAAK;AACR,QAAI,CAAC,iBAAiB,KAAK,OAAO,EAAG,OAAM,IAAI,MAAM,oCAAoC,IAAI,iBAAiB,OAAO,GAAG;AACxH,QAAI,IAAI,IAAI,IAAI,OAAO;AAAA,EACzB;AACA,SAAO;AACT;AAGO,SAAS,UAAU,SAAmC;AAC3D,QAAM,MAAM,QAAQ,QAAQ,UAAU,QAAQ,IAAI,GAAG,QAAQ,IAAI;AACjE,MAAI,WAAW,GAAG,EAAG,OAAM,IAAI,MAAM,GAAG,GAAG,6EAA6E;AACxH,QAAM,MAAM,KAAK,MAAM,aAAa,KAAK,aAAa,cAAc,GAAG,MAAM,CAAC;AAC9E,QAAM,UAAU,CAAC,wBAAwB,2BAA2B,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,QAAQ,WAAW,CAAC,CAAC;AACrI,QAAM,WAAW,EAAE,GAAI,QAAQ,SAAS,OAAO,OAAO,IAAI,CAAC,GAAI,GAAG,QAAQ,SAAS;AACnF,QAAM,QAAQ,SAAS,EAAE,GAAG,SAAS,SAAS,CAAC;AAC/C,aAAW,CAAC,KAAK,IAAI,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC/C,UAAM,OAAO,KAAK,KAAK,GAAG;AAC1B,cAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC5C,kBAAc,MAAM,IAAI;AAAA,EAC1B;AACA,SAAO;AACT;",
6
- "names": []
7
- }
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- buildRegistry,
4
- readIndexExports,
5
- serializeRegistry
6
- } from "./chunk-QDYGB3QN.js";
7
- import "./chunk-MRSBS5OP.js";
8
- export {
9
- buildRegistry,
10
- readIndexExports,
11
- serializeRegistry
12
- };
13
- //# sourceMappingURL=build-GOVLABI6.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../src/registry/find.ts"],
4
- "sourcesContent": ["/**\n * The catalogue's reader (UIG-12): `ui:find <words>`.\n *\n * It exists to prove the format. A schema nobody consumes is a schema nobody\n * has tested, and the questions it has to answer are regression tests, not the\n * scope: \"floating panel\" must find `Popover`, \"scrolling\" `ScrollArea`,\n * \"empty\" `EmptyState`.\n *\n * It is also the answer to \"what do we have for this?\", which is the question\n * the wall cannot answer: a lint rule refuses the wrong thing, and this is\n * where the right one is named (UIG-20 makes a session ask it first).\n */\nimport type { Registry, RegistryEntry } from './schema'\n\nexport interface Finding {\n entry: RegistryEntry\n /** How many of the query's words matched anywhere. Ranked on this first. */\n matched: number\n score: number\n /** Which fields matched, for the reader to say why. */\n where: string[]\n /**\n * The props the query named **by name**, so the answer shows those rather\n * than all of them. A prop whose note merely carries one of the words counts\n * towards the score and is not shown: on \"floating panel\", six of `Popover`'s\n * notes say \"panel\", and printing all six buries the answer.\n */\n props: string[]\n}\n\n/**\n * Words that are in nearly every sentence, and so tell a search nothing.\n *\n * Measured: `ui:find \"truncate a long link\"` printed `Link`'s `external` prop,\n * because its note says \"in **a** new tab\" and the one-letter word matched\n * itself exactly. The minimum length in `alike` only guards a *prefix* match;\n * an exact match short-circuits it, which is right for `tab` and useless for `a`.\n */\nconst STOP = new Set([\n 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'can', 'do', 'for', 'from', 'how', 'i', 'if', 'in', 'is', 'it', 'its', 'me', 'my', 'no', 'not', 'of',\n 'on', 'or', 'our', 'so', 'that', 'the', 'their', 'them', 'then', 'there', 'they', 'this', 'to', 'up', 'was', 'we', 'what', 'when', 'which', 'with', 'you', 'your',\n])\n\n/**\n * The words of a piece of text, with names taken apart.\n *\n * `CommandPalette` is kept whole *and* split, so \"palette\" reaches it and\n * \"commandpalette\" still does. Prose is already words; this costs it nothing.\n */\nfunction words(text: string): string[] {\n const found: string[] = []\n for (const token of text.match(/[A-Za-z0-9]+/g) ?? []) {\n found.push(token.toLowerCase())\n const parts = token.split(/(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/)\n if (parts.length > 1) found.push(...parts.map((part) => part.toLowerCase()))\n }\n return found\n}\n\n/**\n * Whether a word of the query means a word of the text.\n *\n * Either being a prefix of the other covers \"scroll\" against \"scrolling\"; the\n * five-letter key covers \"scrolls\" against \"scrolling\", where neither is. Crude\n * on purpose \u2014 a stemmer is a dependency and a surprise, and the failure here\n * is an extra row in a list of five, not a wrong answer.\n *\n * **Three letters at least**, or a prefix match is no match at all: `person's`\n * puts the word \"s\" in the text, and every query on earth starts with some\n * letter. Measured \u2014 before this, \"scrolling\" found `Avatar`.\n */\nfunction alike(query: string, text: string): boolean {\n if (query === text) return true\n if (Math.min(query.length, text.length) < 3) return false\n if (text.startsWith(query) || query.startsWith(text)) return true\n return query.slice(0, 5) === text.slice(0, 5)\n}\n\nconst hit = (query: string, text: string) => words(text).some((word) => alike(query, word))\n\n/**\n * Search name, purpose, behaviours and variants.\n *\n * A row that answers more of the question comes first, whatever it scored:\n * \"floating panel\" has to reach `Popover`, whose purpose carries both words,\n * over `MenuPanel`, whose name carries one of them loudly.\n */\nexport function findInRegistry(registry: Registry, query: string, { limit = 5 }: { limit?: number } = {}): Finding[] {\n const all = words(query)\n // Drop the words that mean nothing. If the question was only those, keep them\n // rather than answer nothing at all.\n const carrying = all.filter((word) => word.length >= 3 && !STOP.has(word))\n const asked = carrying.length ? carrying : all\n if (asked.length === 0) return []\n\n const findings: Finding[] = []\n for (const entry of registry.entries) {\n let score = 0\n let matched = 0\n const where = new Set<string>()\n const byName = new Set<string>()\n const byNote = new Set<string>()\n for (const word of asked) {\n let best = 0\n if (entry.name.toLowerCase() === word) {\n best = 100\n where.add('name')\n } else if (hit(word, entry.name)) {\n best = 40\n where.add('name')\n }\n if (hit(word, entry.purpose)) {\n best = Math.max(best, 20)\n where.add('purpose')\n }\n for (const owned of entry.ownsBehaviours) {\n if (hit(word, owned.behaviour) || hit(word, owned.id)) {\n best = Math.max(best, 15)\n where.add('behaviour')\n }\n }\n for (const variant of entry.variants) {\n if (variant.values.some((value) => hit(word, value)) || hit(word, variant.prop)) {\n best = Math.max(best, 8)\n where.add('variant')\n byName.add(variant.prop)\n }\n }\n // What it can do. \"does Link already truncate?\" is the question this\n // answers, and answering it is what stops the thing being built twice.\n for (const prop of entry.props) {\n // A prop the question named by name is the answer; one whose note\n // happens to carry the word is a hint. Named ones go first, so a short\n // answer shows the right lines.\n if (hit(word, prop.name)) {\n best = Math.max(best, 14)\n where.add('prop')\n byName.add(prop.name)\n } else if (prop.note !== null && hit(word, prop.note)) {\n best = Math.max(best, 10)\n where.add('prop')\n byNote.add(prop.name)\n }\n }\n if (best > 0) matched += 1\n score += best\n }\n if (matched > 0) findings.push({ entry, matched, score, where: [...where], props: [...byName] })\n }\n\n findings.sort((a, b) => b.matched - a.matched || b.score - a.score || a.entry.name.localeCompare(b.entry.name))\n return findings.slice(0, limit)\n}\n\n/** `http://localhost:6008/?path=/docs/overlays-popover--docs`, or `null` where the entry has no page. */\nexport function docsLink(registry: Registry, entry: RegistryEntry): string | null {\n if (!entry.docsId) return null\n return registry.storybook.devUrl + registry.storybook.docsPath.replace('{docsId}', entry.docsId)\n}\n\n/** What the command prints: the import line first, because that is what the reader came for. */\nexport function formatFindings(registry: Registry, findings: Finding[], query: string): string {\n if (findings.length === 0) {\n return [\n `Nothing in ${registry.builtFrom.repo} matches \"${query}\".`,\n 'If nothing here does what you need, say so and ask \u2014 do not invent a component.',\n ].join('\\n')\n }\n\n const blocks = findings.map((finding) => {\n const { entry } = finding\n const lines = [\n `${entry.name} \u00B7 ${entry.kind} \u00B7 ${entry.repo} (matched ${finding.where.join(', ')})`,\n ` ${entry.purpose}`,\n ` import { ${entry.name} } from '${entry.importPath}'`,\n ]\n if (entry.ownsBehaviours.length) lines.push(` owns: ${entry.ownsBehaviours.map((owned) => owned.behaviour).join(' \u00B7 ')}`)\n if (entry.variants.length) lines.push(` ${entry.variants.map((variant) => `${variant.prop}: ${variant.values.join(' | ')}`).join(' ')}`)\n // The props the question named, with their own line \u2014 not all of them. A\n // component with twenty props would bury its own answer.\n const shown = finding.props\n .filter((name) => !entry.variants.some((variant) => variant.prop === name))\n .slice(0, 3)\n .map((name) => entry.props.find((prop) => prop.name === name))\n for (const prop of shown) {\n if (prop) lines.push(` ${prop.name}: ${prop.takes}${prop.note ? ` \u2014 ${prop.note}` : ''}`)\n }\n if (entry.props.length) lines.push(` ${entry.props.length} props in all; see the page for the rest`)\n const link = docsLink(registry, entry)\n if (link) lines.push(` ${link}`)\n return lines.join('\\n')\n })\n\n return blocks.join('\\n\\n')\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }