@aglyn/plugins-ai 1.0.0-beta.238 → 1.0.0-beta.239

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 (77) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.js +8 -0
  3. package/src/lib/components/ai-job-links.js.map +1 -1
  4. package/src/lib/components/ai-jobs-store.d.ts +12 -0
  5. package/src/lib/components/ai-jobs-store.js +15 -0
  6. package/src/lib/components/ai-jobs-store.js.map +1 -1
  7. package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
  8. package/src/lib/components/ai-usage-meter-refresh.js +63 -0
  9. package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
  10. package/src/lib/components/assist-panel.component.js +4 -0
  11. package/src/lib/components/assist-panel.component.js.map +1 -1
  12. package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
  13. package/src/lib/components/use-ai-usage-meter.js +8 -3
  14. package/src/lib/components/use-ai-usage-meter.js.map +1 -1
  15. package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
  16. package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
  17. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  18. package/src/lib/jobs/ai-job-build-step.js +29 -2
  19. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  20. package/src/lib/jobs/ai-job-draft-ids.js +7 -2
  21. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  22. package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
  23. package/src/lib/jobs/ai-job-form-dataset.js +161 -0
  24. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
  25. package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
  26. package/src/lib/jobs/ai-job-form-step.js +37 -9
  27. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  28. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  29. package/src/lib/jobs/ai-job-page-language.js +37 -2
  30. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  31. package/src/lib/jobs/ai-job-plan-step.d.ts +18 -0
  32. package/src/lib/jobs/ai-job-plan-step.js +41 -4
  33. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  34. package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
  35. package/src/lib/jobs/ai-job-site-datasets.js +499 -0
  36. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
  37. package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
  38. package/src/lib/jobs/ai-job-site-step.js +148 -29
  39. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  40. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
  41. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
  42. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
  43. package/src/lib/layout-language/ai-layout-compiler.d.ts +10 -0
  44. package/src/lib/layout-language/ai-layout-compiler.js +93 -1
  45. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  46. package/src/lib/layout-language/ai-layout-gaps.js +17 -1
  47. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  48. package/src/lib/layout-language/ai-layout-listings.d.ts +32 -3
  49. package/src/lib/layout-language/ai-layout-listings.js +62 -9
  50. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  51. package/src/lib/model/ai-build-job.d.ts +2 -0
  52. package/src/lib/model/ai-build-job.js +16 -1
  53. package/src/lib/model/ai-build-job.js.map +1 -1
  54. package/src/lib/model/ai-build-plan.d.ts +23 -3
  55. package/src/lib/model/ai-build-plan.js +47 -6
  56. package/src/lib/model/ai-build-plan.js.map +1 -1
  57. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  58. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  59. package/src/lib/model/ai-site-job.d.ts +19 -2
  60. package/src/lib/model/ai-site-job.js +21 -6
  61. package/src/lib/model/ai-site-job.js.map +1 -1
  62. package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
  63. package/src/lib/runtime/ai-dataset-generation.js +127 -0
  64. package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
  65. package/src/lib/runtime/ai-doctrine-validators.d.ts +40 -0
  66. package/src/lib/runtime/ai-doctrine-validators.js +130 -1
  67. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  68. package/src/lib/runtime/ai-doctrine.js +8 -3
  69. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  70. package/src/lib/runtime/ai-node-tree.d.ts +2 -0
  71. package/src/lib/runtime/ai-node-tree.js +16 -0
  72. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  73. package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
  74. package/src/lib/tools/ai-dataset-tool.js +231 -0
  75. package/src/lib/tools/ai-dataset-tool.js.map +1 -0
  76. package/src/lib/usage/free-ai-credits-left.js +26 -2
  77. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -754,6 +754,46 @@ export declare function detectPlanSplitLists(plan: AiBuildPlan, inventory: AiSit
754
754
  * can answer with the form's bare name, which is no reference at all.
755
755
  */
756
756
  export declare function aiSettlePlanRefs(plan: AiBuildPlan, inventory?: AiSiteInventory | null): AiBuildPlan;
757
+ /**
758
+ * What customers say, which no dataset of the site's holds: a review, a
759
+ * testimonial, a rating, a quote (AGL-3616). A job never writes them for the
760
+ * people who did not, so a dataset of them is one nothing can seed.
761
+ */
762
+ export declare const AI_DATASET_SAID_NAME: RegExp;
763
+ /**
764
+ * A plan that creates more datasets than this job may make is settled in
765
+ * code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
766
+ * dental practice's plan four times over for a fourth dataset, and a food
767
+ * bank's re-ask for one broke a layout rule its first answer had kept.
768
+ *
769
+ * A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
770
+ * field) is let go first, whatever the cap: both of those plans made one of
771
+ * testimonials, which nothing may seed.
772
+ *
773
+ * The datasets kept are the most useful: the ones the most sections list,
774
+ * a record template counting double, plan order breaking ties. Each one let
775
+ * go leaves the plan whole — its `new:` reference comes off every section,
776
+ * a record template of it becomes a page of its own, and a section that
777
+ * listed it shows fewer items than rule 8 binds, so its items are written
778
+ * out on the page. Where the job may create no dataset at all, rule 7 still
779
+ * asks, as it does for any creation.
780
+ */
781
+ export declare function aiSettlePlanDatasets(plan: AiBuildPlan, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
782
+ /**
783
+ * A form's dataset, settled in code before the rules read the plan
784
+ * (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
785
+ *
786
+ * - A `writesTo` that names nothing the form can write to comes off: a
787
+ * `new:` of no dataset this plan creates, an id the inventory does not
788
+ * hold, or a dataset this job may not create. The form is then the
789
+ * form it always was, its submissions in the Inbox. A dataset the
790
+ * inventory holds, named by its name rather than its id, is given its id.
791
+ * - A dataset this plan creates for a form holds the people who send it:
792
+ * no page lists it and none is its record template, so its `new:` comes
793
+ * off every section and record that named it, and a section that listed
794
+ * it shows fewer items than rule 8 binds.
795
+ */
796
+ export declare function aiSettlePlanFormDatasets(plan: AiBuildPlan, inventory: AiSiteInventory | null, capabilities?: AiPlanCapabilities | null): AiBuildPlan;
757
797
  /**
758
798
  * A page that names no layout, on a site where only one layout can frame it,
759
799
  * is put in that layout before the rules read the plan (2026-10-07). The
@@ -23,7 +23,7 @@ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
23
23
  import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
24
24
  import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
25
25
  import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
26
- import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_BUILD_PLAN_LIMITS, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanItemFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
26
+ import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_BUILD_PLAN_LIMITS, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, AI_PLAN_DATASET_ITEM_OP, aiPlanCreateFor, aiPlanFormDatasetNames, aiPlanItemFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
27
27
  import { aiCreationNoun, aiPlanCanPlaceForm, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
28
28
  import { aiFreeSiteSectionsWithin, aiSiteHomeMinSections, aiSitePlanIsHome } from "../model/ai-site-job.js";
29
29
  import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
@@ -3324,6 +3324,135 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
3324
3324
  screens
3325
3325
  }) : plan;
3326
3326
  }
3327
+ /**
3328
+ * What customers say, which no dataset of the site's holds: a review, a
3329
+ * testimonial, a rating, a quote (AGL-3616). A job never writes them for the
3330
+ * people who did not, so a dataset of them is one nothing can seed.
3331
+ */ export const AI_DATASET_SAID_NAME = /\b(reviews?|testimonials?|ratings?|quotes?|stars?|endorsements?)\b/i;
3332
+ /**
3333
+ * A plan that creates more datasets than this job may make is settled in
3334
+ * code, never re-asked (AGL-3616). The live eval of 2026-10-10 refused a
3335
+ * dental practice's plan four times over for a fourth dataset, and a food
3336
+ * bank's re-ask for one broke a layout rule its first answer had kept.
3337
+ *
3338
+ * A dataset of what customers say (`AI_DATASET_SAID_NAME`, in its name or a
3339
+ * field) is let go first, whatever the cap: both of those plans made one of
3340
+ * testimonials, which nothing may seed.
3341
+ *
3342
+ * The datasets kept are the most useful: the ones the most sections list,
3343
+ * a record template counting double, plan order breaking ties. Each one let
3344
+ * go leaves the plan whole — its `new:` reference comes off every section,
3345
+ * a record template of it becomes a page of its own, and a section that
3346
+ * listed it shows fewer items than rule 8 binds, so its items are written
3347
+ * out on the page. Where the job may create no dataset at all, rule 7 still
3348
+ * asks, as it does for any creation.
3349
+ */ export function aiSettlePlanDatasets(plan, capabilities = null) {
3350
+ var _ref, _entry_left;
3351
+ const entry = capabilities == null ? void 0 : capabilities.create.dataset;
3352
+ if (!(entry == null ? void 0 : entry.allowed)) return plan;
3353
+ const cap = Math.min((_ref = capabilities == null ? void 0 : capabilities.datasetsMax) != null ? _ref : Infinity, (_entry_left = entry.left) != null ? _entry_left : Infinity);
3354
+ const datasets = plan.create.filter((one)=>one.kind === 'dataset');
3355
+ // A dataset a form writes to holds what visitors send (AGL-3616): its
3356
+ // fields may well be a rating or a quote request, and nothing seeds it.
3357
+ const forForms = aiPlanFormDatasetNames(plan);
3358
+ const said = (one)=>!forForms.has(one.name.trim().toLowerCase()) && (AI_DATASET_SAID_NAME.test(one.name) || one.fields.some((field)=>AI_DATASET_SAID_NAME.test(field)));
3359
+ if (!datasets.some(said) && (!Number.isFinite(cap) || datasets.length <= cap)) return plan;
3360
+ const named = (ref, name)=>!!ref && isAiPlanNewRef(ref) && ref.slice('new:'.length).trim().toLowerCase() === name.trim().toLowerCase();
3361
+ // A form's dataset counts as much as a record template: the form the
3362
+ // person asked for is built around it.
3363
+ const score = (name)=>(forForms.has(name.trim().toLowerCase()) ? 2 : 0) + plan.screens.reduce((total, screen)=>{
3364
+ var _screen_record;
3365
+ return total + (named((_screen_record = screen.record) == null ? void 0 : _screen_record.dataset, name) ? 2 : 0) + screen.sections.filter((section)=>section.uses.some((ref)=>named(ref, name))).length;
3366
+ }, 0);
3367
+ const ranked = datasets.filter((one)=>!said(one)).map((one, index)=>({
3368
+ name: one.name,
3369
+ index,
3370
+ score: score(one.name)
3371
+ })).sort((a, b)=>b.score - a.score || a.index - b.index);
3372
+ const kept = new Set(ranked.slice(0, Math.max(0, cap)).map((one)=>one.name.trim().toLowerCase()));
3373
+ const dropped = new Set(datasets.map((one)=>one.name.trim().toLowerCase()).filter((name)=>!kept.has(name)));
3374
+ const isDropped = (ref)=>[
3375
+ ...dropped
3376
+ ].some((name)=>named(ref, name));
3377
+ return _extends({}, plan, {
3378
+ create: plan.create.filter((one)=>one.kind !== 'dataset' || !dropped.has(one.name.trim().toLowerCase()))// A form whose dataset was let go sends its submissions to the Inbox only.
3379
+ .map((one)=>one.kind === 'form' && isDropped(one.writesTo) ? withoutWritesTo(one) : one),
3380
+ screens: plan.screens.map((screen)=>_extends({}, screen, {
3381
+ record: screen.record && isDropped(screen.record.dataset) ? null : screen.record,
3382
+ sections: screen.sections.map((section)=>section.uses.some(isDropped) ? _extends({}, section, {
3383
+ uses: section.uses.filter((ref)=>!isDropped(ref)),
3384
+ items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
3385
+ }) : section)
3386
+ }))
3387
+ });
3388
+ }
3389
+ /** A creation with no `writesTo`, as a plan that never said one reads. */ function withoutWritesTo(creation) {
3390
+ const rest = _extends({}, creation);
3391
+ delete rest.writesTo;
3392
+ return rest;
3393
+ }
3394
+ /**
3395
+ * A form's dataset, settled in code before the rules read the plan
3396
+ * (AGL-3616, Zach 2026-10-10: Aglyn AI's forms write to its datasets).
3397
+ *
3398
+ * - A `writesTo` that names nothing the form can write to comes off: a
3399
+ * `new:` of no dataset this plan creates, an id the inventory does not
3400
+ * hold, or a dataset this job may not create. The form is then the
3401
+ * form it always was, its submissions in the Inbox. A dataset the
3402
+ * inventory holds, named by its name rather than its id, is given its id.
3403
+ * - A dataset this plan creates for a form holds the people who send it:
3404
+ * no page lists it and none is its record template, so its `new:` comes
3405
+ * off every section and record that named it, and a section that listed
3406
+ * it shows fewer items than rule 8 binds.
3407
+ */ export function aiSettlePlanFormDatasets(plan, inventory, capabilities = null) {
3408
+ var _ref;
3409
+ if (!plan.create.some((one)=>one.kind === 'form' && one.writesTo)) return plan;
3410
+ const held = (_ref = inventory == null ? void 0 : inventory.datasets) != null ? _ref : [];
3411
+ const creatable = !capabilities || capabilities.create.dataset.allowed;
3412
+ let changed = false;
3413
+ const create = plan.create.map((one)=>{
3414
+ if (one.kind !== 'form' || !one.writesTo) return one;
3415
+ const ref = one.writesTo.trim();
3416
+ if (isAiPlanNewRef(ref)) {
3417
+ var _aiPlanCreateFor, _aiPlanItemFor;
3418
+ if (creatable && ((_aiPlanCreateFor = aiPlanCreateFor(plan, ref)) == null ? void 0 : _aiPlanCreateFor.kind) === 'dataset') return one;
3419
+ // A build's dataset item, which the data plugin's operation makes.
3420
+ if (((_aiPlanItemFor = aiPlanItemFor(plan, ref)) == null ? void 0 : _aiPlanItemFor.op) === AI_PLAN_DATASET_ITEM_OP) return one;
3421
+ } else {
3422
+ if (held.some((row)=>row.id === ref)) return one;
3423
+ const byName = held.find((row)=>aiNamesMatch(row.name, ref));
3424
+ if (byName) {
3425
+ changed = true;
3426
+ return _extends({}, one, {
3427
+ writesTo: byName.id
3428
+ });
3429
+ }
3430
+ }
3431
+ changed = true;
3432
+ return withoutWritesTo(one);
3433
+ });
3434
+ const settled = _extends({}, plan, {
3435
+ create
3436
+ });
3437
+ const forForms = aiPlanFormDatasetNames(settled);
3438
+ const listsOne = (ref)=>isAiPlanNewRef(ref) && forForms.has(ref.slice('new:'.length).trim().toLowerCase());
3439
+ const screens = plan.screens.map((screen)=>{
3440
+ const record = screen.record && listsOne(screen.record.dataset) ? null : screen.record;
3441
+ const sections = screen.sections.map((section)=>section.uses.some(listsOne) ? _extends({}, section, {
3442
+ uses: section.uses.filter((ref)=>!listsOne(ref)),
3443
+ items: Math.min(section.items, AI_TYPED_LIST_MIN_ITEMS - 1)
3444
+ }) : section);
3445
+ if (record === screen.record && sections.every((section, index)=>section === screen.sections[index])) return screen;
3446
+ changed = true;
3447
+ return _extends({}, screen, {
3448
+ record,
3449
+ sections
3450
+ });
3451
+ });
3452
+ return changed ? _extends({}, settled, {
3453
+ screens
3454
+ }) : plan;
3455
+ }
3327
3456
  /**
3328
3457
  * A page that names no layout, on a site where only one layout can frame it,
3329
3458
  * is put in that layout before the rules read the plan (2026-10-07). The