Tilltrend

Changelog

Every entry on this page is shipped, verified work: no aspirational entries, no roadmap dressed up as progress. This page is generated directly from the repository's CHANGELOG.md by site/build_changelog.py, so the public log and the real build log are the same document.

Newest first. Open any entry to read it in full.

October 2026 21 entries

  1. The Overview shows your headline numbers and revenue chart on one screen, and the logo stands out on the dark menu.

    On a laptop screen of 1440 by 900 pixels or larger, the Overview now shows your total revenue, orders and average order value together with the whole revenue-over-time chart, without scrolling: the page heading, the note on where the numbers come from, the date filters and Sync now share one compact block at the top, and each card sets its small trend line beside the number. On a phone the revenue chart has its own drawing, taller, with labels you can read. The Tilltrend logo in the dark side menu, on the phone's top bar and in your browser tab now has lighter steel faces, so it no longer disappears into the dark background; in dark mode the logo beside Ask answers does too, and the opening animation's logo turns steel as it lands. The side menu is narrower, with new icons, and the page you are on shows a filled icon in a soft green pill.

  2. Our Privacy Policy, Terms of Service and Data Processing Agreement are public.

    You can now read all three on this website, linked at the foot of every page. They say who runs Tilltrend and how to reach us, what we read from your store and why, what the Ask feature sends to our AI provider (the question you type and, when you ask for an interpretation, the results behind the answer, never a name, email or address), where your data is hosted, and how it is deleted when you uninstall. The Data Processing Agreement takes effect when you install Tilltrend, with no signature needed; a signed copy is yours on request. This is a change to this website only.

  3. On this website, the logo in the footer and in your browser tab shows clearly on dark backgrounds.

    The logo at the foot of every page sits on a dark band, and its dark grey faces nearly disappeared against it. It now has the same lighter, steel-grey faces as the menu bar's logo in dark mode. The small logo in your browser tab does the same when your browser is in dark mode, and stays as it was in light mode. Their shape and their green glow are unchanged. This is a change to this website only.

  4. On this website, the logo in the menu bar shows clearly in dark mode.

    When your device is set to dark mode, this website's menu bar is dark, and the small logo at its left was drawn in dark grey, so it nearly disappeared against it. In dark mode the logo now has lighter, steel-grey faces and shows clearly against the bar; its shape and its green glow are unchanged. In light mode nothing changes. On the homepage, the logo from the opening scene still lands exactly on it, and now brightens as it arrives. This is a change to this website only.

  5. On this website's homepage, the logo lands exactly on the logo in the menu bar.

    As you scroll through the homepage's opening scene, our logo shrinks into the small logo at the left of the menu bar. It used to arrive 23% too large and shrink into place as it faded, so for a moment the two showed as a doubled outline. It now lands exactly on the menu bar's logo at every screen size we measured, from a phone to a 4K monitor. The scene and its timing are unchanged. This is a change to this website only.

  6. The opening animation's logo lands exactly on the logo in your sidebar.

    The first time each person opens the dashboard, a short animation plays in which our logo settles into the sidebar. It used to arrive too large and shrink into place as it faded out: 42% too large on computers and wider screens, 18% on phones. On a slow first load it also landed a few pixels to the left. It now lands exactly on the sidebar logo at every screen size we measured, from a phone to a 4K monitor. The animation itself and its timing are unchanged.

  7. On this website's homepage, the heading also waits for the logo on large monitors.

    Earlier today the homepage's first heading began waiting for our logo to land on phones and tall screens. Large monitors, 1920 pixels wide and more, still showed the heading early: on a 2560 by 1440 screen it was in full view when the logo was only just over halfway to the menu bar. On those screens the page now starts rising as the logo arrives, and the heading is in full view a moment after it lands. Laptop and smaller desktop screens are unchanged. This is a change to this website only.

  8. On this website, the menu opens where you are on a phone or tablet.

    On a phone or tablet, tapping the menu button while scrolled down a page opened the menu at the very top of the page, out of sight, and moved the page up by about half a screen with each tap. The menu now opens over the part of the page you are reading, the page stays where it was, and tapping beside the open menu closes it. This is a change to this website only.

  9. On this website's homepage, the heading waits for the logo to land on phones and tall screens.

    Yesterday's change brought the homepage's first heading into view while our logo was still settling into the menu bar. On phones, tablets held upright and other tall screens, the heading arrived while the logo had barely started to move. On those screens the page now starts rising as the logo arrives, and the heading is in full view by the end of the opening scene. Wide screens are unchanged. This is a change to this website only.

  10. The check mark in your Store Data Health-Check is easier to see.

    The green check mark in the report's note about receipts was too pale against the light green box around it: about 3.6 to 1, where comfortable reading needs 4.5 to 1. It now uses the report's deeper green, at 4.8 to 1. The public sample report shows the change too. No figure in the report changed.

  11. On this website's homepage, the page arrives as the opening scene ends.

    The homepage opens with a short scene you scroll through, in which our logo settles into the menu bar. Once it had settled, about a screen of empty dark space used to scroll past before the first heading. The page now rises into view while the logo is still settling, so the heading is already on screen when it lands. The scene itself is unchanged. This is a change to this website only.

  12. The small notes in your Store Data Health-Check are easier to read.

    One grey used for notes in the report, the paragraph above the receipt and the messages a section shows when it has no data, was too pale on the white page: about 2.5 to 1, where comfortable reading needs 4.5 to 1. It now uses the same grey as the report's other secondary text, at 4.8 to 1. The public sample report shows the change too. No figure in the report changed.

  13. Support is in this website's menu, and two pages read better on a phone.

    The menu at the top of every page now lists Support, which before could only be reached from the footer. On narrower screens, tablets included, the menu opens from the button in the corner. On a phone, the table of app pages on the Product page and the commands on the Evidence page no longer run off the edge of the screen. The sample report reads properly in dark mode, where some of its bold text had turned white on its white sheet. These are changes to this website only.

  14. This website is easier to read, in light or dark.

    The pages on this site now follow your device's light or dark setting, and running text is held to a comfortable line length instead of stretching across a wide screen. The homepage puts a plain chat answer, drawn as an illustration, beside a real Tilltrend answer and the receipt behind it, so you can see what the receipt adds. This changelog is now grouped by month: each entry shows its date and title, and opens to the full text when you tap it. The week headings are gone, because "Week 5" had grown to cover four months. The Product, Evidence and Support pages each gain a short list of their sections, so you can jump to the one you want. Nothing in the app changed.

  15. If you uninstall Tilltrend and come back, you now choose a plan again.

    When Tilltrend is uninstalled from a store, Shopify ends that store's Tilltrend subscription and does not tell us. Our record of the plan stayed as it was, so a store that installed Tilltrend again was let straight into its dashboard on its old plan, with no subscription behind it and no plan chosen. Now, when you sign in and our record says your plan is active, we ask Shopify whether it still is. If it is, nothing changes and you notice nothing. If Shopify holds no subscription for your store, you see the plan page, which says your plan is no longer active, and you choose a plan to carry on. A store that left during its free trial and comes back before the trial's end date keeps the days that were left. Leaving changed too: a browser that was still signed in when Tilltrend was uninstalled is now sent to sign in again, where it used to keep opening the dashboard, and an API token that store made stops working. And before we act on Shopify's notice that a store has uninstalled Tilltrend, or erase a store's data two days after it left, we now check with Shopify that the app really is gone. A notice that arrives late can no longer erase the data of a store that has installed Tilltrend again. What a subscribed store is charged is unchanged.

  16. Signing in now finishes only in the browser where it started.

    When you open Tilltrend from your Shopify admin, Shopify sends your browser back to us with a sign-in link. That link used to sign in whichever browser opened it, so a link made for one store could have signed another person's browser in to that store's dashboard, if they were persuaded to open it within a few minutes. Your browser now carries a short-lived sign-in cookie from the moment the sign-in begins, and we finish the sign-in only in the browser that holds it. If your browser arrives without it, for example because your phone handed the link to the Shopify app, we begin the sign-in again in that browser, once, and you should notice nothing. If that does not work either, you see a page saying the sign-in could not be completed, which asks you to open Tilltrend from your Shopify admin again. Your data and your plan are unchanged.

  17. The Power BI project now carries your store's own currency symbol.

    The Power BI project inside your export put a dollar sign in front of every amount unless your store's currency was one of fifteen it knew, while your dashboard knew thirty-one. A store selling in Serbian dinars or Czech koruna downloaded a project that said "$", and a store selling in Brazilian reais got "RR$". The project now uses the same symbols as your dashboard, so Canadian, Australian, Mexican and New Zealand dollars read "C$", "A$", "MX$" and "NZ$" there too, and a currency we have no symbol for is written as its three-letter code. In every case the project now shows what your dashboard shows. The amounts themselves are unchanged: only the symbol in front of them moves. The export is on the Tilltrend Studio plan.

  18. The plan page now says what is true about your plan, however you reached it.

    The page where you choose a Tilltrend plan opened with "Your free trial has ended" for every store that reached it without access, which was true only for a store whose trial really had ended. A store whose subscription Shopify had put on hold was told that, and so was a store that had cancelled, and so was a store that opened Shopify's approval screen during its trial and came back without approving anything. Opened from the dashboard, the same page could tell a store that had cancelled "Your subscription is active". The page now reads your store's own record. It says when a subscription is on hold and why, when a plan is no longer active, until what date a cancelled plan keeps its access, and how many days of a free trial are left. Prices, plans and what you are charged are unchanged.

  19. If our database cannot be reached, your dashboard now says so instead of showing the setup page.

    When Tilltrend could not read your store's data, for example during a database outage, your dashboard showed the page a brand-new store sees while its first sync runs: "Setting up", a spinner, and a count of minutes that was really the time since your last sync began. Nothing was being set up, and nothing had happened to your data. That page now appears only when your store has no data yet, or while a refresh of your store is running and the dashboard is waiting for it to finish. In any other case you see an error page with the real error on it, and the fault is written to our server log, where we read it. Your Shopify data is never touched by any of this.

  20. Two error messages no longer show internal details, and the error page fits a phone.

    If Tilltrend hits a fault before it has checked who you are, or while an install or a Shopify notification is being processed, the message used to include the technical error text, which could name files on our server. Those two messages now say only that something went wrong on our side, and the details go to our server log, where we read them. If you are signed in and a page fails, you still see the real error, as before, and on a phone it now wraps to the screen instead of running off its edge.

  21. A first sync that fails can now be started again from its own page.

    If Tilltrend's first sync of your store did not finish, the page that told you so asked you to reload it and check again, and reloading started nothing: the only retry was our own, at the next hourly check. That page now has a "Try again" button, which starts the sync from the beginning. Two smaller things change with it. If our server restarts while a sync is running, your dashboard now says that the sync did not finish and lets you run it again, where before the "Sync now" button could stay switched off until the next nightly refresh. And the nightly refresh no longer starts a second sync of a store while one you started is still running.

September 2026 45 entries

  1. The nightly refresh now runs in your store's own night.

    Tilltrend refreshes your store data automatically every night, and until now the word "night" was not quite true: the refresh ran on a fixed 24-hour count that restarted whenever our server did, so its time of day wandered, and each restart pushed the next refresh back. Each store is now refreshed once a night, shortly after 3am in the timezone set in your Shopify admin. If the refresh cannot run at that hour, for example because our server was restarting, it runs at the next hourly check rather than a day later, and a restart no longer triggers an extra refresh. "Sync now" on your dashboard works exactly as before.

  2. Explain no longer withholds its explanation because a product's name spells out a number.

    Tilltrend checks the wording of every explanation against the receipts it explains, and it refuses a number written as a word, such as "fifty" or "thousand", because a word cannot be checked against a figure. That rule also caught product names: an explanation that correctly named a product called "Thousand Oaks Candle" or "Fifty Fifty Tee" was refused, and when the rewrite named it again, Explain withheld the explanation and showed only the verified figures. A name like that now counts as the name it is, as long as the explanation spells it exactly the way your store does. Separately, "hundreds of orders" is now refused the same way "thousands of orders" already was, so an explanation states the exact figure instead.

  3. A refused data export now appears in your audit log, and an export built without its definitions says so.

    Downloading your data as a file is on the Tilltrend Studio plan, so on Starter or Pro that download is refused. Until now the refusal left no trace: your audit log showed nothing at all, so anyone asking what happened found silence. The refusal is now recorded there, the way a refused download made with an API token already was, and both are marked in red like every other refusal on that page, which neither of them was. Separately, if Tilltrend cannot reach its metric definitions while building your export, the data dictionary inside the bundle now says so, rather than quietly leaving out the summaries, definition versions and caveats it normally carries.

  4. Ask can explain an answer about a product whose name reads like a number.

    If your catalogue holds a product, category or country named exactly "Infinity", "inf" or one of a few similar words, pressing Explain on an answer that mentioned it gave a server error instead of the explanation. Those words are how several programming languages spell a number, and the check Tilltrend runs over the wording of its own answers read them that way rather than as the labels they are. Your figures were never affected: the answer and its receipt were correct, and only the written explanation failed. Separately, if a stored receipt file is ever damaged so that its rows no longer match their columns, the audited text now says so and shows every value the file holds, instead of dropping values or failing to open.

  5. Tilltrend can now read your full order history, and your dashboard says whether it does.

    Shopify lets an app see the last 60 days of your orders unless you approve a wider permission, and Tilltrend now asks for that permission, so it can read every order your store has taken. Shopify asks you to approve the change before it applies to your store; until you do, Tilltrend still sees the last 60 days only. The dashboard used to say 60 days for every store, which is wrong for a store that has approved. It now checks what your store has actually granted at every sync and says so: the Orders card on Overview and the "Build identity and data provenance" panel say whether your figures cover your full order history or the last 60 days. If your store has not approved yet, a note under the data window links you to Shopify's approval screen; approve there, press Sync now, and your older orders come in. With them, the customer segment, recency and dead-stock views and the comparison with the prior 12 months reach as far back as your orders go. Ask describes your figures the same way.

  6. The customer segment, recency and dead-stock views now say when your order history is too short for them.

    On Overview, customer segments sorts your customers into VIP, Regular and New, and customer recency into Active, At-risk and Churned. VIP and Regular need at least 12 months between a customer's first and last order; At-risk and Churned need 3 and 6 months since their last one. Shopify lets Tilltrend read the last 60 days of your orders, so every customer showed as New, and almost always as Active, including customers who have bought from you for years. Dead stock, on the Products page and in the Store Data Health-Check, had the same problem and a worse one: it only counts products that sold in that window, so a product that stopped selling months ago did not show as dead stock, it did not show at all. Each of these now says so above its chart, with the dates your orders run from and to, and so does every Ask answer about them. The note goes away by itself once your order history is long enough.

  7. The Tableau and Power BI downloads now carry your headline figures in a form you can build.

    The Tableau download from the Export page includes a guide to the calculated fields you can add. Its first section told you to build total revenue, orders, units, active customers and average order value from a sales file the download has never included, so none of those five could be built. The Power BI download left the same five out altogether. Both now build them from the monthly summary the downloads do include, as Total Revenue, Total Orders, Total Units, Active Customers (per month) and Average Order Value, and each one's description says where it differs from the app: sales lines with no order date are left out, and active customers shows one calendar month at a time and stays blank on a chart that spans more, because a customer who buys in two months would otherwise be counted twice.

  8. The trailing-12-month note on Overview now shows where each window stops.

    Under your revenue and order cards, a note names the two 12-month windows the change badges compare. It wrote them as, for example, 2025-10-01 → 2026-10-01 against 2024-10-01 → 2025-10-01, which reads as though each window included its second date. It does not: each window starts on its first date and stops the day before its second. So the note made each window look a month longer than the one we measure, made the two look as though they shared a day, and ended the current one on a date still in the future. It now writes them as [2025-10-01, 2026-10-01) and [2024-10-01, 2025-10-01), the way the Products page already writes that window, and says in plain words that each one stops before its end date. No figure changes: this is the note under the numbers, not the numbers.

  9. When Shopify asks us to wait before trying again, we now wait as long as it asks.

    Shopify limits how fast an app may read a store, and when we go too fast it can tell us how many seconds to pause. We misread that instruction in two ways. A pause written with a decimal point, such as 2.0, which is how Shopify's own documentation shows it, was not understood at all, so we fell back to guessing. And any pause longer than 20 seconds was cut to 20, so we asked again before Shopify was ready and spent our retries on requests it had already said it would refuse. We now wait exactly as long as Shopify asks, up to a minute; when it asks for longer than that, we stop and report what it asked for instead of retrying early. This applies when we read your store's data and when we confirm your plan with Shopify.

  10. The notes beside your figures now claim only what your data supports.

    Every Ask answer, and every chart on the Trends page, carries notes we attach automatically to say what a figure does and does not include. Four of them were wrong or missing. A date range at the edge of your data was told it touched a partial first or last month even when that month was complete; it now uses the same test the monthly trend uses to mark a partial month, so the two always agree. The lifetime-value note credited the customers it leaves out, whose orders all lack a date, with the revenue of every undated order line in your store, including lines belonging to customers who are still counted; it now credits them with their own revenue, and says separately how much undated revenue is missing from the lifetime value of the customers who stay. A customer count over a date range said nothing about undated order lines or partial months, where the same range on your order count said both; it now says both, and the day, week, month, quarter and year trends now say how many undated lines they leave out. Finally, a change figure such as year-over-year that is empty because there is no earlier period to compare against no longer also gets the general note calling it missing data, since the note written for that figure already says why it is empty. No figure changes: these are the notes beside the numbers, not the numbers.

  11. A subscription that came back after a failed payment could leave you locked out.

    When a payment does not go through, Shopify pauses your subscription and tells us, and we close the dashboard until it resumes. When it does resume, Shopify sends us a single message saying so. If we could not confirm that message with Shopify in the moment it arrived, we still answered as though we had dealt with it, so Shopify never sent it again: your subscription was live and billing, and your dashboard stayed closed, with nothing left that would ever reopen it. A message we cannot confirm is now left unanswered, so Shopify sends it again a little later, which is how a brief failure on our side stops costing you access to work you are paying for. And if you reach the plan page while that is still unresolved, choosing a plan now checks with Shopify first: when your subscription turns out to be live already, we put you back into the dashboard instead of starting a second subscription over the top of it.

  12. A tool that breaks now appears in your audit log, instead of leaving no trace.

    Ask answers by calling a small set of governed tools, and every one of those calls is written to your audit log: the ones that succeed, and the ones the governor refuses. A call that did neither, because the tool itself broke part-way, was written down as nothing at all. The tool had run, and may have reached your data, but your log showed a turn in which it was never called, so the record you can check disagreed with what happened. A broken call is now recorded as its own kind of entry, separate from a refusal, because the two mean different things: a refusal is us declining, and this is us failing. The entry names the tool and the kind of failure. It does not carry the underlying error message, which can quote our own server's internals.

  13. Asking for a metric that is switched off now gets an answer, not an error page.

    Some metrics can be turned off for a store, or can be waiting on data that has not finished building. If you named one of those directly, the page failed with a server error instead of telling you what had happened. It now answers in words: the metric is governed, it is not available on this store yet, and it offers to answer with another one. It does not ask you which metric you meant, because you had just said.

  14. A damaged receipt no longer produces an error page carrying a server file path.

    Every Ask answer is backed by a receipt you can export. If the stored file behind one was damaged or could not be read, the export failed with a server error page that printed the underlying fault verbatim, and in one case that text included the full location of the file on our server. It now answers "not found", which is what a receipt that cannot be read amounts to, and records the fault for us instead of showing it to you. No figure you have been shown is affected: this is the export of a receipt, not the making of one.

  15. A refresh that could not start no longer blocks every refresh after it.

    Refreshing your store runs in the background, and while one refresh is running we skip any second request so that two rebuilds cannot collide. Your store was marked as refreshing just before that background job was started. If the job could not be started at all, which happens when the server is briefly at capacity, the mark was left standing with nothing running to clear it, and from then on every refresh for your store was skipped: the button redirected as usual, the page reported that a refresh had begun, and none of them ran until the app was next restarted. The mark is now cleared when the job fails to start, so a failure costs you that one refresh and nothing after it.

  16. A tool Ask may not use is now refused before it runs, rather than after.

    Ask answers by calling a small set of governed tools, and it is allowed to reach only some of them. When it named one of the others, we told you the call had been refused, having already run it. For one of those tools that meant a copy of the receipt was written on the server, and your audit log recorded a completed export on the same turn the page in front of you called that call rejected. No figure in any answer ever came from it, and nothing you were shown was wrong, but your own record of what happened disagreed with your page. The refusal is now made before the tool can run, and the audit log records it as a refusal. The wording you see is unchanged.

  17. A panel that could not load now says so, instead of telling you the figures are not there.

    Every panel on your dashboard is a live query against your own data. If one of those queries did not come back, because it timed out or the connection dropped, the panel fell back to the words it uses when a store genuinely has nothing yet: "No identified buyers yet", "No customer-attributed revenue yet", "No product pairs yet". Those two situations ask you to do opposite things. One says wait for orders and check your store is connected; the other says the numbers exist and something on our side did not answer. A panel whose query fails now says exactly that, names itself, and leaves the rest of the page alone. The genuine empty states are unchanged.

  18. A store where every customer has bought once now sees its repeat-rate chart.

    The repeat versus one-time panel needs two groups to compare. If your store had no repeat customers yet, there was only one group to show, and the panel treated that as having no customer data at all: it said none of your orders were attributed to a customer, which was the opposite of true. Every one of those orders was attributed, to a customer who has bought exactly once. The panel now draws the split, says in words that there are no repeat customers yet, and shows how many buyers you do have. The same applies in reverse if every buyer has ordered more than once.

  19. Cohort retention no longer shows future months as zero.

    The retention grid is as wide as your oldest cohort, so a cohort that started last month had columns stretching months into the future, and the note under the table said a blank cell means nobody came back that month. For months that have not happened yet that reading was wrong, and it made young cohorts look like they had stopped buying. Cells for months your data does not reach are now left blank and unshaded rather than drawn as empty cells, and the note names the last month of order data the grid was measured against. A blank cell inside that range still means what it always did: nobody came back.

  20. A country your data spells two ways is no longer reported as having no map outline.

    The revenue map lists countries it could not place, for example when the location is missing from the order. If your data held two spellings of one country, "UK" and "United Kingdom" say, the second was put in that list as though we had no outline for it, which was not the reason. The map now names it as a second spelling of a country already shown, says its revenue is not included in the shading, and confirms that it is still in your totals. The shading itself is unchanged: we will not add two of your figures together to make a third one.

  21. The audit log's entry count is now the log, not the page's reading limit.

    The Audit page reads a fixed number of recent entries and said "N entries total" underneath, but N could never be larger than the number it had read. On a busy account the page reported that limit as the size of the whole record while describing itself as the complete, append-only log, and the action filter searched only the entries it had loaded. The count is now the real number of entries, and when the page has read a window rather than the whole log it says which window the filter searched.

  22. Ask no longer withholds an answer because a product's name contains a number.

    Every figure in an Ask answer is checked against the receipt behind it, and a figure the receipt does not contain is refused: that is the promise the answer rests on. The check was counting the digits inside a product's NAME as figures of their own. So a question whose answer named a product like "Mountain-200 Black- 46" was refused twice over and you were shown "Interpretation withheld" instead, even though every number in the sentence was correct and came from your own data. Product names carry digits constantly: sizes, model numbers, years, pack counts. Ask now recognises a name quoted from your data and reads it as a name. Nothing that used to be caught gets through: a figure that is not in the receipt is still refused, including one that happens to match digits inside a product name.

  23. One store's failed refresh no longer costs the other stores their night.

    The scheduled overnight refresh works through installed stores one at a time. If a store's refresh broke in an unexpected way, rather than simply reporting that it could not run, the whole pass stopped at that store, and every store after it kept yesterday's figures until the following night. Stores are always worked through in the same order, so the same stores would have been the ones to lose out. Each store's refresh is now sealed off from the others: a failure is recorded against that store alone and the pass carries on down the list.

  24. The "sample data" notice on a report is decided by the data, not by a name.

    A Store Data Health-Check carries a notice when it was produced from our public demo dataset rather than from a real store. Which report got that notice was decided by looking for the letters "test" in the name of the database holding the data, which is not the same question. A real store whose own handle contains those letters, a "contest" or a "protest" shop, could be told its own report was a sample of ours; and a genuine sample could be printed without the notice. The notice now comes from the record of how that data was built, which is the record every receipt in the report already cites.

  25. Your lifetime-value chart now shows the spread instead of one tall bar.

    Covering every customer, which changed last week, revealed a second problem underneath it. The chart split the range of customer values into eight equal price bands, and on a store with a few very valuable customers that puts almost everybody into the first band and leaves the other seven as empty space. The chart was accurate and told you nothing. It now splits your CUSTOMERS into eight equal groups instead, so every bar holds roughly the same number of people and what varies is how wide it is: the range of lifetime value that eighth of your customers covers. A narrow bar means those customers are worth much the same as one another; a wide one means they are spread far apart. The scale along the bottom now steps up by multiples rather than by fixed amounts, which is what keeps your cheapest eighth and your priciest eighth both visible on one chart. The caption and the screen-reader description were rewritten to match, because the old ones described the old shape. Your data has not changed.

  26. The counts down the side of that chart no longer get cut off on a phone.

    On a narrow screen the customer counts on the left-hand edge of the lifetime-value chart were clipped: a count of 2,464 showed as ",464". The space set aside for them is now sized to the numbers that have to fit in it, whatever your store's size. This had been the case since the chart was first added.

  27. A trend that fails to load no longer tells you that you have no sales.

    If the query behind a trend chart timed out or the connection to your data dropped, the page told you there were no dated sales in your store yet. That is a statement about your business, and it was being made on the strength of a failure nobody had seen. The two situations now read differently: a genuinely new store still gets the original message about waiting for its first orders, and a failure says plainly that the figures exist but the request for them did not come back, with a time you can quote to us. Nothing about your data changed.

  28. Your lifetime-value chart now covers every customer, not a thousand of them.

    The chart showing how much your customers are worth over their lifetime was built from at most a thousand customer records, and it happened to take the thousand who had spent the least. If your store has more customers than that, the chart's scale stopped at the smallest of them, while the median and top-decile figures printed underneath were worked out across everyone. The two disagreed, and the median and p90 marker lines were pushed off the edge of the chart where you could not see them, even though the caption still described them. The chart is now built across every customer with a dated order, and both marker lines are always drawn. If you have more than a thousand customers, expect the shape to change: the old picture was the low end of your customer list, zoomed in. Your data has not changed, only what the chart was reading.

  29. Chart legends no longer run off the edge on a phone.

    On a narrow screen the legend under a chart was laid out as a single row that could not wrap, so its last item sat past the right edge and the page scrolled sideways. It wraps now, on every chart.

  30. A brand-new store can ask questions before its first orders arrive.

    Until your first sync brings orders in, your store's data has no dates in it yet. Asking anything with a period in it ("revenue last month", "compare July with August") ran into that and returned an error page instead of an answer. It now tells you plainly that there are no order dates yet, so there is no window to answer within, and it says the same in the caveats when you ask a question with no period at all. Your Shopify data is never touched by any of this, and nothing changes once your first orders land.

  31. Your activity log shows your store, and says so.

    The Audit page is the append-only record of every question asked, every tool the assistant ran, and every refusal, and it is what backs the promise that no number arrives without a receipt. It now shows your own store's activity and nothing else, and the page says as much at the top so you know what you are looking at. One thing worth knowing when you read it: entries recorded before today are not attributed to any single store, so your log starts from today rather than reaching back. Nothing was deleted, and the record is still append-only.

  32. A problem before your dashboard loads now shows a message.

    A handful of faults could happen early enough in a request that the page had no way to tell you about them, and your browser was left with a blank screen and no explanation. Those now render a real error page you can read and report. Your Shopify data is never touched by any of them.

  33. A first sync that fails now says so instead of spinning.

    When you install Tilltrend it builds your private warehouse from your Shopify store, and until that finishes you see a setup page that refreshes itself. If that first build stopped with an error rather than simply failing to find data, the page had no way of learning it had stopped: it kept spinning and kept promising you a dashboard. It now switches to the message it was always meant to show, which tells you your Shopify data is untouched, stops refreshing, and points you at a reply address. The message itself is not new. What is new is that a build that stops this way can reach it.

  34. The plan page now tells you what changing plan does.

    If you are already subscribed, the page listing the plans names the one you are on, marks it, and stops offering you a button to buy it again. Above the plans it now says three things that were nowhere before: your new plan starts the moment you approve it in Shopify and your current one stops at the same moment; whether a free trial is involved; and that Shopify settles the money for the change, including any credit for the part of the period you have already paid for, which appears on your Shopify invoice. The link to cancel used to tell everyone they keep access until the end of the period they had paid for, which is not true of someone still inside a free trial who has paid nothing. It now says which of the two applies to you.

  35. Your free trial is fourteen days, once.

    Every plan comes with a 14-day free trial. If you subscribed part-way through yours, you used to be handed a second one: choosing a plan on the last day of your trial meant your first charge fell 27 days after you installed Tilltrend rather than 14. That was our mistake, in your favour, and it is fixed. Subscribing during your trial now keeps the end date you already had, and the page shows you that date instead of offering you days you have already used. Nothing changes if you subscribe on your first day. A trial that has already run out still gets a full one when you choose a plan, and a merchant who has been charged before is never asked to pay for a trial they have already had.

  36. An answer no longer calls a figure "all time".

    Ask a question without naming a period and Tilltrend runs it over everything in your warehouse. The answer card labelled that "all time", and the written explanation beside it repeated the phrase back to you. Neither could be true: Shopify lets an app like Tilltrend read the last 60 days of your orders, so a figure with no dates on it covers that window and not your store's history. Those answers now say "no date filter", which is what actually happened to the question, and your dashboard already states the exact window your data covers. The same wording is corrected in the definition behind every governed number, in the notes under the Products rankings, in the note on the Overview filter, and in the readme that travels with the data you download for your own BI tool. The figures themselves are unchanged: this is what they were always measuring, described correctly.

  37. Your dashboard now says how far back it can see.

    Shopify lets an app like Tilltrend read the last 60 days of your orders and no further, and that limit was nowhere on the screen. So a store with no orders in the last couple of months looked like a store with no sales, and the Orders card described its figure as covering all time, which it never could. The card now names the window it actually covers. The revenue panel says outright that a store whose most recent order is older than 60 days reads as empty here even though nothing is wrong with it. The build details panel states the same limit on the data behind every number on the page. Nothing changed about what we read from your store; what changed is that the boundary is visible instead of implied.

  38. A refresh that comes back empty no longer clears your dashboard.

    When Tilltrend refreshes your store, it rebuilds your figures from scratch. If your store answered with no records at all, we used to write that down as the truth: the dashboard went blank, and the time at the top still said it had just synced, so nothing on the screen suggested anything had gone wrong. A refresh that comes back empty is now refused. Your existing figures stay exactly as they were, and the bar at the top of your dashboard says the last sync did not finish, so a figure from before it is never mistaken for a fresh one. The same applies when one part of your store goes quiet while the rest answers normally, which is usually a sign that what Tilltrend is allowed to read has changed. Nothing you had is lost, and that permission is the thing worth checking first.

  39. You can cancel your plan yourself, and you keep the time you have left.

    Stopping your subscription used to mean uninstalling the app. The plan page now has a Cancel your plan link, and it shows you exactly what will happen before anything changes. You are not billed again and there is no notice period. If you have been charged, you keep full access to the end of the period you paid for; if you are still inside your free trial, you keep it to the end of the trial and you are never charged. Cancelling does not remove Tilltrend from your store, and if you change your mind before that date you carry straight on.

  40. A question Ask cannot answer now tells you why, instead of losing what you typed.

    Ask hands your question to the service that writes the answer. If that connection was cut partway through, or the service started replying and stopped, we did not recognise the failure for what it was: you got a bare error page and your question was gone from the box. You now stay on the Ask page with your question still in it and the reason shown above it, which is what already happened for every other kind of failure. A second case is new rather than merely handled better: if the service accepted the question and then went quiet, nothing on our side ever stopped waiting, so the page could hang with no explanation at all. We now stop after a minute and say so, so the question can be asked again instead of abandoned.

  41. A damaged session cookie now sends you back to sign in instead of a page that never loads.

    Your browser holds a small signed token that tells us which store you are. If that token came back damaged, in a way it could not have been when we issued it, we did not answer the request at all: the connection closed and the page failed with nothing on it to explain why, and every reload did the same. A token we cannot read is now simply a token we do not recognise, which sends you through the ordinary sign-in path and out the other side. The same correction was applied to the code that checks an install request coming back from Shopify, so that cannot fail this way either.

  42. An answer from Shopify that gets cut off no longer stops that night's refresh for everyone behind you.

    Your store's connection to Shopify is renewed automatically in the background. If Shopify began answering that renewal and the answer was cut off partway through, we did not recognise the failure for what it was: the renewal stopped in a way that also stopped the overnight refresh for every store queued behind yours. A cut-off or unfinished answer is now treated like any other temporary Shopify problem, so it is simply retried and no other store is affected. The same applies while you are connecting a store for the first time, where you now get the readable explanation we had already written for that case instead of a generic failure. One smaller thing ships with it. A request arriving with an unreadable signature, meaning characters that could never appear in one Shopify produced, used to drop the connection or return an error that read as our fault. It is now refused cleanly, the way a plainly wrong signature already was.

  43. Saying no to a plan no longer ends your free trial.

    If you opened a plan during your trial and then declined on Shopify's approval screen, or closed it without answering, Shopify reported that decision to us and we filed it as a cancellation. Your trial stopped on the spot, the dashboard said it had ended, and our own numbers counted you as a lost customer. A declined or unanswered plan now changes nothing: your trial keeps running until you approve a plan or it runs out on its own. Three quieter things ship with it. When Shopify is slow to answer or drops the connection while you are choosing a plan, we now try once more and, if that fails too, tell you plainly that Shopify did not answer and nothing was changed, instead of a blank error page. Any other unexpected failure on the billing pages or while opening your dashboard now shows a readable error rather than a broken connection. And the file that holds every connected store's access details can no longer be overwritten with a single store's entry if a read of it fails at the wrong moment; the write is refused instead, and nothing is lost.

  44. An install that failed halfway now starts over cleanly instead of leaving your store stuck between states.

    Connecting a store does two things: it records the connection and it sets up your warehouse. If the second one failed, the first had already happened, and that combination was worse than a plain failure. The store looked connected, so trying again was treated as a repeat visit rather than a fresh install, which meant the first data load never started and your dashboard stayed empty. Your free trial had already begun counting down, against a warehouse that did not exist yet. Now nothing is recorded until the warehouse is actually ready, so a failed attempt leaves no trace and trying again works exactly like the first time, trial included. Two smaller things ship alongside it. When Shopify itself turns down the connection, you now get the reason it gave instead of a message blaming us for an internal error, which was misleading in the one moment it mattered. And when a store asks us to delete its data, that erasure now finishes in a safe order and picks up where it left off if anything interrupts it, rather than stopping partway.

  45. A store could report itself as disconnected while its Shopify connection was perfectly good.

    Tilltrend keeps a note of when your store's access expires, so it can renew it a moment before it goes stale. When Shopify answered without saying when the access expires, which is allowed and happens on older connections, we filed that as "expired a very long time ago" rather than "not stated". Two things followed. Every single request went back to Shopify to renew access it already had, which is slow and does nothing. And the first time one of those renewals failed for any passing reason, the store read as not connected at all, so billing and syncing both stopped while a working connection sat there unused. A missing expiry is now treated as unknown, which is what it means: the access we hold is used, and if Shopify ever does turn it down, the existing one-time renewal-and-retry handles it. Nothing about this was visible from your side except the symptom, so if you saw a store go quiet and come back on its own, this was the likely reason.

August 2026 4 entries

  1. If Shopify turns down a subscription, you now see why instead of a blank page.

    Choosing a plan sends a request to Shopify, and Shopify can refuse it: an access token that went stale a moment earlier, a store setting that blocks the charge, an outage on their side. Until today one whole class of those refusals took the page down rather than reporting itself, so you got a blank browser error with nothing to act on and no way back to the plan list. That path now shows the reason Shopify gave, with a link back to the plans. Two smaller things ship with it. A token that went stale between loading the page and pressing the button is refreshed and the request retried once, so the most common refusal of the three never reaches you at all. And the honest part: the test meant to be watching this was rehearsing a situation that could no longer happen, which is why the gap lasted three weeks without anyone seeing it. That test now drives the real path, and the gap it missed is the reason we can describe this one precisely.

  2. A large first sync now waits for Shopify's rate limit instead of failing at it.

    Shopify caps how much an app can pull per second, and it enforces that cap in a way that is easy to misread: the refusal often arrives as an ordinary success response with the error tucked inside it. We were treating those as fatal, so a big import could stop partway through and report a failure that really meant "too fast". Tilltrend now reads the budget Shopify returns alongside every response and slows itself down before it runs out, and on the occasions it still gets refused it waits exactly as long as Shopify asks and then carries on from where it stopped. For a store with a large catalog that means the first sync takes as long as it takes, instead of needing someone to notice and start it again. Said plainly because it is the honest part: no store has hit a real rate limit yet, so this is built to Shopify's published limits rather than to a failure we watched happen. What we can show is that the handling itself is tested, including ten deliberate breakages of it that the test suite caught.

  3. The Power BI and Tableau starters cover every governed view again, and the caveats they carry are current.

    The starter files had been generated a while back and had quietly fallen behind the warehouse. Four governed views added since then (per-product monthly, quarterly and yearly trends, plus the trailing-twelve-month product view) were missing from the Power BI model, so they never showed up in the field list even though the data was there. The governed caveat that travels inside each measure description had drifted from the definition it quotes, and each measure's definition stamp pointed at a version that no longer existed. Both starters are regenerated: the model now exposes the whole governed layer, and every description matches the definition it names. Nothing you have already built on the old file changes, because no measure changed its name, its formula or its number format. The part that lasts is smaller and duller: a starter that falls behind the generator it came from now fails our test suite, instead of waiting for someone to notice.

  4. Uninstalling now clears your API tokens, and reinstalling starts from an empty list.

    When you uninstall, Shopify asks us to delete your shop's data. We did that for your warehouse, your shop record and your onboarding state, but not for the personal API tokens you may have created on the Export page. Two things followed from that, and both are fixed. Those token records stored your shop's domain, so a piece of identifying information outlived the deletion it should have been part of. And because a shop that comes back is recognised as the same shop, reinstalling later meant meeting up to five tokens you could no longer use, already filling the five-token limit, with no way to create a new one until you revoked each of them by hand. Uninstalling now removes them alongside everything else. Nothing changes for a shop that stays installed: your existing tokens keep working exactly as they did, and revoking one is still the only thing that deletes it.

July 2026 41 entries

  1. Your best sellers, for a recent window instead of only all time.

    The Products page ranked your catalogue by revenue since the beginning, which is the fairest measure of standing and the slowest to notice a change. Underneath that ranking there is now a second one you can point at a window: the last 12 months, the last quarter, or the last month. Two things about it are deliberate. Each window is a completed period, never a half-finished one, because a month that is three days old ranks three days of sales and reads like a collapse. And when a window holds too few orders to separate one product from another, the page says so in plain words rather than letting the order look meaningful. The all-time ranking above it has not moved, and every number still traces to the same governed metric with the exact dates it covers printed underneath.

  2. Ask can answer questions about a period.

    You can now ask things like "what was my top selling product last month" and get a ranked answer with a receipt, instead of a refusal. Periods are worked out from the latest order in your data rather than from today's date, so the answer stays honest when a sync is a few hours behind, and the window it used is written into the answer. As always the wording tells you which parts are measured and which are the model's own reading.

  3. The receipt panel and the audit log stop quoting our own file paths.

    The receipt under an Ask answer used to end with a command typed against our source code, and the audit log introduced itself by naming the file it lives in on our server. Neither is something you can open, so both now say what they promise instead. The receipt states that it re-runs its own query and re-checks the result with the model taken out of the loop, and the audit log states that entries are only ever added, never rewritten or deleted. If you want the exact command, it still travels with the receipt inside the CSV you export from that same panel. Nothing about your numbers, your receipts, or the stored record behind them changed.

  4. An Ask answer stops labelling itself twice.

    The card holding the plain-language summary is already headed "What this means", with a note that the wording is the model's own and carries no receipts. The summary itself sometimes opened by repeating that label, so a single card said the same thing two ways, and the exact audited text below it did the same. The repeat is gone from both, so the two still match each other word for word. Nothing about the answer, its numbers or its receipt changed, and the stored record behind it is untouched.

  5. Hover text looks like the app, and the menu fits a laptop screen.

    The note explaining why there is no Funnel page used to arrive as a browser tooltip: a wide strip in the system font, stretched most of the way across the window and dropped over the page. It now appears in a small panel that matches the rest of the app, and on a phone the same reason is printed under the entry, where no hover exists. Hover labels that only repeated a name you could already read are gone. On shorter laptop screens the left menu no longer runs off the bottom of the window: every entry, including the light and dark switch, fits at 1280x720 and up. That switch also sits clear of the link address your browser previews in the bottom-left corner, so hovering a menu item no longer hides it.

  6. The small print under your numbers stops talking to us.

    Every KPI and chart carried a footnote naming an internal file as the home of its definition, and the bottom of every page pointed at a script only we can run. The footnotes now name what is useful to you, the governed metric and the view it came from, and the footer states the promise instead: every number traces to a governed metric, and every Ask answer keeps a replayable receipt. Losing the file path also pulled each KPI footnote back onto a single line. The greyed Funnel entry in the menu explains itself now. It used to end by telling you to see a file that exists only on our machines. It says what is actually missing instead: your orders record what sold, not the visits, product views and carts that came before checkout, and we will not estimate those. The page ships when a storefront analytics connector does. On a phone, where nothing has a hover, that reason is printed under the entry rather than hidden inside one.

  7. The health-check report says "(no category)" like the rest of the app, and drops its dashes.

    A product without a category used to show a long dash in the report's Top products table, while the Products page and an Ask answer called the same thing "(no category)". All three surfaces now use the same words, produced by one piece of code, and a check fails the build if they ever drift apart again. Everywhere else the report showed that dash for a value it did not have (a missing amount or count, an empty field in the receipt panel), it now shows a plain "-". The sample report on this site is regenerated to match. In Ask, the table headers for period-over-period change now read WoW, MoM and DoD, the way reports usually write week-over-week and month-over-month, instead of WOW and MOM. The note under a comparison where one period has no data reads more cleanly, and a rare formatting fault that could garble a web address in the model's commentary is fixed. Later the same day the report caught up with the brand: the header now carries the current Tilltrend wordmark, and the segment bars use the same shape and colours as the app's other charts. This applies to the in-app report, the PDF you print from it, and the public sample on this site.

  8. An Ask answer now opens with a readable result, not a wall of fixed-width text.

    Every answer used to arrive the same way whatever you asked: one block of fixed-width text with tables drawn out of spaces. An answer now leads with a readable card. A single figure is shown large, and a result with several rows is a real table you can scan down a column. Money carries your store's currency symbol and two decimals, percentages carry a percent sign, and numbers keep a gap from the column beside them instead of touching it. A very wide result loads much faster than it did. The exact text you saw before is still there, unchanged to the character, one click away under "Show the exact audited text". The card is not a rewrite of your answer: it is a second rendering of the same stored receipt the old text was rendered from. Nothing reads the answer back and interprets it, so what is stored is still what is shown. And if the card cannot be built for any reason, the exact text leads instead, so a display problem can never hide an answer. A few things that were quietly wrong are now right. The year 2026 printed as "2,026" and an order number as "12,345", right-aligned as though you could add them up; years, order numbers and product codes now read as the labels they are. A missing category reads "(no category)", the same words the Products page has always used, instead of the database word NULL. A missing amount or count still reads NULL, because there is no honest phrase for a number that was never measured, and the note explaining that a NULL means "no data in this scope, not zero" is unchanged. The line describing what you asked, the dates and the filters, now shows your request exactly as stored, so a filter value is never restyled. The model's optional "What this means" paragraph is still labelled as the model's own wording with no receipts behind it, and it now reads word for word the same as the audited text below it; the two could previously differ in punctuation.

  9. Tilltrend introduces itself the first time you open it.

    A short animation of the Tilltrend mark now plays when you open the app for the first time. It plays once ever and you can skip it; either way it does not come back. It is the same motion as the one on our homepage.

  10. When Ask asks you a question back, it now says who wrote it.

    If your question was ambiguous, Ask replies with a clarifying question rather than a number. That reply was being shown under the same "governed text" heading we use for answers quoted from our metric definitions, which was wrong: a clarifying question is written by the model, and on that kind of turn there are no receipts, so nothing checks it. It now appears under its own heading that says plainly it is not a governed answer and that nothing was computed. The genuinely governed refusals, the ones quoted word for word from our definitions, are unchanged and still say so. We also made sure a clarifying question can never contain a figure: if the model writes one, we replace the text with our own wording instead of showing you an unchecked number next to numbers that carry receipts.

  11. Charts, amounts, and the Ask page read better.

    A revenue total in the category mix could print a long trail of decimals (a rounding artefact of adding the numbers up, not a wrong figure); totals are now added exactly, and every money amount shows two decimals like a currency should. Bars across every chart have softened ends, rounded only where a bar actually ends and never in the middle of a stacked bar, so the shape still reads as a measurement. On the Ask page, the question box no longer floats over the answer you are reading, long lines wrap instead of running off the side, and the buttons across the app now share one colour and one corner. None of this changes a single number: your receipts and the exact audited text behind every answer are untouched. On the Products page, changing a product's trend grain (month, quarter, or year) or picking a different product used to reload the page with the "Product trends" section collapsed and the view snapped back to the top. It now keeps that section open and returns you right to it, so comparing one product across month, quarter and year stays smooth instead of feeling like the page reset.

  12. We code-reviewed the past week's work and fixed all ten findings.

    A structured review of the recent changes (the animated homepage, the answer-prose cleanup, and the backup-status signal) surfaced ten real issues; every one is fixed and live. The ones you might have noticed: Ask's prose could occasionally glue two words together around punctuation, and if a product name or link in your catalog contains a long dash, the prose now quotes it exactly as your catalog spells it instead of restyling it. The backup-health signal we publish for our own monitoring now treats an implausible timestamp as a fault instead of reporting "ok", so a silent backup failure cannot hide behind a broken clock. And on the website, the homepage animation now picks its tuning by screen size (tablets got a mismatched middle ground before), and the open menu reliably covers the animation on older browsers.

  13. The export bundle now explains how its tables fit together.

    Every data export (the Excel/CSV, Power BI, and Tableau bundles) ships a data dictionary describing each table; it now opens with a short "How the tables relate" section listing the only join keys that line up across the files: product performance and market-basket rows to the product dimension, daily sales to the calendar, and says plainly that every other table is a finished, self-contained aggregate needing no join. The section is built from the actual contents of your bundle (a join is never documented to a table that isn't there), and the customer directory is explicitly marked standalone: per-customer sales detail deliberately never leaves the platform.

  14. Honest failure states, fresher trends, and a faster Products page.

    A review pass over the recent product-trends work fixed ten issues. The ones you might have noticed: if a product's trend can't be queried for a moment (mid-sync, for example), the page now says exactly that and suggests "Sync now", it no longer misreports "No dated sales yet"; on stores with years of history, the daily Trends view now always shows the most recent days (a row cap could previously leave the "latest" figures stale); and the Products page renders noticeably faster on large catalogs. Behind the scenes, a billing safeguard now ignores a stale cancellation notice from an old, replaced subscription, so changing plans can't lock a paying store out. The plan chooser also now says precisely what every plan includes: the per-answer CSV download is on all plans; the downloadable BI bundle and API tokens are the Studio extras.

  15. A public Support & FAQ page is live.

    tilltrend.app/support.html now says plainly how to reach us (contact@tilltrend.app, a human reads every mail, first reply within one business day) and answers the questions merchants actually ask: how syncing works, what the plans include, how to export or delete your data, and why some fields honestly read "unknown" instead of a made-up number. Every page on the site links to it from the footer.

  16. See whether any single product is rising or dying.

    The Products page has a new "Product trends over time" view: pick any product and a grain (month, quarter, or year) to see that one item's revenue trend over time, using the same governed, replayable time-series the store-wide Trends page uses. The trend starts at the product's first sale, and a period with no sale reads as an honest 0, so nothing is faked before the product existed or after it stopped selling.

  17. A calmer, cleaner dashboard.

    A visual pass across the app: the charts now share one restrained "money-green" instead of a bright neon, with a muted rose and gold in place of the loud pink and amber, so nothing shouts for attention. The long Products and Trends pages open to just the headline and fold the deeper breakdowns behind one-line summaries you expand when you want them; the side navigation is larger and easier to read; and a handful of chart labels that could crowd each other were tidied up. Same numbers, same receipts on every figure: just easier on the eyes.

  18. Data export is now part of the Studio plan.

    Studio subscribers can take their numbers out of Tilltrend and into their own tools: download the governed dataset as an Excel, Power BI, or Tableau bundle, or mint a personal API token to re-pull a fresh snapshot on a schedule, every figure still traceable to its receipt, and each file read on its own, with no live connection back into Tilltrend. Starter and Pro keep the full in-app product: all the revenue, product, and cohort views, the receipts, and the per-answer CSV download, with the downloadable bundle and API tokens reserved for Studio. The free trial includes everything, so a store can try the full export before choosing a plan.

  19. The first live data-deletion order arrived, and was honored on schedule.

    Forty-eight hours after a store uninstalls, Shopify sends the app a mandatory "erase this shop's data" order. The first live one (for a test store we uninstalled two days ago) fired overnight, and this morning's check on the server confirmed both halves: the store's record is gone and its entire private warehouse database was dropped. Deletion on uninstall is now verified behavior, not just a line in the privacy policy.

  20. The pages outside your dashboard now look like Tilltrend too.

    Choosing a plan, the install welcome, and the rare billing-error page used to wear a plain, off-brand look: fine at week one, jarring at the exact moment a merchant decides to pay. Those pages now carry the app's own palette and brand mark, follow your system's light or dark preference, and the plan cards read like the dashboard they lead into. Words and behavior unchanged: verified with screenshots in light, dark, and Founding-deal variants before it shipped, and on the live URL after deploy.

  21. Simultaneous updates can no longer overwrite each other's bookkeeping.

    On subscription day Shopify tells us twice at once: the approval you click and a webhook, and each used to rewrite the whole shop record, so an unlucky overlap could erase the plan name the other had just saved (the same name your Ask allowance is read from). Every record write now goes through locked, field-level merges instead. The tests bite: with the locks deliberately removed, 200 simultaneous Ask counts collapsed to 25 and some writes errored outright; with them, counts stay exact. Nine new automated checks pin the behavior.

  22. Ask's plan limits are now real, and you can always see where you stand.

    Each plan has always advertised a monthly Ask allowance (Starter 300, Pro 2,000, Studio unlimited), but nothing counted. Now every answered question counts toward the month, the Ask page quietly shows "N of 300 used this month · resets August 1", and a store that reaches its limit gets a plain notice with the reset date and an upgrade link, instead of a silent failure or a surprise. The boundaries lean the merchant's way: a question that errors out doesn't count, a paid store whose plan we can't confirm is never blocked, and the limit resets on the first of each month. Twenty-one new automated checks pin the behavior.

  23. We code-reviewed our own sign-back-in feature and fixed what we found, before any merchant could hit it.

    The new "opening the app signs you back in" flow reruns the install handshake, and a deep review caught the handshake still carrying its first-install side effects: signing back in could quietly reset a paying store's subscription record to a fresh trial, and re-download the store's data on every re-entry. Both are fixed: only a genuine first install starts a trial and the first sync; signing back in changes nothing. Also from the same review: two data syncs of the same store can no longer run at once; a leftover login cookie from an erased install no longer blocks re-entry; a merchant with several stores who opens the app from store B while signed into store A now lands in B, not A; and a handful of smaller edge cases in who sees the install page versus the password prompt. Nine new automated checks pin every fix.

  24. Changing your plan is now one click from the dashboard.

    Plan changes have always worked through the billing page, but once you subscribed, nothing linked to it: you had to know the address. Now the dashboard header quietly shows your active plan with a "Change plan" link. Verified live the same day: a test store switched from Pro to Starter through Shopify's own approval screen, no reinstall, no support ticket, which is exactly the standard Shopify's app review holds billing to.

  25. Opening the app while signed out now signs you back in instead of locking you out.

    If your login session expired and you opened Tilltrend from your Shopify admin, you previously hit a bare browser password prompt: a dead end. Now the app recognizes a genuine Shopify open (the request carries Shopify's own signature), verifies it, and quietly signs you back in through the normal install handshake. And anyone visiting the app's address directly, including Shopify's reviewers, sees a proper welcome page pointing to the Shopify admin, not an error. Verified live on the public URL after deploy.

  26. Ask answered its first question from the cloud, after we fixed why it couldn't.

    During a full dress rehearsal on a fresh test store (install → sync → ask → subscribe, recorded end to end for Shopify's app review), the Ask page failed on the cloud server: a development-only AI component was baked into startup even though the cloud path never needs it. The fix loads that component only where it exists; in the cloud, Tilltrend now talks to the AI service directly. Verified live: the rehearsal store synced 65 orders, asked "What were my best products last month?", and got the receipted answer, the AI's prose passed the fact-check gate, and every number traces to a replayable receipt.

  27. Nightly backups are now encrypted.

    The warehouse's belt-and-suspenders nightly backup (kept alongside the managed database's own point-in-time recovery) is now AES-256 encrypted the moment it's written, with the key stored separately and escrowed offline. Verified by decrypting a fresh backup and reading its catalog back. A backup that leaks is unreadable; a backup we need still restores.

  28. Leaving Tilltrend now works as cleanly as joining it, because we tested it.

    After the subscription test passed, we uninstalled the app on the test store and watched what happened: nothing. The handling code existed and was fully tested, but Shopify had never been given the delivery address for its "app uninstalled" and privacy-cleanup notifications: a gap invisible in code review, found only by actually leaving. Fixed the same night: all five notification types (uninstall, subscription changes, and the three privacy/data-erasure notices Shopify mandates) are now wired to the live service, and the app moved to Shopify's modern managed-installation flow in the process. Verified by reinstalling and uninstalling again: the departure was recorded within seconds. Merchants who leave get their departure honored, and their data-erasure requests actually arrive.

  29. Store connections meet Shopify's newest security standard, and the full subscription flow is verified end to end.

    Shopify now requires apps like Tilltrend to use short-lived access credentials that renew themselves, instead of one permanent key. That's live: every new store connection gets a one-hour credential plus a sealed renewal token, Tilltrend refreshes it before it's needed, and if Shopify still rejects a call mid-flight, the app renews and retries once automatically. With that in place, the previously blocked final step of the billing test passed on a test store: pick a plan, approve Shopify's charge screen, and the subscription activates, confirmed against Shopify's own records, never our assumption. No real money moved (test mode), and existing store connections were untouched.

  30. Plans are wired in: Starter $39, Pro $99, Studio $249.

    Tilltrend's pricing now lives in the app: a plan chooser shows the three tiers, each with a 14-day free trial and a clear Ask-questions allowance, and every charge runs through Shopify's own Billing API, so we never see card details. Verified live on a test store: the chooser renders the exact ratified prices. Honest status: no one can be charged yet, billing stays in Shopify's test mode until launch, and the final approval step is currently blocked on adopting Shopify's new expiring access tokens (their 2026 requirement for public apps); that upgrade is next. Also shipped: when a billing step fails, you now get a plain-language page saying exactly why, instead of a blank gateway error.

  31. A brand-new store can install itself in the cloud, end to end.

    First fresh install on the new infrastructure, verified with a test store: approve the connection, and Tilltrend provisions a private warehouse for that store on the managed database and runs its first sync automatically, no hands, no laptop, nothing shared with any other store's data. Two setup gaps found and fixed in the process, which is exactly what a test store is for.

  32. The workstation is retired: the cloud is now Tilltrend's only home.

    The follow-up to "Tilltrend is now always on": after the cloud server ran unattended with the old workstation switched off, 19 of 19 independent health checks green across the whole soak, the old Windows serving stack was decommissioned for good. The cloud server is no longer the primary; it's the only one, watched by the independent monitor and backed by nightly proven backups.

  33. Backups are now proven, and the service watches itself.

    Three additions to the new cloud home, each verified before this entry was written. First, we didn't just enable backups, we ran a full restore drill: a backup of the live warehouse was restored into a separate scratch database and every table's row count matched the original exactly. A backup you haven't restored is a hope, not a backup. Second, an automated nightly backup now runs on the server itself, with seven days retained and a guard that refuses a suspiciously empty backup file. Third, an independent monitor outside our own infrastructure now checks the service's health every 15 minutes, around the clock, and alerts us on any failure, and we deliberately triggered a failure to prove the alert actually fires. If Tilltrend goes down, we find out from the monitor, not from you.

  34. Tilltrend is now always on.

    The dashboard, your data warehouse, and the nightly sync moved from a single workstation to a cloud server with a managed database. Until now the service was only reachable while that one machine was awake: overnight and during restarts it was simply off. That window is gone: the app answers around the clock, independent of any PC. Nothing about your access changes: same address, same login, and store credentials stay encrypted at rest on the new host exactly as before. Every governance guarantee was re-verified on the new infrastructure before traffic moved: analytics connections are still read-only, unsafe queries are still refused and audited, and runaway queries are still cut off. Verified live, including a fresh automatic sync rebuilding the warehouse in the cloud, before this entry was written.

  35. Store credentials are now encrypted at rest.

    The access credential your store hands Tilltrend when it connects, and the database address for your warehouse, are now stored encrypted (AES with integrity protection), not as readable text in a file. The encryption key lives only in the server's protected environment, never alongside the data it protects, so a copied data file on its own yields nothing usable. Key rotation is built in, and the server's health endpoint now states plainly whether secrets are sealed, the same no-guessing rule as everywhere else: we report the mode that's actually running, not the one we intend. Verified live before this entry was written.

  36. Export bundles now record your store's currency.

    The manifest.json in every export bundle carries a currency_code field: your store's own currency, read from Shopify at sync time (Tilltrend never converts; your amounts were always in it). On some setups the sync skipped that lookup, so the manifest said null even while the dashboard showed the right symbol. Fixed: every sync path now records it, and the manifest only ever states the currency actually captured from your store, never a guess.

  37. Your Power BI file now opens onto a real report page, with the governance stamp on the canvas.

    Two fixes in one for the Build my Power BI file bundle. First, the fix: the file always opened and loaded your data, but the report page itself could come up blank on current Power BI Desktop, no page tab, an empty canvas. The project file now matches exactly what Power BI Desktop itself writes, so the report page appears the moment the file opens. Second, what you see on it: a banner across the top of the page that says plainly "Governed snapshot - data as of [date] … NOT a live connection", with your data window and build id, the same honesty stamp the page tab already carried, now impossible to miss. Every bundle's READ-ME also now explains how to automate the download with your API token, including the exact one-line command.

  38. Automate your data export with a personal API token.

    The Export page now has an Automate it section: create a personal API token and your scripts can download any bundle on a schedule, a nightly curl with a Bearer token instead of a browser and a password. A token unlocks only the export downloads (up to 20/hour): it can't open your dashboard, run queries, or create more tokens, and you can revoke it on the same page to cut a script off instantly. You see the token once when it's created; Tilltrend keeps only a hash. Every use, and every refused use, lands in the audit log.

  39. Take your governed data into Excel, Power BI and Tableau.

    A new Export page lets you download your store's governed analytics as a self-contained bundle: the same numbers as your dashboard, with the caveats attached. Download my data gives every metric as CSVs that open straight in Excel or Google Sheets (no install, no password). Build my Power BI file gives a ready governed model: your metric views as tables, with measures, drill-downs and currency formatting already set up, that opens in the free Power BI Desktop; we cold-open-tested it and it loads cleanly with your data. Build my Tableau file works with the free Tableau Public (open the bundled CSVs and paste the governed calculations we include) or with Tableau Desktop. Every bundle reads its files offline: no live connection into Tilltrend, nothing to log into, and customer names and birthdates are never included. It's a point-in-time snapshot, so come back for a fresh download when you want current numbers; and any measure you write yourself isn't governed by Tilltrend: the governed definitions, with receipts, stay in the app.

  40. Your report now shows the receipt behind every number.

    The Store Data Health-Check report (the one you read in the app, the PDF you can save, and the public sample anyone can view) now shows its work right on the page. Behind the top-products table there's a receipt panel with the exact SQL Tilltrend ran, the data build it ran against, the content hashes, and a one-line command to replay the figure yourself with no AI in the loop; that same receipt now travels inside the CSV download too. So "every number carries a receipt" is something you, or a sceptical analyst, can see and check on the spot, not just read about. The same pass cleaned up the report itself: the real Tilltrend logo now sits in the header (it was plain text), the "Store Data Health-Check" title that had been showing white-on-white, invisible, is fixed, and the spacing, document card and tables are tidier. It reads correctly on a phone now, too: a large revenue figure that used to clip fits, and the provenance details stack cleanly. Applies to the in-app report and the saved PDF alike; no number, or how it's computed, changed.

  41. A proper mobile menu on the website.

    The marketing site now has a hamburger menu (☰) on small screens that matches the one in the app, replacing the cramped, wrapped row of links that didn't fit on a phone. The desktop navigation is unchanged.

June 2026 39 entries

  1. A cleaner, at-a-glance Overview.

    The dashboard Overview is now a one-screen digest. The Customers, Products and Geography breakdowns fold into tidy collapsible sections, each showing a quick summary right on its header: how many customers and their average lifetime value, your product count and top seller, how many countries and the leading one, so the highlights are visible without expanding. A new Products section brings the top products and the category revenue-mix onto the Overview, and a Revenue over time chart now sits directly under the headline numbers. Every summary figure is governed (never a fabricated 0), and the trend shows an honest note rather than a faked line until a store has more than one month of data. Reads well on phones too. Zero-JS, $0, dependency-free.

  2. New brand typeface and an app-style website.

    Tilltrend now uses its brand typeface (DM Sans) across the dashboard and the website, and the marketing site has been redesigned to match the product: a dark, app-style look with the emerald accent that reads cleanly on desktop and phones. The font is self-hosted (no third-party font CDN: nothing is fetched from elsewhere when the page loads). No number, and no way any number is computed, changed.

  3. Plum is now Tilltrend.

    New name, new logo, and an emerald palette across the merchant dashboard and the public site. Same product, same governed-metrics-with-receipts: only the name changed; existing stores keep all their data and history. Your dashboard now also refreshes automatically every night, so new orders appear without a manual sync.

  4. Connect Tableau to your governed metrics.

    Prefer Tableau? There's now a starter for it too: five text-only .tds data sources that connect live to the gold views (through the read-only plum_bi role), a sales star plus the pre-aggregated views, with the same 8 governed calculations as the Power BI kit, re-expressed in Tableau syntax (Average Order Value as the ratio of sums, never an average of monthly AOVs; Median LTV, the honest figure, not the mean; Active Customers as a non-additive distinct count), each carrying its caveat in the field description. Your Tableau workbooks inherit the same honest numbers as Ask, the dashboard, and Power BI. Text-only, $0, no new dependencies. (Live database connection needs Tableau Desktop: Tableau Public has no PostgreSQL connector.)

  5. One-click Power BI starter.

    The Power BI starter is now a double-click project (plum-starter.pbip): open it and it loads the gold tables and all 8 governed measures, ready to drop visuals onto, no manual model setup. (The connection-only file is still there if you prefer a blank report.)

  6. Connect Power BI to your governed metrics.

    A starter Power BI kit: a one-click connection to the gold views (through the read-only plum_bi role) plus a governed semantic model whose DAX measures are 1:1 with Plum's metric definitions, Total Revenue, Orders, Units, Active Customers, Average Order Value (ratio of sums, never an average of monthly AOVs), Median LTV (the honest figure, not the mean), and Repeat-revenue share, each carrying its caveat. Your Power BI reports inherit the same honest numbers as Ask and the dashboard. Text-only, $0, no new dependencies.

  7. Bigger Trends charts, with the edge dates fully clear.

    The time-series charts on the Trends page are now taller and have more breathing room at the sides, so the first and last dates, and the value tag on the latest point, sit comfortably inside the chart instead of hugging (or clipping at) the edge.

  8. "Frequently bought together" and purchase cadence, now on screen.

    The market-basket and purchase-cadence metrics get on-screen surfaces: a Frequently bought together table on the Products page (the product pairs that show up in the same orders most often, with how many orders and the share), and a Purchase cadence panel on the dashboard (the typical days between orders for repeat customers, median first, with the average and the slow-10% tail). Honest as ever: co-occurrence is flagged as correlation, not causation, and cadence covers the repeat base only. Zero-JS, $0, dependency-free.

  9. Two more answers: "frequently bought together" and how often customers reorder.

    Market basket ranks the product pairs that show up in the same orders most often (with how many orders, and what share of all orders): handy for bundles and cross-sell, with the honest caveat that it's co-occurrence, not proof that one drives the other. Purchase cadence shows how often repeat customers come back: the typical days between orders (median, plus the average and the slow-90th-percentile tail) across customers with two or more orders. Both answerable in Ask, over MCP, and in Power BI / Tableau. $0, dependency-free.

  10. Charts for dead stock and customer age.

    The dead-stock view (products grouped Active / Slow-moving / Dead by how long since their last sale) now appears as a chart on the Products page beside the performance tiers, so you can see at a glance how much of the catalog (and how much past revenue) is sitting idle. Buyers by age group is now a chart on the dashboard, with an honest "Unknown" bar and the exact birthdate coverage when ages aren't on file. Zero-JS, $0, dependency-free.

  11. Charts for customer recency and product tiers, plus tidier date labels.

    The customer recency (Active / At-risk / Churned) and product performance tier (High / Mid / Low) breakdowns now appear as charts in the app: recency beside customer segments on the dashboard, product tiers on the Products page, each a clean split of share-of-customers (or products) versus share-of-revenue, with the cut-offs disclosed under the chart. We also tidied the date labels on the Trends charts: they're now compact (e.g. "Dec '10", "Q4 '10", "Dec 29") and aligned so the last date no longer runs off the edge or overlaps on a phone: the full date is still in the table and on hover. Zero-JS, $0, dependency-free.

  12. Spot dead stock, and see your customer age mix.

    Two more governed metrics: answerable in Ask, over MCP, and in Power BI / Tableau. Product recency flags dead stock: products grouped Active / Slow-moving / Dead by how long since their last sale (measured against your latest data), so you can see at a glance what to restock, push, or retire, and how much past revenue is tied up in it. Customer age groups break identified buyers into the usual brackets (under 20, 20–29, 30–39, 40–49, 50+). Age needs a birthdate on file, which many stores (Shopify included, by default) don't collect; when it's missing the metric says so honestly with an "unknown" group and the exact coverage, never invented demographics. $0 and dependency-free, as ever.

  13. Know your customers and products at a glance: segments, recency, and performance tiers.

    Three new governed metrics, each answerable in Ask, over the MCP connection, and in Power BI / Tableau, from one shared definition. Customer segments (VIP / Regular / New) split customers by how much they spend and how long they've been buying, now shown as a chart on the Overview page where the gap between "share of customers" and "share of revenue" tells you where the money really is. Customer recency flags who's Active, At-risk, or Churned by time since their last order (measured against your latest data, so it reads "now" for a live store). Product performance tiers group products into High / Mid / Low by revenue. The cut-offs (what counts as a "VIP", "at-risk", or a "high-performer") are yours to set, and the exact values are shown on every answer's receipt, so a number always states the rule it used and you can verify it. $0 and dependency-free, as ever.

  14. A friendlier dashboard on your phone.

    Went through every admin page on a phone and reworked the navigation for small screens. The old cramped strip of tiny, unlabelled icons is gone: there's now a hamburger menu (☰) that opens a floating, translucent panel of clearly labelled pages from the side; tap the ✕ or anywhere outside to close it. The Trends grain selector now wraps instead of cutting off "Year", and a few layout tweaks keep text and tables sitting cleanly within the screen. The menu uses no JavaScript, and the desktop layout is unchanged.

  15. Smaller Trends charts, and a single home for revenue over time.

    The Trends charts now render at a more compact size on screen, and the separate Revenue page has been folded into Trends: its month view already shows the same monthly revenue plus a running total, a trailing moving average, and month-over-month / year-over-year change, so there is now one more-capable place for "revenue over time" instead of two overlapping ones. The governed monthly-revenue definition is unchanged: Ask still answers "monthly revenue" with the same receipted numbers and exact SQL.

  16. Trends: sales by day, week, month, quarter, and year, with charts.

    A new Trends page answers the time questions a merchant actually asks: revenue by day / week / month / quarter / year, the latest period, and the most recent day, each as a line/area chart with a running total to date, a trailing moving average, and the change vs the prior period (and, for months and quarters, vs the same period last year). Pick the grain with one click. Underneath it's a proper BI date dimension (gold.dim_date) and five governed period views built with window functions and CTEs (running SUM, trailing AVG, LAG) over your sales, so a period with no orders shows an honest 0 (never a gap), the partial first/last period is flagged (not read as a trend), and a period-over-period change with no prior period reads "no prior period" rather than a fabricated number. Every grain reconciles to the same total. The same governed metrics power Ask, so "show me weekly revenue" now returns a receipted answer with the exact SQL: $0 and dependency-free, as ever.

  17. Merchant dashboard redesign, one-click data refresh, and automatic store currency.

    A first polish pass on the merchant-facing app, all $0 and dependency-free. Look & feel: a calmer, denser dashboard, teal on a charcoal board in dark mode, a clean blue-on-white light mode, a fixed left icon sidebar, and monitor/sun/moon theme icons; the app is now responsive on phones, and chart text is one consistent size. Sync now: because each store's warehouse is a built snapshot, newly added orders didn't appear until a re-sync, there is now a one-click "Sync now" on the dashboard (plus an opt-in nightly re-sync). The rebuild is atomic, so the dashboard stays up while it runs. Currency, the honest way: amounts now show the store's own currency symbol, captured automatically from Shopify at sync, never an invented exchange rate; if the currency isn't known yet, a bare number is shown rather than a wrong symbol.

  18. "Ask" is now a chat, and it shows the exact SQL.

    The Ask page is reframed as a conversation in the Plum palette: your question, then the analyst's receipted answer, and every answer now displays the exact SQL the deterministic layer ran (query, parameters, row count) beside the receipt id and hashes. The model only picks a governed tool; code writes and runs that SQL, and every number traces to it, so the whole process is visible, not just the result. The duplicate query-log table was removed from the Ask page (the Audit page remains its single home).

  19. Multi-store install proven end-to-end against a Shopify development store.

    Ran the full Shopify App Store install flow for real on a free Shopify development store, an engineering validation, not a customer: the store holds Shopify's synthetic test data, so any figures it shows are not real-merchant numbers. The path that had only ever been fixture-tested now works live: OAuth install → HMAC + signed-state verification → token exchange → a live GraphQL Admin API pull (version 2026-04) → a per-tenant warehouse build → trial start → the first-run onboarding page → a working dashboard, with every number receipted. Also bumped the Shopify Admin API client 2025-10 → 2026-04 (latest stable) and taught the production launcher to carry the app credentials.

  20. First-run onboarding: "syncing" state + trial-days banner (D-030, Phase 4).

    Closed the two gaps a merchant hits before the dashboard, both $0 and zero-dependency. (1) First-run syncing page: a freshly OAuth-installed tenant is provisioned empty and built by a background first sync, so until that lands its database has no schemas and the dashboard used to 500. A gate in http_api (merchant session + warehouse not built) now renders a branded, honest syncing_page: zero-JS <meta refresh> so the dashboard appears on its own when the build commits, a CSS-only spinner with a reduced-motion opt-out, and a plain "didn't finish" message (no auto-refresh) on a failed sync. The gate keys on build existence, not "sync running", so a later re-sync of a built tenant never flickers (build.py is atomic). Sync state is tracked via shops.set_sync(running|done|failed, started_at) from sync.sync_tenant. (2) Trial-days banner: entitlements.trial_banner renders days-left (rounded up; "Last day" inside 24h) + a Subscribe CTA, bound into _layout via a _BANNER ContextVar (same pattern as the theme var) and shown only for a trialing merchant session, silent for active subscriptions, expired trials (the entitlement gate shows the subscribe page), and the operator. Verified: new test_onboarding.py (6), trial-banner tests in test_billing.py (+2), sync-status test in test_sync.py (+1), and the real-HTTP smoke_sessions.py extended to 21/21, a trialing session sees the banner and a second EMPTY-provisioned tenant returns the syncing page (200, not a 500) over the production path. Operator smoke_admin green; C8 receipt-integrity suites pass on a demo warehouse (interception 7/7, fault_injection 24/24).

  21. Multi-store foundation + Shopify App Store readiness (Phase 4, D-021–D-029).

    Turned the single-store product multi-tenant, built to the Shopify App Store's requirements: fixture-proven, no new dependencies. A five-expert panel costed the path (D-021); the build then landed the full spine: a tenant-routing seam (D-022) with db-per-tenant isolation (each store its own PostgreSQL database, zero SQL change, D-023); the Shopify OAuth authorization-code install flow plus the three mandatory GDPR/compliance webhooks, with constant-time HMAC and a stateless signed state (D-024); per-tenant warehouse sync reusing the proven build path (D-025); signed-cookie merchant sessions so each store sees only its own data (D-026); a Shopify Billing subscription with a 14-day trial and an entitlement gate (D-028); and per-tenant receipt isolation so the Ask agent is fully tenant-scoped (D-029). A four-agent adversarial security review then hardened the OAuth/session/webhook surface: tenant scoping moved into the warehouse layer, the raw SQL API made operator-only, webhook destructive actions bound to the signed body, fail-closed auth (D-027). Verified by unit tests and real-HTTP smokes; the receipt-integrity gate stayed green.

  22. Ask agent: facts-first answers + a dormant direct-API transport (perf, team design-debate).

    A live model-path question took ~43s; a measured breakdown showed ~98% was two sequential LLM round-trips (tool-selection 12s + interpretation 30.6s) while the actual warehouse/receipt/render work was 0.7s. Two changes, run through the full team cycle (design → Critic → implement → Frontend → independent verify); the Critic surfaced 5 must-fix items including a cross-request honesty hole in the Explain step (proven numerically safe by post-check scoping, but fixed anyway by deriving the question from the receipt, never the client form). (1) Facts-first staging: ask() split into ask_facts (selection+execute → renders RECEIPTED FACTS immediately) and interpret(receipt_ids) (model prose on demand, still under the full two-strike post-check). The admin Ask page shows the verified number first with an "Explain" button carrying only receipt_ids; refusals and ?nomodel=1 show no button; an honest new placeholder distinguishes "not generated yet" from the deterministic "no model ran". (2) Direct Anthropic API transport in model._complete via stdlib urllib (no new dependency), gated on ANTHROPIC_API_KEY with the subscription path as automatic fallback; non-200/parse/empty responses all map to ModelError with no key leakage. Verified on the subscription path: facts render first, Explain yields a PASS-postchecked interpretation, a bad receipt id is an inline error (not a 500), other pages + nomodel unbroken, canonical 11/11, replay MATCH, placeholder green. The API path is implemented but dormant pending Anthropic API credits (valid key, zero balance); once funded it is expected to cut a question from ~43s to single-digit seconds.

  23. Added the active_customers governed metric (D-007), via the team design-debate.

    A live merchant question, *"how many customers this month?"*, hit the Ask agent's honesty guardrail: no governed definition existed, so it refused with no_definition rather than invent a number (the product working as designed). Closed the gap via the charter process: Data Architect proposed → Evidence Engineer critiqued (3 must-fix honesty issues: an orphaned guest-checkout caveat, an inaccurate LTV-distinction claim, an unlabeled live example) → Lead arbitrated (kept comparable, dropped segmentable as out-of-scope) → AI Engineer implemented → Evidence Engineer independently verified. The metric is a scalar COUNT(DISTINCT customer_key) over gold.fact_sales (date_range + country filters; not additive; guest checkouts excluded by design: a count of *identified* customers). Served by the existing run_metric scalar path with no tool-layer change (enums derive from the registry); the guest-exclusion caveat ships in every receipt (_STATIC_CAVEATS), not just the doc. Verified live: the original NL question now answers 2 (model path, claude-haiku-4-5) with a receipt that replays MATCH; deterministic path also 2/MATCH; direct warehouse cross-check 2 (5 of 9 lines are guest, excluded); canonical 11/11; placeholder check green.

  24. Shopify connector went LIVE against a real store (W5-2, D-006).

    Added the --source shopify-live build target and the live auth path: the Dev Dashboard's client_credentials grant (legacy static shpat_ custom apps are retired on new stores). ShopifyClient.from_env now mints a 24h Admin API token from SHOPIFY_CLIENT_ID + SHOPIFY_CLIENT_SECRET (static SHOPIFY_ACCESS_TOKEN still wins if set); load.load_bronze took a node_iter factory so the live path reuses the fixture path's bronze DDL/INSERT and the identical shopify_silver.sql unchanged. Ran live against pfvew0-0x.myshopify.com (RSD): full pipeline built clean, 11/11 canonical checks, receipted Ask total_revenue = 32,250 replayed MATCH (build 8a9628f2). Found & fixed a real defect live: the store's products had no SKUs, so keying products on SKU alone left every line unattributed (v_product_performance empty; NULL product_number failing the canonical PK check). The mapping now keys products on SKU when set, else the variant GID (always present); fixtures carry SKUs, so the fixture path stayed byte-identical (revenue 210,417; 11/11: regression-verified). Honest gaps (live store was tiny, 5 products, 1 guest order, 0 customers): multi-page pagination, customer/LTV/cohort/repeat analytics, and the incremental cursor remain fixture-proven only. Live warehouse left as the repo's current build.

  25. Shopify connector shipped, contract-first and fixture-proven (W5-1, D-006).

    Introduced the connector contract (app/connectors/contract.md): every source maps to bronze.<source>_* → canonical silver entities (silver.customers / silver.products / silver.order_lines, engine-agnostic) → the EXISTING gold star schema + 6 analytics views, unchanged. Refactored the demo as connector #1: the CRM+ERP integration that lived in the gold dim views moved down into a new 02b_canonical_demo.sql with byte-identical logic, and gold was re-pointed at the canonical entities. Equivalence proven, every audited number reproduced EXACTLY: total_revenue 29,356,250; 27,659 orders; 18,484 customers; 295 current products; repeat 62.86%/37.14% with 77.02% of revenue; LTV avg 1,588.10 / median 272.00 / p90 4,825.70 / max 13,294; top customer AW00012132 (France) 13,294; 33/33 demo quality checks; C8 gate 6/6. New source-agnostic 05_canonical_checks.sql + run_canonical_checks.py (PK uniqueness, sales_amount = quantity × price, referential integrity, gold non-degeneracy): 11/11 on the demo build. Connector #2, Shopify (GraphQL Admin API 2025-10, incremental sync by updated_at cursor, Relay pagination): finished client.py (injectable transport; from_env raises a clear error when credentials are absent; 14/14 unit tests over parse/pagination/edge-cases), version-dated synthetic fixtures with guest-checkout, partial/full refund, deleted-product, and incremental-re-sync edge cases, load.py → bronze.shopify_* (raw JSONB), and shopify_silver.sql → canonical entities (dedup by latest updatedAt; refunds NOT netted). Build integration: build.py --source demo|shopify-fixture (default demo, unchanged + identical counts); meta.build_info now records the source. --source shopify-fixture observed: full pipeline to all 6 gold views; bronze 41/25/121 as-received → silver 40/25/317 after dedup; gross revenue 210,417 EUR; guest checkout → 15 NULL-customer lines, deleted product → 1 NULL-product line, both keeping revenue; 11/11 canonical checks; a receipted ask (plum_ask.py --no-model "total_revenue") returned 210,417 and replayed MATCH, proving the D-003 receipt machinery works unchanged over Shopify-shaped data. Demo source rebuilt afterward: repo default state is the demo warehouse. Liveness (verbatim, D-006): tested against API-shaped fixtures; not yet run against a live store. The live GraphQL path (SHOPIFY_SHOP + SHOPIFY_ACCESS_TOKEN) is config-only and unexercised: no live call has ever been made, and there is no --source shopify-live target. No new dependencies (stdlib + psycopg; urllib for the unexercised live path). check_no_placeholders.py passes.

  26. Public marketing site shipped (W4-2): five static pages, evidence-first, every claim paired with its reproduction command.

    New site/: plain HTML + one handwritten CSS file (ink/paper with a ledger-green accent), zero JS, no CDN, no framework, relative links only (renders from file://). Pages: index (one-liner, three honest sections, and a demo block embedding the VERBATIM output of a real model run of plum_ask.py "What was total revenue?" made today, total_revenue 29,356,250 receipted as rcp_c60c017f43c346328383448a66088735, post-check PASS, replay verdict MATCH, labeled "real output, demo dataset"); product (Ask flow, receipts + replay verdicts table, the real governed funnel refusal quoted from a live run, the five existing admin pages, honest absences and not-yet-supported metrics, architecture section); evidence (9 claim→proof rows, each with the exact command, what to observe, and the freshly observed result: ask→replay MATCH, doctored-receipt FAIL, C8 gate 6/6 re-run today, quality checks 33/33 re-run today, fingerprint 3e02a4b72bbd66a222d0ff9dc808e7fb reproducibility, cross-engine equivalence, 10/10 independent validation, governed refusal re-run today, smoke_admin all-green re-run today; plus a "what we don't claim" section and dataset provenance: public training dataset, 2010–2014 bike sales, not real merchants); changelog (GENERATED from this file by new stdlib site/build_changelog.py, one source of truth, IA rule 1); about (solo builder + AI agent team stated plainly, the category teardown story in three sentences without naming names, contact mailto). No pricing page (pricing undecided: nav carries an early-access mailto CTA instead); zero invented traction anywhere (no merchant counts, testimonials, logos, or lift percentages, the footer on every page states pre-launch status). Observed: new site/check_site.py, 201/201 checks PASS (all files exist, every internal link relative and resolving, nav + footer + pre-launch statement on all five pages, no placeholder patterns in any site file, index demo block contains the receipted 29,356,250, evidence has ≥5 claim rows citing the real commands, changelog page generated with Week 1 + Week 4 entries); check_no_placeholders.py over app/ passes. No new dependencies (stdlib only).

  27. Thin admin UI shipped (W4-1, D-005): five server-rendered pages on the existing stdlib server, zero new dependencies.

    New app/server/admin_ui.py (+ app/server/static/admin.css): semantic HTML rendered in Python, no framework, no Node, no CDN tags, no client-side JS; http_api.py routes /admin/* and /static/* to the new handle_ui seam (form-encoded POST bodies on those paths), all JSON routes untouched. Pages, every number queried live at render time with the canonical SQL from app/docs/metrics.md, each KPI footnoted with its governed metric id: /admin Overview (total_revenue 29,356,250; orders 27,659; aov 1,061.36; repeat_rate split 62.86%/37.14% with 77.02% of revenue; customer_ltv_summary avg/median/p90/max; build-identity panel from meta.build_info + dataset window); /admin/revenue (v_revenue_monthly table + dependency-free server-side inline SVG bar chart, 38 bars from the same rows, partial-month caveat quoted); /admin/cohorts (v_cohort_retention as a 38×38 shaded matrix, sparse cells honestly labeled "zero, not missing", governed cohort definition quoted); /admin/ask (form POST → model.ask, blocking, with a visible "calls the model" note; renders the EXACT renderer output, RECEIPTED FACTS code-rendered with receipt ids + hashes, INTERPRETATION visibly tagged "model prose - not receipted", governed REFUSAL tagged distinctly; per-receipt model-free replay command; post-check status line; ?nomodel=1 deterministic toggle via ask_without_model; recent questions from the audit log, new admin.ask/admin.page audit actions); /admin/audit (last-N table with ?action= prefix filter, refusals/rejections highlighted red, build_id column). Honest absences: Funnel/Experiments/Insights get NO pages or mock UIs, the nav carries one greyed non-link "Funnel - requires event data (see docs/metrics.md §Not yet supported)"; every page footer: "Every number on this page traces to a governed metric. Receipts replayable via plum_replay." Observed: new app/scripts/smoke_admin.py, 31/31 deterministic checks PASS (all five pages 200 with real values incl. 29,356,250, 2013-07 = 1,371,595, cohort 5.23%, live fingerprint 3e02a4b72bbd66a222d0ff9dc808e7fb; nomodel Ask returned RECEIPTED FACTS + receipt id; JSON /health and /views unbroken); --with-model run: real Ask "What was total revenue?" over the HTTP form path → rcp_7c61617284374555a3f03ef7add35c75, post-check PASS (attempts 1, regenerated no, fallback no), replay verdict MATCH. C8 gate re-run after the change: all 6 suites PASS; placeholder check passes. No new dependencies (stdlib only).

  28. Model layer wired via Claude Agent SDK (W2-3, D-004); five real end-to-end transcripts, all numbers receipted, 6/6 gate still green.

    New app/agent/model.py: the ONLY module importing claude-agent-sdk (new dependency, 0.2.100, flagged per D-004; rides the local Claude Code subscription, no API key; model default haiku, override PLUM_MODEL/--model). The model does exactly two things: proposes tool calls as JSON (executed solely through tools.ToolSession.call, closed schemas, registry SQL, receipts, audit; rejections go back once, then a deterministic governed refuse) and writes INTERPRETATION from ONLY the code-rendered RECEIPTED FACTS text, enforced by postcheck.enforce (two strikes → deterministic fallback). New CLI app/scripts/plum_ask.py (--no-model, --json, --model); audit extended with model.proposal, model.proposal_invalid, model.repair_round, model.refuse_fallback, model.interpretation (actor "model"). Observed, real model runs (claude-haiku-4-5-20251001): "What was total revenue?" → total_revenue = 29,356,250 receipted (rcp_68b2cff53e4c4a92a93f1d8bff155ba2), post-check PASS, replay MATCH; "Compare June and July 2013 revenue" → 1,642,948 vs 1,371,595, derived delta −271,353 / −16.52% receipted, post-check PASS, MATCH; "Which countries drive revenue?" → country breakdown top-10 (US 9,162,327 … segment_sum 29,356,250) receipted, PASS, MATCH; "Show me the conversion funnel" → governed not_supported refusal quoting metrics.md, after an audited live repair round (first refuse proposal lacked metric_id, rejected, model corrected); adversarial "Why did revenue drop in July 2013?" → model chose compare_periods + July-by-category breakdown, prose stayed inside receipted numbers and stated "the data shows what changed but not why", PASS, both MATCH. Post-check caught a real strike: an earlier run's first draft quoted caveat-only numbers (19; 4,992) → answer.postcheck_violation → regenerated draft clean (answer.regenerated); that run's date-filtered total 29,348,616 was itself fully receipted and replays MATCH, no number ever shipped unreceipted. render.NO_MODEL_NOTE re-worded (model wiring is no longer "the next task"). C8 gate re-run after the change: all 6 suites PASS; placeholder check passes.

  29. Defect C8-1 resolved: replay grades ERROR on malformed receipts; C8 gate re-run PASS.

    AI Engineer remediation of the MAJOR filed by the Evidence Engineer (W2-2). app/agent/replay.py: _grade is now a never-raises wrapper around _grade_strict, structurally malformed receipts (missing keys, wrong types, non-dict top level) grade ERROR with reason "receipt malformed - refusing to grade (<exc>)" instead of crashing out of replay_file/replay_all; execution-field reads moved out of the re-execution try so a missing execution block reads as malformed, not "re-execution failed". Two tests added to app/tests/test_c8_fault_injection.py: 4 malformed shapes all grade ERROR naming malformation, and replay_all survives a corrupt store member (corrupt file ERROR, every other receipt still graded, observed MATCH). Observed: run_c8_gate.py re-run all 6 suites PASS (fault-injection 9 tests, was 7); ask_demo.py regression PASS (3x MATCH, doctored copy FAIL, refusal audited); placeholder check passes. No open BLOCKER/MAJOR remains (C8-2..C8-9 MINOR/LOW, backlog), gate verdict after re-run: PASS (resolution + re-run log appended to app/docs/evidence.md).

  30. C8 verification gate built + Amendment C1a implemented (W2-2); gate verdict FAIL on one open MAJOR.

    Evidence Engineer deliverables: replay.py verdict filter re-based on the content fingerprint per Amendment C1a (fingerprint differs → STALE; fingerprint match + re-execution hash match → MATCH with rebuilt_since: true when build_id differs; fingerprint match + hash mismatch → FAIL; tamper/internal/derived failures FAIL on any build). New gate runner app/scripts/run_c8_gate.py + four suites in app/tests/ (scratch receipt store, live warehouse): test_c8_interception.py (receipt sql/rows asserted as the SAME OBJECTS run_query executed, is, not ==, across the full registry matrix: 9 metrics + filtered variants, 4 compare templates + filtered, 3×4 segment templates ± date_range), test_c8_fault_injection.py (five doctoring layers, each with its exact predicted failure signature: rows-only → internal_result_hash+receipt_hash; rows+result_hash → receipt_hash+reexecution_hash; fully coherent → reexecution_hash only; doctored derived under a coherent hash → derived only; deleted caveat → receipt_hash only; plus C9 collision-is-error), test_c8_renderer.py (stored = shown under in-memory mutation; per-turn binding: ToolSession.receipt_ids is the renderer's and post-check's only source, a true number from another turn is a violation), test_c1a_verdicts.py (all three C1a branches incl. a REAL build.py rebuild mid-suite: build_id flipped, fingerprint identical 3e02a4b72bbd66a222d0ff9dc808e7fb, pre-rebuild receipt → MATCH + rebuilt_since, NOT STALE; coherent lie after rebuild → FAIL, no longer hidden by STALE). Observed: all 6 suites PASS, c8_interception 17.1s, c8_fault_injection 3.2s, c8_renderer 2.2s, c1a_verdicts 6.4s, quality_checks 33/33 1.1s, no_placeholders 0.2s; 22 tests green on first full run. Gate verdict nonetheless FAIL: adversarial pass beyond the prescribed tests filed 9 defects (C8-1…C8-9 in app/docs/evidence.md), and C8-1 is MAJOR, replay crashes (KeyError) instead of grading ERROR on structurally malformed receipts, so one corrupt file kills plum_replay --all. Also filed: canonical num_str crash >28-digit ints / silent rounding of >28-digit decimals / negative-zero asymmetry (C8-2..4, MINOR), coherent-edit residual risk, replay does not cross-check the audit-mirrored receipt_hash (C8-5, MINOR), missing filename↔receipt_id binding (C8-6, MINOR), lowercase "1.5m" magnitude bypass in the post-check (C8-7, MINOR), identical/overlapping compare periods accepted (C8-8, LOW), renderer binding is call-site discipline (C8-9, LOW). Defects filed, not fixed (C8 discipline: remediation is the AI Engineer's); gate re-runs after C8-1 lands. No new dependencies (stdlib unittest + psycopg).

  31. Ask-agent tool layer shipped (W2-1, D-003 C1–C7 + C9).

    app/agent/: deterministic machinery only, model wiring is the next task. canonical.py + registry.py (inherited from the interrupted run) verified against the live warehouse and kept unchanged: canonical-JSON idempotence across dump/load, ROUND_HALF_UP pinning, double-execution hash stability on customer_ltv top-10 confirmed (C2). New: receipts.py (receipts built from the same objects passed to/returned by run_query; build identity from meta.build_info, C1; full-receipt receipt_hash + per-turn binding token, C7; uuid receipt ids persisted to app/data/receipts/, collision = error, C9), tools.py (6 tools + explicit refuse; closed stdlib schema validation, no $ref, top_n ≤ 50 everywhere; EVERY call and EVERY rejection audited with params/result_hash/definition_version/build_id, C6), postcheck.py (critique rules R1–R7: magnitude-marker + number-word bans, presence- scoped allowed set incl. top_n and date components, 0/1/2-dec ROUND_HALF_UP roundings, abs-matching, NULL-safe, two-strike enforce hook: C3/C4), render.py (structural RECEIPTED FACTS / INTERPRETATION contract, facts code-built from the stored receipts, every interpretation line prefixed, C5), replay.py + CLI scripts/plum_replay.py (model-free re-execution; MATCH/STALE/FAIL/ERROR per C1, verdicts audited). Observed (scripts/ask_demo.py, live Postgres): total_revenue 29,356,250; compare 2013-06 → 2013-07: 1,642,948 → 1,371,595, derived delta −271,353 / pct_change −16.52; top-5 country breakdown (US 9,162,327 … France 2,643,751, segment_sum 27,151,692); replay of all three receipts 3× MATCH; doctored copy (rows + both hashes coherently recomputed) → FAIL on reexecution_hash; refuse(session_funnel) quoted the metrics.md "Not yet supported" text verbatim and audit-logged tool.refuse with build_id + turn_id; 6 rejection classes (schema, registry filter, unknown value 'Narnia', out-of-window, top_n 51, missing export id) each audited as tool.rejected. Post-check: faithful prose passed; "1.37M" / "twenty" / foreign number flagged (R1, R2/R3). Warehouse rebuilt (build f7572a6d → b2aa969c, fingerprint unchanged 3e02a4b72bbd66a222d0ff9dc808e7fb) → all 6 stored receipts re-replay STALE with "fingerprint and numbers unchanged" detail. No new dependencies (stdlib + psycopg); placeholder check passes. C8 (named Evidence-harness tests) is the milestone gate owned by the Evidence Engineer: fault-injection shape demonstrated in the demo, interception + renderer tests pending.

  32. Service layer ported DuckDB → PostgreSQL (D-002 complete); build identity on /health (D-003 C1).

    Backend port of the last D-002 remainder: server/warehouse.py now runs on psycopg 3.3.4, duckdb import gone from the server modules. Read-only is double-enforced: every service connection opens with default_transaction_read_only=on + statement_timeout=10000 as libpq startup options (fresh autocommit connection per call), and the unchanged SQL gate (single statement, comment stripping, SELECT/WITH-only, forbidden keywords incl. SET/RESET, 1,000-row cap) keeps it unflippable from SQL. Server-side statement_timeout replaces the DuckDB interrupt watchdog: observed killing the 60,398² cross-join count at 1.08 s on a 1 s budget and 10.12 s on the default 10 s budget (QueryCanceled → QueryTimeout). Params now bind via psycopg %s placeholders through the unchanged run_query(sql, params) signature (D-003 C1); list_views()/get_view_sample() resolve against information_schema whitelisted to the gold schema (9 views); NEW get_build_info() returns the latest meta.build_info row, exposed on GET /health (build_id f7572a6d-…, fingerprint 3e02a4b72bbd66a222d0ff9dc808e7fb). audit.py untouched; http_api.py only gained the health build block. smoke_server.py ported + extended: 21/21 checks PASS, all 13 previous (v_repeat_rate still one_time 11,619 / 62.86% / 22.98% of revenue vs repeat 6,865 / 37.14% / 77.02%; DROP, SELECT 1; SELECT 2, comment-wrapped DELETE all 400 + audited) plus: health returns the real build_id matching meta.build_info; %s param round-trip; SET default_transaction_read_only=off and an INSERT both refused 400 by the gate and logged as query.refused with the exact SQL; belt-and-braces direct-driver INSERT on the service connection refused by Postgres itself ("cannot execute INSERT in a read-only transaction"); statement-timeout kill verified in-suite. No new dependencies (stdlib + psycopg already in place per D-002); placeholder check passes. Docs: service-layer section added to docs/warehouse.md.

  33. Warehouse ported DuckDB → PostgreSQL (D-002), cross-engine equivalence proven.

    Data Architect port to local PostgreSQL 18.4 (database/role goolify, DSN via PLUM_PG_DSN, driver psycopg 3.3.4). SQL files ported in place: 01_bronze.sql is now schemas + DDL only (CSV loading moved to build.py as client-side COPY ... FROM STDIN via psycopg cursor.copy(), no server-visible paths needed); 02_silver.sql (try_strptime → new meta.try_yyyymmdd() NULL-on-invalid plpgsql function, INTERVAL '100 years', explicit ROUND(numeric)::int price derivation); 03_gold.sql (date_diff('month',…) → calendar-month boundary arithmetic, MEDIAN/quantile_cont → percentile_cont(…) WITHIN GROUP, ::numeric division for AOV where Postgres INT/INT truncates); 04_quality_checks.sql (one derived-table alias). build.py rewritten: single-transaction rebuild (DROP SCHEMA … CASCADE → SQL files → COPY loads), per-layer counts + timing, and NEW meta.build_info build identity (build_id uuid, built_at UTC, per-CSV source row counts, per- relation row counts, total revenue, deterministic md5 content fingerprint, the T8 receipts identity, replacing file mtime). run_quality_checks.py ported (read-only psycopg session). Observed equivalence vs the audited DuckDB baseline, every number exact: bronze 18,494 / 397 / 60,398 / 18,484 / 18,484 / 37; silver identical incl. 18,484 deduped customers and 37 birthdate NULLs (16 future + 21 age>100, DQ-1 rule ported); gold dim_customers 18,484, dim_products 295, fact_sales 60,398 with 0 NULL dimension keys; views 38 / 430 / 18,482 / 130 / 2 rows (repeat rate 62.86% / 37.14%, repeat revenue share 77.02%); total revenue 29,356,250 over 27,659 distinct orders; LTV avg 1,588.10 / median 272.00 / p90 4,825.70 / max 13,294; date-filtered revenue 29,351,258 (delta 4,992 = the 19 NULL-date lines); spot checks (2013-07 = 1,371,595 / 1,875 / AOV 731.52; cohort 2013-01: 325 / 17 / 5.23%; Mountain-200 Black- 46 = 1,373,454) all identical. Quality suite on Postgres: 33/33 PASS. Two consecutive builds verified re-runnable with identical content fingerprint 3e02a4b72bbd66a222d0ff9dc808e7fb (distinct build_ids). goolify.duckdb retained on disk untouched as the frozen audited baseline. Docs updated: docs/warehouse.md (engine, rebuild, build_info), docs/metrics.md (engine ground rule; 0 canonical SQL blocks changed, 3 formula descriptions updated to the Postgres function equivalents). Still on DuckDB pending their own port (D-002 scope, not this task): server/warehouse.py read-only service and scripts/smoke_server.py (they keep working against the frozen baseline file).

  34. Defect DQ-1 resolved: implausible birthdates cleansed.

    Data Architect fix for the defect filed by the Evidence Engineer (T4/T6). 02_silver.sql now nulls erp_cust_az12 birthdates implying age > 100 at load (bdate < current_date - INTERVAL 100 YEAR), symmetric with the existing future-date rule. Observed effect after rebuild: 21 bronze rows nulled (the 15 flagged pre-1924 births, 1916–1923, plus 6 born 1924-01-01 to 1926-06-11 caught by the age boundary); silver min birthdate now 1926-09-07; total silver birthdate NULLs 37 (16 future + 21 old, 0 in bronze). Quality suite re-run: 33/33 PASS (was 32/33). No gold metric affected: birthdate feeds no analytics view; all row counts unchanged (silver.erp_cust_az12 still 18,484). Documented in app/docs/warehouse.md and resolution note appended to DQ-1 in app/docs/evidence.md.

  35. Quality checks + independent metric validation (T4 + T6).

    Evidence Engineer pass over the warehouse. T4: 33-check quality suite (app/warehouse/sql/04_quality_checks.sql, runner app/scripts/run_quality_checks.py) covering silver PK duplicates/NULLs, untrimmed strings, standardized-value audits, date validity/order, sales = quantity × price, gold surrogate-key uniqueness, and fact→dim referential integrity. Result: 32 PASS, 1 FAIL, the failure is defect DQ-1: 15 pre-1924 birthdates (1916–1923) pass through silver.erp_cust_az12 uncleansed and undocumented; filed to the Data Architect, check left red on purpose. The two documented source gaps (19 NULL order dates, 7 CO_PE products) are allow-listed at their documented magnitude. T6: every claimed gold metric independently recomputed from silver tables with separate SQL, total revenue 29,356,250; 27,659 distinct orders; dates 2010-12-29 → 2014-01-28; dim_customers 18,484; dim_products 295; fact_sales 60,398 (no join fan-out); repeat 37.14% of customers / 77.02% of revenue; LTV avg 1,588.10 / median 272 / max 13,294 over 18,482 customers (2 excluded, ids 27039 + 16322, all-NULL order dates), 10/10 claims CONFIRMED, including full-table diffs of v_revenue_monthly (38/38 months) and v_cohort_retention (430/430 cells) plus raw-row traces (June 2013 revenue 1,642,948; cohort 2013-01 month 3 = 19 of 325; customer AW00012301 LTV 13,294 across 13 lines; Mountain-200 Black- 46 revenue 1,373,454 traced to bronze). Full audit trail with verbatim queries: app/docs/evidence.md.

  36. Service layer skeleton + audit log shipped (T9).

    app/server/: warehouse.py (read-only query service, single-SELECT gate with comment stripping, forbidden-keyword + multi-statement rejection, 10 s interrupt watchdog verified killing a 60k³ cross join at 1.02 s, 1,000-row cap, list_views() / get_view_sample() resolved against the catalog so no raw identifier interpolation), audit.py (append-only JSONL at app/data/audit/audit.jsonl, ts/actor/action/ detail + optional sql/result_summary + open kwargs so agent tool-calls and diffs slot in without migration), http_api.py (stdlib-only localhost JSON API: /health, /views, /views/{name}/sample, /audit, POST /query; all routing in a framework-agnostic handle_request(), the stdlib shell is the throwaway half pending the T8 framework decision). app/scripts/smoke_server.py: 13/13 checks green on a free port, real query returned both v_repeat_rate segments (one_time 11,619 / repeat 6,865 customers, 22.98% / 77.02% of revenue), DROP TABLE x, SELECT 1; SELECT 2, and a comment-wrapped DELETE all refused with 400 and logged as query.refused with the exact rejected SQL. No new dependencies (stdlib + existing duckdb 1.5.3); placeholder check passes.

  37. Governed metric definitions shipped (T7).

    app/docs/metrics.md: the semantic layer the Ask agent cites in every answer. 8 metric sections (total_revenue, orders, aov, revenue_monthly, cohort_retention, customer_ltv + customer_ltv_summary, product_performance, repeat_rate), each with canonical id, plain-language definition, exact formula, grain, source lineage (gold view → silver tables), tested canonical SQL, real caveats, and a query-verified example value (all examples computed against the built warehouse on 2026-06-12; e.g. revenue for 2013-07 = 1,371,595). Documents the two reconcilable revenue totals (29,356,250 all lines vs 29,351,258 date-filtered; delta = the 19 NULL-date lines' 4,992) and verified order-month additivity (every order has exactly one order date) and that the 7 NULL-category products have zero sales. Includes an honest "Not yet supported" section (session_funnel, promo_performance, connector-dependent) and a versioning rule: definitions change only via dated entries in spec/decisions.md.

  38. Warehouse shipped (T3 bronze/silver + T5 gold).

    DuckDB medallion warehouse at app/warehouse/goolify.duckdb, built by python app/warehouse/build.py from app/warehouse/sql/01_bronze.sql, 02_silver.sql, 03_gold.sql (~2 s rebuild, verified re-runnable across two consecutive builds). Bronze: 6 raw tables (18,494 customers / 397 products / 60,398 sales lines / 18,484 + 18,484 ERP rows / 37 categories). Silver: same 6 cleansed (customers deduped to 18,484; 19 invalid order dates nulled; 23 sales amounts and 12 prices repaired; 200 broken product end dates recomputed via LEAD). Gold: star schema views, dim_customers 18,484, dim_products 295 (current versions), fact_sales 60,398 with zero NULL dimension keys, plus 6 analytics views (v_revenue_monthly 38 months, v_cohort_retention 430 cohort cells, v_customer_ltv 18,482 + v_customer_ltv_summary, v_product_performance 130, v_repeat_rate: 62.86% one-time / 37.14% repeat carrying 77.02% of revenue). Total observed revenue 29,356,250 across 27,659 orders, Dec 2010 – Jan 2014. Session funnel and promo views NOT built: dataset has no event/promo data (documented as connector-dependent in app/docs/warehouse.md).

  39. Project scaffolded.

    app/ structure, Claude Code hook guardrails (.env read guard, action logging), no-placeholder CI check, and the demo store dataset seeded (6 CRM/ERP CSVs, 5.2 MB). Decisions D-001 locked: DuckDB local-first, bootcamp dataset, name "Plum" (spec/decisions.md).