@aglyn/plugins-ai 1.0.0-beta.230 → 1.0.0-beta.231

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-assist-provider-on-host.component.d.ts +66 -0
  3. package/src/lib/components/ai-assist-provider-on-host.component.js +78 -0
  4. package/src/lib/components/ai-assist-provider-on-host.component.js.map +1 -0
  5. package/src/lib/components/ai-assist-provider.component.js +9 -10
  6. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.js +3 -1
  8. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  9. package/src/lib/components/ai-permissions-on-host.component.d.ts +3 -46
  10. package/src/lib/components/ai-permissions-on-host.component.js +5 -53
  11. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  12. package/src/lib/components/ai-site-build-page.component.js +20 -8
  13. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  14. package/src/lib/jobs/ai-build-unit-outcome.js +4 -2
  15. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  16. package/src/lib/jobs/ai-job-auto-confirm.d.ts +8 -0
  17. package/src/lib/jobs/ai-job-auto-confirm.js +30 -6
  18. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -1
  19. package/src/lib/jobs/ai-job-build-step.js +11 -0
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-generation.d.ts +8 -0
  22. package/src/lib/jobs/ai-job-generation.js +13 -2
  23. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  24. package/src/lib/jobs/ai-job-insight-step.js +8 -1
  25. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-layout-language.d.ts +73 -0
  27. package/src/lib/jobs/ai-job-layout-language.js +194 -0
  28. package/src/lib/jobs/ai-job-layout-language.js.map +1 -0
  29. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +13 -2
  30. package/src/lib/jobs/ai-job-layout-site-pages.js +18 -2
  31. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-step.d.ts +5 -0
  33. package/src/lib/jobs/ai-job-layout-step.js +105 -5
  34. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-page-language.d.ts +149 -0
  36. package/src/lib/jobs/ai-job-page-language.js +355 -0
  37. package/src/lib/jobs/ai-job-page-language.js.map +1 -0
  38. package/src/lib/jobs/ai-job-page-sections.d.ts +37 -1
  39. package/src/lib/jobs/ai-job-page-sections.js +93 -11
  40. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  41. package/src/lib/jobs/ai-job-page-step.d.ts +4 -4
  42. package/src/lib/jobs/ai-job-page-step.js +105 -14
  43. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-plan-step.d.ts +1 -1
  45. package/src/lib/jobs/ai-job-plan-step.js +8 -2
  46. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  47. package/src/lib/jobs/ai-job-site-step.d.ts +4 -0
  48. package/src/lib/jobs/ai-job-site-step.js +20 -3
  49. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  50. package/src/lib/jobs/ai-job-workflow-step.js +2 -2
  51. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  52. package/src/lib/jobs/ai-jobs.d.ts +1 -1
  53. package/src/lib/jobs/ai-jobs.js +17 -8
  54. package/src/lib/jobs/ai-jobs.js.map +1 -1
  55. package/src/lib/jobs/ai-layout-full-height.d.ts +40 -0
  56. package/src/lib/jobs/ai-layout-full-height.js +82 -0
  57. package/src/lib/jobs/ai-layout-full-height.js.map +1 -0
  58. package/src/lib/jobs/ai-layout-site-facts.d.ts +54 -0
  59. package/src/lib/jobs/ai-layout-site-facts.js +153 -0
  60. package/src/lib/jobs/ai-layout-site-facts.js.map +1 -0
  61. package/src/lib/layout-language/ai-layout-compiler.d.ts +145 -0
  62. package/src/lib/layout-language/ai-layout-compiler.js +1148 -0
  63. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -0
  64. package/src/lib/layout-language/ai-layout-copy.d.ts +73 -0
  65. package/src/lib/layout-language/ai-layout-copy.js +137 -0
  66. package/src/lib/layout-language/ai-layout-copy.js.map +1 -0
  67. package/src/lib/layout-language/ai-layout-frame.d.ts +52 -0
  68. package/src/lib/layout-language/ai-layout-frame.js +457 -0
  69. package/src/lib/layout-language/ai-layout-frame.js.map +1 -0
  70. package/src/lib/layout-language/ai-layout-gaps.d.ts +63 -0
  71. package/src/lib/layout-language/ai-layout-gaps.js +345 -0
  72. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -0
  73. package/src/lib/layout-language/ai-layout-language.d.ts +161 -0
  74. package/src/lib/layout-language/ai-layout-language.js +446 -0
  75. package/src/lib/layout-language/ai-layout-language.js.map +1 -0
  76. package/src/lib/layout-language/ai-layout-links.d.ts +80 -0
  77. package/src/lib/layout-language/ai-layout-links.js +109 -0
  78. package/src/lib/layout-language/ai-layout-links.js.map +1 -0
  79. package/src/lib/layout-language/ai-layout-store.d.ts +39 -0
  80. package/src/lib/layout-language/ai-layout-store.js +69 -0
  81. package/src/lib/layout-language/ai-layout-store.js.map +1 -0
  82. package/src/lib/layout-language/ai-layout-tree.d.ts +59 -0
  83. package/src/lib/layout-language/ai-layout-tree.js +84 -0
  84. package/src/lib/layout-language/ai-layout-tree.js.map +1 -0
  85. package/src/lib/lazy-widget.d.ts +45 -0
  86. package/src/lib/lazy-widget.js +53 -0
  87. package/src/lib/lazy-widget.js.map +1 -0
  88. package/src/lib/model/ai-build-job.js +2 -1
  89. package/src/lib/model/ai-build-job.js.map +1 -1
  90. package/src/lib/model/ai-build-progress.d.ts +20 -3
  91. package/src/lib/model/ai-build-progress.js +48 -6
  92. package/src/lib/model/ai-build-progress.js.map +1 -1
  93. package/src/lib/model/ai-host-publish-context.d.ts +51 -0
  94. package/src/lib/model/ai-host-publish-context.js +104 -0
  95. package/src/lib/model/ai-host-publish-context.js.map +1 -0
  96. package/src/lib/model/ai-job-activity.d.ts +22 -0
  97. package/src/lib/model/ai-job-activity.js +22 -1
  98. package/src/lib/model/ai-job-activity.js.map +1 -1
  99. package/src/lib/model/ai-job-failure-copy.d.ts +38 -1
  100. package/src/lib/model/ai-job-failure-copy.js +56 -1
  101. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  102. package/src/lib/model/ai-job-notice.d.ts +3 -1
  103. package/src/lib/model/ai-job-notice.js +31 -9
  104. package/src/lib/model/ai-job-notice.js.map +1 -1
  105. package/src/lib/model/ai-jobs.types.d.ts +19 -0
  106. package/src/lib/model/ai-jobs.types.js.map +1 -1
  107. package/src/lib/model/ai-site-build-progress.d.ts +5 -1
  108. package/src/lib/model/ai-site-build-progress.js +27 -4
  109. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  110. package/src/lib/model/ai-site-job.d.ts +8 -0
  111. package/src/lib/model/ai-site-job.js +9 -0
  112. package/src/lib/model/ai-site-job.js.map +1 -1
  113. package/src/lib/model/ai-site-start-seo.d.ts +1 -1
  114. package/src/lib/model/ai-site-start.d.ts +5 -2
  115. package/src/lib/model/ai-site-start.js +5 -2
  116. package/src/lib/model/ai-site-start.js.map +1 -1
  117. package/src/lib/plugin.js +79 -47
  118. package/src/lib/plugin.js.map +1 -1
  119. package/src/lib/providers/catalog.d.ts +1 -1
  120. package/src/lib/providers/catalog.js +3 -3
  121. package/src/lib/providers/catalog.js.map +1 -1
  122. package/src/lib/runtime/ai-doctrine-validators.d.ts +82 -0
  123. package/src/lib/runtime/ai-doctrine-validators.js +436 -4
  124. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine.js +12 -3
  126. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  127. package/src/lib/runtime/ai-icon-library.d.ts +257 -0
  128. package/src/lib/runtime/ai-icon-library.js +254 -0
  129. package/src/lib/runtime/ai-icon-library.js.map +1 -0
  130. package/src/lib/runtime/ai-node-tree.d.ts +9 -0
  131. package/src/lib/runtime/ai-node-tree.js +30 -4
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.d.ts +6 -0
  134. package/src/lib/runtime/ai-palette.generated.js +70 -0
  135. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  136. package/src/lib/runtime/ai-palette.js.map +1 -1
  137. package/src/lib/tools/ai-workflow-tool.d.ts +6 -2
  138. package/src/lib/tools/ai-workflow-tool.js +16 -14
  139. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
@@ -14,11 +14,32 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { AI_JOB_PLAN_STEP } from "./ai-jobs.types.js";
17
+ /**
18
+ * How long a self-confirming plan may sit parked before it reads as waiting
19
+ * for a person after all: a confirmation that never came.
20
+ */ export const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60000;
21
+ /**
22
+ * Whether a job is parked on a plan the machine is about to confirm itself
23
+ * (AGL-3596). A guided site start records its plan step — parking the job on
24
+ * the plan as every planned job does — and confirms it in the next write, so
25
+ * a surface that reads in between sees `needs_review` for an instant. That
26
+ * instant is the build going on, never a stop: read as one, it showed the
27
+ * person a failed site that turned green again a moment later. The job's
28
+ * `updatedAt` there is the clock its step was claimed at, so the grace covers
29
+ * the plan step's own run as well as the confirmation. Past
30
+ * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is
31
+ * what it says.
32
+ */ export function aiJobConfirmingOwnPlan(job, now = Date.now()) {
33
+ var _job_review;
34
+ if (job.status !== 'needs_review' || ((_job_review = job.review) == null ? void 0 : _job_review.reason) !== 'plan' || job.autoConfirm !== true) return false;
35
+ const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN;
36
+ return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS;
37
+ }
17
38
  export function aiJobPhase(job) {
18
39
  if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {
19
40
  return job.status;
20
41
  }
21
- if (job.status === 'needs_review') {
42
+ if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {
22
43
  var _job_review;
23
44
  return ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'plan' ? 'plan-ready' : 'attention';
24
45
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.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 AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review') {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n"],"names":["AI_JOB_PLAN_STEP","aiJobPhase","job","status","review","reason","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","theme","products","experiment","insight","build","aiJobKindNoun","kind"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-activity.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 AI_JOB_PLAN_STEP,\n type AiJobKind,\n type AiJobReviewReason,\n type AiJobStatus,\n type AiJobStepStatus,\n} from './ai-jobs.types'\n\n/**\n * WHAT A JOB IS DOING, IN THE WORDS EVERY SURFACE SHOWS (AGL-3593).\n *\n * A job's `status` is the machine's word: `needs_review` covers both a plan\n * waiting to be confirmed and an answer that broke a building rule twice, and\n * `queued`/`running` say nothing about whether the job is still planning or\n * already building. The console's surfaces — the AI jobs drawer's header and\n * rows, the top-bar indicator, the Assist launcher's badge, the dialog that\n * started the job, and the notification the machine raises — each tell a\n * person the same thing, so they read it from here and nowhere else. A drawer\n * that counted a refused plan as \"running\" while the chip said \"needs you\" is\n * the disagreement this exists to rule out.\n *\n * Pure, and over the fields a stored job and its wire form share, so the\n * server's notifier and the console read the same answer.\n */\n\n/** The fields a phase is read from: a stored job and its wire summary both carry them. */\nexport interface AiJobPhaseSource {\n status: AiJobStatus\n review?: { reason: AiJobReviewReason } | null\n steps: ReadonlyArray<{ name: string; status: AiJobStepStatus }>\n /** The wire summary's: the machine confirms this job's plan itself. */\n autoConfirm?: boolean\n /** When the job last moved; the wire summary's ISO string. */\n updatedAt?: unknown\n}\n\n/**\n * How long a self-confirming plan may sit parked before it reads as waiting\n * for a person after all: a confirmation that never came.\n */\nexport const AI_JOB_SELF_CONFIRM_GRACE_MS = 5 * 60_000\n\n/**\n * Whether a job is parked on a plan the machine is about to confirm itself\n * (AGL-3596). A guided site start records its plan step — parking the job on\n * the plan as every planned job does — and confirms it in the next write, so\n * a surface that reads in between sees `needs_review` for an instant. That\n * instant is the build going on, never a stop: read as one, it showed the\n * person a failed site that turned green again a moment later. The job's\n * `updatedAt` there is the clock its step was claimed at, so the grace covers\n * the plan step's own run as well as the confirmation. Past\n * {@link AI_JOB_SELF_CONFIRM_GRACE_MS} without the confirmation, the park is\n * what it says.\n */\nexport function aiJobConfirmingOwnPlan(\n job: Pick<AiJobPhaseSource, 'status' | 'review' | 'autoConfirm' | 'updatedAt'>,\n now: number = Date.now(),\n): boolean {\n if (job.status !== 'needs_review' || job.review?.reason !== 'plan' || job.autoConfirm !== true) return false\n const at = typeof job.updatedAt === 'string' ? Date.parse(job.updatedAt) : NaN\n return !Number.isFinite(at) || now - at < AI_JOB_SELF_CONFIRM_GRACE_MS\n}\n\n/**\n * Where one job stands, as a person reads it:\n *\n * - `queued` — created, no step has started.\n * - `planning` — its plan step is the one running or next.\n * - `plan-ready` — its plan waits for the person to confirm it.\n * - `building` — a step after the plan (or a kind with no plan) is running or next.\n * - `attention` — it stopped for something only a person can change: an\n * answer that broke a building rule, a site at an allowance, the\n * workspace's credits or a switch.\n * - `done`, `failed`, `canceled` — settled.\n */\nexport type AiJobPhase =\n | 'queued'\n | 'planning'\n | 'plan-ready'\n | 'building'\n | 'attention'\n | 'done'\n | 'failed'\n | 'canceled'\n\nexport function aiJobPhase(job: AiJobPhaseSource): AiJobPhase {\n if (job.status === 'done' || job.status === 'failed' || job.status === 'canceled') {\n return job.status\n }\n if (job.status === 'needs_review' && !aiJobConfirmingOwnPlan(job)) {\n return job.review?.reason === 'plan' ? 'plan-ready' : 'attention'\n }\n if (job.status === 'needs_input') return 'attention'\n // Moving: the first step that has not finished says which half it is in.\n const next = job.steps.find((step) => step.status === 'running' || step.status === 'pending')\n if (next?.name === AI_JOB_PLAN_STEP) return 'planning'\n if (job.status === 'queued' && job.steps.every((step) => step.status === 'pending')) {\n return 'queued'\n }\n return 'building'\n}\n\n/** Each phase in the words a status line and a chip use. */\nexport const AI_JOB_PHASE_LABELS: Readonly<Record<AiJobPhase, string>> = {\n queued: 'Queued',\n planning: 'Planning',\n 'plan-ready': 'Plan ready',\n building: 'Building',\n attention: 'Needs attention',\n done: 'Done',\n failed: 'Failed',\n canceled: 'Canceled',\n}\n\n/**\n * What the top-bar indicator, the launcher badge and the drawer's header\n * count a job as, or `null` for a settled job, which none of them count.\n *\n * - `needs-you` — a plan waits to be confirmed: the one decision that, left\n * alone, leaves the person's site unbuilt.\n * - `attention` — stopped for something a person must change, which is not a\n * plan to confirm.\n * - `running` — queued, planning or building. Never a stopped job.\n */\nexport type AiJobActivityState = 'needs-you' | 'attention' | 'running'\n\n/** Loudest first: the order the indicator picks its one state by. */\nexport const AI_JOB_ACTIVITY_PRECEDENCE: readonly AiJobActivityState[] = [\n 'needs-you',\n 'attention',\n 'running',\n]\n\nexport function aiJobActivityState(job: AiJobPhaseSource): AiJobActivityState | null {\n const phase = aiJobPhase(job)\n if (phase === 'plan-ready') return 'needs-you'\n if (phase === 'attention') return 'attention'\n if (phase === 'queued' || phase === 'planning' || phase === 'building') return 'running'\n return null\n}\n\n/**\n * The statuses a job the indicator counts can have: every one that is not in\n * `AI_JOB_TERMINAL_STATUSES`. The jobs route's `status=active` list asks\n * Firestore for exactly these, under the (status, createdAt) index.\n */\nexport const AI_JOB_ACTIVE_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n]\n\n/** How many of a list's jobs stand in each state. */\nexport type AiJobActivityCounts = Readonly<Record<AiJobActivityState, number>>\n\nexport function aiJobActivityCounts(jobs: readonly AiJobPhaseSource[]): AiJobActivityCounts {\n const counts: Record<AiJobActivityState, number> = { 'needs-you': 0, attention: 0, running: 0 }\n for (const job of jobs) {\n const state = aiJobActivityState(job)\n if (state) counts[state] += 1\n }\n return counts\n}\n\n/**\n * What the indicator and the launcher show for a list of jobs: the loudest\n * state any of them is in, how many are in it, and the job the indicator\n * opens — the oldest of them, which has waited longest. `null` when none is\n * moving or waiting, which is when both show nothing.\n */\nexport interface AiJobsActivity<T> {\n state: AiJobActivityState\n count: number\n /** The job a click targets. */\n job: T\n /** Planning rather than building, for the running chip's word. */\n planning: boolean\n}\n\nexport function aiJobsActivity<T extends AiJobPhaseSource>(\n jobs: readonly T[],\n): AiJobsActivity<T> | null {\n for (const state of AI_JOB_ACTIVITY_PRECEDENCE) {\n const matching = jobs.filter((job) => aiJobActivityState(job) === state)\n if (!matching.length) continue\n return {\n state,\n count: matching.length,\n // Lists arrive newest first.\n job: matching[matching.length - 1],\n planning:\n state === 'running' &&\n matching.every((job) => {\n const phase = aiJobPhase(job)\n return phase === 'planning' || phase === 'queued'\n }),\n }\n }\n return null\n}\n\n/** The chip's words: `AI · plan ready`, `AI · 2 need attention`, `AI · planning`. */\nexport function aiJobsActivityLabel(activity: AiJobsActivity<unknown>): string {\n const { state, count } = activity\n if (state === 'needs-you') {\n return count === 1 ? 'AI · plan ready' : `AI · ${count} plans ready`\n }\n if (state === 'attention') {\n return count === 1 ? 'AI · needs attention' : `AI · ${count} need attention`\n }\n const verb = activity.planning ? 'planning' : 'working'\n return count === 1 ? `AI · ${verb}` : `AI · ${count} ${verb}`\n}\n\n/**\n * The launcher's accessible name: what the button opens, and what waits in it.\n * `Aglyn Assist, 1 AI job needs you`.\n */\nexport function aiJobsLauncherLabel(\n productName: string,\n activity: AiJobsActivity<unknown> | null,\n): string {\n const base = `Open ${productName} Assist`\n if (!activity) return base\n const jobs = activity.count === 1 ? '1 AI job' : `${activity.count} AI jobs`\n const verb = activity.count === 1 ? 'needs' : 'need'\n if (activity.state === 'needs-you') return `${base}, ${jobs} ${verb} you`\n if (activity.state === 'attention') return `${base}, ${jobs} ${verb} attention`\n return `${base}, ${jobs} running`\n}\n\n/** The drawer header's chips, one per state that has a job, loudest first. */\nexport function aiJobsHeaderChips(\n counts: AiJobActivityCounts,\n): Array<{ state: AiJobActivityState; label: string }> {\n return AI_JOB_ACTIVITY_PRECEDENCE.filter((state) => counts[state] > 0).map((state) => ({\n state,\n label:\n state === 'needs-you'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} you`\n : state === 'attention'\n ? `${counts[state]} ${counts[state] === 1 ? 'needs' : 'need'} attention`\n : `${counts[state]} running`,\n }))\n}\n\n/** What a job makes, as a sentence's subject: `Your site`, `Your page`. */\nconst KIND_NOUNS: Partial<Record<AiJobKind, string>> = {\n site: 'site',\n page: 'page',\n template: 'page template',\n layout: 'layout',\n form: 'form',\n component: 'component',\n email: 'email',\n campaign: 'campaign',\n workflow: 'automation',\n logic: 'function',\n theme: 'theme',\n products: 'products',\n experiment: 'experiment',\n insight: 'answer',\n build: 'build',\n}\n\n/** The noun for what a job of this kind makes, or `AI job` for a kind with none. */\nexport function aiJobKindNoun(kind: AiJobKind): string {\n return KIND_NOUNS[kind] ?? 'AI job'\n}\n"],"names":["AI_JOB_PLAN_STEP","AI_JOB_SELF_CONFIRM_GRACE_MS","aiJobConfirmingOwnPlan","job","now","Date","status","review","reason","autoConfirm","at","updatedAt","parse","NaN","Number","isFinite","aiJobPhase","next","steps","find","step","name","every","AI_JOB_PHASE_LABELS","queued","planning","building","attention","done","failed","canceled","AI_JOB_ACTIVITY_PRECEDENCE","aiJobActivityState","phase","AI_JOB_ACTIVE_STATUSES","aiJobActivityCounts","jobs","counts","running","state","aiJobsActivity","matching","filter","length","count","aiJobsActivityLabel","activity","verb","aiJobsLauncherLabel","productName","base","aiJobsHeaderChips","map","label","KIND_NOUNS","site","page","template","layout","form","component","email","campaign","workflow","logic","theme","products","experiment","insight","build","aiJobKindNoun","kind"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { AiJobKind, AiJobStatus, AiJobSummary } from './ai-jobs.types';
17
+ import type { AiJobItemLedger, AiJobKind, AiJobReview, AiJobStatus, AiJobSummary } from './ai-jobs.types';
18
18
  /**
19
19
  * What a member reads when a plan could not be made (AGL-3594): one plain
20
20
  * sentence and one thing to do, never a rule number or a validator's words.
@@ -86,3 +86,40 @@ export declare const AI_SITE_STARTER_FALLBACK_LABEL = "Use the starter site inst
86
86
  * is written only to a site with no page or layout.
87
87
  */
88
88
  export declare function aiSiteStarterFallbackOffered(job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>): boolean;
89
+ /**
90
+ * What a customer reads when an answer still broke a building rule after its
91
+ * re-ask (AGL-3596), in place of the doctrine's own sentence. "Rule 12
92
+ * (Responsive by the theme's breakpoints): Every child of a Grid container…"
93
+ * is what the re-ask is told and what staff read; shown to the person who
94
+ * asked for a site, it reads as the product breaking. The rule's words stay
95
+ * on the job (`review.detail`, an item's `failure.detail`) for staff.
96
+ */
97
+ export declare const AI_DOCTRINE_REFUSED_COPY: string;
98
+ /** The same, for a page. */
99
+ export declare const AI_DOCTRINE_REFUSED_PAGE_COPY: string;
100
+ /** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */
101
+ export declare function aiIsDoctrineRuleText(text: string | null | undefined): boolean;
102
+ /**
103
+ * A sentence a customer may read: the doctrine's own words become the plain
104
+ * refusal, a page's when the thing refused is a page; anything else stands.
105
+ */
106
+ export declare function aiCustomerSafeCopy<T extends string | null | undefined>(text: T, options?: {
107
+ page?: boolean;
108
+ }): T | string;
109
+ /**
110
+ * A doctrine review as a customer reads it (AGL-3596): a message in the
111
+ * doctrine's own words becomes the plain refusal, and those words move to
112
+ * `detail`, which only staff are shown. A review written before the step did
113
+ * this itself reads the same as one written after.
114
+ */
115
+ export declare function aiCustomerSafeReview<T extends Pick<AiJobReview, 'reason' | 'message'> & {
116
+ detail?: string;
117
+ }>(review: T, options?: {
118
+ page?: boolean;
119
+ }): T;
120
+ /**
121
+ * A build item's failure as a customer reads it (AGL-3596): a building rule
122
+ * still broken reads as the plain refusal, a page's for a page, and its
123
+ * checks' own sentence (`detail`) is left off.
124
+ */
125
+ export declare function aiCustomerSafeItem<T extends Pick<AiJobItemLedger, 'op' | 'failure'>>(row: T): T;
@@ -1,3 +1,5 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
1
3
  /**
2
4
  * @license
3
5
  * Copyright 2026 Aglyn LLC
@@ -13,7 +15,8 @@
13
15
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
16
  * See the License for the specific language governing permissions and
15
17
  * limitations under the License.
16
- */ /**
18
+ */ import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
19
+ /**
17
20
  * What a member reads when a plan could not be made (AGL-3594): one plain
18
21
  * sentence and one thing to do, never a rule number or a validator's words.
19
22
  *
@@ -101,5 +104,57 @@
101
104
  if (job.status === 'failed' || job.status === 'canceled') return true;
102
105
  return job.status === 'needs_review' && ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'doctrine';
103
106
  }
107
+ /**
108
+ * What a customer reads when an answer still broke a building rule after its
109
+ * re-ask (AGL-3596), in place of the doctrine's own sentence. "Rule 12
110
+ * (Responsive by the theme's breakpoints): Every child of a Grid container…"
111
+ * is what the re-ask is told and what staff read; shown to the person who
112
+ * asked for a site, it reads as the product breaking. The rule's words stay
113
+ * on the job (`review.detail`, an item's `failure.detail`) for staff.
114
+ */ export const AI_DOCTRINE_REFUSED_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t build this cleanly, so we stopped rather than give you something broken.`;
115
+ /** The same, for a page. */ export const AI_DOCTRINE_REFUSED_PAGE_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t lay this page out cleanly, so we stopped rather than publish a broken page.`;
116
+ /** How the doctrine's refusal sentence starts (`aiDoctrineNeedsInputMessage`). */ const AI_DOCTRINE_REFUSAL_LEAD = 'This could not be built within the building rules.';
117
+ /** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */ export function aiIsDoctrineRuleText(text) {
118
+ if (!text) return false;
119
+ return text.startsWith(AI_DOCTRINE_REFUSAL_LEAD) || /\bRule \d+ \(/.test(text);
120
+ }
121
+ /**
122
+ * A sentence a customer may read: the doctrine's own words become the plain
123
+ * refusal, a page's when the thing refused is a page; anything else stands.
124
+ */ export function aiCustomerSafeCopy(text, options = {}) {
125
+ if (!aiIsDoctrineRuleText(text)) return text;
126
+ return options.page ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY;
127
+ }
128
+ /**
129
+ * A doctrine review as a customer reads it (AGL-3596): a message in the
130
+ * doctrine's own words becomes the plain refusal, and those words move to
131
+ * `detail`, which only staff are shown. A review written before the step did
132
+ * this itself reads the same as one written after.
133
+ */ export function aiCustomerSafeReview(review, options = {}) {
134
+ var _review_detail;
135
+ if (review.reason !== 'doctrine' || !aiIsDoctrineRuleText(review.message)) return review;
136
+ return _extends({}, review, {
137
+ message: aiCustomerSafeCopy(review.message, options),
138
+ detail: (_review_detail = review.detail) != null ? _review_detail : review.message
139
+ });
140
+ }
141
+ /**
142
+ * A build item's failure as a customer reads it (AGL-3596): a building rule
143
+ * still broken reads as the plain refusal, a page's for a page, and its
144
+ * checks' own sentence (`detail`) is left off.
145
+ */ export function aiCustomerSafeItem(row) {
146
+ const failure = row.failure;
147
+ if (!failure) return row;
148
+ const { detail: _detail } = failure, rest = _object_without_properties_loose(failure, [
149
+ "detail"
150
+ ]);
151
+ const doctrine = failure.reason === 'doctrine-refused' || aiIsDoctrineRuleText(failure.message);
152
+ if (!doctrine && _detail === undefined) return row;
153
+ return _extends({}, row, {
154
+ failure: _extends({}, rest, {
155
+ message: doctrine ? row.op === 'page' ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY : failure.message
156
+ })
157
+ });
158
+ }
104
159
 
105
160
  //# sourceMappingURL=ai-job-failure-copy.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-failure-copy.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 { AiJobKind, AiJobStatus, AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a member reads when a plan could not be made (AGL-3594): one plain\n * sentence and one thing to do, never a rule number or a validator's words.\n *\n * The plan step's own checks refuse an answer in the doctrine's vocabulary —\n * \"Rule 10 (Navigation and SEO travel with a page): A page reuses an\n * address…\" — which is what the one re-ask is told and what staff read to\n * tell a rule that should bend from a model that ignored it. Shown to the\n * person who asked for a site, the same words read as the product breaking.\n * So the review a refused plan stops for carries this sentence as its\n * `message`, and the doctrine's sentence and findings ride beside it as\n * `detail` and `findings`, which the job document keeps and only staff are\n * shown.\n *\n * Pure: no React, no Admin SDK. The drawer's plan card and the plan step read\n * the same words.\n */\n\n/** The violation code the Free wall refuses a plan with. */\nexport const AI_PLAN_FREE_WALL_CODE = 'plan-over-free-wall'\n\n/** What the plan was for, as the sentence names it. */\nfunction planningWhat(kind: AiJobKind | string): string {\n switch (kind) {\n case 'site':\n return 'your site'\n case 'page':\n return 'this page'\n default:\n return 'this'\n }\n}\n\nexport interface AiPlanFailureCopyInput {\n /** The job kind whose plan was refused. */\n kind: AiJobKind | string\n /** The codes of the violations its last answer broke. */\n codes: readonly string[]\n /** The step's credits went back to the Free allowance (AGL-3594). */\n refunded: boolean\n}\n\n/**\n * The sentence a refused plan stops its job with. Over the Free wall, the way\n * out is fewer pages or a paid plan; anything else is ours to have got wrong,\n * and the way out is trying again — said to cost nothing only when it did.\n */\nexport function aiPlanFailureCopy(input: AiPlanFailureCopyInput): string {\n const spent = input.refunded ? ', and it did not use any of your AI credits' : ''\n if (input.codes.includes(AI_PLAN_FREE_WALL_CODE)) {\n return `We couldn't fit this plan into your Free AI credits${spent}. Try fewer pages, or upgrade for more.`\n }\n return `Something went wrong planning ${planningWhat(input.kind)}${spent}. Try again.`\n}\n\n/**\n * Why trying a refused plan again cannot work yet, or `null` when it can: the\n * Free allowance left this month is less than a plan of this kind can cost at\n * its worst. Said with the figures, and with the two ways out.\n */\nexport function aiPlanRetryRefusal(input: { creditsLeft: number; planCredits: number }): string | null {\n const left = Math.max(0, Math.floor(input.creditsLeft))\n if (left >= input.planCredits) return null\n return `You have ${left} AI ${left === 1 ? 'credit' : 'credits'} left this month, and a plan needs up to ${input.planCredits}. Your credits refresh next month, or upgrade for more.`\n}\n\n/**\n * What a guided site start that failed on our side says (AGL-3596): one plain\n * sentence, never a rule. Whether its credits came back is said beside it, from\n * the job's own record (`aiJobRefundCopy`).\n */\nexport const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.'\n\n/** What a person's own cancel says about its credits: they paid for what ran until then. */\nexport const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.'\n\n/**\n * What every surface that shows a stopped job says about its credits\n * (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),\n * never worked out again: a failure on our side that gave its credits back\n * says so plainly, and a person's own cancel says they paid for what ran.\n * `null` where there is nothing to say — a job still working, done, or one\n * that stopped without a give-back.\n */\nexport function aiJobRefundCopy(\n job: { status: AiJobStatus } & Partial<Pick<AiJobSummary, 'refundedCredits' | 'creditsSpent'>>,\n): string | null {\n const refunded = Math.max(0, Math.floor(job.refundedCredits ?? 0))\n if (refunded > 0) {\n const credits = `${refunded} ${refunded === 1 ? 'credit' : 'credits'}`\n const back = refunded === 1 ? 'is' : 'are'\n const whole = refunded >= Math.floor(job.creditsSpent ?? 0)\n return whole\n ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.`\n : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`\n }\n if (job.status === 'canceled' && (job.creditsSpent ?? 0) > 0) return AI_JOB_CANCELED_CREDITS_COPY\n return null\n}\n\n/** The guided start's way back to the starter (AGL-3594). */\nexport const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead'\n\n/**\n * Whether a job is a guided start that did not work out and can still be\n * traded for the starter (AGL-3594): a site job that failed, was canceled, or\n * stopped for a person on a refused step — never one waiting on its plan,\n * which is a plan to confirm — and that has built nothing, since the starter\n * is written only to a site with no page or layout.\n */\nexport function aiSiteStarterFallbackOffered(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId) return false\n // The site's listing proposal rides out with the first unit and builds nothing (AGL-3596).\n if (job.outputs.some((output) => output.resource !== 'seo')) return false\n if (job.status === 'failed' || job.status === 'canceled') return true\n return job.status === 'needs_review' && job.review?.reason === 'doctrine'\n}\n"],"names":["AI_PLAN_FREE_WALL_CODE","planningWhat","kind","aiPlanFailureCopy","input","spent","refunded","codes","includes","aiPlanRetryRefusal","left","Math","max","floor","creditsLeft","planCredits","AI_SITE_GUIDED_BUILD_FAILED_COPY","AI_JOB_CANCELED_CREDITS_COPY","aiJobRefundCopy","job","refundedCredits","credits","back","whole","creditsSpent","status","AI_SITE_STARTER_FALLBACK_LABEL","aiSiteStarterFallbackOffered","hostId","outputs","some","output","resource","review","reason"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;CAgBC,GAED,0DAA0D,GAC1D,OAAO,MAAMA,yBAAyB,sBAAqB;AAE3D,qDAAqD,GACrD,SAASC,aAAaC,IAAwB;IAC5C,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAO;IACX;AACF;AAWA;;;;CAIC,GACD,OAAO,SAASC,kBAAkBC,KAA6B;IAC7D,MAAMC,QAAQD,MAAME,QAAQ,GAAG,gDAAgD;IAC/E,IAAIF,MAAMG,KAAK,CAACC,QAAQ,CAACR,yBAAyB;QAChD,OAAO,CAAC,mDAAmD,EAAEK,MAAM,uCAAuC,CAAC;IAC7G;IACA,OAAO,CAAC,8BAA8B,EAAEJ,aAAaG,MAAMF,IAAI,IAAIG,MAAM,YAAY,CAAC;AACxF;AAEA;;;;CAIC,GACD,OAAO,SAASI,mBAAmBL,KAAmD;IACpF,MAAMM,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACT,MAAMU,WAAW;IACrD,IAAIJ,QAAQN,MAAMW,WAAW,EAAE,OAAO;IACtC,OAAO,CAAC,SAAS,EAAEL,KAAK,IAAI,EAAEA,SAAS,IAAI,WAAW,UAAU,yCAAyC,EAAEN,MAAMW,WAAW,CAAC,uDAAuD,CAAC;AACvL;AAEA;;;;CAIC,GACD,OAAO,MAAMC,mCAAmC,iEAAgE;AAEhH,0FAA0F,GAC1F,OAAO,MAAMC,+BAA+B,0CAAyC;AAErF;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdC,GAA8F;QAEtDA,sBASNA;IATlC,MAAMb,WAAWK,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,EAACM,uBAAAA,IAAIC,eAAe,YAAnBD,uBAAuB;IAC/D,IAAIb,WAAW,GAAG;YAGqBa;QAFrC,MAAME,UAAU,GAAGf,SAAS,CAAC,EAAEA,aAAa,IAAI,WAAW,WAAW;QACtE,MAAMgB,OAAOhB,aAAa,IAAI,OAAO;QACrC,MAAMiB,QAAQjB,YAAYK,KAAKE,KAAK,EAACM,qBAAAA,IAAIK,YAAY,YAAhBL,qBAAoB;QACzD,OAAOI,QACH,CAAC,4CAA4C,EAAEF,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC,GACjG,CAAC,qEAAqE,EAAED,QAAQ,SAAS,EAAEC,KAAK,yBAAyB,CAAC;IAChI;IACA,IAAIH,IAAIM,MAAM,KAAK,cAAc,EAACN,oBAAAA,IAAIK,YAAY,YAAhBL,oBAAoB,KAAK,GAAG,OAAOF;IACrE,OAAO;AACT;AAEA,2DAA2D,GAC3D,OAAO,MAAMS,iCAAiC,+BAA8B;AAE5E;;;;;;CAMC,GACD,OAAO,SAASC,6BACdR,GAA4E;QAMpCA;IAJxC,IAAIA,IAAIjB,IAAI,KAAK,UAAU,CAACiB,IAAIS,MAAM,EAAE,OAAO;IAC/C,2FAA2F;IAC3F,IAAIT,IAAIU,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,QAAQ,KAAK,QAAQ,OAAO;IACpE,IAAIb,IAAIM,MAAM,KAAK,YAAYN,IAAIM,MAAM,KAAK,YAAY,OAAO;IACjE,OAAON,IAAIM,MAAM,KAAK,kBAAkBN,EAAAA,cAAAA,IAAIc,MAAM,qBAAVd,YAAYe,MAAM,MAAK;AACjE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-failure-copy.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 { PLATFORM_BRAND_NAME } from '@aglyn/aglyn/app-utils/platform-brand'\nimport type { AiJobItemLedger, AiJobKind, AiJobReview, AiJobStatus, AiJobSummary } from './ai-jobs.types'\n\n/**\n * What a member reads when a plan could not be made (AGL-3594): one plain\n * sentence and one thing to do, never a rule number or a validator's words.\n *\n * The plan step's own checks refuse an answer in the doctrine's vocabulary —\n * \"Rule 10 (Navigation and SEO travel with a page): A page reuses an\n * address…\" — which is what the one re-ask is told and what staff read to\n * tell a rule that should bend from a model that ignored it. Shown to the\n * person who asked for a site, the same words read as the product breaking.\n * So the review a refused plan stops for carries this sentence as its\n * `message`, and the doctrine's sentence and findings ride beside it as\n * `detail` and `findings`, which the job document keeps and only staff are\n * shown.\n *\n * Pure: no React, no Admin SDK. The drawer's plan card and the plan step read\n * the same words.\n */\n\n/** The violation code the Free wall refuses a plan with. */\nexport const AI_PLAN_FREE_WALL_CODE = 'plan-over-free-wall'\n\n/** What the plan was for, as the sentence names it. */\nfunction planningWhat(kind: AiJobKind | string): string {\n switch (kind) {\n case 'site':\n return 'your site'\n case 'page':\n return 'this page'\n default:\n return 'this'\n }\n}\n\nexport interface AiPlanFailureCopyInput {\n /** The job kind whose plan was refused. */\n kind: AiJobKind | string\n /** The codes of the violations its last answer broke. */\n codes: readonly string[]\n /** The step's credits went back to the Free allowance (AGL-3594). */\n refunded: boolean\n}\n\n/**\n * The sentence a refused plan stops its job with. Over the Free wall, the way\n * out is fewer pages or a paid plan; anything else is ours to have got wrong,\n * and the way out is trying again — said to cost nothing only when it did.\n */\nexport function aiPlanFailureCopy(input: AiPlanFailureCopyInput): string {\n const spent = input.refunded ? ', and it did not use any of your AI credits' : ''\n if (input.codes.includes(AI_PLAN_FREE_WALL_CODE)) {\n return `We couldn't fit this plan into your Free AI credits${spent}. Try fewer pages, or upgrade for more.`\n }\n return `Something went wrong planning ${planningWhat(input.kind)}${spent}. Try again.`\n}\n\n/**\n * Why trying a refused plan again cannot work yet, or `null` when it can: the\n * Free allowance left this month is less than a plan of this kind can cost at\n * its worst. Said with the figures, and with the two ways out.\n */\nexport function aiPlanRetryRefusal(input: { creditsLeft: number; planCredits: number }): string | null {\n const left = Math.max(0, Math.floor(input.creditsLeft))\n if (left >= input.planCredits) return null\n return `You have ${left} AI ${left === 1 ? 'credit' : 'credits'} left this month, and a plan needs up to ${input.planCredits}. Your credits refresh next month, or upgrade for more.`\n}\n\n/**\n * What a guided site start that failed on our side says (AGL-3596): one plain\n * sentence, never a rule. Whether its credits came back is said beside it, from\n * the job's own record (`aiJobRefundCopy`).\n */\nexport const AI_SITE_GUIDED_BUILD_FAILED_COPY = 'Something went wrong building your site, and it was not built.'\n\n/** What a person's own cancel says about its credits: they paid for what ran until then. */\nexport const AI_JOB_CANCELED_CREDITS_COPY = 'You paid for what was spent up to then.'\n\n/**\n * What every surface that shows a stopped job says about its credits\n * (AGL-3596), read off the job's RECORDED give-back (`refundedCredits`),\n * never worked out again: a failure on our side that gave its credits back\n * says so plainly, and a person's own cancel says they paid for what ran.\n * `null` where there is nothing to say — a job still working, done, or one\n * that stopped without a give-back.\n */\nexport function aiJobRefundCopy(\n job: { status: AiJobStatus } & Partial<Pick<AiJobSummary, 'refundedCredits' | 'creditsSpent'>>,\n): string | null {\n const refunded = Math.max(0, Math.floor(job.refundedCredits ?? 0))\n if (refunded > 0) {\n const credits = `${refunded} ${refunded === 1 ? 'credit' : 'credits'}`\n const back = refunded === 1 ? 'is' : 'are'\n const whole = refunded >= Math.floor(job.creditsSpent ?? 0)\n return whole\n ? `This one’s on us — you weren’t charged. The ${credits} it used ${back} back in your AI credits.`\n : `This one’s on us — you weren’t charged for the part that failed. The ${credits} it used ${back} back in your AI credits.`\n }\n if (job.status === 'canceled' && (job.creditsSpent ?? 0) > 0) return AI_JOB_CANCELED_CREDITS_COPY\n return null\n}\n\n/** The guided start's way back to the starter (AGL-3594). */\nexport const AI_SITE_STARTER_FALLBACK_LABEL = 'Use the starter site instead'\n\n/**\n * Whether a job is a guided start that did not work out and can still be\n * traded for the starter (AGL-3594): a site job that failed, was canceled, or\n * stopped for a person on a refused step — never one waiting on its plan,\n * which is a plan to confirm — and that has built nothing, since the starter\n * is written only to a site with no page or layout.\n */\nexport function aiSiteStarterFallbackOffered(\n job: Pick<AiJobSummary, 'kind' | 'status' | 'hostId' | 'outputs' | 'review'>,\n): boolean {\n if (job.kind !== 'site' || !job.hostId) return false\n // The site's listing proposal rides out with the first unit and builds nothing (AGL-3596).\n if (job.outputs.some((output) => output.resource !== 'seo')) return false\n if (job.status === 'failed' || job.status === 'canceled') return true\n return job.status === 'needs_review' && job.review?.reason === 'doctrine'\n}\n\n/**\n * What a customer reads when an answer still broke a building rule after its\n * re-ask (AGL-3596), in place of the doctrine's own sentence. \"Rule 12\n * (Responsive by the theme's breakpoints): Every child of a Grid container…\"\n * is what the re-ask is told and what staff read; shown to the person who\n * asked for a site, it reads as the product breaking. The rule's words stay\n * on the job (`review.detail`, an item's `failure.detail`) for staff.\n */\nexport const AI_DOCTRINE_REFUSED_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t build this cleanly, so we stopped rather than give you something broken.`\n\n/** The same, for a page. */\nexport const AI_DOCTRINE_REFUSED_PAGE_COPY = `${PLATFORM_BRAND_NAME} AI couldn’t lay this page out cleanly, so we stopped rather than publish a broken page.`\n\n/** How the doctrine's refusal sentence starts (`aiDoctrineNeedsInputMessage`). */\nconst AI_DOCTRINE_REFUSAL_LEAD = 'This could not be built within the building rules.'\n\n/** Whether a sentence is the doctrine's own words: its refusal, or a rule cited by number. */\nexport function aiIsDoctrineRuleText(text: string | null | undefined): boolean {\n if (!text) return false\n return text.startsWith(AI_DOCTRINE_REFUSAL_LEAD) || /\\bRule \\d+ \\(/.test(text)\n}\n\n/**\n * A sentence a customer may read: the doctrine's own words become the plain\n * refusal, a page's when the thing refused is a page; anything else stands.\n */\nexport function aiCustomerSafeCopy<T extends string | null | undefined>(text: T, options: { page?: boolean } = {}): T | string {\n if (!aiIsDoctrineRuleText(text)) return text\n return options.page ? AI_DOCTRINE_REFUSED_PAGE_COPY : AI_DOCTRINE_REFUSED_COPY\n}\n\n/**\n * A doctrine review as a customer reads it (AGL-3596): a message in the\n * doctrine's own words becomes the plain refusal, and those words move to\n * `detail`, which only staff are shown. A review written before the step did\n * this itself reads the same as one written after.\n */\nexport function aiCustomerSafeReview<T extends Pick<AiJobReview, 'reason' | 'message'> & { detail?: string }>(\n review: T,\n options: { page?: boolean } = {},\n): T {\n if (review.reason !== 'doctrine' || !aiIsDoctrineRuleText(review.message)) return review\n return { ...review, message: aiCustomerSafeCopy(review.message, options), detail: review.detail ?? review.message }\n}\n\n/**\n * A build item's failure as a customer reads it (AGL-3596): a building rule\n * still broken reads as the plain refusal, a page's for a page, and its\n * checks' own sentence (`detail`) is left off.\n */\nexport function aiCustomerSafeItem<T extends Pick<AiJobItemLedger, 'op' | 'failure'>>(row: T): T {\n const failure = row.failure\n if (!failure) return row\n const { detail: _detail, ...rest } = failure\n const doctrine = failure.reason === 'doctrine-refused' || aiIsDoctrineRuleText(failure.message)\n if (!doctrine && _detail === undefined) return row\n return {\n ...row,\n failure: {\n ...rest,\n message: doctrine\n ? row.op === 'page'\n ? AI_DOCTRINE_REFUSED_PAGE_COPY\n : AI_DOCTRINE_REFUSED_COPY\n : failure.message,\n },\n }\n}\n"],"names":["PLATFORM_BRAND_NAME","AI_PLAN_FREE_WALL_CODE","planningWhat","kind","aiPlanFailureCopy","input","spent","refunded","codes","includes","aiPlanRetryRefusal","left","Math","max","floor","creditsLeft","planCredits","AI_SITE_GUIDED_BUILD_FAILED_COPY","AI_JOB_CANCELED_CREDITS_COPY","aiJobRefundCopy","job","refundedCredits","credits","back","whole","creditsSpent","status","AI_SITE_STARTER_FALLBACK_LABEL","aiSiteStarterFallbackOffered","hostId","outputs","some","output","resource","review","reason","AI_DOCTRINE_REFUSED_COPY","AI_DOCTRINE_REFUSED_PAGE_COPY","AI_DOCTRINE_REFUSAL_LEAD","aiIsDoctrineRuleText","text","startsWith","test","aiCustomerSafeCopy","options","page","aiCustomerSafeReview","message","detail","aiCustomerSafeItem","row","failure","_detail","rest","doctrine","undefined","op"],"mappings":"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"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { AglynNotification } from '@aglyn/aglyn/app-utils/notifications';
18
- import type { AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types';
18
+ import type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types';
19
19
  /** The besigner segment each versioned resource lives under. */
20
20
  export declare const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>>;
21
21
  /**
@@ -44,6 +44,8 @@ export interface AiJobNoticeSource {
44
44
  /** What the job spent, and gave back for a failure on our side (AGL-3596). */
45
45
  creditsSpent?: number;
46
46
  refundedCredits?: number;
47
+ /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */
48
+ items?: AiJobItemLedger[] | null;
47
49
  }
48
50
  export type AiJobNotice = Pick<AglynNotification, 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'>;
49
51
  /**
@@ -14,8 +14,9 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { aiBuildOutcomeLine, aiSitePartialCopy } from "./ai-build-progress.js";
17
18
  import { aiJobKindNoun } from "./ai-job-activity.js";
18
- import { aiJobRefundCopy } from "./ai-job-failure-copy.js";
19
+ import { aiCustomerSafeCopy, aiJobRefundCopy } from "./ai-job-failure-copy.js";
19
20
  /** The besigner segment each versioned resource lives under. */ export const AI_JOB_BESIGNER_SEGMENT = {
20
21
  screen: 'screens',
21
22
  reusableComponent: 'components',
@@ -93,13 +94,28 @@ import { aiJobRefundCopy } from "./ai-job-failure-copy.js";
93
94
  type: 'content.aiJobNeedsYou',
94
95
  level: 'warning',
95
96
  title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,
96
- body: withRefund(job, ((_job_review1 = job.review) == null ? void 0 : _job_review1.message) || job.error || 'It stopped for your decision. It waits in AI jobs.', 'needs_review'),
97
+ // The doctrine's own words never reach a customer (AGL-3596).
98
+ body: withRefund(job, aiCustomerSafeCopy(((_job_review1 = job.review) == null ? void 0 : _job_review1.message) || job.error || 'It stopped for your decision. It waits in AI jobs.', {
99
+ page: job.kind === 'page'
100
+ }), 'needs_review'),
97
101
  link
98
102
  }, scope);
99
103
  }
100
104
  if (to === 'done') {
101
- var _ref, _job_outputs;
105
+ var _job_items, _ref, _job_outputs;
102
106
  var _job_sitePublish;
107
+ // A job that finished with part of it unbuilt (AGL-3596) says so in the
108
+ // words its page uses, as a warning: "done" alone hid a page that failed.
109
+ const items = (_job_items = job.items) != null ? _job_items : [];
110
+ const unbuilt = items.some((row)=>row.status === 'failed' || row.status === 'skipped');
111
+ const level = unbuilt ? 'warning' : 'success';
112
+ const partial = job.kind === 'site' ? aiSitePartialCopy({
113
+ items,
114
+ status: 'done'
115
+ }) : unbuilt ? aiBuildOutcomeLine({
116
+ items,
117
+ status: 'done'
118
+ }) : null;
103
119
  // A guided site start publishes its pages when it finishes (AGL-3596):
104
120
  // with one live, the site is, and the build page lists any that stayed
105
121
  // drafts with the reason for each.
@@ -108,11 +124,12 @@ import { aiJobRefundCopy } from "./ai-job-failure-copy.js";
108
124
  var _ref1;
109
125
  var _job_sitePublish1;
110
126
  const drafts = (_ref1 = (_job_sitePublish1 = job.sitePublish) == null ? void 0 : _job_sitePublish1.drafts.length) != null ? _ref1 : 0;
127
+ const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`;
111
128
  return _extends({
112
129
  type: 'content.aiJobDone',
113
- level: 'success',
114
- title: 'Your site is live',
115
- body: drafts > 0 ? `Your pages are published, except ${drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`}. Open it to see why.` : 'Your pages are published. Open it to view your site or edit your pages.',
130
+ level,
131
+ title: partial ? 'Your site is live, but part of it wasn’t built' : 'Your site is live',
132
+ body: partial ? `${partial} ${drafts > 0 ? `What was built is published, except ${stayed}.` : 'What was built is published.'}` : drafts > 0 ? `Your pages are published, except ${stayed}. Open it to see why.` : 'Your pages are published. Open it to view your site or edit your pages.',
116
133
  link
117
134
  }, scope);
118
135
  }
@@ -120,9 +137,12 @@ import { aiJobRefundCopy } from "./ai-job-failure-copy.js";
120
137
  const title = job.kind === 'site' || pages > 1 ? 'Your site’s draft pages are ready' : job.kind === 'page' ? 'Your draft page is ready' : noun === 'AI job' ? 'Your AI job finished' : `Your ${noun} is ready`;
121
138
  return _extends({
122
139
  type: 'content.aiJobDone',
123
- level: 'success',
140
+ level,
124
141
  title,
125
- body: job.kind === 'insight' || job.kind === 'text' ? 'Open AI jobs to read it.' : 'Everything it built is an unpublished draft until you publish it.',
142
+ body: job.kind === 'insight' || job.kind === 'text' ? 'Open AI jobs to read it.' : [
143
+ partial,
144
+ 'Everything it built is an unpublished draft until you publish it.'
145
+ ].filter(Boolean).join(' '),
126
146
  link
127
147
  }, scope);
128
148
  }
@@ -130,7 +150,9 @@ import { aiJobRefundCopy } from "./ai-job-failure-copy.js";
130
150
  type: 'content.aiJobFailed',
131
151
  level: 'warning',
132
152
  title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,
133
- body: withRefund(job, job.error || 'It stopped before it finished.', 'failed'),
153
+ body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', {
154
+ page: job.kind === 'page'
155
+ }), 'failed'),
134
156
  link
135
157
  }, scope);
136
158
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.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 { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n if (job.kind === 'site' && job.hostId) return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\nexport function aiJobNotice(job: AiJobNoticeSource, to: AiJobNoticeTransition): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n body: withRefund(job, job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', 'needs_review'),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const live = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (live > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n return {\n type: 'content.aiJobDone',\n level: 'success',\n title: 'Your site is live',\n body:\n drafts > 0\n ? `Your pages are published, except ${drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`}. Open it to see why.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level: 'success',\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : 'Everything it built is an unpublished draft until you publish it.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, job.error || 'It stopped before it finished.', 'failed'),\n link,\n ...scope,\n }\n}\n"],"names":["aiJobKindNoun","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","kind","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","id","aiJobNotice","noun","scope","orgId","link","review","reason","type","level","title","body","message","error","live","sitePublish","published","drafts","pages"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-job-notice.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 { AglynNotification } from '@aglyn/aglyn/app-utils/notifications'\nimport { aiBuildOutcomeLine, aiSitePartialCopy } from './ai-build-progress'\nimport { aiJobKindNoun } from './ai-job-activity'\nimport { aiCustomerSafeCopy, aiJobRefundCopy } from './ai-job-failure-copy'\nimport type { AiJobItemLedger, AiJobKind, AiJobOutput, AiJobReviewReason, AiJobSitePublish } from './ai-jobs.types'\n\n/** The besigner segment each versioned resource lives under. */\nexport const AI_JOB_BESIGNER_SEGMENT: Partial<Record<AiJobOutput['resource'], string>> = {\n screen: 'screens',\n reusableComponent: 'components',\n layout: 'layouts',\n template: 'templates',\n // An email design is a screen: it opens in the screen besigner, as the\n // Emails page's own Edit design does.\n emailScreen: 'screens',\n}\n\n/**\n * The query parameter a console address carries to open AI jobs on one job\n * (AGL-3593): the Assist panel reads it on arrival.\n */\nexport const AI_JOB_LINK_PARAM = 'aiJob'\n\n/** Where a site's \"Building your site\" page lives, under the site (AGL-3594). */\nexport const AI_SITE_BUILD_HREF = '/ai-jobs'\n\n/** A change the person who started a job is told about. */\nexport type AiJobNoticeTransition = 'needs-review' | 'done' | 'failed'\n\n/** The fields of a stored job a notice is written from. */\nexport interface AiJobNoticeSource {\n $id: string\n orgId: string\n hostId?: string | null\n kind: AiJobKind\n review?: { reason: AiJobReviewReason; message?: string | null } | null\n outputs?: readonly AiJobOutput[] | null\n error?: string | null\n /** What a guided site start put live (AGL-3596); absent on every other job. */\n sitePublish?: Pick<AiJobSitePublish, 'published' | 'drafts'> | null\n /** What the job spent, and gave back for a failure on our side (AGL-3596). */\n creditsSpent?: number\n refundedCredits?: number\n /** A build's or a guided start's items (AGL-3616), so a finished one says what was not built. */\n items?: AiJobItemLedger[] | null\n}\n\n/** A stopped job's sentence with what became of its credits, from the job's recorded give-back (AGL-3596). */\nfunction withRefund(job: AiJobNoticeSource, text: string, status: 'failed' | 'needs_review'): string {\n const refund = aiJobRefundCopy({ status, refundedCredits: job.refundedCredits, creditsSpent: job.creditsSpent })\n return refund ? `${text} ${refund}` : text\n}\n\nexport type AiJobNotice = Pick<\n AglynNotification,\n 'type' | 'title' | 'body' | 'link' | 'orgId' | 'hostId' | 'level'\n>\n\n/**\n * Where a notice's link goes, in the stored form the console rewrites when it\n * is followed (`normalizeNotificationLink`): `/{hostId}/…` for the job's site,\n * `/org…` for a job of the workspace's.\n *\n * A job that is done leads to what it built (AGL-3593): several pages of the\n * job's site open that site's Pages list, one versioned output opens in the\n * editor on the version the job wrote. Everything else — a plan to confirm, a\n * job that stopped, a done job with nothing of its own to open — opens AI\n * jobs on the job.\n */\nexport function aiJobNoticeLink(job: AiJobNoticeSource, to: AiJobNoticeTransition): string {\n const base = job.hostId ? `/${job.hostId}` : '/org'\n // A site job's every notice opens its \"Building your site\" page (AGL-3594),\n // which leads with the next step whatever the job's state.\n if (job.kind === 'site' && job.hostId) return `${base}${AI_SITE_BUILD_HREF}/${encodeURIComponent(job.$id)}`\n if (to === 'done' && job.hostId) {\n const own = (job.outputs ?? []).filter((output) => output.hostId === job.hostId)\n const screens = own.filter((output) => output.resource === 'screen')\n if (screens.length > 1) return `${base}/screens`\n const versioned = own.find(\n (output) => AI_JOB_BESIGNER_SEGMENT[output.resource] && output.versionId,\n )\n if (own.length === 1 && versioned) {\n const segment = AI_JOB_BESIGNER_SEGMENT[versioned.resource]\n return `${base}/${segment}/${versioned.id}/versions/${versioned.versionId}/besigner`\n }\n }\n return `${base}?${AI_JOB_LINK_PARAM}=${encodeURIComponent(job.$id)}`\n}\n\n/**\n * The in-app notification for one change of one job (AGL-3593), for the\n * person who created it. Customer-safe throughout: a stopped job's sentence is\n * the job's own `error`, which the machine writes as a fixed sentence.\n */\nexport function aiJobNotice(job: AiJobNoticeSource, to: AiJobNoticeTransition): AiJobNotice {\n const noun = aiJobKindNoun(job.kind)\n const scope = { orgId: job.orgId, ...(job.hostId ? { hostId: job.hostId } : {}) }\n const link = aiJobNoticeLink(job, to)\n if (to === 'needs-review') {\n if (job.review?.reason === 'plan') {\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title:\n noun === 'AI job'\n ? 'Your plan is ready: confirm it to build'\n : `Your ${noun} plan is ready: confirm it to build`,\n body: 'Nothing is built until you confirm the plan. It waits in AI jobs.',\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobNeedsYou',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} needs you`,\n // The doctrine's own words never reach a customer (AGL-3596).\n body: withRefund(\n job,\n aiCustomerSafeCopy(job.review?.message || job.error || 'It stopped for your decision. It waits in AI jobs.', { page: job.kind === 'page' }),\n 'needs_review',\n ),\n link,\n ...scope,\n }\n }\n if (to === 'done') {\n // A job that finished with part of it unbuilt (AGL-3596) says so in the\n // words its page uses, as a warning: \"done\" alone hid a page that failed.\n const items = job.items ?? []\n const unbuilt = items.some((row) => row.status === 'failed' || row.status === 'skipped')\n const level = unbuilt ? 'warning' : 'success'\n const partial =\n job.kind === 'site'\n ? aiSitePartialCopy({ items, status: 'done' })\n : unbuilt\n ? aiBuildOutcomeLine({ items, status: 'done' })\n : null\n // A guided site start publishes its pages when it finishes (AGL-3596):\n // with one live, the site is, and the build page lists any that stayed\n // drafts with the reason for each.\n const live = job.kind === 'site' ? (job.sitePublish?.published.length ?? 0) : 0\n if (live > 0) {\n const drafts = job.sitePublish?.drafts.length ?? 0\n const stayed = drafts === 1 ? 'one that stayed a draft' : `${drafts} that stayed drafts`\n return {\n type: 'content.aiJobDone',\n level,\n title: partial ? 'Your site is live, but part of it wasn’t built' : 'Your site is live',\n body: partial\n ? `${partial} ${drafts > 0 ? `What was built is published, except ${stayed}.` : 'What was built is published.'}`\n : drafts > 0\n ? `Your pages are published, except ${stayed}. Open it to see why.`\n : 'Your pages are published. Open it to view your site or edit your pages.',\n link,\n ...scope,\n }\n }\n const pages = (job.outputs ?? []).filter((output) => output.resource === 'screen').length\n const title =\n job.kind === 'site' || pages > 1\n ? 'Your site’s draft pages are ready'\n : job.kind === 'page'\n ? 'Your draft page is ready'\n : noun === 'AI job'\n ? 'Your AI job finished'\n : `Your ${noun} is ready`\n return {\n type: 'content.aiJobDone',\n level,\n title,\n body:\n job.kind === 'insight' || job.kind === 'text'\n ? 'Open AI jobs to read it.'\n : [partial, 'Everything it built is an unpublished draft until you publish it.'].filter(Boolean).join(' '),\n link,\n ...scope,\n }\n }\n return {\n type: 'content.aiJobFailed',\n level: 'warning',\n title: `Your ${noun === 'AI job' ? 'AI job' : `${noun} job`} stopped`,\n body: withRefund(job, aiCustomerSafeCopy(job.error || 'It stopped before it finished.', { page: job.kind === 'page' }), 'failed'),\n link,\n ...scope,\n }\n}\n"],"names":["aiBuildOutcomeLine","aiSitePartialCopy","aiJobKindNoun","aiCustomerSafeCopy","aiJobRefundCopy","AI_JOB_BESIGNER_SEGMENT","screen","reusableComponent","layout","template","emailScreen","AI_JOB_LINK_PARAM","AI_SITE_BUILD_HREF","withRefund","job","text","status","refund","refundedCredits","creditsSpent","aiJobNoticeLink","to","base","hostId","kind","encodeURIComponent","$id","own","outputs","filter","output","screens","resource","length","versioned","find","versionId","segment","id","aiJobNotice","noun","scope","orgId","link","review","reason","type","level","title","body","message","error","page","items","unbuilt","some","row","partial","live","sitePublish","published","drafts","stayed","pages","Boolean","join"],"mappings":"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"}
@@ -342,6 +342,12 @@ export interface AiJobItemFailure {
342
342
  reason: AiJobItemFailureReason;
343
343
  /** Customer-safe. */
344
344
  message: string;
345
+ /**
346
+ * The checks' own sentence behind a `doctrine-refused` failure (AGL-3596),
347
+ * rule numbers and all: kept on the job for staff and debugging, never on
348
+ * the wire a customer reads.
349
+ */
350
+ detail?: string;
345
351
  }
346
352
  export interface AiJobItemLedger {
347
353
  /** The unit's slot within the plan: `c0` a creation, `p0` a page, `i0` an item. */
@@ -368,6 +374,12 @@ export interface AiJobItemLedger {
368
374
  degradedBy?: string[];
369
375
  /** Customer-safe: what the person should know about this item. */
370
376
  note?: string | null;
377
+ /**
378
+ * When this attempt settled, as an ISO string (AGL-3596): the moment the
379
+ * next item started, which is what its progress row counts from. Absent
380
+ * while the item is open, and on a ledger written before it was kept.
381
+ */
382
+ settledAt?: string | null;
371
383
  }
372
384
  /** Whether a build's own planning was charged or given back (AGL-3616). */
373
385
  export interface AiJobOrchestration {
@@ -538,6 +550,13 @@ export interface AiJobSummary {
538
550
  * on every other kind.
539
551
  */
540
552
  siteInputs?: Record<string, unknown> | null;
553
+ /**
554
+ * Whether the machine confirms this job's plan itself (AGL-3594): a guided
555
+ * site start. Its plan parks for the instant the confirmation takes, and a
556
+ * surface reads that park as the build going on (AGL-3596). Absent on every
557
+ * other job.
558
+ */
559
+ autoConfirm?: boolean;
541
560
  /** A `build` job's items (AGL-3616); absent on every other job. */
542
561
  items?: AiJobItemLedger[];
543
562
  /**