@aglyn/plugins-ai 1.0.0-beta.217 → 1.0.0-beta.219
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.
- package/README.md +2 -2
- package/package.json +17 -17
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/activity/ai-activity-actions.js +10 -2
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-assist-provider.component.js +2 -2
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.js +3 -3
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +1 -1
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +8 -0
- package/src/lib/components/ai-job-plan.component.js +21 -4
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
- package/src/lib/components/ai-permissions-on-host.component.js +12 -3
- package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
- package/src/lib/components/ai-product-import-option.component.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +124 -0
- package/src/lib/components/ai-product-zones.js +32 -0
- package/src/lib/components/ai-product-zones.js.map +1 -0
- package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-seo-audit-card.component.js +35 -92
- package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +21 -15
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +2 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/assist-signals-page.component.js +2 -2
- package/src/lib/components/assist-signals-page.component.js.map +1 -1
- package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
- package/src/lib/jobs/ai-email-bindings.js +75 -42
- package/src/lib/jobs/ai-email-bindings.js.map +1 -1
- package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-campaign-step.js +5 -10
- package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
- package/src/lib/jobs/ai-job-component-step.js +9 -3
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
- package/src/lib/jobs/ai-job-drafts.js +12 -5
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-email-step.js +1 -3
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +1 -1
- package/src/lib/jobs/ai-job-generation.js +10 -2
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +1 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
- package/src/lib/jobs/ai-job-page-sections.js +7 -4
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +7 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.js +33 -0
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-plan-step.js +29 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
- package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
- package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-products-step.js +54 -32
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
- package/src/lib/jobs/ai-job-seo-step.js +35 -108
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +1 -0
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-theme-step.js +1 -1
- package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.js +17 -9
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +19 -11
- package/src/lib/jobs/ai-workflow-records.js +60 -27
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/model/ai-allotments.d.ts +11 -3
- package/src/lib/model/ai-allotments.js +21 -4
- package/src/lib/model/ai-allotments.js.map +1 -1
- package/src/lib/model/ai-automation-outline.d.ts +21 -2
- package/src/lib/model/ai-automation-outline.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +57 -0
- package/src/lib/model/ai-build-plan.js +116 -7
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-seo.d.ts +11 -51
- package/src/lib/model/ai-seo.js +0 -17
- package/src/lib/model/ai-seo.js.map +1 -1
- package/src/lib/model/ai-site-inventory.js +1 -1
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-template-subjects.js +2 -2
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/model/ai-theme-proposal.d.ts +1 -1
- package/src/lib/model/ai-theme-proposal.js +1 -1
- package/src/lib/model/ai-theme-proposal.js.map +1 -1
- package/src/lib/model/ai-workflow-job.d.ts +1 -3
- package/src/lib/model/ai-workflow-job.js +1 -2
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +149 -0
- package/src/lib/plan-entitlements.js +168 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
- package/src/lib/runtime/ai-doctrine-validators.js +179 -23
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -5
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +29 -14
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +8 -1
- package/src/lib/runtime/ai-eval.js +29 -2
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +3 -3
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +6 -0
- package/src/lib/runtime/ai-palette.generated.js +21 -255
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +7 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/seo-audit.d.ts +13 -63
- package/src/lib/runtime/seo-audit.js +22 -207
- package/src/lib/runtime/seo-audit.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +1 -1
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-org.js +4 -25
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +2 -19
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-assist-prompts.js +3 -3
- package/src/lib/server/ai-assist-prompts.js.map +1 -1
- package/src/lib/server/ai-insight-digest-route.js +8 -1
- package/src/lib/server/ai-insight-digest-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +11 -10
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-view-context.js +23 -23
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
- package/src/lib/testing/stand-in-automation-index.js +102 -0
- package/src/lib/testing/stand-in-automation-index.js.map +1 -0
- package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
- package/src/lib/testing/stand-in-list-send-time.js +77 -0
- package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
- package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
- package/src/lib/testing/stand-in-pipeline-index.js +59 -0
- package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
- package/src/lib/testing/stand-in-product-index.d.ts +20 -0
- package/src/lib/testing/stand-in-product-index.js +105 -0
- package/src/lib/testing/stand-in-product-index.js.map +1 -0
- package/src/lib/tools/ai-component-tool.js +2 -2
- package/src/lib/tools/ai-component-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +6 -18
- package/src/lib/tools/ai-seo-tool.js +7 -38
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
- package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
- package/src/lib/tools/ai-workflow-tool.js +1 -1
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.d.ts +3 -0
- package/src/lib/usage/ai-allotment-alerts.js +20 -8
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
- package/src/lib/usage/assist-free-taste.js +2 -1
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-signal-mining.js +1 -1
- package/src/lib/usage/assist-signal-mining.js.map +1 -1
- package/src/lib/usage-axes.d.ts +39 -0
- package/src/lib/usage-axes.js +70 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/runtime/seo-page-facts.d.ts +0 -61
- package/src/lib/runtime/seo-page-facts.js +0 -150
- package/src/lib/runtime/seo-page-facts.js.map +0 -1
|
@@ -17,11 +17,12 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { sanitizeAuthorHtml } from "@aglyn/aglyn/app-utils/author-html";
|
|
18
18
|
import { formatBytes, measureNodeMap, nodeMapBytes } from "@aglyn/aglyn/app-utils/measure-node-map";
|
|
19
19
|
import { MEDIA_CDN_VARIANT_WIDTHS, parseMediaRef } from "@aglyn/aglyn/app-utils/media-ref";
|
|
20
|
-
import { ESTIMATED_PAGE_TRANSFER_BYTES
|
|
20
|
+
import { ESTIMATED_PAGE_TRANSFER_BYTES } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
21
|
+
import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
|
|
21
22
|
import { REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY } from "@aglyn/aglyn/app-utils/reusable-component-keys";
|
|
22
23
|
import { parseBreakpointSpan } from "@aglyn/shared-data-enums/breakpoint-span";
|
|
23
24
|
import { renderEmailHtml } from "@aglyn/shared-util-email/email-render";
|
|
24
|
-
import { aiPlanCreateFor, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
25
|
+
import { AI_BUILD_PLAN_CREATION_NOUNS, AI_BUILD_PLAN_EMBED_HOST_NAMES, AI_PLAN_ASKS_FOR_VIDEO, aiEmbedHostOf, aiEmbedVideoKey, aiPlanCreateFor, aiPlanSlugKey, aiPlanUndeclaredRefs, isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
25
26
|
import { aiCreationNoun, aiPlanUncreatable, aiPlanUncreatableKind } from "../model/ai-plan-capabilities.js";
|
|
26
27
|
import { AI_INSTANCE_REF_PROP, validateAiNodeTree } from "./ai-node-tree.js";
|
|
27
28
|
import { AI_PAGE_SCROLL_TO_PROP, aiPageLinkTarget, aiStoredScrollTargets } from "./ai-page-links.js";
|
|
@@ -1421,7 +1422,7 @@ const GRID = 'muiGrid';
|
|
|
1421
1422
|
{
|
|
1422
1423
|
rule: 10,
|
|
1423
1424
|
code: 'link-unrelated-screen',
|
|
1424
|
-
message: `"${sent[0].label}" links the home page, which does not do what its words say. Link the
|
|
1425
|
+
message: `"${sent[0].label}" links the home page, which does not do what its words say. Link the page that does, or leave the link out when the site has none.`,
|
|
1425
1426
|
nodeIds: unique(sent.map((entry)=>entry.id))
|
|
1426
1427
|
}
|
|
1427
1428
|
];
|
|
@@ -1505,7 +1506,7 @@ const GRID = 'muiGrid';
|
|
|
1505
1506
|
violations.push({
|
|
1506
1507
|
rule: 10,
|
|
1507
1508
|
code: 'link-fragment',
|
|
1508
|
-
message: `${wordsOf(first)} links "${first.value}", an anchor, and no element on a page carries an id an anchor could name, so it goes nowhere. ${context.pageSections ? `${toSection} Otherwise give it the "screenId" of a
|
|
1509
|
+
message: `${wordsOf(first)} links "${first.value}", an anchor, and no element on a page carries an id an anchor could name, so it goes nowhere. ${context.pageSections ? `${toSection} Otherwise give it the "screenId" of a page the site has that does what its words say, or take it out.` : 'Give it the "screenId" of a page the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives, or take it out.'}`,
|
|
1509
1510
|
nodeIds: unique(fragments.map((entry)=>entry.id))
|
|
1510
1511
|
});
|
|
1511
1512
|
}
|
|
@@ -1524,7 +1525,7 @@ const GRID = 'muiGrid';
|
|
|
1524
1525
|
violations.push({
|
|
1525
1526
|
rule: 10,
|
|
1526
1527
|
code: 'link-without-destination',
|
|
1527
|
-
message: `${wordsOf(first)} goes nowhere. Give it the "screenId" of a
|
|
1528
|
+
message: `${wordsOf(first)} goes nowhere. Give it the "screenId" of a page the site has that does what its words say, or an "href" that is a path on this site or an https: address the brief gives.${instead}`,
|
|
1528
1529
|
nodeIds: unique(dead.map((entry)=>entry.id))
|
|
1529
1530
|
});
|
|
1530
1531
|
}
|
|
@@ -1880,17 +1881,29 @@ const PURE_CONTAINERS = new Set([
|
|
|
1880
1881
|
]);
|
|
1881
1882
|
const EMBED_COMPONENTS = new Set([
|
|
1882
1883
|
'videoEmbed',
|
|
1883
|
-
'custom-html'
|
|
1884
|
-
'functionWidget'
|
|
1884
|
+
'custom-html'
|
|
1885
1885
|
]);
|
|
1886
1886
|
const DUPLICATE_SX_MIN_KEYS = 2;
|
|
1887
1887
|
const DUPLICATE_SX_MIN_NODES = 4;
|
|
1888
|
+
/**
|
|
1889
|
+
* Whether a third-party player is one the confirmed plan lists (AGL-3433). A
|
|
1890
|
+
* Video embed is admitted where the plan lists a player, playing the video
|
|
1891
|
+
* the brief gave or none yet, which the site owner pastes. Raw HTML never is.
|
|
1892
|
+
*/ function embedVerdict(node, planned) {
|
|
1893
|
+
var _node_props;
|
|
1894
|
+
if (node.componentId !== 'videoEmbed' || !planned.length) return 'unplanned';
|
|
1895
|
+
const url = typeof ((_node_props = node.props) == null ? void 0 : _node_props['url']) === 'string' ? node.props['url'].trim() : '';
|
|
1896
|
+
if (!url) return 'planned';
|
|
1897
|
+
const key = aiEmbedVideoKey(url);
|
|
1898
|
+
return key && planned.some((embed)=>embed.url && aiEmbedVideoKey(embed.url) === key) ? 'planned' : 'unplanned-link';
|
|
1899
|
+
}
|
|
1888
1900
|
/**
|
|
1889
1901
|
* Rule 16. The flattest tree that renders the design: no container that
|
|
1890
1902
|
* wraps one other container and adds nothing, no empty containers, no inline
|
|
1891
1903
|
* style repeated where a token or a component prop would carry it, no font
|
|
1892
1904
|
* beyond the theme's, images lazy below the first, video by poster rather
|
|
1893
|
-
* than autoplay,
|
|
1905
|
+
* than autoplay, no third-party player the confirmed plan does not list, and
|
|
1906
|
+
* none playing a video the brief did not give.
|
|
1894
1907
|
*/ export function detectHeavyDocument(tree, outputKind, context = {}) {
|
|
1895
1908
|
const visits = walkTree(tree);
|
|
1896
1909
|
const wrappers = [];
|
|
@@ -1899,6 +1912,7 @@ const DUPLICATE_SX_MIN_NODES = 4;
|
|
|
1899
1912
|
const eager = [];
|
|
1900
1913
|
const video = [];
|
|
1901
1914
|
const embeds = [];
|
|
1915
|
+
const embedLinks = [];
|
|
1902
1916
|
const sxSeen = new Map();
|
|
1903
1917
|
let images = 0;
|
|
1904
1918
|
for (const { id, node } of visits){
|
|
@@ -1921,7 +1935,12 @@ const DUPLICATE_SX_MIN_NODES = 4;
|
|
|
1921
1935
|
if (node.componentId === 'video' && (((_node_props2 = node.props) == null ? void 0 : _node_props2['autoPlay']) === true || ((_node_props3 = node.props) == null ? void 0 : _node_props3['preload']) === 'auto' || typeof ((_node_props4 = node.props) == null ? void 0 : _node_props4['src']) === 'string' && node.props['src'] && !((_node_props5 = node.props) == null ? void 0 : _node_props5['poster']))) {
|
|
1922
1936
|
video.push(id);
|
|
1923
1937
|
}
|
|
1924
|
-
if (EMBED_COMPONENTS.has(node.componentId)
|
|
1938
|
+
if (EMBED_COMPONENTS.has(node.componentId)) {
|
|
1939
|
+
var _context_plannedEmbeds;
|
|
1940
|
+
const verdict = embedVerdict(node, (_context_plannedEmbeds = context.plannedEmbeds) != null ? _context_plannedEmbeds : []);
|
|
1941
|
+
if (verdict === 'unplanned') embeds.push(id);
|
|
1942
|
+
if (verdict === 'unplanned-link') embedLinks.push(id);
|
|
1943
|
+
}
|
|
1925
1944
|
const sx = (_node_sx1 = node.sx) != null ? _node_sx1 : {};
|
|
1926
1945
|
if (Object.keys(sx).length >= DUPLICATE_SX_MIN_KEYS) {
|
|
1927
1946
|
var _sxSeen_get;
|
|
@@ -1953,7 +1972,8 @@ const DUPLICATE_SX_MIN_NODES = 4;
|
|
|
1953
1972
|
add('extra-font', fonts, "This names a font the theme does not load. Use the theme's typography.");
|
|
1954
1973
|
add('eager-image', eager, 'An image below the first loads eagerly. Leave loading unset so images further down the page load when they are reached.');
|
|
1955
1974
|
add('autoplay-video', video, 'A video loads its file before anyone plays it. Give it a poster, and let it play on click.');
|
|
1956
|
-
add('third-party-embed', embeds,
|
|
1975
|
+
add('third-party-embed', embeds, "This embeds a third-party player, which loads its host's code on every visit, and the confirmed plan lists none here. Remove it; a film from the media library plays in a Video (video).");
|
|
1976
|
+
add('embed-link-unplanned', embedLinks, 'This player plays a video the brief did not give. Use the link the plan lists, or leave the link empty for the site owner to paste.');
|
|
1957
1977
|
return violations;
|
|
1958
1978
|
}
|
|
1959
1979
|
/** The items a list or a row of cards repeats, as a re-ask names them. */ const ITEM_NOUNS = {
|
|
@@ -2514,7 +2534,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2514
2534
|
violations.push({
|
|
2515
2535
|
rule: 1,
|
|
2516
2536
|
code: 'plan-section-across-screens',
|
|
2517
|
-
message: `The "${entries[0].section.name}" section is planned on ${screens.size}
|
|
2537
|
+
message: `The "${entries[0].section.name}" section is planned on ${screens.size} pages. Plan it as one reusable component and place it on each.`,
|
|
2518
2538
|
paths: entries.map((entry)=>entry.path)
|
|
2519
2539
|
});
|
|
2520
2540
|
}
|
|
@@ -2639,7 +2659,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2639
2659
|
violations.push({
|
|
2640
2660
|
rule: 2,
|
|
2641
2661
|
code: 'plan-screen-without-layout',
|
|
2642
|
-
message: noLayoutToName ? 'A
|
|
2662
|
+
message: noLayoutToName ? 'A page names a layout the site does not have, and this job may not create one. Leave the layout empty.' : mayCreateLayout ? "A page names no layout the site has or the plan creates. Put every page in the site's layout, or plan one." : 'A page names no layout the site has, and this job may not create one. Put every page in a layout the site has.',
|
|
2643
2663
|
paths: unlaid.map(({ index })=>`screens[${index}].layout`)
|
|
2644
2664
|
});
|
|
2645
2665
|
}
|
|
@@ -2648,7 +2668,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2648
2668
|
violations.push({
|
|
2649
2669
|
rule: 2,
|
|
2650
2670
|
code: 'plan-layout-region-section',
|
|
2651
|
-
message: 'A
|
|
2671
|
+
message: 'A page plans its own header, navigation or footer. Those live in the layout; take the section off the page.',
|
|
2652
2672
|
paths: regions.map((entry)=>entry.path)
|
|
2653
2673
|
});
|
|
2654
2674
|
}
|
|
@@ -2657,7 +2677,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2657
2677
|
violations.push({
|
|
2658
2678
|
rule: 2,
|
|
2659
2679
|
code: 'plan-layout-in-section',
|
|
2660
|
-
message: "A section places a layout. A layout frames a whole
|
|
2680
|
+
message: "A section places a layout. A layout frames a whole page and is never placed inside one: name it as the page's layout, and take it out of the section's uses.",
|
|
2661
2681
|
paths: placed
|
|
2662
2682
|
});
|
|
2663
2683
|
}
|
|
@@ -2704,7 +2724,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2704
2724
|
violations.push({
|
|
2705
2725
|
rule: 4,
|
|
2706
2726
|
code: 'plan-similar-pages',
|
|
2707
|
-
message: `${indexes.length}
|
|
2727
|
+
message: `${indexes.length} pages share one shape. Plan one template and apply it ${indexes.length} times with each page's copy, or bind it to a collection.`,
|
|
2708
2728
|
paths: indexes.map((index)=>`screens[${index}].template`)
|
|
2709
2729
|
});
|
|
2710
2730
|
}
|
|
@@ -2713,7 +2733,7 @@ const FORM_SECTION_NAME = /\bform\b|\bsign[- ]?up\b|\bsubscribe\b|\bnewsletter\b
|
|
|
2713
2733
|
violations.push({
|
|
2714
2734
|
rule: 4,
|
|
2715
2735
|
code: 'plan-template-in-section',
|
|
2716
|
-
message: "A section places a template. A template is applied to a whole
|
|
2736
|
+
message: "A section places a template. A template is applied to a whole page and is never placed inside one: take it out of the section's uses, and name it as the page's template only when the whole page is built from it.",
|
|
2717
2737
|
paths: placed
|
|
2718
2738
|
});
|
|
2719
2739
|
}
|
|
@@ -2852,15 +2872,15 @@ const REUSE_BEFORE_CREATE_KINDS = new Set([
|
|
|
2852
2872
|
const section = first.field === 'uses' ? screen.sections[first.sectionIndex] : null;
|
|
2853
2873
|
const kind = section ? undeclaredPlacementKind(first.name, section, plan, kinds) : 'template';
|
|
2854
2874
|
const refused = capabilities ? aiPlanUncreatableKind(plan, kind, capabilities) : null;
|
|
2855
|
-
const where = section ? `${section.name ? `The "${section.name}" section` : 'A section'} places a creation named "${first.name}", but the plan never creates it` : `${screen.title ? `The
|
|
2875
|
+
const where = section ? `${section.name ? `The "${section.name}" section` : 'A section'} places a creation named "${first.name}", but the plan never creates it` : `${screen.title ? `The page "${screen.title}"` : 'A page'} applies a template named "${first.name}", but the plan never creates it`;
|
|
2856
2876
|
let message;
|
|
2857
2877
|
if (!refused) {
|
|
2858
|
-
const otherwise = section ? `place ${aiCreationNoun(kind)} the site already has by its id` : "leave the
|
|
2878
|
+
const otherwise = section ? `place ${aiCreationNoun(kind)} the site already has by its id` : "leave the page's template empty";
|
|
2859
2879
|
message = `${where}. Declare it in create as ${aiCreationNoun(kind)}, with why nothing the site has will do, or ${otherwise}.`;
|
|
2860
2880
|
} else if (section) {
|
|
2861
2881
|
message = `${where}, and ${refused.reason}. ${refused.instead} Take it out of the section's uses.`;
|
|
2862
2882
|
} else {
|
|
2863
|
-
message = `${where}, and ${refused.reason}. Leave the
|
|
2883
|
+
message = `${where}, and ${refused.reason}. Leave the page's template empty, or apply a template the site already has.`;
|
|
2864
2884
|
}
|
|
2865
2885
|
return {
|
|
2866
2886
|
rule: 7,
|
|
@@ -2952,7 +2972,7 @@ function slugKey(slug) {
|
|
|
2952
2972
|
violations.push({
|
|
2953
2973
|
rule: 10,
|
|
2954
2974
|
code: 'plan-slug',
|
|
2955
|
-
message: 'A
|
|
2975
|
+
message: 'A page has no usable address. Give each one a slug of lowercase words joined by hyphens.',
|
|
2956
2976
|
paths: badSlugs
|
|
2957
2977
|
});
|
|
2958
2978
|
}
|
|
@@ -2960,7 +2980,7 @@ function slugKey(slug) {
|
|
|
2960
2980
|
violations.push({
|
|
2961
2981
|
rule: 10,
|
|
2962
2982
|
code: 'plan-slug-taken',
|
|
2963
|
-
message: 'A
|
|
2983
|
+
message: 'A page reuses an address the site or the plan already uses. Give each page its own slug.',
|
|
2964
2984
|
paths: collisions
|
|
2965
2985
|
});
|
|
2966
2986
|
}
|
|
@@ -2968,7 +2988,7 @@ function slugKey(slug) {
|
|
|
2968
2988
|
violations.push({
|
|
2969
2989
|
rule: 10,
|
|
2970
2990
|
code: 'plan-seo',
|
|
2971
|
-
message: `A
|
|
2991
|
+
message: `A page is missing its search title or description, or runs past ${AI_SEO_TITLE_MAX} and ${AI_SEO_DESCRIPTION_MAX} characters. Write both for every page.`,
|
|
2972
2992
|
paths: seo
|
|
2973
2993
|
});
|
|
2974
2994
|
}
|
|
@@ -3015,7 +3035,7 @@ function slugKey(slug) {
|
|
|
3015
3035
|
violations.push({
|
|
3016
3036
|
rule: 15,
|
|
3017
3037
|
code: 'plan-missed-duplicate',
|
|
3018
|
-
message: `The site already has "${nearest.name}". Start the new
|
|
3038
|
+
message: `The site already has "${nearest.name}". Start the new page from a duplicate of it, which keeps its bindings and SEO, and edit that.`,
|
|
3019
3039
|
paths: [
|
|
3020
3040
|
`screens[${index}].duplicateOf`
|
|
3021
3041
|
]
|
|
@@ -3156,6 +3176,141 @@ function slugKey(slug) {
|
|
|
3156
3176
|
}
|
|
3157
3177
|
];
|
|
3158
3178
|
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Rule 16 (plan): a third-party player is planned where it can play
|
|
3181
|
+
* (AGL-3433). A screen of the plan or a component it creates carries at most
|
|
3182
|
+
* its kind's budget. A template renders for every record it serves and a
|
|
3183
|
+
* layout around every page, so neither carries one, and a form, an email or a
|
|
3184
|
+
* dataset has nowhere to. A link the plan gives is one its host plays.
|
|
3185
|
+
*/ export function detectPlanEmbeds(plan) {
|
|
3186
|
+
var _plan_embeds;
|
|
3187
|
+
const nowhere = [];
|
|
3188
|
+
const elsewhere = [];
|
|
3189
|
+
const wrongHost = [];
|
|
3190
|
+
const placed = new Map();
|
|
3191
|
+
((_plan_embeds = plan.embeds) != null ? _plan_embeds : []).forEach((embed, index)=>{
|
|
3192
|
+
var _placed_get;
|
|
3193
|
+
const path = `embeds[${index}]`;
|
|
3194
|
+
if (embed.url !== null && aiEmbedHostOf(embed.url) !== embed.host) wrongHost.push(`${path}.url`);
|
|
3195
|
+
let target = null;
|
|
3196
|
+
if (isAiPlanNewRef(embed.where)) {
|
|
3197
|
+
const creation = aiPlanCreateFor(plan, embed.where);
|
|
3198
|
+
if (creation && creation.kind !== 'component') {
|
|
3199
|
+
elsewhere.push({
|
|
3200
|
+
kind: creation.kind,
|
|
3201
|
+
path: `${path}.where`
|
|
3202
|
+
});
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
if (creation) target = {
|
|
3206
|
+
key: embed.where.trim().toLowerCase(),
|
|
3207
|
+
label: creation.name,
|
|
3208
|
+
kind: 'component'
|
|
3209
|
+
};
|
|
3210
|
+
} else {
|
|
3211
|
+
const screen = plan.screens.find((row)=>aiPlanSlugKey(row.slug) === aiPlanSlugKey(embed.where));
|
|
3212
|
+
if (screen) target = {
|
|
3213
|
+
key: aiPlanSlugKey(screen.slug),
|
|
3214
|
+
label: screen.slug,
|
|
3215
|
+
kind: 'page'
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
if (!target) {
|
|
3219
|
+
nowhere.push(`${path}.where`);
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
const entry = (_placed_get = placed.get(target.key)) != null ? _placed_get : {
|
|
3223
|
+
label: target.label,
|
|
3224
|
+
kind: target.kind,
|
|
3225
|
+
paths: []
|
|
3226
|
+
};
|
|
3227
|
+
entry.paths.push(path);
|
|
3228
|
+
placed.set(target.key, entry);
|
|
3229
|
+
});
|
|
3230
|
+
const violations = [];
|
|
3231
|
+
if (nowhere.length) {
|
|
3232
|
+
violations.push({
|
|
3233
|
+
rule: 16,
|
|
3234
|
+
code: 'plan-embed-nowhere',
|
|
3235
|
+
message: 'A player is planned where the plan builds nothing. Name a screen of the plan by its slug, or new:<name> of a component it creates.',
|
|
3236
|
+
paths: nowhere
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
if (elsewhere.length) {
|
|
3240
|
+
const nouns = [
|
|
3241
|
+
...new Set(elsewhere.map((entry)=>AI_BUILD_PLAN_CREATION_NOUNS[entry.kind].noun))
|
|
3242
|
+
];
|
|
3243
|
+
violations.push({
|
|
3244
|
+
rule: 16,
|
|
3245
|
+
code: 'plan-embed-refused',
|
|
3246
|
+
message: `Only a page or a component carries a third-party player, and this plan puts one on a ${nouns.join(' and a ')}. A template or a layout would load the player's code on every page it serves. Place it on a page or a component, or leave it off.`,
|
|
3247
|
+
paths: elsewhere.map((entry)=>entry.path)
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
for (const entry of placed.values()){
|
|
3251
|
+
const limit = AI_OUTPUT_BUDGETS[entry.kind].embeds;
|
|
3252
|
+
if (entry.paths.length <= limit) continue;
|
|
3253
|
+
violations.push({
|
|
3254
|
+
rule: 16,
|
|
3255
|
+
code: 'plan-embed-over-budget',
|
|
3256
|
+
message: `${entry.label} is planned ${entry.paths.length} players, and a ${entry.kind} carries at most ${limit}. Keep the one the brief asks for most.`,
|
|
3257
|
+
paths: entry.paths
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
if (wrongHost.length) {
|
|
3261
|
+
violations.push({
|
|
3262
|
+
rule: 16,
|
|
3263
|
+
code: 'plan-embed-link-host',
|
|
3264
|
+
message: `A planned player's link is not a video link of its host, ${Object.values(AI_BUILD_PLAN_EMBED_HOST_NAMES).join(' or ')}. Give the brief's own link for that host, or null.`,
|
|
3265
|
+
paths: wrongHost
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
return violations;
|
|
3269
|
+
}
|
|
3270
|
+
/** Text as two copies of one phrase compare: lowercase, no quotes, one space. */ function phraseKey(text) {
|
|
3271
|
+
return text.toLowerCase().replace(/[“”"‘’'`]/g, '').replace(/\s+/g, ' ').trim();
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Rule 16 (plan, against the brief): a player is planned only where the brief
|
|
3275
|
+
* asks for one (AGL-3433). The plan quotes the brief's words, and those words
|
|
3276
|
+
* are found in the brief and ask for a video; a link it gives is one the brief
|
|
3277
|
+
* gives. The plan step holds this, being the one door that reads the brief
|
|
3278
|
+
* beside the plan.
|
|
3279
|
+
*/ export function aiPlanEmbedBriefViolations(plan, brief) {
|
|
3280
|
+
var _brief_match, _plan_embeds;
|
|
3281
|
+
const said = phraseKey(brief);
|
|
3282
|
+
const given = new Set(((_brief_match = brief.match(/https:\/\/\S+/g)) != null ? _brief_match : []).map((link)=>aiEmbedVideoKey(link.replace(/[).,;!?]+$/, ''))).filter((key)=>key !== null));
|
|
3283
|
+
const unasked = [];
|
|
3284
|
+
const ungiven = [];
|
|
3285
|
+
((_plan_embeds = plan.embeds) != null ? _plan_embeds : []).forEach((embed, index)=>{
|
|
3286
|
+
const asked = phraseKey(embed.asked);
|
|
3287
|
+
if (!asked || !said.includes(asked) || !AI_PLAN_ASKS_FOR_VIDEO.test(asked)) {
|
|
3288
|
+
unasked.push(`embeds[${index}].asked`);
|
|
3289
|
+
}
|
|
3290
|
+
if (embed.url !== null) {
|
|
3291
|
+
const key = aiEmbedVideoKey(embed.url);
|
|
3292
|
+
if (!key || !given.has(key)) ungiven.push(`embeds[${index}].url`);
|
|
3293
|
+
}
|
|
3294
|
+
});
|
|
3295
|
+
const violations = [];
|
|
3296
|
+
if (unasked.length) {
|
|
3297
|
+
violations.push({
|
|
3298
|
+
rule: 16,
|
|
3299
|
+
code: 'plan-embed-not-asked',
|
|
3300
|
+
message: 'A player is planned that the brief does not ask for: its words are not words of the brief asking for a video. Take it off the plan; a film from the media library plays in a Video.',
|
|
3301
|
+
paths: unasked
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
if (ungiven.length) {
|
|
3305
|
+
violations.push({
|
|
3306
|
+
rule: 16,
|
|
3307
|
+
code: 'plan-embed-link-not-given',
|
|
3308
|
+
message: "A planned player's link is not one the brief gives. Give the brief's own link, or null for the site owner to paste.",
|
|
3309
|
+
paths: ungiven
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
return violations;
|
|
3313
|
+
}
|
|
3159
3314
|
/**
|
|
3160
3315
|
* Every plan rule, against the site inventory the plan was made from and,
|
|
3161
3316
|
* where the job read them, what it may create there (AGL-3030). `null`
|
|
@@ -3170,6 +3325,7 @@ function slugKey(slug) {
|
|
|
3170
3325
|
...detectPlanInlineForms(plan, inventory, capabilities),
|
|
3171
3326
|
...detectUntemplatedSimilarPages(plan, inventory),
|
|
3172
3327
|
...detectPlanLiteralColors(plan),
|
|
3328
|
+
...detectPlanEmbeds(plan),
|
|
3173
3329
|
...detectCreateBeforeReuse(plan, inventory),
|
|
3174
3330
|
...detectPlanUncreatable(plan, capabilities),
|
|
3175
3331
|
...detectPlanUndeclaredCreations(plan, inventory, capabilities),
|