Skip to content

fix: broken prompt rendering due to tracking, harden name and version (v0.4.6) - #18

Merged
veeqtoh merged 3 commits into
0.xfrom
v0.4.6
Aug 6, 2026
Merged

fix: broken prompt rendering due to tracking, harden name and version (v0.4.6)#18
veeqtoh merged 3 commits into
0.xfrom
v0.4.6

Conversation

@veeqtoh

@veeqtoh veeqtoh commented Aug 5, 2026

Copy link
Copy Markdown
Member

Motivation and Context

Tracking defaulted to on in production, but its tables ship as a separate opt-in publish step. So the natural install composer require, publish config, deploy threw on the first Deck::get():

DECK_TRACKING_ENABLED=true php artisan prompt:test order-summary
SQLSTATE[HY000]: General error: 1 no such table: prompt_versions

Dependencies

None. One new migration, only if you use tracking. Two upgrade impacts, both in UPGRADE.md:

Test Instructions

composer install
composer test         
composer test:lint

veeqtoh added 2 commits August 5, 2026 08:53
Tracking defaulted to on whenever APP_DEBUG was false, but its tables are
published in a separate opt-in step. The natural production install therefore
threw "no such table: prompt_versions" on the first Deck::get(). Caching gave
no protection, because the active version is resolved before the cache is
consulted, and prompt:activate failed the same way.

Tracking now defaults to off. Every database interaction is guarded and
degrades to metadata.json with a single logged warning:

  - The connection is resolved through a latched helper, so a broken database
    is attempted once per instance rather than once per prompt load. An
    unreachable host would otherwise cost a connect timeout on every render,
    which is worse than the fast crash it replaces. Throwable is caught there
    because an undefined DECK_DB_CONNECTION raises InvalidArgumentException
    from the connection resolver, not QueryException.
  - track() catches Throwable and never rethrows. It runs after a completed,
    paid-for AI call, and no analytics failure is worth discarding that.
  - activate() degrades only when the table is genuinely absent, and rethrows
    anything else. Swallowing a deadlock or constraint violation would write
    metadata.json, return true, and leave the database — which
    getActiveVersion() prefers — still pointing at the previous version.

activate() also now upserts rather than issuing an UPDATE that matched no rows
because nothing ever inserted them. The table was permanently empty, so the
lookup that crashed installs could fail but never succeed. Recording a version
requires user_prompt to be nullable, since Deck keeps content on disk.

Also fixes two unrelated defects in the same class:

  - Prompt names were interpolated into filesystem paths unvalidated, so a
    name containing '..' or a separator could read any file on disk.
  - The version directory pattern was unanchored and applied to the full path,
    so rev2, dev3 and archive-v9 registered as versions 2, 3 and 9 — listed by
    prompt:list --all, then failing to load. PromptManager and make:prompt
    must agree, so both are anchored against the directory name.
Records that tracking is now opt-in and how to turn it on, that prompt
rendering never depends on it, and the constraints on prompt names.

Documents the behaviour change with the widest reach: now that activate()
genuinely populates prompt_versions, the database takes precedence over
metadata.json, so editing active_version in git and deploying no longer
changes what is served. Activation is environment state; the file is the
bootstrap default.

Adds an UPGRADE section covering the default flip, the new migration, and
that anyone who published config/deck.php is unaffected.
@mintlify

mintlify Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
Deck 🟢 Ready View Preview Aug 5, 2026, 7:55 AM

track() validated the prompt name before its try block, so it threw on the one
input the release had just taught it to reject — contradicting the promise in
its own docblock two lines above. It builds no path; the name is only a column
value, so the guard protected nothing.

make:prompt still created names the manager refuses. Kebab-casing passes path
separators through, so `make:prompt Support/Reply` scaffolded a nested
support/reply that PromptManager rejects and prompt:list renders as a broken
`support` entry at v0. Nested prompts never worked — prompt:list only ever
scanned the top level — so the generator now fails cleanly rather than writing
something unreadable.

That is the same generator/loader disagreement this release already fixed for
the version directory pattern, one file over, so the name pattern moves into a
ValidatesPromptNames concern that both share. Two drifts of the same shape is
enough to stop relying on the two staying in step by hand.

The pattern is also re-anchored with \A and \z: $ matches before a trailing
newline, so "order-summary\n" was accepted.

Adds an UPGRADE note for anyone who organised prompts into subdirectories,
which worked in 0.4.5 and does not now.
@veeqtoh veeqtoh changed the title 0.4.6 — tracking must never break prompt rendering, plus name and version hardening fix: broken prompt rendering due to tracking, harden name and version (v0.4.6) Aug 6, 2026
@veeqtoh
veeqtoh merged commit 170743d into 0.x Aug 6, 2026
8 checks passed
@veeqtoh
veeqtoh deleted the v0.4.6 branch August 6, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant