snice 7.0.3 → 7.1.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 (218) hide show
  1. package/.agents/skills/snice/SKILL.md +4 -0
  2. package/.agents/skills/snice/references/core-kitchen-sink.ts +39 -1
  3. package/README.md +59 -14
  4. package/adapters/react/SniceProvider.js +2 -2
  5. package/adapters/react/SniceRouter.js +2 -2
  6. package/adapters/react/matchRoute.js +2 -2
  7. package/adapters/react/useRequestHandler.js +2 -2
  8. package/bin/analyzer-contracts.js +6 -1
  9. package/bin/postinstall.js +59 -0
  10. package/bin/project-analyzer.js +466 -5
  11. package/bin/snice.js +3 -0
  12. package/dist/app-context.d.ts +11 -0
  13. package/dist/cdn/accordion/snice-accordion.js +2 -2
  14. package/dist/cdn/action-bar/snice-action-bar.js +2 -2
  15. package/dist/cdn/activity-feed/snice-activity-feed.js +2 -2
  16. package/dist/cdn/alert/snice-alert.js +2 -2
  17. package/dist/cdn/app-tiles/snice-app-tiles.js +2 -2
  18. package/dist/cdn/approval-flow/snice-approval-flow.js +2 -2
  19. package/dist/cdn/audio-recorder/snice-audio-recorder.js +2 -2
  20. package/dist/cdn/availability/snice-availability.js +2 -2
  21. package/dist/cdn/avatar/snice-avatar.js +2 -2
  22. package/dist/cdn/avatar-group/snice-avatar-group.js +2 -2
  23. package/dist/cdn/badge/snice-badge.js +2 -2
  24. package/dist/cdn/banner/snice-banner.js +2 -2
  25. package/dist/cdn/binpack/snice-binpack.js +2 -2
  26. package/dist/cdn/book/snice-book.js +2 -2
  27. package/dist/cdn/booking/snice-booking.js +2 -2
  28. package/dist/cdn/breadcrumbs/snice-breadcrumbs.js +2 -2
  29. package/dist/cdn/button/snice-button.js +2 -2
  30. package/dist/cdn/calendar/snice-calendar.js +2 -2
  31. package/dist/cdn/camera/snice-camera.js +2 -2
  32. package/dist/cdn/camera-annotate/snice-camera-annotate.js +2 -2
  33. package/dist/cdn/candlestick/snice-candlestick.js +2 -2
  34. package/dist/cdn/card/snice-card.js +2 -2
  35. package/dist/cdn/carousel/snice-carousel.js +2 -2
  36. package/dist/cdn/cart/snice-cart.js +2 -2
  37. package/dist/cdn/chart/snice-chart.js +2 -2
  38. package/dist/cdn/chat/snice-chat.js +2 -2
  39. package/dist/cdn/checkbox/snice-checkbox.js +2 -2
  40. package/dist/cdn/chip/snice-chip.js +2 -2
  41. package/dist/cdn/code-block/snice-code-block.js +2 -2
  42. package/dist/cdn/color-display/snice-color-display.js +2 -2
  43. package/dist/cdn/color-picker/snice-color-picker.js +2 -2
  44. package/dist/cdn/command-palette/snice-command-palette.js +2 -2
  45. package/dist/cdn/comments/snice-comments.js +2 -2
  46. package/dist/cdn/countdown/snice-countdown.js +2 -2
  47. package/dist/cdn/cropper/snice-cropper.js +2 -2
  48. package/dist/cdn/data-card/snice-data-card.js +2 -2
  49. package/dist/cdn/date-picker/snice-date-picker.js +2 -2
  50. package/dist/cdn/date-range-picker/snice-date-range-picker.js +2 -2
  51. package/dist/cdn/date-time-picker/snice-date-time-picker.js +2 -2
  52. package/dist/cdn/diff/snice-diff.js +2 -2
  53. package/dist/cdn/divider/snice-divider.js +2 -2
  54. package/dist/cdn/doc/snice-doc.js +2 -2
  55. package/dist/cdn/draw/snice-draw.js +2 -2
  56. package/dist/cdn/drawer/snice-drawer.js +2 -2
  57. package/dist/cdn/empty-state/snice-empty-state.js +2 -2
  58. package/dist/cdn/estimate/snice-estimate.js +2 -2
  59. package/dist/cdn/file-gallery/snice-file-gallery.js +2 -2
  60. package/dist/cdn/file-upload/snice-file-upload.js +2 -2
  61. package/dist/cdn/flip-card/snice-flip-card.js +2 -2
  62. package/dist/cdn/flow/snice-flow.js +2 -2
  63. package/dist/cdn/form-layout/snice-form-layout.js +2 -2
  64. package/dist/cdn/funnel/snice-funnel.js +2 -2
  65. package/dist/cdn/gantt/snice-gantt.js +2 -2
  66. package/dist/cdn/gauge/snice-gauge.js +2 -2
  67. package/dist/cdn/grid/snice-grid.js +2 -2
  68. package/dist/cdn/heatmap/snice-heatmap.js +2 -2
  69. package/dist/cdn/image/snice-image.js +2 -2
  70. package/dist/cdn/input/snice-input.js +2 -2
  71. package/dist/cdn/invoice/snice-invoice.js +2 -2
  72. package/dist/cdn/kanban/snice-kanban.js +2 -2
  73. package/dist/cdn/key-value/snice-key-value.js +2 -2
  74. package/dist/cdn/kpi/snice-kpi.js +2 -2
  75. package/dist/cdn/layout/README.md +17 -2
  76. package/dist/cdn/layout/snice-layout.js +1656 -11
  77. package/dist/cdn/layout/snice-layout.js.map +1 -1
  78. package/dist/cdn/layout/snice-layout.min.js +379 -2
  79. package/dist/cdn/layout/snice-layout.min.js.map +1 -1
  80. package/dist/cdn/leaderboard/snice-leaderboard.js +2 -2
  81. package/dist/cdn/link/snice-link.js +2 -2
  82. package/dist/cdn/link-preview/snice-link-preview.js +2 -2
  83. package/dist/cdn/list/snice-list.js +2 -2
  84. package/dist/cdn/location/snice-location.js +2 -2
  85. package/dist/cdn/login/snice-login.js +2 -2
  86. package/dist/cdn/map/snice-map.js +2 -2
  87. package/dist/cdn/markdown/snice-markdown.js +2 -2
  88. package/dist/cdn/masonry/snice-masonry.js +2 -2
  89. package/dist/cdn/menu/snice-menu.js +2 -2
  90. package/dist/cdn/message-strip/snice-message-strip.js +2 -2
  91. package/dist/cdn/modal/snice-modal.js +2 -2
  92. package/dist/cdn/music-player/snice-music-player.js +2 -2
  93. package/dist/cdn/nav/snice-nav.js +2 -2
  94. package/dist/cdn/network-graph/snice-network-graph.js +2 -2
  95. package/dist/cdn/notification-center/snice-notification-center.js +2 -2
  96. package/dist/cdn/order-tracker/snice-order-tracker.js +2 -2
  97. package/dist/cdn/org-chart/snice-org-chart.js +2 -2
  98. package/dist/cdn/pagination/snice-pagination.js +2 -2
  99. package/dist/cdn/paint/snice-paint.js +2 -2
  100. package/dist/cdn/pdf-viewer/snice-pdf-viewer.js +2 -2
  101. package/dist/cdn/permission-matrix/snice-permission-matrix.js +2 -2
  102. package/dist/cdn/podcast-player/snice-podcast-player.js +2 -2
  103. package/dist/cdn/popover/snice-popover.js +2 -2
  104. package/dist/cdn/pricing-table/snice-pricing-table.js +2 -2
  105. package/dist/cdn/product-card/snice-product-card.js +2 -2
  106. package/dist/cdn/progress/snice-progress.js +2 -2
  107. package/dist/cdn/progress-ring/snice-progress-ring.js +2 -2
  108. package/dist/cdn/qr-code/snice-qr-code.js +2 -2
  109. package/dist/cdn/qr-reader/snice-qr-reader.js +2 -2
  110. package/dist/cdn/radio/snice-radio.js +2 -2
  111. package/dist/cdn/range-slider/snice-range-slider.js +2 -2
  112. package/dist/cdn/rating/snice-rating.js +2 -2
  113. package/dist/cdn/receipt/snice-receipt.js +2 -2
  114. package/dist/cdn/recipe/snice-recipe.js +2 -2
  115. package/dist/cdn/runtime/README.md +2 -2
  116. package/dist/cdn/runtime/snice-runtime.esm.js +3732 -3343
  117. package/dist/cdn/runtime/snice-runtime.esm.js.map +1 -1
  118. package/dist/cdn/runtime/snice-runtime.esm.min.js +1 -1
  119. package/dist/cdn/runtime/snice-runtime.esm.min.js.map +1 -1
  120. package/dist/cdn/runtime/snice-runtime.js +3727 -3335
  121. package/dist/cdn/runtime/snice-runtime.js.map +1 -1
  122. package/dist/cdn/runtime/snice-runtime.min.js +1 -1
  123. package/dist/cdn/runtime/snice-runtime.min.js.map +1 -1
  124. package/dist/cdn/sankey/snice-sankey.js +2 -2
  125. package/dist/cdn/segmented-control/snice-segmented-control.js +2 -2
  126. package/dist/cdn/select/snice-select.js +2 -2
  127. package/dist/cdn/skeleton/snice-skeleton.js +2 -2
  128. package/dist/cdn/slider/snice-slider.js +2 -2
  129. package/dist/cdn/sortable/snice-sortable.js +2 -2
  130. package/dist/cdn/sparkline/snice-sparkline.js +2 -2
  131. package/dist/cdn/spinner/snice-spinner.js +2 -2
  132. package/dist/cdn/split-button/snice-split-button.js +2 -2
  133. package/dist/cdn/split-pane/snice-split-pane.js +2 -2
  134. package/dist/cdn/spotlight/snice-spotlight.js +2 -2
  135. package/dist/cdn/stat-group/snice-stat-group.js +2 -2
  136. package/dist/cdn/step-input/snice-step-input.js +2 -2
  137. package/dist/cdn/stepper/snice-stepper.js +2 -2
  138. package/dist/cdn/switch/snice-switch.js +2 -2
  139. package/dist/cdn/table/README.md +6 -2
  140. package/dist/cdn/table/snice-table.esm.js +347 -54
  141. package/dist/cdn/table/snice-table.esm.js.map +1 -1
  142. package/dist/cdn/table/snice-table.esm.min.js +105 -44
  143. package/dist/cdn/table/snice-table.esm.min.js.map +1 -1
  144. package/dist/cdn/table/snice-table.js +348 -57
  145. package/dist/cdn/table/snice-table.js.map +1 -1
  146. package/dist/cdn/table/snice-table.min.js +153 -92
  147. package/dist/cdn/table/snice-table.min.js.map +1 -1
  148. package/dist/cdn/tabs/snice-tabs.js +2 -2
  149. package/dist/cdn/tag/snice-tag.js +2 -2
  150. package/dist/cdn/tag-input/snice-tag-input.js +2 -2
  151. package/dist/cdn/terminal/snice-terminal.js +2 -2
  152. package/dist/cdn/testimonial/snice-testimonial.js +2 -2
  153. package/dist/cdn/textarea/snice-textarea.js +2 -2
  154. package/dist/cdn/time-picker/snice-time-picker.js +2 -2
  155. package/dist/cdn/time-range-picker/snice-time-range-picker.js +2 -2
  156. package/dist/cdn/timeline/snice-timeline.js +2 -2
  157. package/dist/cdn/timer/snice-timer.js +2 -2
  158. package/dist/cdn/toast/snice-toast.js +2 -2
  159. package/dist/cdn/tooltip/snice-tooltip.js +2 -2
  160. package/dist/cdn/tree/snice-tree.js +2 -2
  161. package/dist/cdn/treemap/snice-treemap.js +2 -2
  162. package/dist/cdn/user-card/snice-user-card.js +2 -2
  163. package/dist/cdn/video-player/snice-video-player.js +2 -2
  164. package/dist/cdn/virtual-scroller/snice-virtual-scroller.js +2 -2
  165. package/dist/cdn/waterfall/snice-waterfall.js +2 -2
  166. package/dist/cdn/weather/snice-weather.js +2 -2
  167. package/dist/cdn/work-order/snice-work-order.js +2 -2
  168. package/dist/context-provider.d.ts +16 -0
  169. package/dist/daemon-target.d.ts +10 -0
  170. package/dist/daemon.d.ts +10 -0
  171. package/dist/index.cjs +3721 -3329
  172. package/dist/index.cjs.map +1 -1
  173. package/dist/index.d.ts +3 -0
  174. package/dist/index.esm.js +3719 -3330
  175. package/dist/index.esm.js.map +1 -1
  176. package/dist/index.iife.js +3721 -3329
  177. package/dist/index.iife.js.map +1 -1
  178. package/dist/keyboard-filter.d.ts +25 -0
  179. package/dist/on.d.ts +1 -1
  180. package/dist/parts.d.ts +0 -17
  181. package/dist/react/SniceProvider.js +2 -2
  182. package/dist/react/SniceRouter.js +2 -2
  183. package/dist/react/index.js +2 -2
  184. package/dist/react/matchRoute.js +2 -2
  185. package/dist/react/useRequestHandler.js +2 -2
  186. package/dist/request-response.d.ts +9 -1
  187. package/dist/symbols.cjs +11 -2
  188. package/dist/symbols.cjs.map +1 -1
  189. package/dist/symbols.d.ts +3 -0
  190. package/dist/symbols.esm.js +9 -3
  191. package/dist/symbols.esm.js.map +1 -1
  192. package/dist/testing.d.ts +1 -1
  193. package/dist/transitions.cjs +2 -2
  194. package/dist/transitions.esm.js +2 -2
  195. package/dist/types/app-context.d.ts +9 -58
  196. package/dist/types/daemon.d.ts +8 -0
  197. package/dist/types/dispatch-options.d.ts +2 -0
  198. package/dist/types/index.d.ts +1 -0
  199. package/dist/types/on-options.d.ts +5 -0
  200. package/dist/types/request-options.d.ts +2 -0
  201. package/dist/types/respond-options.d.ts +2 -0
  202. package/docs/ai/DEVELOPMENT.md +15 -1
  203. package/docs/ai/README.md +5 -1
  204. package/docs/ai/api.md +13 -4
  205. package/docs/ai/architecture.md +50 -1
  206. package/docs/ai/cli.md +9 -0
  207. package/docs/ai/daemons.md +80 -0
  208. package/docs/ai/decorators.md +6 -4
  209. package/docs/ai/events.md +6 -0
  210. package/docs/ai/patterns.md +45 -1
  211. package/docs/ai/request-response.md +26 -9
  212. package/docs/ai/routing.md +16 -1
  213. package/docs/cli.md +7 -0
  214. package/docs/daemons.md +200 -0
  215. package/docs/events.md +19 -2
  216. package/docs/request-response.md +20 -13
  217. package/docs/routing.md +25 -1
  218. package/package.json +6 -4
@@ -450,6 +450,12 @@ const RULE_DEFINITIONS = [
450
450
  fix: "Add type: 'hash' or type: 'pushstate' to the Router options."
451
451
  });
452
452
  }
453
+ if (!/\b(?:target|outlet)\b\s*(?=[:,}])/.test(call.body)) {
454
+ context.report(call.index, {
455
+ message: 'Router() requires a target selector for the page outlet.',
456
+ fix: "Add target: '#app' and ensure that element exists before initialize() runs."
457
+ });
458
+ }
453
459
  const outletIndex = call.body.search(/\boutlet\s*:/);
454
460
  if (outletIndex >= 0 && !/\btarget\s*:/.test(call.body)) {
455
461
  context.report(call.bodyStart + outletIndex, {
@@ -1101,11 +1107,43 @@ const RULE_DEFINITIONS = [
1101
1107
  projectOnly: true,
1102
1108
  check() {}
1103
1109
  },
1110
+ {
1111
+ id: 'snice/router-initialization',
1112
+ severity: 'error',
1113
+ category: 'router',
1114
+ description: 'Require each constructed Router instance to be initialized.',
1115
+ projectOnly: true,
1116
+ check() {}
1117
+ },
1118
+ {
1119
+ id: 'snice/page-element-double-decoration',
1120
+ severity: 'error',
1121
+ category: 'router',
1122
+ description: 'Reject combining Router page and element decorators on the same class.',
1123
+ projectOnly: true,
1124
+ check() {}
1125
+ },
1126
+ {
1127
+ id: 'snice/recommend-project-structure',
1128
+ severity: 'suggestion',
1129
+ category: 'architecture',
1130
+ description: 'Recommend conventional folders for pages, components, controllers, and daemons.',
1131
+ projectOnly: true,
1132
+ check() {}
1133
+ },
1134
+ {
1135
+ id: 'snice/recommend-page-controller',
1136
+ severity: 'suggestion',
1137
+ category: 'architecture',
1138
+ description: 'Recommend extracting substantial or repeated non-visual page logic into a controller.',
1139
+ projectOnly: true,
1140
+ check() {}
1141
+ },
1104
1142
  {
1105
1143
  id: 'snice/unused-dependency',
1106
1144
  severity: 'error',
1107
1145
  category: 'configuration',
1108
- description: 'Reject a project that declares the Snice dependency but never imports, registers, or renders it.',
1146
+ description: 'Reject a project that declares the Snice dependency but never uses, registers, or renders it.',
1109
1147
  projectOnly: true,
1110
1148
  check() {}
1111
1149
  },
@@ -1213,6 +1251,9 @@ export function analyzeProject(files) {
1213
1251
  const snicePackageManifests = [];
1214
1252
  const requestChannels = [];
1215
1253
  const respondChannels = new Set();
1254
+ const routerConstructions = [];
1255
+ const architectureDeclarations = [];
1256
+ const pageClasses = [];
1216
1257
  let usesSnice = false;
1217
1258
 
1218
1259
  for (const file of normalized) {
@@ -1223,6 +1264,22 @@ export function analyzeProject(files) {
1223
1264
  }
1224
1265
  const analyzable = maskComments(file.source);
1225
1266
  const provenance = buildSourceProvenance(analyzable);
1267
+ routerConstructions.push(
1268
+ ...findRouterConstructions(analyzable, localsFor(provenance.rootBindings, 'Router'))
1269
+ .map(construction => ({ ...construction, file }))
1270
+ );
1271
+ for (const convention of [
1272
+ { decorator: 'page', folder: 'pages', kind: 'page' },
1273
+ { decorator: 'element', folder: 'components', kind: 'component' },
1274
+ { decorator: 'controller', folder: 'controllers', kind: 'controller' },
1275
+ { decorator: 'daemon', folder: 'daemons', kind: 'daemon' }
1276
+ ]) {
1277
+ for (const declaration of findDecoratedClasses(analyzable, convention.decorator)) {
1278
+ const architectural = { ...declaration, ...convention, file };
1279
+ architectureDeclarations.push(architectural);
1280
+ if (convention.decorator === 'page') pageClasses.push(architectural);
1281
+ }
1282
+ }
1226
1283
  for (const match of analyzable.matchAll(/@request\(\s*['"]([^'"]+)['"]\s*(?:,\s*\{([^}]*)\})?/g)) {
1227
1284
  requestChannels.push({
1228
1285
  file,
@@ -1240,7 +1297,12 @@ export function analyzeProject(files) {
1240
1297
  if (COMPONENT_MODULE_PATHS.has(entry.path) && !isTypeOnlyImport(entry)) {
1241
1298
  registrations.add(entry.path);
1242
1299
  }
1243
- if (entry.path === 'snice' || entry.path.startsWith('snice/')) usesSnice = true;
1300
+ if (
1301
+ (entry.path === 'snice' || entry.path.startsWith('snice/')) &&
1302
+ importHasProjectUse(analyzable, entry)
1303
+ ) {
1304
+ usesSnice = true;
1305
+ }
1244
1306
  }
1245
1307
  if (!usesSnice && findOpeningTags(analyzable, Object.keys(COMPONENT_CONTRACTS)).length) {
1246
1308
  usesSnice = true;
@@ -1407,14 +1469,123 @@ export function analyzeProject(files) {
1407
1469
  });
1408
1470
  }
1409
1471
 
1472
+ const projectCode = normalized
1473
+ .filter(file => !/(?:^|[/\\])package\.json$/.test(file.filename))
1474
+ .map(file => maskComments(file.source))
1475
+ .join('\n');
1476
+ for (const construction of routerConstructions) {
1477
+ const initialized = construction.initializers.some(name => hasInvocation(projectCode, name)) ||
1478
+ (construction.instance && new RegExp(
1479
+ `\\b${escapeRegExp(construction.instance)}\\s*\\.\\s*initialize\\s*\\(`
1480
+ ).test(projectCode));
1481
+ if (initialized) continue;
1482
+ const location = sourceLocation(construction.file.source, construction.index);
1483
+ diagnostics.push({
1484
+ severity: 'error',
1485
+ ruleId: 'snice/router-initialization',
1486
+ message: 'Router() is constructed but its initialize() function is never called, so registered pages will not start routing.',
1487
+ fix: 'Export the Router-returned initialize function, import all page modules, then call initialize() from the application entry point. Review docs/ai/routing.md.',
1488
+ file: construction.file.filename,
1489
+ line: location.line,
1490
+ column: location.column
1491
+ });
1492
+ }
1493
+
1494
+ const doubleDecoratedPages = new Set();
1495
+ const declarationsByClass = new Map();
1496
+ for (const declaration of architectureDeclarations) {
1497
+ const key = `${declaration.file.filename}:${declaration.name}`;
1498
+ if (!declarationsByClass.has(key)) declarationsByClass.set(key, []);
1499
+ declarationsByClass.get(key).push(declaration);
1500
+ }
1501
+ for (const [key, declarations] of declarationsByClass) {
1502
+ const page = declarations.find(declaration => declaration.decorator === 'page');
1503
+ const element = declarations.find(declaration => declaration.decorator === 'element');
1504
+ if (!page || !element) continue;
1505
+ doubleDecoratedPages.add(key);
1506
+ const location = sourceLocation(element.file.source, element.index);
1507
+ diagnostics.push({
1508
+ severity: 'error',
1509
+ ruleId: 'snice/page-element-double-decoration',
1510
+ message: `${page.name} combines @page and @element, so Snice element functionality is applied twice.`,
1511
+ fix: 'Remove @element from the routed class; the Router-returned @page decorator registers the custom element and applies element functionality. Review docs/ai/routing.md.',
1512
+ file: element.file.filename,
1513
+ line: location.line,
1514
+ column: location.column
1515
+ });
1516
+ }
1517
+
1518
+ for (const declaration of architectureDeclarations) {
1519
+ const key = `${declaration.file.filename}:${declaration.name}`;
1520
+ if (declaration.decorator === 'element' && doubleDecoratedPages.has(key)) continue;
1521
+ if (isInSourceFolder(declaration.file.filename, declaration.folder)) continue;
1522
+ const target = `src/${declaration.folder}/${camelToKebab(declaration.name)}.ts`;
1523
+ const location = sourceLocation(declaration.file.source, declaration.index);
1524
+ diagnostics.push({
1525
+ severity: 'suggestion',
1526
+ ruleId: 'snice/recommend-project-structure',
1527
+ message: `${declaration.name} is a Snice ${declaration.kind} defined outside the conventional src/${declaration.folder}/ folder.`,
1528
+ fix: `Move the class to ${target} and import it from the application composition root. Review docs/ai/architecture.md.`,
1529
+ file: declaration.file.filename,
1530
+ line: location.line,
1531
+ column: location.column
1532
+ });
1533
+ }
1534
+
1535
+ const pageControllerReported = new Set();
1536
+ for (const page of pageClasses) {
1537
+ const profile = pageLogicProfile(page.body);
1538
+ if (!profile.recommend) continue;
1539
+ const location = sourceLocation(page.file.source, page.index);
1540
+ diagnostics.push({
1541
+ severity: 'suggestion',
1542
+ ruleId: 'snice/recommend-page-controller',
1543
+ message: `${page.name} contains substantial non-visual logic (${profile.effectfulMethods} effectful methods, ${profile.effects} external effects, ${profile.decisions} decision points).`,
1544
+ fix: `Keep route orchestration and presentation in the page; move reusable data, business, timer, storage, or server behavior into a controller under src/controllers/. Review docs/ai/controllers.md and docs/ai/architecture.md.`,
1545
+ file: page.file.filename,
1546
+ line: location.line,
1547
+ column: location.column
1548
+ });
1549
+ pageControllerReported.add(`${page.file.filename}:${page.index}`);
1550
+ }
1551
+
1552
+ const repeatedPageMethods = new Map();
1553
+ for (const page of pageClasses) {
1554
+ for (const method of findClassMethods(page.body, page.bodyStart)) {
1555
+ const fingerprint = sharedLogicFingerprint(method.body);
1556
+ if (!fingerprint) continue;
1557
+ if (!repeatedPageMethods.has(fingerprint)) repeatedPageMethods.set(fingerprint, []);
1558
+ repeatedPageMethods.get(fingerprint).push({ page, method });
1559
+ }
1560
+ }
1561
+ for (const occurrences of repeatedPageMethods.values()) {
1562
+ const files = [...new Set(occurrences.map(({ page }) => page.file.filename))];
1563
+ if (files.length < 2) continue;
1564
+ for (const { page, method } of occurrences) {
1565
+ const reportKey = `${page.file.filename}:${page.index}`;
1566
+ if (pageControllerReported.has(reportKey)) continue;
1567
+ const location = sourceLocation(page.file.source, method.index);
1568
+ diagnostics.push({
1569
+ severity: 'suggestion',
1570
+ ruleId: 'snice/recommend-page-controller',
1571
+ message: `${method.name}() repeats substantial non-visual logic across ${files.length} page files.`,
1572
+ fix: `Extract the shared behavior into a controller under src/controllers/ and let each page orchestrate it. Also review docs/ai/controllers.md and docs/ai/architecture.md.`,
1573
+ file: page.file.filename,
1574
+ line: location.line,
1575
+ column: location.column
1576
+ });
1577
+ pageControllerReported.add(reportKey);
1578
+ }
1579
+ }
1580
+
1410
1581
  if (!usesSnice) {
1411
1582
  for (const manifest of snicePackageManifests) {
1412
1583
  const location = sourceLocation(manifest.source, manifest.index);
1413
1584
  diagnostics.push({
1414
1585
  severity: 'error',
1415
1586
  ruleId: 'snice/unused-dependency',
1416
- message: 'package.json declares the snice dependency, but no project source imports from snice or snice/react, registers a Snice component, or renders a released snice-* custom element.',
1417
- fix: "Use the dependency (import from 'snice' or 'snice/react', register a component via its 'snice/components/<name>/snice-<name>' path, or render a snice-* element), or remove snice from package.json.",
1587
+ message: 'package.json declares the snice dependency, but no project source references a Snice import, registers a Snice component, or renders a released snice-* custom element.',
1588
+ fix: "Use an imported binding from 'snice' or 'snice/react', add a side-effect registration import from the documented snice/components path, render a released snice-* element, or remove snice from package.json.",
1418
1589
  file: manifest.filename,
1419
1590
  line: location.line,
1420
1591
  column: location.column
@@ -1502,13 +1673,20 @@ export function findImports(source) {
1502
1673
  const statement = source.slice(start.index, start.index + 4000);
1503
1674
  const sideEffect = statement.match(/^\s*import\s*(['"])([^'"]+)\1\s*;?/);
1504
1675
  if (sideEffect) {
1505
- imports.push({ index: start.index, clause: null, path: sideEffect[2], typeOnly: false });
1676
+ imports.push({
1677
+ index: start.index,
1678
+ end: start.index + sideEffect[0].length,
1679
+ clause: null,
1680
+ path: sideEffect[2],
1681
+ typeOnly: false
1682
+ });
1506
1683
  continue;
1507
1684
  }
1508
1685
  const from = statement.match(/^\s*import\s+(?:type\s+)?([\s\S]*?)\s+from\s*(['"])([^'"]+)\2\s*;?/);
1509
1686
  if (!from || /\n\s*import\b/.test(from[1])) continue;
1510
1687
  imports.push({
1511
1688
  index: start.index,
1689
+ end: start.index + from[0].length,
1512
1690
  clause: from[1].trim(),
1513
1691
  path: from[3],
1514
1692
  typeOnly: /^\s*import\s+type\b/.test(statement)
@@ -1549,6 +1727,175 @@ function findClassBodies(source) {
1549
1727
  return classes;
1550
1728
  }
1551
1729
 
1730
+ function findRouterConstructions(source, routerNames) {
1731
+ const constructions = [];
1732
+ for (const routerName of routerNames) {
1733
+ const escaped = escapeRegExp(routerName);
1734
+ const assigned = new Set();
1735
+ const destructured = new RegExp(
1736
+ `\\b(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${escaped}\\s*\\(`,
1737
+ 'g'
1738
+ );
1739
+ for (const match of source.matchAll(destructured)) {
1740
+ const initialize = /(?:^|,)\s*initialize(?:\s*:\s*([A-Za-z_$][\w$]*))?(?=\s*,|\s*$)/
1741
+ .exec(match[1]);
1742
+ constructions.push({
1743
+ index: match.index,
1744
+ initializers: initialize ? [initialize[1] ?? 'initialize'] : [],
1745
+ instance: null
1746
+ });
1747
+ assigned.add(source.indexOf(routerName, match.index));
1748
+ }
1749
+
1750
+ const instance = new RegExp(
1751
+ `\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${escaped}\\s*\\(`,
1752
+ 'g'
1753
+ );
1754
+ for (const match of source.matchAll(instance)) {
1755
+ const initializers = [];
1756
+ const instanceDestructuring = new RegExp(
1757
+ `\\b(?:export\\s+)?(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${escapeRegExp(match[1])}\\b`,
1758
+ 'g'
1759
+ );
1760
+ for (const destructuring of source.matchAll(instanceDestructuring)) {
1761
+ const initialize = /(?:^|,)\s*initialize(?:\s*:\s*([A-Za-z_$][\w$]*))?(?=\s*,|\s*$)/
1762
+ .exec(destructuring[1]);
1763
+ if (initialize) initializers.push(initialize[1] ?? 'initialize');
1764
+ }
1765
+ constructions.push({ index: match.index, initializers, instance: match[1] });
1766
+ assigned.add(source.indexOf(routerName, match.index));
1767
+ }
1768
+
1769
+ for (const call of findObjectCalls(source, routerName)) {
1770
+ if (assigned.has(call.index)) continue;
1771
+ constructions.push({ index: call.index, initializers: [], instance: null });
1772
+ }
1773
+ }
1774
+ return constructions.sort((left, right) => left.index - right.index);
1775
+ }
1776
+
1777
+ function hasInvocation(source, name) {
1778
+ const pattern = new RegExp(`\\b${escapeRegExp(name)}\\s*\\(`, 'g');
1779
+ for (const match of source.matchAll(pattern)) {
1780
+ const open = source.indexOf('(', match.index);
1781
+ const close = findMatchingDelimiter(source, open, '(', ')');
1782
+ if (close < 0) continue;
1783
+ let next = close + 1;
1784
+ while (/\s/.test(source[next] ?? '')) next++;
1785
+ // A function or method declaration has a body immediately after its
1786
+ // parameter list. Router initializers are zero-argument function calls.
1787
+ if (source[next] !== '{') return true;
1788
+ }
1789
+ return false;
1790
+ }
1791
+
1792
+ function findDecoratedClasses(source, decoratorName) {
1793
+ const declarations = [];
1794
+ const pattern = new RegExp(`@${escapeRegExp(decoratorName)}\\b`, 'g');
1795
+ for (const match of source.matchAll(pattern)) {
1796
+ let declarationStart = match.index + match[0].length;
1797
+ while (/\s/.test(source[declarationStart] ?? '')) declarationStart++;
1798
+ if (source[declarationStart] === '(') {
1799
+ const close = findMatchingDelimiter(source, declarationStart, '(', ')');
1800
+ if (close < 0) continue;
1801
+ declarationStart = close + 1;
1802
+ }
1803
+ // Class decorators stack above the same declaration. Walk past any
1804
+ // intervening decorators so each Snice role resolves to that class.
1805
+ while (true) {
1806
+ while (/\s/.test(source[declarationStart] ?? '')) declarationStart++;
1807
+ const nextDecorator = /^@[A-Za-z_$][\w$]*/.exec(source.slice(declarationStart));
1808
+ if (!nextDecorator) break;
1809
+ declarationStart += nextDecorator[0].length;
1810
+ while (/\s/.test(source[declarationStart] ?? '')) declarationStart++;
1811
+ if (source[declarationStart] === '(') {
1812
+ const close = findMatchingDelimiter(source, declarationStart, '(', ')');
1813
+ if (close < 0) break;
1814
+ declarationStart = close + 1;
1815
+ }
1816
+ }
1817
+ const declaration = /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)[^\{]*\{/
1818
+ .exec(source.slice(declarationStart, declarationStart + 2_000));
1819
+ if (!declaration) continue;
1820
+ const open = declarationStart + declaration.index + declaration[0].lastIndexOf('{');
1821
+ const close = findMatchingDelimiter(source, open, '{', '}');
1822
+ if (close < 0) continue;
1823
+ declarations.push({
1824
+ index: match.index,
1825
+ name: declaration[1],
1826
+ bodyStart: open + 1,
1827
+ body: source.slice(open + 1, close)
1828
+ });
1829
+ }
1830
+ return declarations;
1831
+ }
1832
+
1833
+ function findClassMethods(body, bodyStart = 0) {
1834
+ const code = blankStringContents(body);
1835
+ const depths = new Uint16Array(code.length + 1);
1836
+ let depth = 0;
1837
+ for (let index = 0; index < code.length; index++) {
1838
+ depths[index] = depth;
1839
+ if (code[index] === '{') depth++;
1840
+ else if (code[index] === '}') depth = Math.max(0, depth - 1);
1841
+ }
1842
+ const methods = [];
1843
+ const pattern = /(?:^|\n)\s*(?:(?:public|private|protected|static|readonly|override|abstract|declare)\s+)*(?:async\s+)?(?:\*\s*)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::\s*[^\n{]+)?\s*\{/g;
1844
+ const excluded = new Set(['if', 'for', 'while', 'switch', 'catch', 'with']);
1845
+ for (const match of code.matchAll(pattern)) {
1846
+ const open = match.index + match[0].lastIndexOf('{');
1847
+ if (depths[open] !== 0 || excluded.has(match[1])) continue;
1848
+ const close = findMatchingDelimiter(code, open, '{', '}');
1849
+ if (close < 0) continue;
1850
+ methods.push({
1851
+ name: match[1],
1852
+ index: bodyStart + match.index + match[0].indexOf(match[1]),
1853
+ body: body.slice(open + 1, close)
1854
+ });
1855
+ }
1856
+ return methods;
1857
+ }
1858
+
1859
+ function pageLogicProfile(body) {
1860
+ const code = blankStringContents(body);
1861
+ const methods = findClassMethods(body);
1862
+ const effectPattern = /\bfetch\s*\(|\bnew\s+(?:WebSocket|EventSource|Worker)\s*\(|\b(?:localStorage|sessionStorage|indexedDB|caches)\b|\b(?:setInterval|setTimeout)\s*\(/g;
1863
+ const effectTest = /\bfetch\s*\(|\bnew\s+(?:WebSocket|EventSource|Worker)\s*\(|\b(?:localStorage|sessionStorage|indexedDB|caches)\b|\b(?:setInterval|setTimeout)\s*\(/;
1864
+ const decisionPattern = /\b(?:if|else\s+if|switch|for|while|try|catch)\b|\.(?:filter|map|reduce|sort|find|some|every)\s*\(/g;
1865
+ const effects = [...code.matchAll(effectPattern)].length;
1866
+ const decisions = [...code.matchAll(decisionPattern)].length;
1867
+ const effectfulMethods = methods.filter(method => {
1868
+ const methodCode = blankStringContents(method.body);
1869
+ return effectTest.test(methodCode);
1870
+ }).length;
1871
+ // A page is supposed to orchestrate. Recommend extraction only when several
1872
+ // separate methods own substantial external/business behavior, or when a
1873
+ // very large page also carries a dense decision tree. One fetch in @ready()
1874
+ // or a small route-state branch remains clean.
1875
+ const recommend = (
1876
+ (effectfulMethods >= 4 && effects >= 6) ||
1877
+ (effectfulMethods >= 3 && effects >= 4 && decisions >= 8) ||
1878
+ (code.length >= 4_000 && effects >= 2 && decisions >= 16)
1879
+ );
1880
+ return { recommend, effectfulMethods, effects, decisions };
1881
+ }
1882
+
1883
+ function sharedLogicFingerprint(body) {
1884
+ const code = blankStringContents(maskComments(body))
1885
+ .replace(/\s+/g, ' ')
1886
+ .trim();
1887
+ if (code.length < 140 || (code.match(/;/g) ?? []).length < 3) return null;
1888
+ if (!/\bfetch\s*\(|\bnew\s+(?:WebSocket|EventSource|Worker)\s*\(|\b(?:localStorage|sessionStorage|indexedDB)\b|\b(?:if|for|while|switch)\b|\.(?:filter|map|reduce|sort)\s*\(/.test(code)) {
1889
+ return null;
1890
+ }
1891
+ return code;
1892
+ }
1893
+
1894
+ function isInSourceFolder(filename, folder) {
1895
+ const normalized = filename.replaceAll('\\', '/');
1896
+ return new RegExp(`(?:^|/)src/${escapeRegExp(folder)}(?:/|$)`).test(normalized);
1897
+ }
1898
+
1552
1899
  function findMatchingDelimiter(source, start, open, close) {
1553
1900
  let depth = 0;
1554
1901
  let quote = '';
@@ -1761,6 +2108,120 @@ export function isTypeOnlyImport(entry) {
1761
2108
  return specifiers.length > 0 && specifiers.every(specifier => /^type\s+/.test(specifier));
1762
2109
  }
1763
2110
 
2111
+ function importHasProjectUse(source, entry) {
2112
+ // Side-effect imports are themselves runtime usage (and are the documented
2113
+ // way to register individual Snice custom elements).
2114
+ if (!entry.clause) return true;
2115
+
2116
+ const namespace = entry.clause.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
2117
+ // The observed gauntlet miss is specifically a cargo-culted namespace
2118
+ // import. Named/default import liveness belongs to TypeScript or a linter;
2119
+ // broadening this project rule into an incomplete identifier-use checker
2120
+ // would create false positives for otherwise valid authored imports.
2121
+ if (!namespace) return true;
2122
+
2123
+ const outsideImport = `${source.slice(0, entry.index)}${' '.repeat(entry.end - entry.index)}${source.slice(entry.end)}`;
2124
+ return hasCodeIdentifier(outsideImport, namespace[1]);
2125
+ }
2126
+
2127
+ function hasCodeIdentifier(source, target) {
2128
+ const stack = [{ kind: 'code', templateExpression: false, braces: 0 }];
2129
+ let index = 0;
2130
+ while (index < source.length) {
2131
+ const frame = stack.at(-1);
2132
+ const character = source[index];
2133
+
2134
+ if (frame.kind === 'quoted') {
2135
+ if (character === '\\') index += 2;
2136
+ else {
2137
+ index++;
2138
+ if (character === frame.quote) stack.pop();
2139
+ }
2140
+ continue;
2141
+ }
2142
+ if (frame.kind === 'regex') {
2143
+ if (character === '\\') {
2144
+ index += 2;
2145
+ } else if (character === '[') {
2146
+ frame.characterClass = true;
2147
+ index++;
2148
+ } else if (character === ']' && frame.characterClass) {
2149
+ frame.characterClass = false;
2150
+ index++;
2151
+ } else if (character === '/' && !frame.characterClass) {
2152
+ index++;
2153
+ while (/[A-Za-z]/.test(source[index] ?? '')) index++;
2154
+ stack.pop();
2155
+ } else {
2156
+ index++;
2157
+ }
2158
+ continue;
2159
+ }
2160
+ if (frame.kind === 'template') {
2161
+ if (character === '\\') {
2162
+ index += 2;
2163
+ } else if (character === '`') {
2164
+ stack.pop();
2165
+ index++;
2166
+ } else if (character === '$' && source[index + 1] === '{') {
2167
+ stack.push({ kind: 'code', templateExpression: true, braces: 1 });
2168
+ index += 2;
2169
+ } else {
2170
+ index++;
2171
+ }
2172
+ continue;
2173
+ }
2174
+
2175
+ if (character === "'" || character === '"') {
2176
+ stack.push({ kind: 'quoted', quote: character });
2177
+ index++;
2178
+ continue;
2179
+ }
2180
+ if (character === '`') {
2181
+ stack.push({ kind: 'template' });
2182
+ index++;
2183
+ continue;
2184
+ }
2185
+ if (character === '/' && startsRegexLiteral(source, index)) {
2186
+ stack.push({ kind: 'regex', characterClass: false });
2187
+ index++;
2188
+ continue;
2189
+ }
2190
+ if (frame.templateExpression && character === '{') {
2191
+ frame.braces++;
2192
+ index++;
2193
+ continue;
2194
+ }
2195
+ if (frame.templateExpression && character === '}') {
2196
+ frame.braces--;
2197
+ index++;
2198
+ if (frame.braces === 0) stack.pop();
2199
+ continue;
2200
+ }
2201
+ if (/[A-Za-z_$]/.test(character)) {
2202
+ let end = index + 1;
2203
+ while (/[\w$]/.test(source[end] ?? '')) end++;
2204
+ if (source.slice(index, end) === target) return true;
2205
+ index = end;
2206
+ continue;
2207
+ }
2208
+ index++;
2209
+ }
2210
+ return false;
2211
+ }
2212
+
2213
+ function startsRegexLiteral(source, slashIndex) {
2214
+ let previous = slashIndex - 1;
2215
+ while (previous >= 0 && /\s/.test(source[previous])) previous--;
2216
+ if (previous < 0 || /[([{=:;,!?&|+*%^~<>-]/.test(source[previous])) return true;
2217
+ if (!/[\w$]/.test(source[previous])) return false;
2218
+ let start = previous;
2219
+ while (start > 0 && /[\w$]/.test(source[start - 1])) start--;
2220
+ return /^(?:return|case|throw|else|do|typeof|instanceof|in|of|yield|await|void|delete)$/.test(
2221
+ source.slice(start, previous + 1)
2222
+ );
2223
+ }
2224
+
1764
2225
  function reactExportReplacement(name) {
1765
2226
  if (name === 'SniceRoute') return 'Route';
1766
2227
  if (name === 'SniceApp') return 'SniceProvider (or SniceRouter)';
package/bin/snice.js CHANGED
@@ -49,6 +49,9 @@ Commands:
49
49
  generate-component Print a current Snice element scaffold (use --out to write it).
50
50
  build-component Build a CDN component from a Snice source checkout.
51
51
 
52
+ AI setup:
53
+ npx snice init-ai Install version-matched guidance before an agent writes Snice code.
54
+
52
55
  Build options:
53
56
  --output=<dir> Output directory (default: ./dist/cdn)
54
57
  --format=iife,es Output formats (default: iife; table: iife,es)
@@ -0,0 +1,11 @@
1
+ import { getContext } from './context-provider';
2
+ import type { AppContext } from './types/app-context';
3
+ /**
4
+ * Makes one explicit application context visible beneath a root.
5
+ *
6
+ * Daemon instances must already exist in context.daemons. The returned cleanup
7
+ * removes the provider and deactivates framework communication for daemons no
8
+ * longer provided by any context.
9
+ */
10
+ export declare function provideContext(root: EventTarget, context: AppContext): () => void;
11
+ export { getContext };
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */
@@ -1,6 +1,6 @@
1
1
  /*!
2
- * snice v7.0.3
3
- * A decorator-driven web component library with differential rendering, routing, controllers, and 130+ ready-made UI components. Use as much or as little as you want. Zero dependencies, works anywhere.
2
+ * snice v7.1.0
3
+ * A decorator-driven web component library with routing, controllers, daemons, and 130+ UI components. For better coding-agent results, run npx snice init-ai.
4
4
  * (c) 2024
5
5
  * Released under the MIT License.
6
6
  */