@aglyn/plugins-ai 1.0.0-beta.237 → 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.
- package/package.json +17 -17
- package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
- package/src/lib/components/ai-credits-prompt.component.js +84 -34
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
- package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
- package/src/lib/components/ai-job-cancel.component.js +156 -0
- package/src/lib/components/ai-job-cancel.component.js.map +1 -0
- 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-plan.component.js +12 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +141 -72
- package/src/lib/components/ai-jobs-list-page.component.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-site-build-page.component.js +22 -2
- package/src/lib/components/ai-site-build-page.component.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/ai-usage-strip.component.d.ts +19 -2
- package/src/lib/components/ai-usage-strip.component.js +40 -9
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +12 -16
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +91 -51
- 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 +33 -3
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
- package/src/lib/jobs/ai-job-cancel.js +102 -0
- package/src/lib/jobs/ai-job-cancel.js.map +1 -0
- package/src/lib/jobs/ai-job-component-step.js +4 -1
- package/src/lib/jobs/ai-job-component-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-drafts.js +2 -0
- package/src/lib/jobs/ai-job-drafts.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 +41 -10
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.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 +45 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +12 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +38 -1
- package/src/lib/jobs/ai-job-plan-step.js +90 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
- package/src/lib/jobs/ai-job-site-content.js +66 -7
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
- package/src/lib/jobs/ai-job-site-datasets.js +499 -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 +152 -30
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +4 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +37 -12
- package/src/lib/jobs/ai-jobs.js +184 -28
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
- package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
- 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/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -0
- package/src/lib/layout-language/ai-layout-compiler.js +397 -9
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
- package/src/lib/layout-language/ai-layout-design.js +5 -3
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -3
- package/src/lib/layout-language/ai-layout-frame.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 +44 -3
- package/src/lib/layout-language/ai-layout-listings.js +118 -50
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
- package/src/lib/layout-language/ai-layout-storefront.js +223 -0
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +11 -1
- package/src/lib/model/ai-build-job.js +35 -6
- 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-build-progress.d.ts +2 -2
- package/src/lib/model/ai-build-progress.js +6 -4
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +69 -2
- package/src/lib/model/ai-credit-estimate.js +91 -3
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
- package/src/lib/model/ai-job-cancel-copy.js +53 -0
- package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
- package/src/lib/model/ai-job-failure-copy.js +8 -3
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +20 -0
- package/src/lib/model/ai-jobs.types.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-build-progress.js +7 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +38 -4
- package/src/lib/model/ai-site-job.js +50 -14
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +4 -1
- package/src/lib/model/ai-site-kinds.js +34 -6
- package/src/lib/model/ai-site-kinds.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 +77 -1
- package/src/lib/runtime/ai-doctrine-validators.js +280 -6
- 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/runtime/ai-palette.generated.js +53 -7
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
- package/src/lib/server/ai-jobs-cancel.js +27 -9
- package/src/lib/server/ai-jobs-cancel.js.map +1 -1
- package/src/lib/server/ai-jobs-gate.d.ts +12 -2
- package/src/lib/server/ai-jobs-gate.js +20 -9
- package/src/lib/server/ai-jobs-gate.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/ai-usage-meter.d.ts +2 -1
- package/src/lib/usage/ai-usage-meter.js +17 -1
- package/src/lib/usage/ai-usage-meter.js.map +1 -1
- package/src/lib/usage/ai-usage-wire.d.ts +6 -0
- package/src/lib/usage/ai-usage-wire.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +7 -0
- package/src/lib/usage/assist-usage.js +14 -4
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
- package/src/lib/usage/free-ai-credits-left.js +30 -4
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
|
-
* Cancel a job (AGL-2904).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* Cancel a job (AGL-2904, AGL-3616). `POST /api/ai/jobs/{jobId}/cancel`
|
|
19
|
+
* `{ orgId }`.
|
|
20
|
+
*
|
|
21
|
+
* Who may: a member who holds `ai.generate` on the job's site (the
|
|
22
|
+
* permission that started it), or staff, member or not. Anyone else is
|
|
23
|
+
* refused 403 and the job is untouched.
|
|
24
|
+
*
|
|
25
|
+
* What it does is `cancelAiJob`'s, in one transaction: a job no step holds
|
|
26
|
+
* ends `canceled` now; a job whose step is in flight records the cancel and
|
|
27
|
+
* ends `canceled` when that step finishes or aborts, which the answer's
|
|
28
|
+
* `job.cancelRequested` says. Idempotent: a job already done, failed or
|
|
29
|
+
* canceled answers as it is, `changed: false`. Audited per cancel that
|
|
30
|
+
* changed something.
|
|
23
31
|
*/
|
|
24
32
|
export declare function POST(request: Request, context: {
|
|
25
33
|
params: Promise<{
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -15,13 +16,22 @@
|
|
|
15
16
|
* limitations under the License.
|
|
16
17
|
*/ // lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts
|
|
17
18
|
import { aiJobSummary, cancelAiJob, getAiJob, writeAiJobAudit } from "../jobs/ai-jobs.js";
|
|
19
|
+
import { memberHasPermissionOnHost, permissionRefusal } from "@aglyn/tenant-data-admin/server/organizations";
|
|
18
20
|
import { aiJobsGate } from "./ai-jobs-gate.js";
|
|
19
21
|
/**
|
|
20
|
-
* Cancel a job (AGL-2904).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Cancel a job (AGL-2904, AGL-3616). `POST /api/ai/jobs/{jobId}/cancel`
|
|
23
|
+
* `{ orgId }`.
|
|
24
|
+
*
|
|
25
|
+
* Who may: a member who holds `ai.generate` on the job's site (the
|
|
26
|
+
* permission that started it), or staff, member or not. Anyone else is
|
|
27
|
+
* refused 403 and the job is untouched.
|
|
28
|
+
*
|
|
29
|
+
* What it does is `cancelAiJob`'s, in one transaction: a job no step holds
|
|
30
|
+
* ends `canceled` now; a job whose step is in flight records the cancel and
|
|
31
|
+
* ends `canceled` when that step finishes or aborts, which the answer's
|
|
32
|
+
* `job.cancelRequested` says. Idempotent: a job already done, failed or
|
|
33
|
+
* canceled answers as it is, `changed: false`. Audited per cancel that
|
|
34
|
+
* changed something.
|
|
25
35
|
*/ export async function POST(request, context) {
|
|
26
36
|
var _ref, _gate_decoded_email;
|
|
27
37
|
let payload;
|
|
@@ -30,7 +40,9 @@ import { aiJobsGate } from "./ai-jobs-gate.js";
|
|
|
30
40
|
} catch (unused) {
|
|
31
41
|
payload = null;
|
|
32
42
|
}
|
|
33
|
-
const gate = await aiJobsGate(request, String((_ref = payload == null ? void 0 : payload.orgId) != null ? _ref : '')
|
|
43
|
+
const gate = await aiJobsGate(request, String((_ref = payload == null ? void 0 : payload.orgId) != null ? _ref : ''), {
|
|
44
|
+
staffWithoutMembership: true
|
|
45
|
+
});
|
|
34
46
|
if (gate instanceof Response) return gate;
|
|
35
47
|
const { jobId } = await context.params;
|
|
36
48
|
const existing = await getAiJob(gate.firestore, gate.orgId, jobId);
|
|
@@ -39,6 +51,10 @@ import { aiJobsGate } from "./ai-jobs-gate.js";
|
|
|
39
51
|
}, {
|
|
40
52
|
status: 404
|
|
41
53
|
});
|
|
54
|
+
// The job's own permission, on the job's own site (AGL-3616).
|
|
55
|
+
if (!gate.staff && !await memberHasPermissionOnHost(gate.orgId, existing.hostId, gate.member, 'ai.generate')) {
|
|
56
|
+
return permissionRefusal('ai.generate');
|
|
57
|
+
}
|
|
42
58
|
const now = new Date();
|
|
43
59
|
// The machine writes the org feed's row with this actor when the cancel
|
|
44
60
|
// changed something; the staff audit row below is this route's own.
|
|
@@ -54,11 +70,13 @@ import { aiJobsGate } from "./ai-jobs-gate.js";
|
|
|
54
70
|
actorEmail: (_gate_decoded_email1 = gate.decoded.email) != null ? _gate_decoded_email1 : null,
|
|
55
71
|
orgId: gate.orgId,
|
|
56
72
|
jobId,
|
|
57
|
-
after: {
|
|
58
|
-
status:
|
|
73
|
+
after: _extends({
|
|
74
|
+
status: job.status,
|
|
59
75
|
wasStatus: existing.status,
|
|
60
76
|
kind: existing.kind
|
|
61
|
-
}
|
|
77
|
+
}, gate.staff ? {
|
|
78
|
+
staff: true
|
|
79
|
+
} : {})
|
|
62
80
|
});
|
|
63
81
|
}
|
|
64
82
|
return Response.json({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-cancel.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\n// lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts\n\nimport {\n aiJobSummary,\n cancelAiJob,\n getAiJob,\n writeAiJobAudit,\n} from '../jobs/ai-jobs'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * Cancel a job (AGL-2904).
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-cancel.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\n// lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts\n\nimport {\n aiJobSummary,\n cancelAiJob,\n getAiJob,\n writeAiJobAudit,\n} from '../jobs/ai-jobs'\nimport { memberHasPermissionOnHost, permissionRefusal } from '@aglyn/tenant-data-admin/server/organizations'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * Cancel a job (AGL-2904, AGL-3616). `POST /api/ai/jobs/{jobId}/cancel`\n * `{ orgId }`.\n *\n * Who may: a member who holds `ai.generate` on the job's site (the\n * permission that started it), or staff, member or not. Anyone else is\n * refused 403 and the job is untouched.\n *\n * What it does is `cancelAiJob`'s, in one transaction: a job no step holds\n * ends `canceled` now; a job whose step is in flight records the cancel and\n * ends `canceled` when that step finishes or aborts, which the answer's\n * `job.cancelRequested` says. Idempotent: a job already done, failed or\n * canceled answers as it is, `changed: false`. Audited per cancel that\n * changed something.\n */\nexport async function POST(\n request: Request,\n context: { params: Promise<{ jobId: string }> },\n): Promise<Response> {\n let payload: Record<string, unknown> | null\n try {\n payload = (await request.json()) as Record<string, unknown>\n } catch {\n payload = null\n }\n const gate = await aiJobsGate(request, String(payload?.orgId ?? ''), { staffWithoutMembership: true })\n if (gate instanceof Response) return gate\n const { jobId } = await context.params\n const existing = await getAiJob(gate.firestore, gate.orgId, jobId)\n if (!existing) return Response.json({ error: 'Unknown job' }, { status: 404 })\n // The job's own permission, on the job's own site (AGL-3616).\n if (!gate.staff && !(await memberHasPermissionOnHost(gate.orgId, existing.hostId, gate.member, 'ai.generate'))) {\n return permissionRefusal('ai.generate')\n }\n const now = new Date()\n // The machine writes the org feed's row with this actor when the cancel\n // changed something; the staff audit row below is this route's own.\n const { job, changed } = await cancelAiJob(gate.firestore, gate.orgId, jobId, now, {\n uid: gate.uid,\n email: gate.decoded.email ?? null,\n })\n if (changed) {\n await writeAiJobAudit(gate.firestore, {\n action: 'ai.job.cancel',\n actorUid: gate.uid,\n actorEmail: gate.decoded.email ?? null,\n orgId: gate.orgId,\n jobId,\n after: {\n status: job.status,\n wasStatus: existing.status,\n kind: existing.kind,\n ...(gate.staff ? { staff: true } : {}),\n },\n })\n }\n return Response.json({ job: aiJobSummary(job, now), changed }, { status: 200 })\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["aiJobSummary","cancelAiJob","getAiJob","writeAiJobAudit","memberHasPermissionOnHost","permissionRefusal","aiJobsGate","POST","request","context","gate","payload","json","String","orgId","staffWithoutMembership","Response","jobId","params","existing","firestore","error","status","staff","hostId","member","now","Date","job","changed","uid","email","decoded","action","actorUid","actorEmail","after","wasStatus","kind","dynamic","maxDuration"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,mEAAmE;AAEnE,SACEA,YAAY,EACZC,WAAW,EACXC,QAAQ,EACRC,eAAe,QACV,qBAAiB;AACxB,SAASC,yBAAyB,EAAEC,iBAAiB,QAAQ,gDAA+C;AAC5G,SAASC,UAAU,QAAQ,oBAAgB;AAE3C;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,KACpBC,OAAgB,EAChBC,OAA+C;cAsBtCC;IApBT,IAAIC;IACJ,IAAI;QACFA,UAAW,MAAMH,QAAQI,IAAI;IAC/B,EAAE,eAAM;QACND,UAAU;IACZ;IACA,MAAMD,OAAO,MAAMJ,WAAWE,SAASK,eAAOF,2BAAAA,QAASG,KAAK,mBAAI,KAAK;QAAEC,wBAAwB;IAAK;IACpG,IAAIL,gBAAgBM,UAAU,OAAON;IACrC,MAAM,EAAEO,KAAK,EAAE,GAAG,MAAMR,QAAQS,MAAM;IACtC,MAAMC,WAAW,MAAMjB,SAASQ,KAAKU,SAAS,EAAEV,KAAKI,KAAK,EAAEG;IAC5D,IAAI,CAACE,UAAU,OAAOH,SAASJ,IAAI,CAAC;QAAES,OAAO;IAAc,GAAG;QAAEC,QAAQ;IAAI;IAC5E,8DAA8D;IAC9D,IAAI,CAACZ,KAAKa,KAAK,IAAI,CAAE,MAAMnB,0BAA0BM,KAAKI,KAAK,EAAEK,SAASK,MAAM,EAAEd,KAAKe,MAAM,EAAE,gBAAiB;QAC9G,OAAOpB,kBAAkB;IAC3B;IACA,MAAMqB,MAAM,IAAIC;IAChB,wEAAwE;IACxE,oEAAoE;IACpE,MAAM,EAAEC,GAAG,EAAEC,OAAO,EAAE,GAAG,MAAM5B,YAAYS,KAAKU,SAAS,EAAEV,KAAKI,KAAK,EAAEG,OAAOS,KAAK;QACjFI,KAAKpB,KAAKoB,GAAG;QACbC,KAAK,GAAErB,sBAAAA,KAAKsB,OAAO,CAACD,KAAK,YAAlBrB,sBAAsB;IAC/B;IACA,IAAImB,SAAS;YAIGnB;QAHd,MAAMP,gBAAgBO,KAAKU,SAAS,EAAE;YACpCa,QAAQ;YACRC,UAAUxB,KAAKoB,GAAG;YAClBK,UAAU,GAAEzB,uBAAAA,KAAKsB,OAAO,CAACD,KAAK,YAAlBrB,uBAAsB;YAClCI,OAAOJ,KAAKI,KAAK;YACjBG;YACAmB,OAAO;gBACLd,QAAQM,IAAIN,MAAM;gBAClBe,WAAWlB,SAASG,MAAM;gBAC1BgB,MAAMnB,SAASmB,IAAI;eACf5B,KAAKa,KAAK,GAAG;gBAAEA,OAAO;YAAK,IAAI,CAAC;QAExC;IACF;IACA,OAAOP,SAASJ,IAAI,CAAC;QAAEgB,KAAK5B,aAAa4B,KAAKF;QAAMG;IAAQ,GAAG;QAAEP,QAAQ;IAAI;AAC/E;AAEA,OAAO,MAAMiB,UAAU,gBAAe;AACtC,OAAO,MAAMC,cAAc,GAAE"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { DecodedIdToken } from 'firebase-admin/auth';
|
|
18
|
-
import type { AglynOrganization } from '@aglyn/aglyn/foundation/definitions/organization.types';
|
|
18
|
+
import type { AglynOrganization, AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types';
|
|
19
19
|
/**
|
|
20
20
|
* The gate the READ and CANCEL doors of AI jobs climb (AGL-2904).
|
|
21
21
|
*
|
|
@@ -42,6 +42,16 @@ export interface AiJobsGateContext {
|
|
|
42
42
|
staff: boolean;
|
|
43
43
|
orgId: string;
|
|
44
44
|
org: Partial<AglynOrganization>;
|
|
45
|
+
/** The caller's membership; `null` only for staff let in by `staffWithoutMembership`. */
|
|
46
|
+
member: AglynOrgMember | null;
|
|
45
47
|
firestore: FirebaseFirestore.Firestore;
|
|
46
48
|
}
|
|
47
|
-
export
|
|
49
|
+
export interface AiJobsGateOptions {
|
|
50
|
+
/**
|
|
51
|
+
* A verified staff claim passes the membership rung for an org it is not a
|
|
52
|
+
* member of (AGL-3616): staff stop a customer's job from the job's own
|
|
53
|
+
* page. Every rung after it is climbed as for anyone else.
|
|
54
|
+
*/
|
|
55
|
+
staffWithoutMembership?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function aiJobsGate(request: Request, rawOrgId: string, options?: AiJobsGateOptions): Promise<Response | AiJobsGateContext>;
|
|
@@ -28,8 +28,7 @@ function bearerToken(request) {
|
|
|
28
28
|
const authorization = (_request_headers_get = request.headers.get('authorization')) != null ? _request_headers_get : '';
|
|
29
29
|
return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
30
30
|
}
|
|
31
|
-
export async function aiJobsGate(request, rawOrgId) {
|
|
32
|
-
var _resolved_org;
|
|
31
|
+
export async function aiJobsGate(request, rawOrgId, options = {}) {
|
|
33
32
|
const idToken = bearerToken(request);
|
|
34
33
|
if (!idToken) {
|
|
35
34
|
return Response.json({
|
|
@@ -63,14 +62,25 @@ export async function aiJobsGate(request, rawOrgId) {
|
|
|
63
62
|
});
|
|
64
63
|
}
|
|
65
64
|
const resolved = await getOrgForUser(decoded.uid, orgId);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
let org;
|
|
66
|
+
let member;
|
|
67
|
+
if (resolved && resolved.orgId === orgId) {
|
|
68
|
+
var _resolved_org, _resolved_member;
|
|
69
|
+
org = (_resolved_org = resolved.org) != null ? _resolved_org : {};
|
|
70
|
+
member = (_resolved_member = resolved.member) != null ? _resolved_member : null;
|
|
71
|
+
} else {
|
|
72
|
+
var _staffOrg_data;
|
|
73
|
+
const staffOrg = staff && options.staffWithoutMembership ? await app.firestore().collection('orgs').doc(orgId).get() : null;
|
|
74
|
+
if (!(staffOrg == null ? void 0 : staffOrg.exists)) {
|
|
75
|
+
return Response.json({
|
|
76
|
+
error: 'You are not a member of that organization'
|
|
77
|
+
}, {
|
|
78
|
+
status: 403
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
org = (_staffOrg_data = staffOrg.data()) != null ? _staffOrg_data : {};
|
|
82
|
+
member = null;
|
|
72
83
|
}
|
|
73
|
-
const org = (_resolved_org = resolved.org) != null ? _resolved_org : {};
|
|
74
84
|
if (!staff && !await isServerReleaseFlagOnForOrg('release_ai_generative', orgId)) {
|
|
75
85
|
return Response.json({
|
|
76
86
|
error: 'Not found'
|
|
@@ -99,6 +109,7 @@ export async function aiJobsGate(request, rawOrgId) {
|
|
|
99
109
|
staff,
|
|
100
110
|
orgId,
|
|
101
111
|
org,
|
|
112
|
+
member,
|
|
102
113
|
firestore: app.firestore()
|
|
103
114
|
};
|
|
104
115
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-gate.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 type { DecodedIdToken } from 'firebase-admin/auth'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport type { AglynOrganization } from '@aglyn/aglyn/foundation/definitions/organization.types'\n// By their own entry points rather than the barrel, for the reason the chat\n// route gives for the runtime: the route specs replace the barrel with a\n// closed-world factory, and a leaf that is mocked at its own seam is one\n// the spec can drive rung by rung.\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getOrgForUser } from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/**\n * The gate the READ and CANCEL doors of AI jobs climb (AGL-2904).\n *\n * `aiGateLadder` is the ladder a door that SPENDS climbs: it is POST-only\n * and ends by taking a reservation, because its whole purpose is to bound\n * provider spend. Listing a workspace's jobs, watching one, or canceling\n * one spends nothing and answers a GET, so those doors climb the same\n * rungs up to the lockdown verdict and stop there — no rate window and no\n * reservation. The rungs are in the ladder's order, for the ladder's\n * reason: each one refused discloses nothing a lower rung would.\n *\n * 401 no bearer token, or a token the verifier refused\n * 403 email unverified (an impersonation session is exempt)\n * 400 the request did not NAME the org (AGL-1934)\n * 403 the caller is not a member of that org\n * 404 the release flag is off — a released-off feature does not exist;\n * a verified staff claim previews through it\n * 403 the org's plan lacks `aiGenerative`\n * 423 lockdown: the scope verdict (platform/org/user)\n */\nexport interface AiJobsGateContext {\n uid: string\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n firestore: FirebaseFirestore.Firestore\n}\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n}\n\nexport async function aiJobsGate(\n request: Request,\n rawOrgId: string,\n): Promise<Response | AiJobsGateContext> {\n const idToken = bearerToken(request)\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n const app = firebaseAdmin.app()\n let decoded: DecodedIdToken\n try {\n decoded = await app.auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's fault and a 401; anything else\n // is ours and propagates so the door answers 500 rather than telling\n // every user their credential is bad during an outage (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n\n const orgId = rawOrgId.trim()\n if (!orgId) {\n return Response.json(\n { error: 'Open a workspace before using this feature' },\n { status: 400 },\n )\n }\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-gate.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 type { DecodedIdToken } from 'firebase-admin/auth'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport type { AglynOrganization, AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\n// By their own entry points rather than the barrel, for the reason the chat\n// route gives for the runtime: the route specs replace the barrel with a\n// closed-world factory, and a leaf that is mocked at its own seam is one\n// the spec can drive rung by rung.\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getOrgForUser } from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/**\n * The gate the READ and CANCEL doors of AI jobs climb (AGL-2904).\n *\n * `aiGateLadder` is the ladder a door that SPENDS climbs: it is POST-only\n * and ends by taking a reservation, because its whole purpose is to bound\n * provider spend. Listing a workspace's jobs, watching one, or canceling\n * one spends nothing and answers a GET, so those doors climb the same\n * rungs up to the lockdown verdict and stop there — no rate window and no\n * reservation. The rungs are in the ladder's order, for the ladder's\n * reason: each one refused discloses nothing a lower rung would.\n *\n * 401 no bearer token, or a token the verifier refused\n * 403 email unverified (an impersonation session is exempt)\n * 400 the request did not NAME the org (AGL-1934)\n * 403 the caller is not a member of that org\n * 404 the release flag is off — a released-off feature does not exist;\n * a verified staff claim previews through it\n * 403 the org's plan lacks `aiGenerative`\n * 423 lockdown: the scope verdict (platform/org/user)\n */\nexport interface AiJobsGateContext {\n uid: string\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n /** The caller's membership; `null` only for staff let in by `staffWithoutMembership`. */\n member: AglynOrgMember | null\n firestore: FirebaseFirestore.Firestore\n}\n\nexport interface AiJobsGateOptions {\n /**\n * A verified staff claim passes the membership rung for an org it is not a\n * member of (AGL-3616): staff stop a customer's job from the job's own\n * page. Every rung after it is climbed as for anyone else.\n */\n staffWithoutMembership?: boolean\n}\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n}\n\nexport async function aiJobsGate(\n request: Request,\n rawOrgId: string,\n options: AiJobsGateOptions = {},\n): Promise<Response | AiJobsGateContext> {\n const idToken = bearerToken(request)\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n const app = firebaseAdmin.app()\n let decoded: DecodedIdToken\n try {\n decoded = await app.auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's fault and a 401; anything else\n // is ours and propagates so the door answers 500 rather than telling\n // every user their credential is bad during an outage (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n\n const orgId = rawOrgId.trim()\n if (!orgId) {\n return Response.json(\n { error: 'Open a workspace before using this feature' },\n { status: 400 },\n )\n }\n const resolved = await getOrgForUser(decoded.uid, orgId)\n let org: Partial<AglynOrganization>\n let member: AglynOrgMember | null\n if (resolved && resolved.orgId === orgId) {\n org = resolved.org ?? {}\n member = resolved.member ?? null\n } else {\n const staffOrg =\n staff && options.staffWithoutMembership\n ? await app.firestore().collection('orgs').doc(orgId).get()\n : null\n if (!staffOrg?.exists) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n org = (staffOrg.data() ?? {}) as Partial<AglynOrganization>\n member = null\n }\n\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n if (!checkEntitlement(org, 'aiGenerative')) {\n return Response.json(\n {\n error: \"This workspace's plan does not include that feature\",\n reason: 'entitlement',\n },\n { status: 403 },\n )\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n decoded,\n staff,\n orgId,\n org,\n member,\n firestore: app.firestore(),\n }\n}\n"],"names":["checkEntitlement","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","lockdownRefusal","getOrgForUser","isServerReleaseFlagOnForOrg","invalidIdTokenResponse","bearerToken","request","authorization","headers","get","startsWith","slice","length","undefined","aiJobsGate","rawOrgId","options","idToken","Response","json","error","status","app","decoded","auth","verifyIdToken","unauthenticated","email_verified","staff","orgId","trim","resolved","uid","org","member","staffOrg","staffWithoutMembership","firestore","collection","doc","exists","data","reason","locked"],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
*/
|
|
17
|
+
import type { AiTool } from '../providers/contract';
|
|
18
|
+
import type { AiGenerationCheckResult } from '../runtime/ai-doctrine';
|
|
19
|
+
/**
|
|
20
|
+
* The strict tool a site's datasets are designed through (AGL-3616), and the
|
|
21
|
+
* check that holds each one to what the site publishes as its own words.
|
|
22
|
+
*
|
|
23
|
+
* A dataset is a menu, a team, a list of services or portfolio pieces: like
|
|
24
|
+
* things, each a record a repeat shows on a page the day the site goes live.
|
|
25
|
+
* So nothing in a record may mark a gap for later (no square brackets), link
|
|
26
|
+
* anywhere, or say what nobody said: no review, testimonial, rating or quote,
|
|
27
|
+
* no price the brief does not state, and the storefront copy rules hold every
|
|
28
|
+
* value (no health, financial or legal promise; no award or certification the
|
|
29
|
+
* brief does not give). The planned field names are kept, in order, because
|
|
30
|
+
* the pages planned around the dataset already name them.
|
|
31
|
+
*/
|
|
32
|
+
export declare const AI_DATASET_TOOL_NAME = "submit_dataset";
|
|
33
|
+
/** The field types a dataset's design may use: the data plugin writer's own words for them. */
|
|
34
|
+
export declare const AI_DATASET_FIELD_TYPES: readonly ["text", "number", "integer", "boolean", "list"];
|
|
35
|
+
export type AiDatasetFieldType = (typeof AI_DATASET_FIELD_TYPES)[number];
|
|
36
|
+
/** A dataset's bounds, as the tool states them and its check holds them. */
|
|
37
|
+
export declare const AI_DATASET_LIMITS: {
|
|
38
|
+
/** The most fields, the planned ones included. */
|
|
39
|
+
readonly fieldsMax: 12;
|
|
40
|
+
/** The most fields it may add to the planned ones. */
|
|
41
|
+
readonly extraFieldsMax: 3;
|
|
42
|
+
readonly fieldNameMax: 40;
|
|
43
|
+
readonly recordsMin: 3;
|
|
44
|
+
readonly recordsMax: 12;
|
|
45
|
+
/** The longest value; a description is a few sentences. */
|
|
46
|
+
readonly valueMax: 400;
|
|
47
|
+
};
|
|
48
|
+
/** What a list value's items are joined by inside one value. */
|
|
49
|
+
export declare const AI_DATASET_LIST_SEPARATOR = "; ";
|
|
50
|
+
/** A designed dataset as its check reads it. */
|
|
51
|
+
export interface AiDataset {
|
|
52
|
+
fields: Array<{
|
|
53
|
+
name: string;
|
|
54
|
+
type: AiDatasetFieldType;
|
|
55
|
+
}>;
|
|
56
|
+
/** Each record's values in field order; an empty string is no value. */
|
|
57
|
+
records: string[][];
|
|
58
|
+
}
|
|
59
|
+
export declare const AI_DATASET_TOOL: AiTool;
|
|
60
|
+
/**
|
|
61
|
+
* A dataset's answer held to its plan and the copy rules. `planned` are the
|
|
62
|
+
* field names the plan gave it; `merchantWords` are the brief and the site's
|
|
63
|
+
* answers, the only source a stated claim or price may come from.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkAiDataset(answer: Record<string, unknown>, context: {
|
|
66
|
+
planned: readonly string[];
|
|
67
|
+
merchantWords: string;
|
|
68
|
+
}): AiGenerationCheckResult<AiDataset>;
|
|
@@ -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":"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"}
|
|
@@ -25,7 +25,8 @@ import { type AiUsageMeterWire } from './ai-usage-wire';
|
|
|
25
25
|
* Every figure is credits. The pool comes through `publicAssistQuota`, the
|
|
26
26
|
* one projection allowed to turn our provider bill into a customer figure;
|
|
27
27
|
* the caller's month comes off the allotment gate, which the meter wrote in
|
|
28
|
-
* credits in the first place
|
|
28
|
+
* credits in the first place — or, on the Free plan, off the person's Free
|
|
29
|
+
* allowance the reservation read, net of give-backs (AGL-3722).
|
|
29
30
|
*
|
|
30
31
|
* `last` is added to both months, because the reservation measured them
|
|
31
32
|
* BEFORE this exchange and the strip is read after it.
|