@aglyn/plugins-ai 1.0.0-beta.238 → 1.0.0-beta.240
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/package.json +17 -17
- package/src/lib/components/ai-job-links.js +8 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +5 -0
- package/src/lib/components/ai-job-requests.js +4 -2
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +12 -0
- package/src/lib/components/ai-jobs-store.js +15 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
- package/src/lib/components/ai-usage-meter-refresh.js +63 -0
- package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
- package/src/lib/components/assist-panel.component.js +17 -1
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
- package/src/lib/components/use-ai-usage-meter.js +8 -3
- package/src/lib/components/use-ai-usage-meter.js.map +1 -1
- package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
- package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
- package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +29 -2
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +7 -2
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
- package/src/lib/jobs/ai-job-form-dataset.js +161 -0
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
- package/src/lib/jobs/ai-job-form-step.js +37 -9
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +7 -2
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
- package/src/lib/jobs/ai-job-page-language.js +44 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +13 -2
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +32 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +18 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -4
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +183 -0
- package/src/lib/jobs/ai-job-site-datasets.js +530 -0
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
- package/src/lib/jobs/ai-job-site-step.js +148 -29
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-compiler.js +93 -1
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +17 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +32 -3
- package/src/lib/layout-language/ai-layout-listings.js +62 -9
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.js +12 -4
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +2 -0
- package/src/lib/model/ai-build-job.js +16 -1
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +23 -3
- package/src/lib/model/ai-build-plan.js +47 -6
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +19 -2
- package/src/lib/model/ai-site-job.js +21 -6
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
- package/src/lib/runtime/ai-dataset-generation.js +127 -0
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +84 -0
- package/src/lib/runtime/ai-doctrine-validators.js +307 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +10 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +2 -0
- package/src/lib/runtime/ai-node-tree.js +16 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
- package/src/lib/tools/ai-dataset-tool.js +231 -0
- package/src/lib/tools/ai-dataset-tool.js.map +1 -0
- package/src/lib/usage/free-ai-credits-left.js +26 -2
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { findInventedPrices, findStorefrontClaims, storefrontClaimViolations } from "../model/ai-storefront-claims.js";
|
|
17
|
+
import { AI_DATASET_SAID_NAME, detectOffVoiceCopy } from "../runtime/ai-doctrine-validators.js";
|
|
18
|
+
/**
|
|
19
|
+
* The strict tool a site's datasets are designed through (AGL-3616), and the
|
|
20
|
+
* check that holds each one to what the site publishes as its own words.
|
|
21
|
+
*
|
|
22
|
+
* A dataset is a menu, a team, a list of services or portfolio pieces: like
|
|
23
|
+
* things, each a record a repeat shows on a page the day the site goes live.
|
|
24
|
+
* So nothing in a record may mark a gap for later (no square brackets), link
|
|
25
|
+
* anywhere, or say what nobody said: no review, testimonial, rating or quote,
|
|
26
|
+
* no price the brief does not state, and the storefront copy rules hold every
|
|
27
|
+
* value (no health, financial or legal promise; no award or certification the
|
|
28
|
+
* brief does not give). The planned field names are kept, in order, because
|
|
29
|
+
* the pages planned around the dataset already name them.
|
|
30
|
+
*/ export const AI_DATASET_TOOL_NAME = 'submit_dataset';
|
|
31
|
+
/** The field types a dataset's design may use: the data plugin writer's own words for them. */ export const AI_DATASET_FIELD_TYPES = [
|
|
32
|
+
'text',
|
|
33
|
+
'number',
|
|
34
|
+
'integer',
|
|
35
|
+
'boolean',
|
|
36
|
+
'list'
|
|
37
|
+
];
|
|
38
|
+
/** A dataset's bounds, as the tool states them and its check holds them. */ export const AI_DATASET_LIMITS = {
|
|
39
|
+
/** The most fields, the planned ones included. */ fieldsMax: 12,
|
|
40
|
+
/** The most fields it may add to the planned ones. */ extraFieldsMax: 3,
|
|
41
|
+
fieldNameMax: 40,
|
|
42
|
+
recordsMin: 3,
|
|
43
|
+
recordsMax: 12,
|
|
44
|
+
/** The longest value; a description is a few sentences. */ valueMax: 400
|
|
45
|
+
};
|
|
46
|
+
/** What a list value's items are joined by inside one value. */ export const AI_DATASET_LIST_SEPARATOR = '; ';
|
|
47
|
+
const string = (description)=>({
|
|
48
|
+
type: 'string',
|
|
49
|
+
description
|
|
50
|
+
});
|
|
51
|
+
export const AI_DATASET_TOOL = {
|
|
52
|
+
name: AI_DATASET_TOOL_NAME,
|
|
53
|
+
description: 'Design one dataset of this site: its typed fields and its first records. Every field is required.',
|
|
54
|
+
strict: true,
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
required: [
|
|
59
|
+
'fields',
|
|
60
|
+
'records'
|
|
61
|
+
],
|
|
62
|
+
properties: {
|
|
63
|
+
fields: {
|
|
64
|
+
type: 'array',
|
|
65
|
+
description: `The planned fields first, in order and named as planned, then at most ${AI_DATASET_LIMITS.extraFieldsMax} more; at most ${AI_DATASET_LIMITS.fieldsMax} in all.`,
|
|
66
|
+
items: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
required: [
|
|
70
|
+
'name',
|
|
71
|
+
'type'
|
|
72
|
+
],
|
|
73
|
+
properties: {
|
|
74
|
+
name: string(`Plain words, at most ${AI_DATASET_LIMITS.fieldNameMax} characters.`),
|
|
75
|
+
type: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
enum: [
|
|
78
|
+
...AI_DATASET_FIELD_TYPES
|
|
79
|
+
],
|
|
80
|
+
description: 'text for words (a date or time too, written as a visitor reads it); number or integer for a figure; boolean for yes or no; list for a few short tags.'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
records: {
|
|
86
|
+
type: 'array',
|
|
87
|
+
description: `${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records.`,
|
|
88
|
+
items: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
required: [
|
|
92
|
+
'values'
|
|
93
|
+
],
|
|
94
|
+
properties: {
|
|
95
|
+
values: {
|
|
96
|
+
type: 'array',
|
|
97
|
+
description: `One value per field, in field order: "" where the brief gives nothing for it; a list's items joined by "${AI_DATASET_LIST_SEPARATOR}"; a boolean "yes" or "no".`,
|
|
98
|
+
items: {
|
|
99
|
+
type: 'string'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function fail(findings, at, code, message, value) {
|
|
109
|
+
findings.violations.push({
|
|
110
|
+
rule: null,
|
|
111
|
+
code,
|
|
112
|
+
message,
|
|
113
|
+
paths: [
|
|
114
|
+
at
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
if (!(at in findings.offending)) findings.offending[at] = value;
|
|
118
|
+
}
|
|
119
|
+
const line = (value)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
120
|
+
const HTML = /<\/?[a-z][^>]*>/i;
|
|
121
|
+
const EMOJI = RegExp("[\\u00A9\\u00AE\\u203C\\u2049\\u2122\\u2139\\u2194-\\u2199\\u21A9-\\u21AA\\u231A-\\u231B\\u2328\\u23CF\\u23E9-\\u23F3\\u23F8-\\u23FA\\u24C2\\u25AA-\\u25AB\\u25B6\\u25C0\\u25FB-\\u25FE\\u2600-\\u2604\\u260E\\u2611\\u2614-\\u2615\\u2618\\u261D\\u2620\\u2622-\\u2623\\u2626\\u262A\\u262E-\\u262F\\u2638-\\u263A\\u2640\\u2642\\u2648-\\u2653\\u265F-\\u2660\\u2663\\u2665-\\u2666\\u2668\\u267B\\u267E-\\u267F\\u2692-\\u2697\\u2699\\u269B-\\u269C\\u26A0-\\u26A1\\u26A7\\u26AA-\\u26AB\\u26B0-\\u26B1\\u26BD-\\u26BE\\u26C4-\\u26C5\\u26C8\\u26CE-\\u26CF\\u26D1\\u26D3-\\u26D4\\u26E9-\\u26EA\\u26F0-\\u26F5\\u26F7-\\u26FA\\u26FD\\u2702\\u2705\\u2708-\\u270D\\u270F\\u2712\\u2714\\u2716\\u271D\\u2721\\u2728\\u2733-\\u2734\\u2744\\u2747\\u274C\\u274E\\u2753-\\u2755\\u2757\\u2763-\\u2764\\u2795-\\u2797\\u27A1\\u27B0\\u27BF\\u2934-\\u2935\\u2B05-\\u2B07\\u2B1B-\\u2B1C\\u2B50\\u2B55\\u3030\\u303D\\u3297\\u3299\\u{1F004}\\u{1F02C}-\\u{1F02F}\\u{1F094}-\\u{1F09F}\\u{1F0AF}-\\u{1F0B0}\\u{1F0C0}\\u{1F0CF}-\\u{1F0D0}\\u{1F0F6}-\\u{1F0FF}\\u{1F170}-\\u{1F171}\\u{1F17E}-\\u{1F17F}\\u{1F18E}\\u{1F191}-\\u{1F19A}\\u{1F1AE}-\\u{1F1E5}\\u{1F201}-\\u{1F20F}\\u{1F21A}\\u{1F22F}\\u{1F232}-\\u{1F23A}\\u{1F23C}-\\u{1F23F}\\u{1F249}-\\u{1F25F}\\u{1F266}-\\u{1F321}\\u{1F324}-\\u{1F393}\\u{1F396}-\\u{1F397}\\u{1F399}-\\u{1F39B}\\u{1F39E}-\\u{1F3F0}\\u{1F3F3}-\\u{1F3F5}\\u{1F3F7}-\\u{1F3FA}\\u{1F400}-\\u{1F4FD}\\u{1F4FF}-\\u{1F53D}\\u{1F549}-\\u{1F54E}\\u{1F550}-\\u{1F567}\\u{1F56F}-\\u{1F570}\\u{1F573}-\\u{1F57A}\\u{1F587}\\u{1F58A}-\\u{1F58D}\\u{1F590}\\u{1F595}-\\u{1F596}\\u{1F5A4}-\\u{1F5A5}\\u{1F5A8}\\u{1F5B1}-\\u{1F5B2}\\u{1F5BC}\\u{1F5C2}-\\u{1F5C4}\\u{1F5D1}-\\u{1F5D3}\\u{1F5DC}-\\u{1F5DE}\\u{1F5E1}\\u{1F5E3}\\u{1F5E8}\\u{1F5EF}\\u{1F5F3}\\u{1F5FA}-\\u{1F64F}\\u{1F680}-\\u{1F6C5}\\u{1F6CB}-\\u{1F6D2}\\u{1F6D5}-\\u{1F6E5}\\u{1F6E9}\\u{1F6EB}-\\u{1F6F0}\\u{1F6F3}-\\u{1F6FF}\\u{1F7DA}-\\u{1F7FF}\\u{1F80C}-\\u{1F80F}\\u{1F848}-\\u{1F84F}\\u{1F85A}-\\u{1F85F}\\u{1F888}-\\u{1F88F}\\u{1F8AE}-\\u{1F8AF}\\u{1F8BC}-\\u{1F8BF}\\u{1F8C2}-\\u{1F8CF}\\u{1F8D9}-\\u{1F8FF}\\u{1F90C}-\\u{1F93A}\\u{1F93C}-\\u{1F945}\\u{1F947}-\\u{1F9FF}\\u{1FA58}-\\u{1FA5F}\\u{1FA6E}-\\u{1FAFF}\\u{1FC00}-\\u{1FFFD}]", "u");
|
|
122
|
+
const BRACKET = /[[\]]/;
|
|
123
|
+
const URL = /\bhttps?:\/\/|\bwww\./i;
|
|
124
|
+
/** A rating or review score nobody gave. */ const RATING = /\b\d(?:\.\d)?\s*(?:\/\s*(?:5|10)\b|out of (?:5|five|10|ten)\b|stars?\b)|\b(?:five|5|four|4)[- ]star\b|\brated\b/i;
|
|
125
|
+
/** A field that would hold what customers said, which no job writes for them. */ const SAID_FIELD = AI_DATASET_SAID_NAME;
|
|
126
|
+
/** A field holding a price, whose values must be the brief's own. */ const PRICE_FIELD = /\b(price|prices|cost|costs|fee|fees|rate|rates)\b/i;
|
|
127
|
+
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
128
|
+
const BOOLEAN = /^(yes|no|true|false)$/i;
|
|
129
|
+
/** Where the records sit in the answer, as a violation names it. */ const RECORDS_AT = 'records';
|
|
130
|
+
/** A planned field name as names compare: case and spacing aside. */ const key = (name)=>line(name).toLowerCase();
|
|
131
|
+
/**
|
|
132
|
+
* A dataset's answer held to its plan and the copy rules. `planned` are the
|
|
133
|
+
* field names the plan gave it; `merchantWords` are the brief and the site's
|
|
134
|
+
* answers, the only source a stated claim or price may come from.
|
|
135
|
+
*/ export function checkAiDataset(answer, context) {
|
|
136
|
+
const findings = {
|
|
137
|
+
violations: [],
|
|
138
|
+
offending: {}
|
|
139
|
+
};
|
|
140
|
+
const limits = AI_DATASET_LIMITS;
|
|
141
|
+
const fields = [];
|
|
142
|
+
const rawFields = Array.isArray(answer['fields']) ? answer['fields'] : [];
|
|
143
|
+
if (!rawFields.length) fail(findings, 'fields', 'missing', 'The dataset needs its fields.', answer['fields']);
|
|
144
|
+
rawFields.forEach((raw, index)=>{
|
|
145
|
+
const entry = raw != null ? raw : {};
|
|
146
|
+
const name = line(entry['name']);
|
|
147
|
+
const type = entry['type'];
|
|
148
|
+
const at = `fields[${index}]`;
|
|
149
|
+
if (!name) return fail(findings, at, 'missing', 'Each field needs a name.', raw);
|
|
150
|
+
if (name.length > limits.fieldNameMax) fail(findings, at, 'too-long', `The field "${name}" has a name longer than ${limits.fieldNameMax} characters.`, raw);
|
|
151
|
+
if (!AI_DATASET_FIELD_TYPES.includes(type)) fail(findings, at, 'type', `The field "${name}" is one of ${AI_DATASET_FIELD_TYPES.join(', ')}.`, raw);
|
|
152
|
+
if (fields.some((field)=>key(field.name) === key(name))) fail(findings, at, 'duplicate', `Two fields are both called "${name}".`, raw);
|
|
153
|
+
if (SAID_FIELD.test(name)) {
|
|
154
|
+
fail(findings, at, 'said-field', `A dataset holds no "${name}": what customers say is theirs to write, never the site's.`, raw);
|
|
155
|
+
}
|
|
156
|
+
fields.push({
|
|
157
|
+
name,
|
|
158
|
+
type
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
if (fields.length > limits.fieldsMax) fail(findings, 'fields', 'too-many', `A dataset has at most ${limits.fieldsMax} fields.`, answer['fields']);
|
|
162
|
+
const planned = context.planned.map(line).filter(Boolean);
|
|
163
|
+
const missing = planned.filter((name)=>!fields.some((field)=>key(field.name) === key(name)));
|
|
164
|
+
if (missing.length) {
|
|
165
|
+
fail(findings, 'fields', 'planned-field', `Keep the planned fields, named as planned: ${missing.map((name)=>`"${name}"`).join(', ')}.`, answer['fields']);
|
|
166
|
+
} else if (fields.length - planned.length > limits.extraFieldsMax) {
|
|
167
|
+
fail(findings, 'fields', 'too-many', `Add at most ${limits.extraFieldsMax} fields to the planned ones.`, answer['fields']);
|
|
168
|
+
}
|
|
169
|
+
if (fields[0] && fields[0].type !== 'text') {
|
|
170
|
+
fail(findings, 'fields[0]', 'first-field', 'The first field names each record, so it is text.', rawFields[0]);
|
|
171
|
+
}
|
|
172
|
+
const rawRecords = Array.isArray(answer['records']) ? answer['records'] : [];
|
|
173
|
+
if (rawRecords.length < limits.recordsMin || rawRecords.length > limits.recordsMax) {
|
|
174
|
+
fail(findings, RECORDS_AT, 'count', `Write ${limits.recordsMin} to ${limits.recordsMax} records; this has ${rawRecords.length}.`, answer['records']);
|
|
175
|
+
}
|
|
176
|
+
const records = [];
|
|
177
|
+
const samples = [];
|
|
178
|
+
const names = new Set();
|
|
179
|
+
rawRecords.forEach((raw, index)=>{
|
|
180
|
+
var _values_, _ref;
|
|
181
|
+
var _fields_;
|
|
182
|
+
const at = `records[${index}]`;
|
|
183
|
+
const values = Array.isArray(raw == null ? void 0 : raw['values']) ? raw['values'].map((value)=>line(value)) : [];
|
|
184
|
+
if (values.length !== fields.length) {
|
|
185
|
+
fail(findings, at, 'values', `Record ${index + 1} has ${values.length} values for ${fields.length} fields; give one per field, "" where there is none.`, raw);
|
|
186
|
+
}
|
|
187
|
+
const first = (_values_ = values[0]) != null ? _values_ : '';
|
|
188
|
+
if (!first) fail(findings, at, 'unnamed', `Record ${index + 1} has no ${(_ref = (_fields_ = fields[0]) == null ? void 0 : _fields_.name) != null ? _ref : 'name'}.`, raw);
|
|
189
|
+
else if (names.has(first.toLowerCase())) fail(findings, at, 'duplicate', `Two records are both "${first}".`, raw);
|
|
190
|
+
names.add(first.toLowerCase());
|
|
191
|
+
values.forEach((value, column)=>{
|
|
192
|
+
const field = fields[column];
|
|
193
|
+
if (!value || !field) return;
|
|
194
|
+
const where = `${at}.values[${column}]`;
|
|
195
|
+
if (value.length > limits.valueMax) fail(findings, where, 'too-long', `"${field.name}" of record ${index + 1} is longer than ${limits.valueMax} characters.`, value);
|
|
196
|
+
if (HTML.test(value) || EMOJI.test(value) || URL.test(value)) {
|
|
197
|
+
fail(findings, where, 'markup', `"${field.name}" of record ${index + 1} is plain words, with no link, markup or emoji.`, value);
|
|
198
|
+
}
|
|
199
|
+
if (BRACKET.test(value)) {
|
|
200
|
+
fail(findings, where, 'bracket', `"${field.name}" of record ${index + 1} is published as written, so it marks no missing fact: leave the value "" instead.`, value);
|
|
201
|
+
}
|
|
202
|
+
if ((field.type === 'number' || field.type === 'integer') && !NUMBER.test(value.replace(/,/g, ''))) {
|
|
203
|
+
fail(findings, where, 'number', `"${field.name}" of record ${index + 1} is a number, or "".`, value);
|
|
204
|
+
}
|
|
205
|
+
if (field.type === 'boolean' && !BOOLEAN.test(value)) fail(findings, where, 'boolean', `"${field.name}" of record ${index + 1} is "yes", "no" or "".`, value);
|
|
206
|
+
if (PRICE_FIELD.test(field.name) && !context.merchantWords.includes(value.replace(/[^\d.]/g, '') || '\u0000')) {
|
|
207
|
+
fail(findings, where, 'price-invented', `"${field.name}" of record ${index + 1} states a price the brief does not give; leave it "".`, value);
|
|
208
|
+
}
|
|
209
|
+
const rating = RATING.exec(value);
|
|
210
|
+
if (rating && !context.merchantWords.toLowerCase().includes(rating[0].toLowerCase())) {
|
|
211
|
+
fail(findings, where, 'rating-invented', `Record ${index + 1} gives a rating or review nobody gave. Remove: "${rating[0]}".`, value);
|
|
212
|
+
}
|
|
213
|
+
if (field.type === 'text') samples.push({
|
|
214
|
+
at: where,
|
|
215
|
+
text: value
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
records.push(values);
|
|
219
|
+
});
|
|
220
|
+
findings.violations.push(...storefrontClaimViolations(findStorefrontClaims(samples, context.merchantWords), findInventedPrices(samples, context.merchantWords)), ...detectOffVoiceCopy(samples, null));
|
|
221
|
+
return {
|
|
222
|
+
value: findings.violations.length ? null : {
|
|
223
|
+
fields,
|
|
224
|
+
records
|
|
225
|
+
},
|
|
226
|
+
violations: findings.violations,
|
|
227
|
+
offending: findings.offending
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
//# sourceMappingURL=ai-dataset-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-dataset-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n findInventedPrices,\n findStorefrontClaims,\n storefrontClaimViolations,\n type AiStorefrontCopySample,\n} from '../model/ai-storefront-claims'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport { AI_DATASET_SAID_NAME, detectOffVoiceCopy, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tool a site's datasets are designed through (AGL-3616), and the\n * check that holds each one to what the site publishes as its own words.\n *\n * A dataset is a menu, a team, a list of services or portfolio pieces: like\n * things, each a record a repeat shows on a page the day the site goes live.\n * So nothing in a record may mark a gap for later (no square brackets), link\n * anywhere, or say what nobody said: no review, testimonial, rating or quote,\n * no price the brief does not state, and the storefront copy rules hold every\n * value (no health, financial or legal promise; no award or certification the\n * brief does not give). The planned field names are kept, in order, because\n * the pages planned around the dataset already name them.\n */\n\nexport const AI_DATASET_TOOL_NAME = 'submit_dataset'\n\n/** The field types a dataset's design may use: the data plugin writer's own words for them. */\nexport const AI_DATASET_FIELD_TYPES = ['text', 'number', 'integer', 'boolean', 'list'] as const\nexport type AiDatasetFieldType = (typeof AI_DATASET_FIELD_TYPES)[number]\n\n/** A dataset's bounds, as the tool states them and its check holds them. */\nexport const AI_DATASET_LIMITS = {\n /** The most fields, the planned ones included. */\n fieldsMax: 12,\n /** The most fields it may add to the planned ones. */\n extraFieldsMax: 3,\n fieldNameMax: 40,\n recordsMin: 3,\n recordsMax: 12,\n /** The longest value; a description is a few sentences. */\n valueMax: 400,\n} as const\n\n/** What a list value's items are joined by inside one value. */\nexport const AI_DATASET_LIST_SEPARATOR = '; '\n\n/** A designed dataset as its check reads it. */\nexport interface AiDataset {\n fields: Array<{ name: string; type: AiDatasetFieldType }>\n /** Each record's values in field order; an empty string is no value. */\n records: string[][]\n}\n\nconst string = (description: string) => ({ type: 'string', description })\n\nexport const AI_DATASET_TOOL: AiTool = {\n name: AI_DATASET_TOOL_NAME,\n description: 'Design one dataset of this site: its typed fields and its first records. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['fields', 'records'],\n properties: {\n fields: {\n type: 'array',\n description: `The planned fields first, in order and named as planned, then at most ${AI_DATASET_LIMITS.extraFieldsMax} more; at most ${AI_DATASET_LIMITS.fieldsMax} in all.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type'],\n properties: {\n name: string(`Plain words, at most ${AI_DATASET_LIMITS.fieldNameMax} characters.`),\n type: {\n type: 'string',\n enum: [...AI_DATASET_FIELD_TYPES],\n description: 'text for words (a date or time too, written as a visitor reads it); number or integer for a figure; boolean for yes or no; list for a few short tags.',\n },\n },\n },\n },\n records: {\n type: 'array',\n description: `${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['values'],\n properties: {\n values: {\n type: 'array',\n description: `One value per field, in field order: \"\" where the brief gives nothing for it; a list's items joined by \"${AI_DATASET_LIST_SEPARATOR}\"; a boolean \"yes\" or \"no\".`,\n items: { type: 'string' },\n },\n },\n },\n },\n },\n },\n}\n\ninterface Findings {\n violations: AiDoctrineViolation[]\n offending: Record<string, unknown>\n}\n\nfunction fail(findings: Findings, at: string, code: string, message: string, value: unknown): void {\n findings.violations.push({ rule: null, code, message, paths: [at] })\n if (!(at in findings.offending)) findings.offending[at] = value\n}\n\nconst line = (value: unknown): string => (typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : '')\n\nconst HTML = /<\\/?[a-z][^>]*>/i\nconst EMOJI = /\\p{Extended_Pictographic}/u\nconst BRACKET = /[[\\]]/\nconst URL = /\\bhttps?:\\/\\/|\\bwww\\./i\n/** A rating or review score nobody gave. */\nconst RATING = /\\b\\d(?:\\.\\d)?\\s*(?:\\/\\s*(?:5|10)\\b|out of (?:5|five|10|ten)\\b|stars?\\b)|\\b(?:five|5|four|4)[- ]star\\b|\\brated\\b/i\n/** A field that would hold what customers said, which no job writes for them. */\nconst SAID_FIELD = AI_DATASET_SAID_NAME\n/** A field holding a price, whose values must be the brief's own. */\nconst PRICE_FIELD = /\\b(price|prices|cost|costs|fee|fees|rate|rates)\\b/i\nconst NUMBER = /^-?\\d+(?:\\.\\d+)?$/\nconst BOOLEAN = /^(yes|no|true|false)$/i\n\n/** Where the records sit in the answer, as a violation names it. */\nconst RECORDS_AT = 'records'\n\n/** A planned field name as names compare: case and spacing aside. */\nconst key = (name: string) => line(name).toLowerCase()\n\n/**\n * A dataset's answer held to its plan and the copy rules. `planned` are the\n * field names the plan gave it; `merchantWords` are the brief and the site's\n * answers, the only source a stated claim or price may come from.\n */\nexport function checkAiDataset(\n answer: Record<string, unknown>,\n context: { planned: readonly string[]; merchantWords: string },\n): AiGenerationCheckResult<AiDataset> {\n const findings: Findings = { violations: [], offending: {} }\n const limits = AI_DATASET_LIMITS\n const fields: AiDataset['fields'] = []\n const rawFields = Array.isArray(answer['fields']) ? (answer['fields'] as unknown[]) : []\n if (!rawFields.length) fail(findings, 'fields', 'missing', 'The dataset needs its fields.', answer['fields'])\n rawFields.forEach((raw, index) => {\n const entry = (raw ?? {}) as Record<string, unknown>\n const name = line(entry['name'])\n const type = entry['type'] as AiDatasetFieldType\n const at = `fields[${index}]`\n if (!name) return fail(findings, at, 'missing', 'Each field needs a name.', raw)\n if (name.length > limits.fieldNameMax) fail(findings, at, 'too-long', `The field \"${name}\" has a name longer than ${limits.fieldNameMax} characters.`, raw)\n if (!AI_DATASET_FIELD_TYPES.includes(type)) fail(findings, at, 'type', `The field \"${name}\" is one of ${AI_DATASET_FIELD_TYPES.join(', ')}.`, raw)\n if (fields.some((field) => key(field.name) === key(name))) fail(findings, at, 'duplicate', `Two fields are both called \"${name}\".`, raw)\n if (SAID_FIELD.test(name)) {\n fail(findings, at, 'said-field', `A dataset holds no \"${name}\": what customers say is theirs to write, never the site's.`, raw)\n }\n fields.push({ name, type })\n })\n if (fields.length > limits.fieldsMax) fail(findings, 'fields', 'too-many', `A dataset has at most ${limits.fieldsMax} fields.`, answer['fields'])\n const planned = context.planned.map(line).filter(Boolean)\n const missing = planned.filter((name) => !fields.some((field) => key(field.name) === key(name)))\n if (missing.length) {\n fail(findings, 'fields', 'planned-field', `Keep the planned fields, named as planned: ${missing.map((name) => `\"${name}\"`).join(', ')}.`, answer['fields'])\n } else if (fields.length - planned.length > limits.extraFieldsMax) {\n fail(findings, 'fields', 'too-many', `Add at most ${limits.extraFieldsMax} fields to the planned ones.`, answer['fields'])\n }\n if (fields[0] && fields[0].type !== 'text') {\n fail(findings, 'fields[0]', 'first-field', 'The first field names each record, so it is text.', rawFields[0])\n }\n\n const rawRecords = Array.isArray(answer['records']) ? (answer['records'] as unknown[]) : []\n if (rawRecords.length < limits.recordsMin || rawRecords.length > limits.recordsMax) {\n fail(findings, RECORDS_AT, 'count', `Write ${limits.recordsMin} to ${limits.recordsMax} records; this has ${rawRecords.length}.`, answer['records'])\n }\n const records: string[][] = []\n const samples: AiStorefrontCopySample[] = []\n const names = new Set<string>()\n rawRecords.forEach((raw, index) => {\n const at = `records[${index}]`\n const values = Array.isArray((raw as Record<string, unknown> | null)?.['values'])\n ? ((raw as Record<string, unknown>)['values'] as unknown[]).map((value) => line(value))\n : []\n if (values.length !== fields.length) {\n fail(findings, at, 'values', `Record ${index + 1} has ${values.length} values for ${fields.length} fields; give one per field, \"\" where there is none.`, raw)\n }\n const first = values[0] ?? ''\n if (!first) fail(findings, at, 'unnamed', `Record ${index + 1} has no ${fields[0]?.name ?? 'name'}.`, raw)\n else if (names.has(first.toLowerCase())) fail(findings, at, 'duplicate', `Two records are both \"${first}\".`, raw)\n names.add(first.toLowerCase())\n values.forEach((value, column) => {\n const field = fields[column]\n if (!value || !field) return\n const where = `${at}.values[${column}]`\n if (value.length > limits.valueMax) fail(findings, where, 'too-long', `\"${field.name}\" of record ${index + 1} is longer than ${limits.valueMax} characters.`, value)\n if (HTML.test(value) || EMOJI.test(value) || URL.test(value)) {\n fail(findings, where, 'markup', `\"${field.name}\" of record ${index + 1} is plain words, with no link, markup or emoji.`, value)\n }\n if (BRACKET.test(value)) {\n fail(findings, where, 'bracket', `\"${field.name}\" of record ${index + 1} is published as written, so it marks no missing fact: leave the value \"\" instead.`, value)\n }\n if ((field.type === 'number' || field.type === 'integer') && !NUMBER.test(value.replace(/,/g, ''))) {\n fail(findings, where, 'number', `\"${field.name}\" of record ${index + 1} is a number, or \"\".`, value)\n }\n if (field.type === 'boolean' && !BOOLEAN.test(value)) fail(findings, where, 'boolean', `\"${field.name}\" of record ${index + 1} is \"yes\", \"no\" or \"\".`, value)\n if (PRICE_FIELD.test(field.name) && !context.merchantWords.includes(value.replace(/[^\\d.]/g, '') || '\\u0000')) {\n fail(findings, where, 'price-invented', `\"${field.name}\" of record ${index + 1} states a price the brief does not give; leave it \"\".`, value)\n }\n const rating = RATING.exec(value)\n if (rating && !context.merchantWords.toLowerCase().includes(rating[0].toLowerCase())) {\n fail(findings, where, 'rating-invented', `Record ${index + 1} gives a rating or review nobody gave. Remove: \"${rating[0]}\".`, value)\n }\n if (field.type === 'text') samples.push({ at: where, text: value })\n })\n records.push(values)\n })\n findings.violations.push(\n ...storefrontClaimViolations(\n findStorefrontClaims(samples, context.merchantWords),\n findInventedPrices(samples, context.merchantWords),\n ),\n ...detectOffVoiceCopy(samples, null),\n )\n return {\n value: findings.violations.length ? null : { fields, records },\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n"],"names":["findInventedPrices","findStorefrontClaims","storefrontClaimViolations","AI_DATASET_SAID_NAME","detectOffVoiceCopy","AI_DATASET_TOOL_NAME","AI_DATASET_FIELD_TYPES","AI_DATASET_LIMITS","fieldsMax","extraFieldsMax","fieldNameMax","recordsMin","recordsMax","valueMax","AI_DATASET_LIST_SEPARATOR","string","description","type","AI_DATASET_TOOL","name","strict","inputSchema","additionalProperties","required","properties","fields","items","enum","records","values","fail","findings","at","code","message","value","violations","push","rule","paths","offending","line","replace","trim","HTML","EMOJI","BRACKET","URL","RATING","SAID_FIELD","PRICE_FIELD","NUMBER","BOOLEAN","RECORDS_AT","key","toLowerCase","checkAiDataset","answer","context","limits","rawFields","Array","isArray","length","forEach","raw","index","entry","includes","join","some","field","test","planned","map","filter","Boolean","missing","rawRecords","samples","names","Set","first","has","add","column","where","merchantWords","rating","exec","text"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,kBAAkB,EAClBC,oBAAoB,EACpBC,yBAAyB,QAEpB,mCAA+B;AAGtC,SAASC,oBAAoB,EAAEC,kBAAkB,QAAkC,uCAAmC;AAEtH;;;;;;;;;;;;CAYC,GAED,OAAO,MAAMC,uBAAuB,iBAAgB;AAEpD,6FAA6F,GAC7F,OAAO,MAAMC,yBAAyB;IAAC;IAAQ;IAAU;IAAW;IAAW;CAAO,CAAS;AAG/F,0EAA0E,GAC1E,OAAO,MAAMC,oBAAoB;IAC/B,gDAAgD,GAChDC,WAAW;IACX,oDAAoD,GACpDC,gBAAgB;IAChBC,cAAc;IACdC,YAAY;IACZC,YAAY;IACZ,yDAAyD,GACzDC,UAAU;AACZ,EAAU;AAEV,8DAA8D,GAC9D,OAAO,MAAMC,4BAA4B,KAAI;AAS7C,MAAMC,SAAS,CAACC,cAAyB,CAAA;QAAEC,MAAM;QAAUD;IAAY,CAAA;AAEvE,OAAO,MAAME,kBAA0B;IACrCC,MAAMd;IACNW,aAAa;IACbI,QAAQ;IACRC,aAAa;QACXJ,MAAM;QACNK,sBAAsB;QACtBC,UAAU;YAAC;YAAU;SAAU;QAC/BC,YAAY;YACVC,QAAQ;gBACNR,MAAM;gBACND,aAAa,CAAC,sEAAsE,EAAET,kBAAkBE,cAAc,CAAC,eAAe,EAAEF,kBAAkBC,SAAS,CAAC,QAAQ,CAAC;gBAC7KkB,OAAO;oBACLT,MAAM;oBACNK,sBAAsB;oBACtBC,UAAU;wBAAC;wBAAQ;qBAAO;oBAC1BC,YAAY;wBACVL,MAAMJ,OAAO,CAAC,qBAAqB,EAAER,kBAAkBG,YAAY,CAAC,YAAY,CAAC;wBACjFO,MAAM;4BACJA,MAAM;4BACNU,MAAM;mCAAIrB;6BAAuB;4BACjCU,aAAa;wBACf;oBACF;gBACF;YACF;YACAY,SAAS;gBACPX,MAAM;gBACND,aAAa,GAAGT,kBAAkBI,UAAU,CAAC,IAAI,EAAEJ,kBAAkBK,UAAU,CAAC,SAAS,CAAC;gBAC1Fc,OAAO;oBACLT,MAAM;oBACNK,sBAAsB;oBACtBC,UAAU;wBAAC;qBAAS;oBACpBC,YAAY;wBACVK,QAAQ;4BACNZ,MAAM;4BACND,aAAa,CAAC,wGAAwG,EAAEF,0BAA0B,2BAA2B,CAAC;4BAC9KY,OAAO;gCAAET,MAAM;4BAAS;wBAC1B;oBACF;gBACF;YACF;QACF;IACF;AACF,EAAC;AAOD,SAASa,KAAKC,QAAkB,EAAEC,EAAU,EAAEC,IAAY,EAAEC,OAAe,EAAEC,KAAc;IACzFJ,SAASK,UAAU,CAACC,IAAI,CAAC;QAAEC,MAAM;QAAML;QAAMC;QAASK,OAAO;YAACP;SAAG;IAAC;IAClE,IAAI,CAAEA,CAAAA,MAAMD,SAASS,SAAS,AAAD,GAAIT,SAASS,SAAS,CAACR,GAAG,GAAGG;AAC5D;AAEA,MAAMM,OAAO,CAACN,QAA4B,OAAOA,UAAU,WAAWA,MAAMO,OAAO,CAAC,QAAQ,KAAKC,IAAI,KAAK;AAE1G,MAAMC,OAAO;AACb,MAAMC,QAAQ;AACd,MAAMC,UAAU;AAChB,MAAMC,MAAM;AACZ,0CAA0C,GAC1C,MAAMC,SAAS;AACf,+EAA+E,GAC/E,MAAMC,aAAa9C;AACnB,mEAAmE,GACnE,MAAM+C,cAAc;AACpB,MAAMC,SAAS;AACf,MAAMC,UAAU;AAEhB,kEAAkE,GAClE,MAAMC,aAAa;AAEnB,mEAAmE,GACnE,MAAMC,MAAM,CAACnC,OAAiBsB,KAAKtB,MAAMoC,WAAW;AAEpD;;;;CAIC,GACD,OAAO,SAASC,eACdC,MAA+B,EAC/BC,OAA8D;IAE9D,MAAM3B,WAAqB;QAAEK,YAAY,EAAE;QAAEI,WAAW,CAAC;IAAE;IAC3D,MAAMmB,SAASpD;IACf,MAAMkB,SAA8B,EAAE;IACtC,MAAMmC,YAAYC,MAAMC,OAAO,CAACL,MAAM,CAAC,SAAS,IAAKA,MAAM,CAAC,SAAS,GAAiB,EAAE;IACxF,IAAI,CAACG,UAAUG,MAAM,EAAEjC,KAAKC,UAAU,UAAU,WAAW,iCAAiC0B,MAAM,CAAC,SAAS;IAC5GG,UAAUI,OAAO,CAAC,CAACC,KAAKC;QACtB,MAAMC,QAASF,cAAAA,MAAO,CAAC;QACvB,MAAM9C,OAAOsB,KAAK0B,KAAK,CAAC,OAAO;QAC/B,MAAMlD,OAAOkD,KAAK,CAAC,OAAO;QAC1B,MAAMnC,KAAK,CAAC,OAAO,EAAEkC,MAAM,CAAC,CAAC;QAC7B,IAAI,CAAC/C,MAAM,OAAOW,KAAKC,UAAUC,IAAI,WAAW,4BAA4BiC;QAC5E,IAAI9C,KAAK4C,MAAM,GAAGJ,OAAOjD,YAAY,EAAEoB,KAAKC,UAAUC,IAAI,YAAY,CAAC,WAAW,EAAEb,KAAK,yBAAyB,EAAEwC,OAAOjD,YAAY,CAAC,YAAY,CAAC,EAAEuD;QACvJ,IAAI,CAAC3D,uBAAuB8D,QAAQ,CAACnD,OAAOa,KAAKC,UAAUC,IAAI,QAAQ,CAAC,WAAW,EAAEb,KAAK,YAAY,EAAEb,uBAAuB+D,IAAI,CAAC,MAAM,CAAC,CAAC,EAAEJ;QAC9I,IAAIxC,OAAO6C,IAAI,CAAC,CAACC,QAAUjB,IAAIiB,MAAMpD,IAAI,MAAMmC,IAAInC,QAAQW,KAAKC,UAAUC,IAAI,aAAa,CAAC,4BAA4B,EAAEb,KAAK,EAAE,CAAC,EAAE8C;QACpI,IAAIhB,WAAWuB,IAAI,CAACrD,OAAO;YACzBW,KAAKC,UAAUC,IAAI,cAAc,CAAC,oBAAoB,EAAEb,KAAK,2DAA2D,CAAC,EAAE8C;QAC7H;QACAxC,OAAOY,IAAI,CAAC;YAAElB;YAAMF;QAAK;IAC3B;IACA,IAAIQ,OAAOsC,MAAM,GAAGJ,OAAOnD,SAAS,EAAEsB,KAAKC,UAAU,UAAU,YAAY,CAAC,sBAAsB,EAAE4B,OAAOnD,SAAS,CAAC,QAAQ,CAAC,EAAEiD,MAAM,CAAC,SAAS;IAChJ,MAAMgB,UAAUf,QAAQe,OAAO,CAACC,GAAG,CAACjC,MAAMkC,MAAM,CAACC;IACjD,MAAMC,UAAUJ,QAAQE,MAAM,CAAC,CAACxD,OAAS,CAACM,OAAO6C,IAAI,CAAC,CAACC,QAAUjB,IAAIiB,MAAMpD,IAAI,MAAMmC,IAAInC;IACzF,IAAI0D,QAAQd,MAAM,EAAE;QAClBjC,KAAKC,UAAU,UAAU,iBAAiB,CAAC,2CAA2C,EAAE8C,QAAQH,GAAG,CAAC,CAACvD,OAAS,CAAC,CAAC,EAAEA,KAAK,CAAC,CAAC,EAAEkD,IAAI,CAAC,MAAM,CAAC,CAAC,EAAEZ,MAAM,CAAC,SAAS;IAC5J,OAAO,IAAIhC,OAAOsC,MAAM,GAAGU,QAAQV,MAAM,GAAGJ,OAAOlD,cAAc,EAAE;QACjEqB,KAAKC,UAAU,UAAU,YAAY,CAAC,YAAY,EAAE4B,OAAOlD,cAAc,CAAC,4BAA4B,CAAC,EAAEgD,MAAM,CAAC,SAAS;IAC3H;IACA,IAAIhC,MAAM,CAAC,EAAE,IAAIA,MAAM,CAAC,EAAE,CAACR,IAAI,KAAK,QAAQ;QAC1Ca,KAAKC,UAAU,aAAa,eAAe,qDAAqD6B,SAAS,CAAC,EAAE;IAC9G;IAEA,MAAMkB,aAAajB,MAAMC,OAAO,CAACL,MAAM,CAAC,UAAU,IAAKA,MAAM,CAAC,UAAU,GAAiB,EAAE;IAC3F,IAAIqB,WAAWf,MAAM,GAAGJ,OAAOhD,UAAU,IAAImE,WAAWf,MAAM,GAAGJ,OAAO/C,UAAU,EAAE;QAClFkB,KAAKC,UAAUsB,YAAY,SAAS,CAAC,MAAM,EAAEM,OAAOhD,UAAU,CAAC,IAAI,EAAEgD,OAAO/C,UAAU,CAAC,mBAAmB,EAAEkE,WAAWf,MAAM,CAAC,CAAC,CAAC,EAAEN,MAAM,CAAC,UAAU;IACrJ;IACA,MAAM7B,UAAsB,EAAE;IAC9B,MAAMmD,UAAoC,EAAE;IAC5C,MAAMC,QAAQ,IAAIC;IAClBH,WAAWd,OAAO,CAAC,CAACC,KAAKC;YAQTrC;YAC0DJ;QARxE,MAAMO,KAAK,CAAC,QAAQ,EAAEkC,MAAM,CAAC,CAAC;QAC9B,MAAMrC,SAASgC,MAAMC,OAAO,CAAEG,uBAAD,AAACA,GAAwC,CAAC,SAAS,IAC5E,AAAC,AAACA,GAA+B,CAAC,SAAS,CAAeS,GAAG,CAAC,CAACvC,QAAUM,KAAKN,UAC9E,EAAE;QACN,IAAIN,OAAOkC,MAAM,KAAKtC,OAAOsC,MAAM,EAAE;YACnCjC,KAAKC,UAAUC,IAAI,UAAU,CAAC,OAAO,EAAEkC,QAAQ,EAAE,KAAK,EAAErC,OAAOkC,MAAM,CAAC,YAAY,EAAEtC,OAAOsC,MAAM,CAAC,oDAAoD,CAAC,EAAEE;QAC3J;QACA,MAAMiB,SAAQrD,WAAAA,MAAM,CAAC,EAAE,YAATA,WAAa;QAC3B,IAAI,CAACqD,OAAOpD,KAAKC,UAAUC,IAAI,WAAW,CAAC,OAAO,EAAEkC,QAAQ,EAAE,QAAQ,WAAEzC,WAAAA,MAAM,CAAC,EAAE,qBAATA,SAAWN,IAAI,mBAAI,OAAO,CAAC,CAAC,EAAE8C;aACjG,IAAIe,MAAMG,GAAG,CAACD,MAAM3B,WAAW,KAAKzB,KAAKC,UAAUC,IAAI,aAAa,CAAC,sBAAsB,EAAEkD,MAAM,EAAE,CAAC,EAAEjB;QAC7Ge,MAAMI,GAAG,CAACF,MAAM3B,WAAW;QAC3B1B,OAAOmC,OAAO,CAAC,CAAC7B,OAAOkD;YACrB,MAAMd,QAAQ9C,MAAM,CAAC4D,OAAO;YAC5B,IAAI,CAAClD,SAAS,CAACoC,OAAO;YACtB,MAAMe,QAAQ,GAAGtD,GAAG,QAAQ,EAAEqD,OAAO,CAAC,CAAC;YACvC,IAAIlD,MAAM4B,MAAM,GAAGJ,OAAO9C,QAAQ,EAAEiB,KAAKC,UAAUuD,OAAO,YAAY,CAAC,CAAC,EAAEf,MAAMpD,IAAI,CAAC,YAAY,EAAE+C,QAAQ,EAAE,gBAAgB,EAAEP,OAAO9C,QAAQ,CAAC,YAAY,CAAC,EAAEsB;YAC9J,IAAIS,KAAK4B,IAAI,CAACrC,UAAUU,MAAM2B,IAAI,CAACrC,UAAUY,IAAIyB,IAAI,CAACrC,QAAQ;gBAC5DL,KAAKC,UAAUuD,OAAO,UAAU,CAAC,CAAC,EAAEf,MAAMpD,IAAI,CAAC,YAAY,EAAE+C,QAAQ,EAAE,+CAA+C,CAAC,EAAE/B;YAC3H;YACA,IAAIW,QAAQ0B,IAAI,CAACrC,QAAQ;gBACvBL,KAAKC,UAAUuD,OAAO,WAAW,CAAC,CAAC,EAAEf,MAAMpD,IAAI,CAAC,YAAY,EAAE+C,QAAQ,EAAE,kFAAkF,CAAC,EAAE/B;YAC/J;YACA,IAAI,AAACoC,CAAAA,MAAMtD,IAAI,KAAK,YAAYsD,MAAMtD,IAAI,KAAK,SAAQ,KAAM,CAACkC,OAAOqB,IAAI,CAACrC,MAAMO,OAAO,CAAC,MAAM,MAAM;gBAClGZ,KAAKC,UAAUuD,OAAO,UAAU,CAAC,CAAC,EAAEf,MAAMpD,IAAI,CAAC,YAAY,EAAE+C,QAAQ,EAAE,oBAAoB,CAAC,EAAE/B;YAChG;YACA,IAAIoC,MAAMtD,IAAI,KAAK,aAAa,CAACmC,QAAQoB,IAAI,CAACrC,QAAQL,KAAKC,UAAUuD,OAAO,WAAW,CAAC,CAAC,EAAEf,MAAMpD,IAAI,CAAC,YAAY,EAAE+C,QAAQ,EAAE,sBAAsB,CAAC,EAAE/B;YACvJ,IAAIe,YAAYsB,IAAI,CAACD,MAAMpD,IAAI,KAAK,CAACuC,QAAQ6B,aAAa,CAACnB,QAAQ,CAACjC,MAAMO,OAAO,CAAC,WAAW,OAAO,WAAW;gBAC7GZ,KAAKC,UAAUuD,OAAO,kBAAkB,CAAC,CAAC,EAAEf,MAAMpD,IAAI,CAAC,YAAY,EAAE+C,QAAQ,EAAE,qDAAqD,CAAC,EAAE/B;YACzI;YACA,MAAMqD,SAASxC,OAAOyC,IAAI,CAACtD;YAC3B,IAAIqD,UAAU,CAAC9B,QAAQ6B,aAAa,CAAChC,WAAW,GAAGa,QAAQ,CAACoB,MAAM,CAAC,EAAE,CAACjC,WAAW,KAAK;gBACpFzB,KAAKC,UAAUuD,OAAO,mBAAmB,CAAC,OAAO,EAAEpB,QAAQ,EAAE,gDAAgD,EAAEsB,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAErD;YAChI;YACA,IAAIoC,MAAMtD,IAAI,KAAK,QAAQ8D,QAAQ1C,IAAI,CAAC;gBAAEL,IAAIsD;gBAAOI,MAAMvD;YAAM;QACnE;QACAP,QAAQS,IAAI,CAACR;IACf;IACAE,SAASK,UAAU,CAACC,IAAI,IACnBnC,0BACDD,qBAAqB8E,SAASrB,QAAQ6B,aAAa,GACnDvF,mBAAmB+E,SAASrB,QAAQ6B,aAAa,OAEhDnF,mBAAmB2E,SAAS;IAEjC,OAAO;QACL5C,OAAOJ,SAASK,UAAU,CAAC2B,MAAM,GAAG,OAAO;YAAEtC;YAAQG;QAAQ;QAC7DQ,YAAYL,SAASK,UAAU;QAC/BI,WAAWT,SAASS,SAAS;IAC/B;AACF"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
|
|
17
17
|
import { aiFreeCreditsResetOn } from "../model/ai-site-job.js";
|
|
18
|
+
import { aiUsageMeterState } from "./ai-usage-wire.js";
|
|
18
19
|
import { assistCreditsFromUsd, resolveAssistCreditBudget } from "./assist-credits.js";
|
|
19
20
|
import { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from "./assist-credit-returns.js";
|
|
20
21
|
import { freeAccountUsageRef, freeAssistAccount } from "./assist-free-taste.js";
|
|
@@ -42,15 +43,38 @@ import { freeAccountUsageRef, freeAssistAccount } from "./assist-free-taste.js";
|
|
|
42
43
|
account.accountUid ? freeAccountUsageRef(firestore, account.accountUid, month).get() : Promise.resolve(null)
|
|
43
44
|
]);
|
|
44
45
|
const pending = Number.isFinite(input.pendingUsd) && ((_input_pendingUsd = input.pendingUsd) != null ? _input_pendingUsd : 0) > 0 ? input.pendingUsd : 0;
|
|
45
|
-
const
|
|
46
|
+
const spend = {
|
|
46
47
|
accountSpentUsd: accountMonth ? assistSpendAfterReturnsUsd(accountMonth.get('estCostUsd'), accountMonth.get(ASSIST_RETURNED_USD_FIELD)) + pending : null,
|
|
47
48
|
orgSpentUsd: assistSpendAfterReturnsUsd(orgMonth.get('estCostUsd'), orgMonth.get(ASSIST_RETURNED_USD_FIELD)) + pending,
|
|
48
49
|
orgBandCredits: resolveAssistCreditBudget(org)
|
|
50
|
+
};
|
|
51
|
+
const left = freeAiCreditsLeftFrom(spend);
|
|
52
|
+
const accountCredits = spend.accountSpentUsd === null ? null : assistCreditsFromUsd(spend.accountSpentUsd);
|
|
53
|
+
const orgCredits = assistCreditsFromUsd(spend.orgSpentUsd);
|
|
54
|
+
const state = aiUsageMeterState({
|
|
55
|
+
mine: {
|
|
56
|
+
used: accountCredits != null ? accountCredits : orgCredits,
|
|
57
|
+
limit: FREE_AI_TASTE_CREDITS_PER_MONTH,
|
|
58
|
+
mode: 'hard',
|
|
59
|
+
scope: null,
|
|
60
|
+
free: true
|
|
61
|
+
},
|
|
62
|
+
pool: {
|
|
63
|
+
used: orgCredits,
|
|
64
|
+
limit: spend.orgBandCredits
|
|
65
|
+
},
|
|
66
|
+
refused: left <= 0
|
|
49
67
|
});
|
|
50
68
|
return {
|
|
51
69
|
left,
|
|
52
70
|
total: FREE_AI_TASTE_CREDITS_PER_MONTH,
|
|
53
|
-
resetsOn: aiFreeCreditsResetOn(input.now)
|
|
71
|
+
resetsOn: aiFreeCreditsResetOn(input.now),
|
|
72
|
+
used: {
|
|
73
|
+
account: accountCredits,
|
|
74
|
+
org: orgCredits,
|
|
75
|
+
orgBand: spend.orgBandCredits,
|
|
76
|
+
state
|
|
77
|
+
}
|
|
54
78
|
};
|
|
55
79
|
} catch (error) {
|
|
56
80
|
console.error('free ai credits read failed', {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/free-ai-credits-left.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\nimport { aiFreeCreditsResetOn, type AiFreeCreditsLeft } from '../model/ai-site-job'\nimport { assistCreditsFromUsd, resolveAssistCreditBudget } from './assist-credits'\nimport { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from './assist-credit-returns'\nimport {\n freeAccountUsageRef,\n freeAssistAccount,\n type AssistMeteredOrg,\n} from './assist-free-taste'\n\n/**\n * WHAT A FREE WORKSPACE HAS LEFT, BEFORE IT SPENDS (AGL-3660).\n *\n * The reservation refuses a Free step at the FIRST of two walls it reaches:\n * the workspace's own band (`orgs/{orgId}/assistUsage/{month}`) and its\n * owner's allowance, which every Free workspace the owner holds draws on\n * (`users/{uid}/aiUsage/{month}`). A door that asks before a job starts —\n * the guided start's dialog, the create door behind it — has to read the\n * same two figures, net of give-backs, or it quotes the 300 a fresh\n * workspace has to a person whose other workspace spent 230 of it, and a\n * site start runs out between its form and its first page.\n *\n * A read, not a reservation: the step's own reservation still decides, inside\n * its transaction. So a read that fails answers `null` — nothing known — and\n * the door admits, as `aiBuildFreeBalanceRefusal` does.\n */\n\n/** Both balances' spend, in billed USD, and the workspace's band in credits. */\nexport interface FreeAiSpend {\n /** The owner's month across all their Free workspaces; `null` for a workspace with no owner. */\n accountSpentUsd: number | null\n /** This workspace's month. */\n orgSpentUsd: number\n /** This workspace's band, or `null` when its plan resolves none. */\n orgBandCredits: number | null\n}\n\n/** The less of the two balances, in whole credits, never below zero. */\nexport function freeAiCreditsLeftFrom(spend: FreeAiSpend): number {\n const balances = [\n spend.accountSpentUsd === null\n ? null\n : FREE_AI_TASTE_CREDITS_PER_MONTH - assistCreditsFromUsd(spend.accountSpentUsd),\n spend.orgBandCredits === null ? null : spend.orgBandCredits - assistCreditsFromUsd(spend.orgSpentUsd),\n ].filter((balance): balance is number => balance !== null)\n if (!balances.length) return FREE_AI_TASTE_CREDITS_PER_MONTH\n return Math.max(0, Math.min(...balances))\n}\n\n/**\n * What a Free workspace has left this month, or `null` for a paid workspace\n * (whose band is not a wall a job can be stopped by before it is told) and\n * for a read that failed.\n */\nexport async function readFreeAiCreditsLeft(\n firestore: FirebaseFirestore.Firestore,\n input: {\n orgId: string\n org: object | null\n now: Date\n /**\n * Spend already made but not yet on either meter, in billed USD: the plan\n * step asks what is left from inside the step, before the machine meters\n * what that step just spent (AGL-3722). Without it the plan card quoted\n * 101 left when the plan's own 54 credits made it 48.\n */\n pendingUsd?: number\n },\n): Promise<AiFreeCreditsLeft | null> {\n const org = input.org as AssistMeteredOrg | null\n const account = freeAssistAccount(org)\n if (!account) return null\n try {\n const month = input.now.toISOString().slice(0, 7)\n const [orgMonth, accountMonth] = await Promise.all([\n firestore.collection('orgs').doc(input.orgId).collection('assistUsage').doc(month).get(),\n account.accountUid ? freeAccountUsageRef(firestore, account.accountUid, month).get() : Promise.resolve(null),\n ])\n const pending = Number.isFinite(input.pendingUsd) && (input.pendingUsd ?? 0) > 0 ? (input.pendingUsd as number) : 0\n const
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/free-ai-credits-left.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\nimport { aiFreeCreditsResetOn, type AiFreeCreditsLeft } from '../model/ai-site-job'\nimport { aiUsageMeterState } from './ai-usage-wire'\nimport { assistCreditsFromUsd, resolveAssistCreditBudget } from './assist-credits'\nimport { ASSIST_RETURNED_USD_FIELD, assistSpendAfterReturnsUsd } from './assist-credit-returns'\nimport {\n freeAccountUsageRef,\n freeAssistAccount,\n type AssistMeteredOrg,\n} from './assist-free-taste'\n\n/**\n * WHAT A FREE WORKSPACE HAS LEFT, BEFORE IT SPENDS (AGL-3660).\n *\n * The reservation refuses a Free step at the FIRST of two walls it reaches:\n * the workspace's own band (`orgs/{orgId}/assistUsage/{month}`) and its\n * owner's allowance, which every Free workspace the owner holds draws on\n * (`users/{uid}/aiUsage/{month}`). A door that asks before a job starts —\n * the guided start's dialog, the create door behind it — has to read the\n * same two figures, net of give-backs, or it quotes the 300 a fresh\n * workspace has to a person whose other workspace spent 230 of it, and a\n * site start runs out between its form and its first page.\n *\n * A read, not a reservation: the step's own reservation still decides, inside\n * its transaction. So a read that fails answers `null` — nothing known — and\n * the door admits, as `aiBuildFreeBalanceRefusal` does.\n */\n\n/** Both balances' spend, in billed USD, and the workspace's band in credits. */\nexport interface FreeAiSpend {\n /** The owner's month across all their Free workspaces; `null` for a workspace with no owner. */\n accountSpentUsd: number | null\n /** This workspace's month. */\n orgSpentUsd: number\n /** This workspace's band, or `null` when its plan resolves none. */\n orgBandCredits: number | null\n}\n\n/** The less of the two balances, in whole credits, never below zero. */\nexport function freeAiCreditsLeftFrom(spend: FreeAiSpend): number {\n const balances = [\n spend.accountSpentUsd === null\n ? null\n : FREE_AI_TASTE_CREDITS_PER_MONTH - assistCreditsFromUsd(spend.accountSpentUsd),\n spend.orgBandCredits === null ? null : spend.orgBandCredits - assistCreditsFromUsd(spend.orgSpentUsd),\n ].filter((balance): balance is number => balance !== null)\n if (!balances.length) return FREE_AI_TASTE_CREDITS_PER_MONTH\n return Math.max(0, Math.min(...balances))\n}\n\n/**\n * What a Free workspace has left this month, or `null` for a paid workspace\n * (whose band is not a wall a job can be stopped by before it is told) and\n * for a read that failed.\n */\nexport async function readFreeAiCreditsLeft(\n firestore: FirebaseFirestore.Firestore,\n input: {\n orgId: string\n org: object | null\n now: Date\n /**\n * Spend already made but not yet on either meter, in billed USD: the plan\n * step asks what is left from inside the step, before the machine meters\n * what that step just spent (AGL-3722). Without it the plan card quoted\n * 101 left when the plan's own 54 credits made it 48.\n */\n pendingUsd?: number\n },\n): Promise<AiFreeCreditsLeft | null> {\n const org = input.org as AssistMeteredOrg | null\n const account = freeAssistAccount(org)\n if (!account) return null\n try {\n const month = input.now.toISOString().slice(0, 7)\n const [orgMonth, accountMonth] = await Promise.all([\n firestore.collection('orgs').doc(input.orgId).collection('assistUsage').doc(month).get(),\n account.accountUid ? freeAccountUsageRef(firestore, account.accountUid, month).get() : Promise.resolve(null),\n ])\n const pending = Number.isFinite(input.pendingUsd) && (input.pendingUsd ?? 0) > 0 ? (input.pendingUsd as number) : 0\n const spend: FreeAiSpend = {\n accountSpentUsd: accountMonth\n ? assistSpendAfterReturnsUsd(accountMonth.get('estCostUsd'), accountMonth.get(ASSIST_RETURNED_USD_FIELD)) +\n pending\n : null,\n orgSpentUsd:\n assistSpendAfterReturnsUsd(orgMonth.get('estCostUsd'), orgMonth.get(ASSIST_RETURNED_USD_FIELD)) + pending,\n orgBandCredits: resolveAssistCreditBudget(org),\n }\n const left = freeAiCreditsLeftFrom(spend)\n const accountCredits = spend.accountSpentUsd === null ? null : assistCreditsFromUsd(spend.accountSpentUsd)\n const orgCredits = assistCreditsFromUsd(spend.orgSpentUsd)\n const state = aiUsageMeterState({\n mine: { used: accountCredits ?? orgCredits, limit: FREE_AI_TASTE_CREDITS_PER_MONTH, mode: 'hard', scope: null, free: true },\n pool: { used: orgCredits, limit: spend.orgBandCredits },\n refused: left <= 0,\n })\n return {\n left,\n total: FREE_AI_TASTE_CREDITS_PER_MONTH,\n resetsOn: aiFreeCreditsResetOn(input.now),\n used: { account: accountCredits, org: orgCredits, orgBand: spend.orgBandCredits, state },\n }\n } catch (error) {\n console.error('free ai credits read failed', { orgId: input.orgId, error })\n return null\n }\n}\n"],"names":["FREE_AI_TASTE_CREDITS_PER_MONTH","aiFreeCreditsResetOn","aiUsageMeterState","assistCreditsFromUsd","resolveAssistCreditBudget","ASSIST_RETURNED_USD_FIELD","assistSpendAfterReturnsUsd","freeAccountUsageRef","freeAssistAccount","freeAiCreditsLeftFrom","spend","balances","accountSpentUsd","orgBandCredits","orgSpentUsd","filter","balance","length","Math","max","min","readFreeAiCreditsLeft","firestore","input","org","account","month","now","toISOString","slice","orgMonth","accountMonth","Promise","all","collection","doc","orgId","get","accountUid","resolve","pending","Number","isFinite","pendingUsd","left","accountCredits","orgCredits","state","mine","used","limit","mode","scope","free","pool","refused","total","resetsOn","orgBand","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,+BAA+B,QAAQ,0BAAsB;AACtE,SAASC,oBAAoB,QAAgC,0BAAsB;AACnF,SAASC,iBAAiB,QAAQ,qBAAiB;AACnD,SAASC,oBAAoB,EAAEC,yBAAyB,QAAQ,sBAAkB;AAClF,SAASC,yBAAyB,EAAEC,0BAA0B,QAAQ,6BAAyB;AAC/F,SACEC,mBAAmB,EACnBC,iBAAiB,QAEZ,yBAAqB;AA6B5B,sEAAsE,GACtE,OAAO,SAASC,sBAAsBC,KAAkB;IACtD,MAAMC,WAAW;QACfD,MAAME,eAAe,KAAK,OACtB,OACAZ,kCAAkCG,qBAAqBO,MAAME,eAAe;QAChFF,MAAMG,cAAc,KAAK,OAAO,OAAOH,MAAMG,cAAc,GAAGV,qBAAqBO,MAAMI,WAAW;KACrG,CAACC,MAAM,CAAC,CAACC,UAA+BA,YAAY;IACrD,IAAI,CAACL,SAASM,MAAM,EAAE,OAAOjB;IAC7B,OAAOkB,KAAKC,GAAG,CAAC,GAAGD,KAAKE,GAAG,IAAIT;AACjC;AAEA;;;;CAIC,GACD,OAAO,eAAeU,sBACpBC,SAAsC,EACtCC,KAWC;IAED,MAAMC,MAAMD,MAAMC,GAAG;IACrB,MAAMC,UAAUjB,kBAAkBgB;IAClC,IAAI,CAACC,SAAS,OAAO;IACrB,IAAI;YAMoDF;QALtD,MAAMG,QAAQH,MAAMI,GAAG,CAACC,WAAW,GAAGC,KAAK,CAAC,GAAG;QAC/C,MAAM,CAACC,UAAUC,aAAa,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACjDX,UAAUY,UAAU,CAAC,QAAQC,GAAG,CAACZ,MAAMa,KAAK,EAAEF,UAAU,CAAC,eAAeC,GAAG,CAACT,OAAOW,GAAG;YACtFZ,QAAQa,UAAU,GAAG/B,oBAAoBe,WAAWG,QAAQa,UAAU,EAAEZ,OAAOW,GAAG,KAAKL,QAAQO,OAAO,CAAC;SACxG;QACD,MAAMC,UAAUC,OAAOC,QAAQ,CAACnB,MAAMoB,UAAU,KAAK,EAACpB,oBAAAA,MAAMoB,UAAU,YAAhBpB,oBAAoB,KAAK,IAAKA,MAAMoB,UAAU,GAAc;QAClH,MAAMjC,QAAqB;YACzBE,iBAAiBmB,eACbzB,2BAA2ByB,aAAaM,GAAG,CAAC,eAAeN,aAAaM,GAAG,CAAChC,8BAC5EmC,UACA;YACJ1B,aACER,2BAA2BwB,SAASO,GAAG,CAAC,eAAeP,SAASO,GAAG,CAAChC,8BAA8BmC;YACpG3B,gBAAgBT,0BAA0BoB;QAC5C;QACA,MAAMoB,OAAOnC,sBAAsBC;QACnC,MAAMmC,iBAAiBnC,MAAME,eAAe,KAAK,OAAO,OAAOT,qBAAqBO,MAAME,eAAe;QACzG,MAAMkC,aAAa3C,qBAAqBO,MAAMI,WAAW;QACzD,MAAMiC,QAAQ7C,kBAAkB;YAC9B8C,MAAM;gBAAEC,IAAI,EAAEJ,yBAAAA,iBAAkBC;gBAAYI,OAAOlD;gBAAiCmD,MAAM;gBAAQC,OAAO;gBAAMC,MAAM;YAAK;YAC1HC,MAAM;gBAAEL,MAAMH;gBAAYI,OAAOxC,MAAMG,cAAc;YAAC;YACtD0C,SAASX,QAAQ;QACnB;QACA,OAAO;YACLA;YACAY,OAAOxD;YACPyD,UAAUxD,qBAAqBsB,MAAMI,GAAG;YACxCsB,MAAM;gBAAExB,SAASoB;gBAAgBrB,KAAKsB;gBAAYY,SAAShD,MAAMG,cAAc;gBAAEkC;YAAM;QACzF;IACF,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,+BAA+B;YAAEvB,OAAOb,MAAMa,KAAK;YAAEuB;QAAM;QACzE,OAAO;IACT;AACF"}
|