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.