PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/desktop/desktopRegistry.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:26:55 UTC 11,432 bytes, 345 lines download raw bytes

1import { ROUTES } from '../services/viewState.js?v=3.8.36';
2
3const APP_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4const APP_GROUPS = new Set(['explore', 'research', 'help']);
5const APP_SURFACES = new Set(['desktop', 'start']);
6const AVAILABILITY_STATES = new Set(['ready', 'planned', 'blocked']);
7const LAUNCH_KINDS = new Set(['route', 'shell', 'action', 'path']);
8const TOOL_STATUSES = new Set(['ready', 'beta', 'experimental']);
9const ROUTE_DESTINATIONS = new Set(Object.values(ROUTES));
10const PATH_DESTINATIONS = new Set([
11  'features/participate/',
12  'features/winer-method/',
13]);
14
15const freezeApp = (app) => Object.freeze({
16  ...app,
17  surfaces: Object.freeze([...(app.surfaces || [])]),
18  launch: app.launch ? Object.freeze({ ...app.launch }) : null,
19});
20
21export const DESKTOP_APPS = Object.freeze([
22  {
23    id: 'archive',
24    label: 'Archive',
25    description: 'Search and browse the canonical record collection.',
26    icon: 'archive',
27    group: 'explore',
28    surfaces: ['desktop', 'start'],
29    availability: 'ready',
30    launch: { kind: 'shell', destination: 'archive' },
31  },
32  {
33    id: 'folders',
34    label: 'Folders',
35    description: 'Enter the collection through its thematic folders.',
36    icon: 'folders',
37    group: 'explore',
38    surfaces: ['desktop', 'start'],
39    availability: 'ready',
40    launch: { kind: 'shell', destination: 'folders' },
41  },
42  {
43    id: 'start',
44    label: 'Start here',
45    description: 'Choose a guided path into the archive.',
46    icon: 'start',
47    group: 'explore',
48    surfaces: ['desktop', 'start'],
49    availability: 'ready',
50    launch: { kind: 'shell', destination: 'start' },
51  },
52  {
53    id: 'findings',
54    label: 'Selected findings',
55    description: 'An authored trail through real archive records.',
56    icon: 'findings',
57    group: 'explore',
58    surfaces: ['start'],
59    availability: 'ready',
60    launch: { kind: 'shell', destination: 'findings' },
61  },
62  {
63    id: 'entities',
64    label: 'People & ideas',
65    description: 'Trace people, organizations, concepts, and relationships.',
66    icon: 'entities',
67    group: 'research',
68    surfaces: ['desktop', 'start'],
69    availability: 'ready',
70    launch: { kind: 'shell', destination: 'entities' },
71  },
72  {
73    id: 'dissertation',
74    label: 'Dissertation',
75    description: 'Explore The Impossible Press through its mind map.',
76    icon: 'dissertation',
77    group: 'research',
78    surfaces: ['desktop', 'start'],
79    availability: 'ready',
80    launch: { kind: 'shell', destination: 'dissertation' },
81  },
82  {
83    id: 'analytics',
84    label: 'Analytics',
85    description: 'View patterns across the archive at collection scale.',
86    icon: 'analytics',
87    group: 'research',
88    surfaces: ['desktop', 'start'],
89    availability: 'ready',
90    launch: { kind: 'shell', destination: 'analytics' },
91  },
92  {
93    id: 'tools',
94    label: 'Tools',
95    description: 'Open maintained reading and research tools.',
96    icon: 'tools',
97    group: 'research',
98    surfaces: ['desktop', 'start'],
99    availability: 'ready',
100    launch: { kind: 'shell', destination: 'tools' },
101  },
102  {
103    id: 'method',
104    label: 'The method',
105    description: 'See a bounded, evidence-backed archive method demonstration.',
106    icon: 'method',
107    group: 'research',
108    surfaces: ['start'],
109    availability: 'ready',
110    launch: { kind: 'path', destination: 'features/winer-method/' },
111  },
112  {
113    id: 'about',
114    label: 'About the archive',
115    description: 'Read about the collection, its scope, and its curator.',
116    icon: 'about',
117    group: 'help',
118    surfaces: ['desktop', 'start'],
119    availability: 'ready',
120    launch: { kind: 'route', destination: ROUTES.about },
121  },
122  {
123    id: 'report',
124    label: 'Report a problem',
125    description: 'Tell the curator about a broken link or archive error.',
126    icon: 'report',
127    group: 'help',
128    surfaces: ['desktop', 'start'],
129    availability: 'ready',
130    launch: { kind: 'action', destination: 'report' },
131  },
132  {
133    id: 'readme',
134    label: 'Read me',
135    description: 'Learn what this desktop is and how to move around it.',
136    icon: 'readme',
137    group: 'help',
138    surfaces: ['desktop', 'start'],
139    availability: 'ready',
140    launch: { kind: 'shell', destination: 'readme' },
141  },
142  {
143    id: 'participate',
144    label: 'Participate',
145    description: 'Approved ways to support and contribute to the archive.',
146    icon: 'participate',
147    group: 'help',
148    surfaces: ['start'],
149    availability: 'ready',
150    launch: { kind: 'path', destination: 'features/participate/' },
151  },
152  {
153    id: 'making-of',
154    label: 'How it was made',
155    description: 'The curator-approved making-of narrative.',
156    icon: 'making-of',
157    group: 'help',
158    surfaces: [],
159    availability: 'blocked',
160    availabilityNote: 'Connect only after the interview and editorial gates are complete and the curator gives explicit publication approval.',
161    launch: null,
162  },
163].map(freezeApp));
164
165export const DESKTOP_TOOL_LINKS = Object.freeze([
166  {
167    id: 'method-demo',
168    label: 'Archive method demonstration',
169    description: 'Follow an independent, evidence-backed example built from 11 frozen public-source records.',
170    icon: 'method',
171    href: 'features/winer-method/',
172    status: 'experimental',
173  },
174  {
175    id: 'dissertation-release',
176    label: 'Dissertation release',
177    description: 'Context, responses, and paths through The Impossible Press.',
178    icon: 'dissertation',
179    href: 'dissertation/',
180    status: 'ready',
181  },
182  {
183    id: 'dissertation-reader',
184    label: 'Dissertation reader',
185    description: 'Read the complete 1986 dissertation with navigation and citations.',
186    icon: 'readme',
187    href: 'dissertation/reader/',
188    status: 'ready',
189  },
190  {
191    id: 'foreword',
192    label: 'Foreword',
193    description: 'Read the foreword to the public dissertation release.',
194    icon: 'readme',
195    href: 'dissertation/foreword/',
196    status: 'ready',
197  },
198  {
199    id: 'network-effect',
200    label: 'Network and public life',
201    description: 'Follow an illustrated path from the film Network to the dissertation.',
202    icon: 'findings',
203    href: 'dissertation/network-effect/',
204    status: 'ready',
205  },
206  {
207    id: 'faq',
208    label: 'Archive FAQ',
209    description: 'Answers about the archive and the dissertation.',
210    icon: 'about',
211    href: 'faq/',
212    status: 'ready',
213  },
214  {
215    id: 'dataviz',
216    label: 'Data visualization',
217    description: 'Filter the archive and inspect charts and collection statistics.',
218    icon: 'analytics',
219    href: 'tools/active/dataviz/dataviz.html',
220    status: 'beta',
221  },
222].map((tool) => Object.freeze({ ...tool })));
223
224export function validateDesktopToolLinks(tools) {
225  if (!Array.isArray(tools) || tools.length === 0) {
226    throw new TypeError('Desktop tool registry must be a non-empty array');
227  }
228
229  const ids = new Set();
230  const destinations = new Set();
231  for (const tool of tools) {
232    if (!APP_ID_PATTERN.test(tool?.id || '')) {
233      throw new TypeError(`Invalid desktop tool id: ${tool?.id || '(missing)'}`);
234    }
235    if (ids.has(tool.id)) {
236      throw new TypeError(`Duplicate desktop tool id: ${tool.id}`);
237    }
238    ids.add(tool.id);
239
240    for (const field of ['label', 'description', 'icon']) {
241      if (typeof tool[field] !== 'string' || tool[field].trim() === '') {
242        throw new TypeError(`Desktop tool ${tool.id} needs a ${field}`);
243      }
244    }
245    if (!TOOL_STATUSES.has(tool.status)) {
246      throw new TypeError(`Desktop tool ${tool.id} has an unknown status`);
247    }
248
249    const href = tool.href;
250    const pathSegments = typeof href === 'string' ? href.split('/') : [];
251    const hasSafePath = typeof href === 'string'
252      && /^[a-z0-9][a-z0-9._/-]*$/.test(href)
253      && !href.includes('//')
254      && !pathSegments.some((segment) => segment === '.' || segment === '..')
255      && (href.endsWith('/') || href.endsWith('.html'));
256    if (!hasSafePath) {
257      throw new TypeError(`Desktop tool ${tool.id} has an unsafe standalone destination`);
258    }
259    if (destinations.has(href)) {
260      throw new TypeError(`Duplicate desktop tool destination: ${href}`);
261    }
262    destinations.add(href);
263  }
264
265  return true;
266}
267
268export function validateDesktopRegistry(registry) {
269  if (!Array.isArray(registry) || registry.length === 0) {
270    throw new TypeError('Desktop app registry must be a non-empty array');
271  }
272
273  const ids = new Set();
274  for (const app of registry) {
275    if (!APP_ID_PATTERN.test(app?.id || '')) {
276      throw new TypeError(`Invalid desktop app id: ${app?.id || '(missing)'}`);
277    }
278    if (ids.has(app.id)) {
279      throw new TypeError(`Duplicate desktop app id: ${app.id}`);
280    }
281    ids.add(app.id);
282
283    if (typeof app.label !== 'string' || app.label.trim() === '') {
284      throw new TypeError(`Desktop app ${app.id} needs a label`);
285    }
286    if (typeof app.description !== 'string' || app.description.trim() === '') {
287      throw new TypeError(`Desktop app ${app.id} needs a description`);
288    }
289    if (typeof app.icon !== 'string' || app.icon.trim() === '') {
290      throw new TypeError(`Desktop app ${app.id} needs an icon key`);
291    }
292    if (!APP_GROUPS.has(app.group)) {
293      throw new TypeError(`Desktop app ${app.id} has an unknown group`);
294    }
295    if (!AVAILABILITY_STATES.has(app.availability)) {
296      throw new TypeError(`Desktop app ${app.id} has an unknown availability state`);
297    }
298    if (!Array.isArray(app.surfaces) || app.surfaces.some((surface) => !APP_SURFACES.has(surface))) {
299      throw new TypeError(`Desktop app ${app.id} has an invalid surface`);
300    }
301    if (new Set(app.surfaces).size !== app.surfaces.length) {
302      throw new TypeError(`Desktop app ${app.id} repeats a surface`);
303    }
304
305    if (app.availability !== 'ready') {
306      if (app.surfaces.length > 0 || app.launch !== null) {
307        throw new TypeError(`Unavailable desktop app ${app.id} must not be actionable`);
308      }
309      if (typeof app.availabilityNote !== 'string' || app.availabilityNote.trim() === '') {
310        throw new TypeError(`Unavailable desktop app ${app.id} needs a dependency note`);
311      }
312      continue;
313    }
314
315    if (app.surfaces.length === 0) {
316      throw new TypeError(`Ready desktop app ${app.id} needs a surface`);
317    }
318    if (!app.launch || !LAUNCH_KINDS.has(app.launch.kind)) {
319      throw new TypeError(`Ready desktop app ${app.id} has an invalid launch kind`);
320    }
321    if (app.launch.kind === 'route' && (
322      !ROUTE_DESTINATIONS.has(app.launch.destination) || app.launch.destination === ROUTES.desktop
323    )) {
324      throw new TypeError(`Desktop app ${app.id} has an unknown route destination`);
325    }
326    if (app.launch.kind === 'shell' && app.launch.destination !== app.id) {
327      throw new TypeError(`Desktop app ${app.id} must use its id as the shell destination`);
328    }
329    if (app.launch.kind === 'action' && app.launch.destination !== 'report') {
330      throw new TypeError(`Desktop app ${app.id} has an unknown action destination`);
331    }
332    if (app.launch.kind === 'path' && !PATH_DESTINATIONS.has(app.launch.destination)) {
333      throw new TypeError(`Desktop app ${app.id} has an unknown standalone destination`);
334    }
335  }
336
337  return true;
338}
339
340validateDesktopRegistry(DESKTOP_APPS);
341validateDesktopToolLinks(DESKTOP_TOOL_LINKS);
342
343export const getReadyDesktopApps = () => DESKTOP_APPS.filter((app) => app.availability === 'ready');
344
345export const getDesktopApp = (id) => DESKTOP_APPS.find((app) => app.id === id) || null;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.