36 Commits
Author SHA1 Message Date
James MaesandClaude Opus 5.5 39c86cdcae refactor(help): register the help bundle with core HelpBundles
maven / build (push) Successful in 1m3s
build / maven (push) Successful in 1m3s
Replace the MarketingHelp STAND-IN (qqq#1198), which applied
help/qbit-marketing.yaml straight onto app and table metadata, with
core HelpBundles.register in the producer. Core's startup sync and
HelpTextRenderer now apply and resolve the bundle; the QBIT layer keeps
host help winning.

HelpBundleCoverageTest now runs core HelpBundleAssertions over the
qbit's own objects (QQQ's generated bulk processes stay core's), plus
widget/possible-value-source coverage and non-blank content, which the
core gate does not require. The empty-state help guardrail checks the
registered bundle as rendered by core, and a HOST-layer fixture proves
host help is not overridden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:45:24 -06:00
james.maesandClaude Sonnet 5.5 1c3d163104 feat(widgets): business-only Marketing home; omit widgets without read
Take Configuration Status and Agent Activity (provider configuration, model/tool
calls and tokens) off the home; expose them as MarketingWidgets.ADMIN_WIDGETS
gated on marketing.admin.read. Needs Attention gains a business-worded row per
active channel that cannot send. Home and record widgets now require
marketing.read via MarketingPermissionChecker (HIDDEN) and Base.render no longer
returns a "You need marketing.read" shell.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 09:12:20 -05:00
james.maesandClaude Opus 5.5 b733cba229 fix(help): the help bundle carries the artifact version (Maven-filtered) instead of a hardcoded SNAPSHOT
On release branches the bundle's version and the producer's differed, so the
bundle test that asserts they match failed, and released bundles recorded
SNAPSHOT as their provenance. help/*.yaml is now filtered:
version: ${project.version}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:10:27 -05:00
james.maesandClaude Opus 5.5 4bd28c5764 feat: channel rules staleness check, table help applied at run time with how rows get created
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:30:22 -05:00
james.maesandClaude Opus 5.5 adbefe7c33 fix(widgets): show widget dates in the user's time zone
The record screens show date-times in the user's zone, so widget dates
printed in UTC disagreed with them (12:26 PM vs 5:26 PM UTC for the same
moment). Widgets now use the session's zone (else the instance's), with
the zone name: "Oct 14, 2026 12:26 PM CDT".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:33:23 -05:00
james.maes 7aa971bb22 Merge pull request #22 from QRun-IO/feature/demo-server
test: local demo server for screenshots and manual checks
2026-10-14 12:24:01 -05:00
james.maes 36a1b35ea1 Merge pull request #21 from QRun-IO/fix/widget-dates
fix(widgets): readable dates; a used approval is not "Not approved"
2026-10-14 12:23:57 -05:00
james.maesandClaude Opus 5.5 5b2589b1d5 fix(widgets): readable dates, and a used approval is not "Not approved"
Needs Attention and Proposed Publication printed raw ISO instants; they
now read like "Oct 14, 2026 4:53 PM UTC". Proposed Publication said
"Not approved" for content whose approval an execution had consumed; it
now says who approved it and that an execution used it.

Found in the demo-server screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:04:59 -05:00
james.maes acc8e1a40a Merge remote-tracking branch 'origin/develop' into feature/demo-server 2026-10-14 12:01:41 -05:00
james.maesandClaude Opus 5.5 2adf563a4a test: local demo server for screenshots and manual checks
MarketingDemoServer (test sources, not shipped): H2 in memory, mock login
with every marketing permission, the verified starter data, and history
produced through the real actions (readiness, an agent draft on the
scripted provider, review, approval, schedule, a test-transport send).
Run it with the Next UI dev server; see its javadoc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:01:40 -05:00
james.maes 1ee64c95fe Merge remote-tracking branch 'origin/develop' into feature/measurement-rollup
# Conflicts:
#	README.md
2026-10-14 11:57:48 -05:00
james.maes b4387881c8 Merge pull request #19 from QRun-IO/feature/help-bundle
feat(help): help bundle (qqq#1198 format) with a coverage gate
2026-10-14 11:56:30 -05:00
james.maesandClaude Opus 5.5 70d3bc65e6 docs(help): daily results table, Rebuild Results and Import Outcomes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:55:26 -05:00
james.maes 89a1a76952 Merge remote-tracking branch 'origin/feature/help-bundle' into feature/measurement-rollup
# Conflicts:
#	CHANGELOG.md
2026-10-14 11:55:25 -05:00
james.maesandClaude Opus 5.5 d5979d7308 docs(help): marketing_agent_run.heartbeatAt (from #18)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:53:54 -05:00
james.maes a40d2ad5ed Merge remote-tracking branch 'origin/develop' into feature/help-bundle 2026-10-14 11:53:03 -05:00
james.maes 1c0fa34e7c Merge remote-tracking branch 'origin/develop' into feature/crm-table-names
# Conflicts:
#	CHANGELOG.md
2026-10-14 11:52:54 -05:00
james.maes 0e37be1070 Merge remote-tracking branch 'origin/develop' into feature/measurement-rollup 2026-10-14 11:50:26 -05:00
james.maesandClaude Opus 5.5 30182b4805 feat(measurement): daily results rollup and an outcome importer SPI (#4)
marketing_metric_daily (19th table, system-written, child of the campaign)
holds per-day raw counts and unique actors per stage and event type,
recomputed after every outcome-event insert, update and delete, plus a
Rebuild Results campaign action. MeasurementReport sums the rollup and
counts campaign-wide unique actors with a backend COUNT_DISTINCT instead
of loading every event. MarketingOutcomeImporter lets hosts plug in
analytics sources, run by the Import Outcomes campaign action through
the normal (source, external id) dedupe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:50:25 -05:00
james.maesandClaude Opus 5.5 a590ae47d1 feat(help): help bundle for every marketing app, table, field, action, widget and value source
help/qbit-marketing.yaml in the qqq#1198 bundle format (layer QBIT, 346
entries), with live {{values:}}, {{label:}} and {{maxLength:}} references.
HelpBundleCoverageTest is the stand-in coverage gate until core's
HelpBundleAssertions ships; registration follows with qqq#1198.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:48:44 -05:00
james.maesandClaude Opus 5.5 1a05444e61 feat(agent): count agent concurrency and model-call rate in the database (#4)
maxConcurrentAgentRuns and maxModelCallsPerMinute were per-JVM, in memory.
Preflight now fails stale RUNNING runs (no heartbeat for maxRunSeconds
plus 60s) and counts RUNNING runs across all products; the per-minute
limit counts model-call steps in the last minute and waits instead of
failing. Runs record heartbeatAt (new column) on each model call. The
policy's maxConcurrentAgentRuns override is now honoured.

The count-then-start window remains racy; the ConditionalUpdateAction
claim from qqq#1178 (QQQ 4.1.0-RC.7) closes it in a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:46:46 -05:00
james.maesandClaude Opus 5.5 5dd68eb20b refactor(crm): crm_* table names only, no legacy fallback
qbit-crm 0.2.0 renames its tables to crm_* with no compatibility shims
(owner decision via the coordinator), so the gateway uses the snake_case
names directly and drops the camelCase fallback and LEGACY_* constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:46:24 -05:00
james.maesandClaude Opus 5.5 d732cef626 feat(crm): resolve qbit-crm's snake_case table names, legacy fallback
qbit-crm is renaming its tables to crm_* snake_case. QbitCrmGateway now
resolves each table from the instance at call time, preferring the
snake_case name and falling back to the legacy camelCase one, so either
qbit-crm generation works. Tests run the gateway against both schemes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:40:48 -05:00
james.maesandClaude Opus 5.5 0088e3b77b build: assistant provider SPI RC.12 and parent 2.1.2, no allow-lists
Move to qbit-assistant-provider-api 0.1.0-RC.12, whose types now live in
com.kingsrook.qbits.assistant.spi (ModelPricing included), and to
qbit-build-parent 2.1.2, which allows that package by default. Drop the
per-repository guardrail allow-lists. qbit-assistant-provider-openai is
only used by tests, so it becomes test scope; hosts that register it add
it themselves.

Fix the stale README status (RCs are on Central; hosted in Voyage#113).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:35:23 -05:00
james.maesandClaude Opus 5.5 78ad854f41 refactor(email): use qbit-email's public api package (fixes #13)
QbitEmailTransport and MarketingRuntime use com.kingsrook.qbits.email.api
from qbit-email 0.1.0-RC.3; getStatus() keeps its mapping through
EmailApiReadiness.failedChecks (NOT_CONFIGURED only for the base URL and
unsubscribe key; a missing provider or brand address is still REJECTED
at send). qbit-build-parent 2.1.1 allows qbit-email's api by default, so
the temporary email allowances are gone; only the existing assistant
provider SPI allowances remain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:30:56 -05:00
james.maesandClaude Opus 5.5 e790a1091f Merge origin/develop into feature/arch-provider-api
Brings in the qbit-email transport (#11). Conflicts are resolved in
favour of the RC.6/provider-api side, keeping qbit-email.

qbit-email is a new cross-qbit implementation dependency that the
enforced G1 guardrail rejects. It is allow-listed explicitly and
temporarily until qbit-email publishes an API (#13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:18:27 -05:00
james.maes 1c5bada863 Merge pull request #11 from QRun-IO/feature/email-qbit
feat(email): QbitEmailTransport for MARKETING sends through qbit-email
2026-10-14 08:10:59 -05:00
james.maesandClaude Opus 5.5 3ec8776c2a refactor(providers): depend only on qbit-assistant-provider-api and provider-openai
Marketing compiled against qbit-assistant internals, and installing it
pulled in the whole assistant runtime. It now depends on
qbit-assistant-provider-api (the provider SPI and ModelPricing) and on the
published qbit-assistant-provider-openai, both 0.1.0-RC.11. The local
OpenAiCompatibleModelProvider is gone, and tests use a local scripted
double, so the full assistant is not needed even in test scope.

Hosts register the published provider through ProviderSettings. Its
provider name is "openai-compatible", so Marketing Policy provider
overrides must use that name.

Also move to QQQ 4.1.0-RC.6 and qbit-build-parent 2.1.0 with the
guardrails enforced. The provider-api packages and provider-openai are
explicitly allow-listed.

Closes #9 (architecture review QRun-IO/qqq#1150, finding F5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:00:04 -05:00
james.maesandClaude Opus 5.5 6ef32bcefe feat(email): QbitEmailTransport for MARKETING sends through qbit-email
Phase (d) of QRun-IO/qbit-email docs/DESIGN.md section 11. qbit-email is an
optional dependency (0.1.0-SNAPSHOT until its first RC) used through one
adapter class behind a classpath and instance guard; without it the
existing behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:12:44 -05:00
james.maesandClaude Opus 5.5 fa98cc082b feat: contingent possible values for scoped foreign keys
Dependent choices follow their parent selection on the same form (QQQ possibleValueSourceFilter with ${input.x}); filter builders still list everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:25:11 -05:00
james.maesandClaude Opus 5.5 368f40e062 docs(spec): record owner decisions, provider defaults and AI-content policy
Owner decisions (2026-10-12): all recommendations accepted. Records the
provider defaults (Claude via qbit-assistant, SES email from qrun.io, no
social transports), the AI-content channel policy (owned channels with
disclosure, release notes allowed, community channels not allowed) and a
USD 25 monthly cost limit; the starter data carries the same values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:19:09 -05:00
james.maesandClaude Opus 5.5 9e71b2956d fix: close the gaps found in code review
HIGH:
- An edit through the UI or API passes the id as a String, so the content
  customizer's old-record lookup missed. The edit then skipped the hash
  recompute, the revision, approval invalidation and the status guard, and
  edited text could go out under the old approval. Ids are now normalized,
  the old row is re-read when it is missing, and the update errors instead
  of failing open. Execution also compares the approval with a hash
  recomputed from the fields it is about to send.
- One failing execution (a CRM outage, a malformed scope, a channel with no
  type) aborted the worker batch on every run and starved every later
  execution. Each execution is now isolated: a failure before the claim
  blocks it with the reason; a failure after the claim leaves it Running for
  its lease to expire into reconciliation. Recipient scopes are validated
  at submit and approve time.

MEDIUM:
- The policy fails closed. Without the default row, or without an explicit
  "on", the agent and execution are off. Configuration Status shows the
  policy state.
- The channel hourly rate and the agent budgets are counted as the system
  user, so record locks can't hide other products' usage.
- The agent's write tools need the requester's marketing.write, and the agent
  revises only content that is still a Draft.
- The entry points of CRM results, readiness checks and starter data check
  their own permission. CRM activities are written with the user's CRM
  permissions (ENFORCE).
- Closing or downgrading an open blocker finding needs
  marketing.approve.hasAccess, whether through the action, the table or the
  API.
- Approve pins the content hash shown on the Review screen. Content that
  changed before execute is skipped ("review it again").

ReviewFixesTest covers each fix. Fixtures create the default policy row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:59:56 -05:00
james.maesandClaude Opus 5.5 ed62add2de docs: README, design, operations and starter data
- README: what the qbit does, how to install it, permissions, the UI, and an
  honest table of what is implemented versus what is not configured.
- docs/DESIGN.md:
  - the patterns reused from qbit-assistant, the QQQ AI tool kernel and core
    permission enforcement, the existing qbits, qbit-crm, QQQ messaging and
    the ESB / Quick Search
  - the data model
  - content, revisions and approval
  - execution and the transports
  - measurement
  - the CRM integration
  - the agent and its tools
  - the configuration status
- docs/OPERATIONS.md: every setting, the policy row, the schema workflow, the
  credentials to configure later, run-books (publishing, blocked and ambiguous
  executions, the kill switch, recording outcomes, agent runs), and the scale
  limits stated plainly.
- docs/STARTER-DATA.md: the three products' readiness, the sources verified
  on 2026-10-12, the disputed claims, and the starter campaigns.
- CLAUDE.md / AGENTS.md: the architecture and the generated-code rule.
- Tests use obviously fake key strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:46:24 -05:00
james.maesandClaude Opus 5.5 094f728d85 test: verification matrix, persistence across restart, provider fixture
Tests for the required checks, and the fixes they drove:

- Unauthorized access is denied with core permission enforcement on.
  Preparation, approval, execution and the agent need separate permissions.
  The optional product security key bounds what users and the agent's tools
  see.
- Drafts are made without publication authority. The agent has no approve,
  schedule or send tool. A draft that cites a non-released claim gets a
  blocker finding, which stops approval.
- Approval is invalidated by any approved-field edit, including a schedule
  change. Status and approval rows can't be forged.
- Pause, resume and cancel. Duplicate scheduling and runs are refused.
  Ambiguous outcomes are reconciled or decided by a person and never retried
  blindly. Repeats are deduplicated by the idempotency key. Retries happen
  only when the request was certainly not sent. Expired leases need
  reconciliation.
- At execution time: the kill switch, the schedule, consent and suppression,
  the bulk limit, channel AI-content rules, and unconfigured transports.
- Outcome events are deduplicated by (source, external id). Raw events and
  unique actors are reported separately, and uninstrumented metrics show as
  "Not measured". CRM import is idempotent, and leads are qualified only by
  an explicit rule.
- Missing provider: the run fails with a plain reason and manual work still
  runs. Provider errors are retried or failed by kind, and no secret appears
  in errors. Budgets, the concurrency limit, cancellation and the model-call
  limit are enforced.
- Persistence across a restart, on the RDBMS module over a file-backed H2
  database created from the shipped DDL.
- The OpenAI-compatible provider is tested against a local HTTP fixture.

Fixes:
- Service entry points run as trusted internal work after their own
  permission check, the same way QQQ trusts nested calls. Agent tools still
  force PermissionEnforcement.ENFORCE.
- Content status and authorType are managed rather than required, and
  productId is derived.
- Schedules are kept to the second, so stored values hash the same as
  approved ones on every database.
- The Result screen lists skipped records with their reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:35:26 -05:00
james.maesandClaude Opus 5.5 7a7972d661 feat: Marketing QBit with its agent, approvals and controlled execution
The first version of qbit-marketing. It manages products and their evidence,
campaigns, content with revision history, exact human approval, durable
execution through transports, measurement, and a marketing agent. Everything
lives in 18 standard marketing_* tables inside the host's QQQ app.

- Model: products, audiences, evidence sources, claims (released / planned /
  unverified / disputed), campaigns, channels with their rules, content and
  immutable revisions, approvals, executions and attempts, test deliveries,
  agent runs and steps, findings, metrics, outcome events and the policy.
  scripts/generate-model.py generates the entities, enums and DDL from one
  spec.
- Approval binds a hash of the text, destination, recipient scope, schedule
  and channel. Any edit invalidates it, sends the content back to draft and
  blocks pending executions. System-written tables are read-only and are
  guarded so that only Marketing's services write them.
- Execution: idempotency keys, leases, retries with backoff only when the
  request was certainly not sent, and no blind retry of ambiguous outcomes
  (reconcile, or let a person decide). Every send re-checks the kill switch,
  approval, schedule, channel, transport, AI-content rules, rate limit,
  consent and suppression. A controlled test transport and a QQQ messaging
  email transport are provided.
- Agent: qbit-assistant's model-provider SPI (Anthropic, scripted) plus an
  OpenAI-compatible provider. Narrow typed tools run as the user with
  permission enforcement, inside the untrusted-data envelope, and with
  redaction. The tools can draft and record findings but cannot approve or
  send. Runs are bounded and recorded with their usage and cost.
- CRM: a narrow gateway SPI with a qbit-crm adapter. Results are imported as
  outcome events, and leads are qualified only by a campaign's explicit rule.
- UI: the app home dashboard, record widgets, and streamed-ETL record actions;
  the nav holds only the main and settings tables.
- Starter data: QQQ, gclo and MarkdownViewr for Windows, verified 2026-10-12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:23:38 -05:00
james.maesandClaude Opus 5.5 b26f832fc9 chore: bootstrap qbit-marketing with the QRun qbit repository standard
Maven scaffold (qbit-build-parent 2.0.0, com.kingsrook.qbits, <revision>
versioning, qqq 4.1.0-RC.4 BOM, JaCoCo gate), QBit config and producer,
CircleCI qqq-orb pipeline, and the standard repository files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:41:30 -05:00