Skip to content
Skip to main content
Novel Systems home
Decision log

Every decision, including the ones we got wrong

109 entries between August 3, 2026 and August 19, 2026. Each one states what was decided, what was rejected, and what would have to be true for it to be the wrong call. Some supersede earlier ones and say so. This is the repository’s own DECISIONS.md, read when the site is compiled — not a summary of it.

If you are evaluating this platform and want to know how it is built rather than what it does, the evidence page has the artifacts. This has the reasoning.

How this differs from the release notes

The release notes describe the product in the shape a customer receives it: versions, dates, deprecations. They are written for someone planning an upgrade, and the page says on its face which parts of it are the roadmap rather than a shipping record.

This log is the opposite artifact. It is written for someone deciding whether to trust the engineering, it is not curated, and it is unflattering in places by construction — an entry that only recorded the good decisions would be worth nothing to the person reading it.

  1. D-109August 19, 2026

    Pages are titled with the category a buyer searches for, not the name we call them internally

    Each page's <title>, the first clause of its description, and its <h1> lead with the category the page belongs to, and the line that made the page memorable follows the dash rather than being replaced by it. /platform/cpq reads "CPQ software for trade contractors — parametric quoting that can't be wrong": both halves, in the order a stranger needs them.

  2. D-108August 19, 2026

    A canonical URL is declared per route, never once at the root, and a build check enforces it

    The root layout declares no canonical. It is a fact about one URL and there is exactly one URL it can be true for — and the homepage already declares its own in app/page.tsx, where it is true. The en-CA hreflang was removed alongside it: the site has one language, so a lone self-referential alternate says nothing Google acts on, and inheriting it leaked the homepage URL onto three more pages for no benefit.

  3. D-107August 18, 2026

    The homepage says its own name, and the footer links it in words rather than only in a logo

    Five changes, no restructuring. title became { absolute: "Novel Systems | Enterprise CPQ & Field Service Management" } — 57 characters. absolute is required, not stylistic: a plain string here would have the root template append the suffix and print the brand twice.

  4. D-106August 9, 2026

    The LinkedIn mark ships in the footer's contact list, and sameAs is derived from the same string

    siteConfig.socials.linkedin now holds https://www.linkedin.com/company/novelsystems-ca. It is the first entry this site has ever published in that object, and the first entry in the Organization graph's sameAs.

  5. D-105August 9, 2026

    The LinkedIn page carries no location, and its cover is rebuilt from the OG card's own pixels rather than cropped from it

    LinkedIn's Locations form marks City as required. It stays required with "This location has no street address" ticked — that checkbox suppresses the street line, not the locality. So the page can carry a city or carry nothing.

  6. D-104August 9, 2026

    Commits are authored from a GitHub-matchable email, and the push refuses to run when they are not

    every commit that reaches origin/main is authored by an address GitHub can resolve to the account that owns the repository — in practice khan.ahmadz6370@gmail.com, the value git config user.email already carries, or that account's users.noreply.github.com form. push.command runs check:commit-identity against HEAD before it pushes anything, and aborts without touching either remote if the check fails.

  7. D-103August 7, 2026

    Mail moved from Cloudflare forwarding to Google Workspace, and the two failures that only appeared after DNS was correct

    Supersedes D-033. Cloudflare Email Routing is disabled. The apex MX now points at Google Workspace across five records — ASPMX.L.GOOGLE.COM at priority 1, ALT1 and ALT2 at 5, ALT3 and ALT4 at 10, all DNS only, never proxied. The apex carries exactly one SPF record, v=spf1 include:_spf.google.com ~all; DKIM is published at google._domainkey; _dmarc is v=DMARC1; p=none; rua=mailto:ahmad@novelsystems.ca. support@, security@, sales@ and careers@ are Workspace groups rather than forwarding targets, and eight people hold accounts on the domain.

  8. D-102August 7, 2026

    The published telephone number is real, and a check enforces that it stays real

    Decision. COMPANY.phoneDisplay is (437) 237-6827 and COMPANY.phoneHref is tel:+14372376827, a real line confirmed by the owner. A new build check, check:phone, refuses to let that pair regress. VERIFY_CHECK_COUNT moved 23 → 24.

  9. D-101August 7, 2026

    Search Console ownership is proved by a file at the site root, not by the meta tag

    Decision. https://www.novelsystems.ca/ is a URL-prefix property in Google Search Console, on the Google account signed into this workstation, and ownership is proved by the HTML file method: public/google852f2393ed012f13.html, which contains one line and is served at the site root. Verification was confirmed on 7 August 2026 — the property shows "Ownership verified", method "HTML file". The meta-tag path stays wired in app/layout.tsx and documented in .env.example as a second, currently-unset method.

  10. D-100August 7, 2026

    The organization logo, and one name for the parent relationship

    Decision. The logo node in the site's Organization structured data now points at a real square brand mark instead of the Open Graph card, the mark is generated from the same geometry the site renders and guarded byte-for-byte, and the two surfaces that described the parent relationship in their own words now read it from CONTRACTING_ENTITY like everything else.

  11. D-099August 6, 2026

    Eight named functions on /about, with the biographies moved to meet the site's claims

    Decision. /about now carries a "Leadership & functions" section listing the eight working functions of the division, each with a name and a remit, directly beneath the four signing offices. Where a supplied biography asserted more than the rest of the site asserts, the biography was edited and the site was left alone — three such edits, listed below.

  12. D-098August 6, 2026

    The corrections were published where nobody doubting us would look

    Every published claim on this site that has changed is listed at /corrections — six entries, each with its date, the retired wording quoted verbatim, the current wording derived from the module that owns it, the reason it moved, and links to the decisions that argued it. The four surfaces where the doubt actually forms link to it. A build guard fails red if any entry cites a decision or a route that does not exist.

  13. D-097August 6, 2026

    Renaming the tenant did not cover the client field, and the client field named eighteen real organisations

    Every named third-party organisation was removed from the demo tenant fixtures and replaced with a facility type. The removed-names guard, which had been scoped to a single file, now walks the whole shipped source tree and carries both the fabricated names and the real ones.

  14. D-096August 6, 2026

    The demo tenants were the source of the invented customer names, so the disclosure moved into the name

    Rename the three demo tenants to Sample Workspace — <trade> Contractor, delete the customerSince field from TenantWorkspace entirely, replace the two city headquarters with regions, and change the CPQ account-name placeholder so it stops naming a company.

  15. D-095August 6, 2026

    A figure that moves has to say so, and a figure that was corrected has to say when

    Timestamp every availability figure at the point it is rendered and state in words that it is re-polled, rather than freezing it. Give the founding narrative one owning sentence, ORIGIN_STATEMENT, and add a thirteenth claim rule that fails the build on a founding or spin-out year typed as a literal.

  16. D-094August 6, 2026

    Named customers came off the site, and a self-declared status was replaced by a committed artifact

    Remove every named third-party company and every attributed testimonial from the marketing surfaces. Delete the TestimonialStatus = "placeholder" | "approved" enum and replace it with consentRecord: string, a repository-relative path to a committed permission artifact that the build resolves against the filesystem. Delete PLATFORM_MILESTONES in the same pass.

  17. D-093August 6, 2026

    The header linked to nothing, so the billing evidence was unreachable; give it a route of its own

    Split the Stripe billing evidence out of /integrations into its own short route at /billing, fix DesktopNavItem so a top-row item with a dropdown still renders its own anchor, and add check:nav to keep it that way.

  18. D-092August 6, 2026

    A second diagram, and a toolchain that discovers diagrams instead of listing them

    Draw the CPQ subsystem as its own diagram rather than enlarging the existing one, and make every part of the diagram toolchain discover docs/.mmd rather than name a file.

  19. D-091August 6, 2026

    The evidence was in the served bytes and the reviewer still recorded it as absent

    Three additions and one correction. A coverage badge is generated at public/coverage.svg by the same script that runs the suite, drawn from the figures in public/coverage.json, byte-stable, and re-derived and diffed by check:coverage. The test runner's own stdout is published verbatim at public/test-run.txt, and its SHA-256 is recorded in the artifact and verified on every build. A new EvidenceIndex component lists every published artifact by its literal served path, high on /developers and /security. And the sentence on /developers that read "There is no CI badge on this page because the repository is private" was deleted, because it was wrong.

  20. D-090August 5, 2026

    Publish the workflow definitions, not a run history

    The CI workflows are published as fetchable artifacts — /pipeline.yml for the definitions verbatim, /pipeline.json for the derived shape the page renders — generated from .github/workflows and drift-checked on every build by check:pipeline, the twentieth check in the verify chain. /engineering#ci renders every workflow, job and step from that JSON, and states in as many words why there is no status badge beside it.

  21. D-089August 5, 2026

    A page that reads an artifact must be able to reach it at render time

    The set of files the evidence pages read off disk is declared once, as EVIDENCE_ARTIFACTS, traced into the bundle of every route that can render at runtime, and checked on every build. A route that reads an artifact is either force-static — so the read provably happens during the build — or it appears in outputFileTracingIncludes with the complete list. There is no third option, and check:evidence-tracing is what makes that true rather than aspirational.

  22. D-088August 5, 2026

    Evidence goes in the body of the page the reader was sent to

    Decision. An evidence artifact is published where the claim is made, as the kind of thing the claim asks for — a percentage as a number, a diagram as an <img> — in the body of the page, not in the footer and not one click away.

  23. D-087August 5, 2026

    A hand-rendered artifact may not carry a fact that moves

    The labels are not corrected. They are removed, and the class of claim is banned from the artifact. lib/connector-readiness.ts already states the rule about itself: whether a connector can transact "differs per connector and changes with a deployment's environment, which is exactly the shape of fact that must not be typed into a page." That reasoning applies to a diagram with more force, not less. A page is re-rendered on every request from a live reading. This diagram is a PNG produced by a script that drives Chrome on one particular Mac, because mermaid measures every label with getBBox and there is no Chromium for the arm64 Linux this repository builds on. A fact that changes when somebody sets an environment variable in Vercel cannot live in an artifact whose correction requires physical access to a laptop.

  24. D-086August 5, 2026

    Publish the result, not the gate, and fingerprint the tree it measured

    the backend suite is run, and its real output — 409 tests, 50 suites, per-file coverage — is committed to public/coverage.json and rendered on /engineering. A new check:coverage in the verify chain refuses to let the artifact drift from the code it describes.

  25. D-085August 5, 2026

    Evidence that requires a click is evidence most readers never see

    /integrations renders a finalised Stripe test invoice on the server, before any interaction, in addition to the interactive runner. The static half also names the mode and prints the test card in the markup rather than in a post-run branch.

  26. D-084August 5, 2026

    The invoice API reports a subtotal net of our own tax lines, not Stripe's

    Subtract the lines we recognise as ours. subtotalCents is Stripe's subtotal less the sum of the lines whose description matches HST_LINE_DESCRIPTION — the same lines taxCents is derived from, so the two fields can no longer disagree by construction. The published invariant is subtotal_cents + tax_cents = total_cents whenever the first two are non-null, it is stated in the schema, and a test asserts it rather than implying it.

  27. D-083August 5, 2026

    The billing sandbox publishes why it created a quote, and waits for the invoice it caused

    The reason travels with the answer. findApprovedQuote returns { quote, lookup }, where lookup is one of found, none_approved, http_<status>, unreadable, or malformed_row. It is a field on the response and a sentence on the panel, not a log line. The question it settles — is this route creating a quote per click, and if so why — was answerable on 5 August 2026 only from Stripe's dashboard, and a public evidence panel whose diagnosis requires a vendor login is not evidence.

  28. D-082August 5, 2026

    The API says which commit it is running

    /api/health carries a build object on both arms: commit, ref, environment, read once at module load from Vercel's system environment.

  29. D-081August 5, 2026

    The billing sandbox will not exhibit an invoice that does not add up

    Before reusing an approved quote, read its recorded invoice and check that it adds up. If it does not, do not reuse the quote — fall through to the existing create-and-approve path, which writes a new quote through the fixed adapter.

  30. D-080August 5, 2026

    Every Stripe invoice this platform ever wrote was empty, and the tests could not have known

    The pending queue is not used. The draft invoice is created first, with an explicit currency and pending_invoice_items_behavior=exclude; every item then names invoice=<draft id>; the draft is finalised last. Nothing depends on Stripe moving an object we did not tell it to move.

  31. D-079August 5, 2026

    A sentence that named a retry we had not built, and the retry we built rather than delete it

    POST /api/v1/quotes/{id}/invoice — same path as the read, no /retry suffix, because it acts on the same resource and returns the same representation. It answers 201 with created: true when it wrote one and 200 with created: false when one was already there.

  32. D-078August 5, 2026

    The tenant-isolation job never ran, and the reason was a signing key it does not use

    the CI job "API — tenant isolation against Postgres" now sets JWT_SECRET inline in its env: block. Until this change it did not, and the consequence was not a flaky job or a slow one — it was that the job had never executed a single assertion since the day it was added.

  33. D-077August 5, 2026

    Reading the finalised Stripe invoice back rather than screenshotting it

    Add GET /api/v1/quotes/:id/invoice, which looks up the recorded invoice id, retrieves the object from Stripe with its lines expanded, and returns its own fields. The billing sandbox panel renders those fields.

  34. D-076August 5, 2026

    Turning Stripe's Managed Payments off per request, rather than letting it pick the payment methods

    Send managed_payments[enabled]=false alongside payment_method_types[0]=card. Stripe offered both ways out and they are not equivalent. Dropping payment_method_types is less code and it surrenders the guarantee that field exists to make: card is named explicitly so that an account with acss_debit enabled cannot offer a pre-authorised debit on a page this code describes as settling on approval. Under that failure a quote sits unpaid for days and nothing anywhere reports an error. Letting Managed Payments choose would move that decision into a dashboard toggle no reader of this repository can see.

  35. D-075August 5, 2026

    A provider's refusal is not our defect, and reporting it as one costs days

    ProviderHttpError answers 502 provider_error, carrying the provider's name, the status it answered with, and its own identifier fields — Stripe's code/param/type, QuickBooks' fault code, Salesforce's errorCode. The frontend proxy forwards the same identifiers under upstream, behind an allowlist. A reader now gets param: "line_items[0][price_data][unit_amount]" in the response body instead of a request id and a trip to a dashboard.

  36. D-074August 5, 2026

    An idempotency key is a promise that the body will not move, so the body must not read the clock

    The billing runner shipped in D-073 was reachable, described accurately, and broken. Pressing the button returned upstream_unavailable. The backend was returning 500, and the 500 was Stripe returning 400:

  37. D-073August 5, 2026

    Make the connector do something on the page that sells it, and read its mode off the artifact rather than declaring it

    Three things, and the middle one is the load-bearing one.

  38. D-072August 5, 2026

    Link the evidence from the sentence that rests on it, because a link somewhere on the site is not a link where the claim is made

    A page that publishes a figure attributed to an outside party links that party from the paragraph making the attribution, not from a sibling route. /security links status.novelsystems.ca where it reports measured availability. /contact names Cal.com, its host and what it does, in server HTML, beside the button that goes there — and still embeds nothing.

  39. D-071August 5, 2026

    Render the OpenAPI document ourselves, and put Engineering on the header, because a page reachable only from the footer is a page nobody read

    public/openapi.json is rendered as server HTML at /developers/reference; /swagger, /api-docs and /reference 308 to it; and Engineering replaces Careers on the six-wide header nav, carrying /engineering, /engineering/decisions and the new reference in its dropdown.

  40. D-070August 5, 2026

    Publish the DDL, because a count is a claim about a document and the reviewer came for the document

    the four migration files are concatenated, unedited and in application order, into public/schema.sql; check:schema regenerates and diffs on every build; /schema, /migrations and /migrations.sql 308 to it; and both pages that assert something about the database now link to it.

  41. D-069August 5, 2026

    The spec answers the addresses people guess, and the homepage sandbox says where the server is

    /api/openapi.json, /openapi, /api/openapi and /swagger.json all 308 to /openapi.json, and the homepage pricing sandbox now links to the live API runner and the spec from the paragraph that explains it computes in the browser.

  42. D-068August 5, 2026

    No third-party profile is registered until the legal entity question is answered

    the LinkedIn Company Page and the Crunchbase submission are both held. Nothing is registered, socials stays empty, VERIFIED_PROFILES stays empty, and check:social continues to report zero verified profiles. The drafts in docs/directory-listings.md stay written and unsubmitted.

  43. D-067August 5, 2026

    The status page is linked from the board that cannot carry its history

    the service board on /support#status carries a link to status.novelsystems.ca, the monitoring provider's own status page, stated as the place past incidents and scheduled maintenance are published.

  44. D-066August 5, 2026

    This file is published, and /changelog says what it is instead

    this file is rendered at /engineering/decisions, parsed from the markdown rather than transcribed into a data module, one static page per entry; and /changelog now states in its own copy that the release history it shows is the roadmap in release-note form rather than an audited record of what was merged, and points at this log for the record that is.

  45. D-065August 5, 2026

    A public endpoint says which connectors this deployment can actually begin

    GET /api/connectors — unauthenticated, outside /v1, alongside /health and /routes — reports one of three derived states per provider, and /integrations renders it on the same page as the field mapping table.

  46. D-064August 5, 2026

    The margin floor refuses; it does not clamp, and the page says the harder thing

    /developers describes the 0.5 margin floor as a refusal — a request asking for less comes back 400 bad_request naming both figures — and not as a clamp that silently returns a quote at the floor.

  47. D-063August 5, 2026

    The API page runs the call rather than describing it, through the proxy that already existed

    /developers#playground makes a real POST to the pricing engine from the reader's browser, on page load and again on every change, and prints the HTTP status, the round-trip time and the unedited response body. It calls /api/cpq/calculate — this site's existing anonymous proxy — and says so in the panel, alongside the two-step authenticated cURL a reader's own client would use.

  48. D-062August 5, 2026

    A social link asserts identity, so it needs a human verification, and the check enforces that a human did it

    No URL on an identity host — LinkedIn, GitHub, X, Facebook, Instagram, YouTube, Crunchbase, Clutch — may appear in shipped source unless somebody has signed in to that account, confirmed we administer it, and written the date and the login into VERIFIED_PROFILES. The register is empty, so the footer's social row renders nothing rather than something.

  49. D-061August 5, 2026

    The evidence page reads its exhibits by value, not by name

    /engineering publishes the artifacts behind the claims made on /security#engineering — the migration SQL that enforces the margin floor in Postgres, the sixteen checks in the verify chain with each one's purpose read out of its own header comment, the test modules and their coverage gates, the five Lighthouse reports, the architecture diagram, and the commit being served. Every figure is read from the filesystem at build time. Nothing on the page is typed.

  50. D-060August 5, 2026

    The two margin controls behave differently, and each now says which it is

    Each margin slider states its own floor mechanic in copy beside the control. The product tab says the floor is a lower bound that the applied margin cannot cross; the commercial tab says its floor warns rather than clamps. The product tab's readout is now labelled applied and shows the requested figure struck through beside it whenever the two differ.

  51. D-059August 5, 2026

    The parent's year comes back, in its own field, with a rule holding it there

    2019 is published again on /about, attached to Novel Blinds Inc. and to nothing else. COMPANY.founded stays 2023 and remains the only number any count of software years may derive from.

  52. D-058August 5, 2026

    The workflow diagram gets a link, because an artifact nobody finds is not published

    The homepage Smart Tech card points at /smart-tech#workflow and is labelled "See the Smart Tech workflow". /smart-tech carries a matching link in its own hero, naming the three stages the diagram draws.

  53. D-057August 5, 2026

    Method, not apology; and a closing CTA that is an offer rather than an instruction

    Three lines were rewritten without any underlying figure or selection changing. The availability note. It explained that the published number is the worst-performing check, phrased as a caveat. A figure that opens by explaining why it looks bad invites a reader to discount it before they know what it measures. The selection has not changed — lib/uptime-monitor.ts still takes the minimum — but it is now stated as method: we publish the lowest-performing check rather than the average across checks, so a healthy endpoint can never mask a degraded one. That is the same fact, and it is the reason the fact is worth publishing.

  54. D-056August 5, 2026

    A floor is not a lock, and the badge has to say which one it is

    The at-floor margin message reads "At the 50.0% floor. The target moves up from here, not below it." It read "Locked at the 50.0% margin floor." until today.

  55. D-055August 5, 2026

    Provision · Commission · Monitor ships as a diagram after all, superseding D-053

    /smart-tech#workflow now renders an SVG pipeline above the three cards, from md up. D-053 declined to build this. That decision is superseded, not reversed on a whim — its reasoning was sound about a different drawing.

  56. D-054August 5, 2026

    One CTA label per action, not one CTA label per site

    Where two pages offer the same action, they use the same words for it, taken from PRIMARY_CTA. Where a page offers a different action, it keeps its own label even though the button looks identical. The rule is scoped to the action, not to the site.

  57. D-053August 5, 2026

    Provision · Commission · Monitor ships as a linked strip, not as a diagram

    The three-stage hardware workflow on /smart-tech is published as three numbered cards, each stating what enters and what leaves it, each linking to the section that carries that stage in full. It is not published as a connected diagram, and it is not wrapped in <figure>.

  58. D-052August 5, 2026

    A window belongs to the check that produced the figure, and an unreported window is null

    platformAvailability and windowDays are read from the same component. observedSince is nullable, and a provider that omits a start date produces null rather than "today". windowLabel renders that null as "an unreported window".

  59. D-051August 5, 2026

    A published promise is gated on a round trip, not on a credential, whenever a date can invalidate it

    Where a page states what will happen when a visitor acts, the statement is gated on a live check that the thing can happen, not on the presence of the credential that would make it possible. /support now prints its ticket promise behind helpdeskCanFileTickets(), which is one identity request to Zammad per revalidation window, instead of behind helpdeskHealth(), which reads four environment variables.

  60. D-050August 5, 2026

    Engineering practice is published as declared figures with a guard, not as prose

    /security#engineering publishes four claims about what runs before a change ships — test modules and coverage gates, the verify chain and the claim rules, the migration jobs, the Lighthouse audit. Every figure in them is declared once in lib/engineering-practice.ts and re-derived from its artifact by scripts/check-engineering-practice.mjs, which is the fifteenth check in verify and runs in prebuild.

  61. D-049August 5, 2026

    There are two margin floors, and no figure may be published without saying which

    No margin-floor percentage may be written as a literal outside lib/cpq-engine.ts and lib/trade-quoting.ts. A tenth claim rule, margin-floor-figure, fails the build on any that appears, and every surface that names a floor must interpolate one of the two constants and say which business it describes.

  62. D-048August 5, 2026

    The published spec is linked from the site chrome, not only generated

    /developers#spec is a named section carrying links to /openapi.json, the narrative reference on docs.novelsystems.ca, and the sandbox host, and { title: "OpenAPI Spec", href: "/developers#spec" } sits in the footer's Product column.

  63. D-047August 5, 2026

    A count of a rendered list is derived from that list, and a guard enforces it

    any sentence stating how many trades or verticals the platform covers interpolates the length of the array it describes. TRADE_PROFILE_COUNT is exported from lib/trade-profiles.ts as TRADE_PROFILES.length and read by the CPQ sandbox heading; the /solutions OG description reads INDUSTRY_VERTICALS.length. A ninth rule in check-claim-consistency.mjs fails the build on a hand-typed count anywhere outside the three registry files and the sample-tenant fixtures.

  64. D-046August 5, 2026

    "Book Enterprise Demo" resolves to the scheduler control, not to the page containing it

    PRIMARY_CTA.href is /contact#book, and #book is the id on the BookDemoTrigger row. The homepage hero link carries the same fragment.

  65. D-045August 4, 2026

    Connectors publish a deployment path, not a build state

    every connector in the catalogue carries a required maturity field on one of three levels — "Generally available", "Limited release", "Implementation-led" — and the level answers the question how do I turn this on, not the question does this exist. MATURITY_LEVELS carries a selfServe boolean per level; SELF_SERVE_CONNECTOR_COUNT is derived from the catalogue and feeds the "9 of 15" figure in the copy above the grid.

  66. D-044August 4, 2026

    The integration mark is drawn, not borrowed

    every connector card and the mapping drawer carry the same first-party glyph in the slot a vendor logo would occupy — an inline SVG of a dashed ring around a solid diamond — and FIRST_PARTY_STATEMENT explains it once, above the grid, rather than fifteen times into a screen reader.

  67. D-043August 4, 2026

    The homepage trade count is derived from the marquee beneath it

    TRADE_VERTICAL_COUNT is exported from TRADE_VERTICALS.length and interpolated into the marquee caption, which previously read "Configured for six field-service trades" with the word typed by hand.

  68. D-042August 4, 2026

    A published figure has one owner, and a script enforces it

    Every operational figure that appears on more than one surface is declared in exactly one module and interpolated everywhere else, and scripts/check-claim-consistency.mjs fails the build when one is typed as a literal somewhere it is not owned. Eight rules, one per contradiction that actually shipped. It runs in prebuild and verify.

  69. D-041August 4, 2026

    The compliance badge states the audit window, not a posture

    The trust strip no longer says "SOC 2 Type II aligned". It says "SOC 2 Type II audit underway", and /security publishes the observation window from SOC2_WINDOW_LABEL.

  70. D-040August 4, 2026

    Audit log retention is tiered, and the pricing table is the source

    Audit logs are retained for 13 months on Starter Operator, 7 years on Growth Contractor, and configurably on Enterprise. AUDIT_LOG_RETENTION in lib/pricing-plans.ts is the single declaration; the privacy policy and the comparison row both read from it. The flat auditLogMonths: 24 in lib/legal-documents.ts was deleted rather than corrected.

  71. D-039August 4, 2026

    Only the top tier carries a service credit, and the field is named for that

    Availability credits are an Enterprise term. Starter Operator and Growth Contractor publish targets with no contractual credit. The AvailabilityTier field that carried this was renamed from serviceCredit to remedy.

  72. D-038August 4, 2026

    SSO starts at Growth Contractor; SCIM is Enterprise

    Single sign-on is a Growth Contractor feature and SCIM provisioning is an Enterprise one, stated plainly on both the pricing page and /security. The security page previously implied SSO ships on every paid tier.

  73. D-037August 4, 2026

    The placeholder check stays a warning; the residue check is fatal

    scripts/check-proof-copy.mjs now makes two checks with deliberately unequal severity. A testimonial still at status: "placeholder" warns and exits 0, and PROOF_STRICT=1 is not set in CI. A testimonial marked approved that still carries PLACEHOLDER_ATTRIBUTION or a DRAFT — prefix fails the build unconditionally, with no flag to turn it off.

  74. D-036August 4, 2026

    Named customers are published on assertion; their words are not

    lib/customer-proof.ts now carries five named customers — Lloyd's Electric & HVAC, Haller Mechanical Contractors, Spark Power Corp, J & S Heating and Air Conditioning, Borealis Electric — in place of the five representative profiles it held before. SCENARIO_NOTE no longer says the deployments are modelled, because on the business owner's account they are not. Every quote attributed to a named individual stayed status: "placeholder" and therefore out of the production render until the business confirmed the wording was what each named individual actually said.

  75. D-035August 4, 2026

    A credential is revoked only after the code proves nothing reads it

    three pieces of leftover state were removed — the read-write Better Stack tokens My Uptime token and My Telemetry token, the never-used Zammad token novelsystems-website, and the __probe_publishable_key__ row in public.quotes — and in each case the proof of safety was gathered before the deletion, from code or from the vendor's own telemetry, never from the absence of a reason to keep it.

  76. D-034August 4, 2026

    The uptime monitor matches on two fields, because one of them is the thing that can silently go missing

    the Better Stack monitor for the site health endpoint asserts the keyword … and not the shorter, more obvious "status":"ok".

  77. D-033August 4, 2026

    Deleting the Google Workspace MX, and why an unpaid mail provider is worse than none

    the apex MX record … was deleted, and Cloudflare Email Routing was enabled in its place with a single catch-all rule forwarding to khan.ahmadz6370@gmail.com. The record is written out verbatim above so the decision can be reversed by retyping it.

  78. D-032August 4, 2026

    A health check that reads environment variables is not a health check

    /api/health now answers two different questions and says which one it answered. The default GET reports credential presence and labels itself checks: "configuration". GET /api/health?probe=deep sends a real request to each of the four dependencies and labels itself checks: "reachability". The deep arm is cached in module scope for 30 seconds and rate limited on a cache miss. scripts/check-health-honesty.mjs holds the shape in CI.

  79. D-031August 4, 2026

    A helpdesk token is proven by its Last Used column, not by the code that sends it

    When /support returned not filed (helpdesk returned HTTP 401 — Can't find User for Token), the request shape was not changed. The token was replaced. lib/helpdesk.ts is unmodified by this fix.

  80. D-030August 4, 2026

    Two Supabase projects, one of them named prod, and the site wrote to the other one

    The frontend's NEXT_PUBLIC_SUPABASE_URL was repointed from wwcbbjcgoxjraiqcusjw to pevgijvcgqhdivdljjkh, and the second project was renamed from a bare ref to novel-systems-site so that the dashboard shows which is which without opening either.

  81. D-029August 4, 2026

    An environment variable is a name, not a promise; the key inside it is checked for shape

    lib/supabase-admin.ts gained looksLikeSecretKey(), and getSupabaseAdmin() now throws a distinct error when SUPABASE_SERVICE_ROLE_KEY holds a browser-level key — separate from the error it throws when the variable is absent.

  82. D-028August 4, 2026

    The demo is twenty minutes because one sentence on the site is not gated on the scheduler

    The Cal.com event type published at cal.com/novelsystems/demo is 20 minutes, not 15 and not 30. No code changed to accommodate it.

  83. D-027August 4, 2026

    The webhook destination is subscribed to exactly what the code branches on, and the old one pointed at nothing

    The Stripe event destination we_1Tzp6bPiVC0ozAWK1OLu8cdN was edited in place to point at https://api.novelsystems.ca/api/v1/integrations/stripe/webhook and subscribed to exactly four events — checkout.session.completed, checkout.session.expired, invoice.paid, invoice.payment_failed — which is precisely the set handleStripeEvent branches on. payment_intent.succeeded was removed from the subscription.

  84. D-026August 4, 2026

    A deployment publishes its own route table, because the check that asked it could not fail

    The API serves GET /api/routes, an unauthenticated list of every method and path the running process will dispatch, walked out of the live Express router stack. scripts/check-deployed-api.mjs now diffs that list against public/openapi.json instead of inferring routing from the status codes of unauthenticated probes.

  85. D-025August 4, 2026

    Two ways to pay for one quote, converging on a single paidAt

    An approved quote can be paid two ways. The invoice path already existed: approval fans out to Stripe and raises a net-14 document that is emailed and sits in the customer's accounts-payable queue. The Checkout path is new — POST /api/v1/quotes/{id}/checkout-session returns a hosted card page as a URL. Neither is the default, both are supported, and both inbound webhooks write the same quote.paidAt.

  86. D-024August 4, 2026

    Publish the architecture that exists, rather than provisioning a host to make a false sentence true

    sandbox.api.novelsystems.ca was published by the hosted developer docs as a base endpoint and does not resolve. It was removed from the docs. It was not provisioned, and the DNS record was deliberately not created.

  87. D-023August 4, 2026

    A green dot is a claim, and a code comment is not a disclosure

    /integrations now carries a visitor-facing sentence stating that the connected states and sync times describe a sample workspace. The data did not change and the cards were not removed.

  88. D-022August 4, 2026

    A build check may only read evidence that is committed

    render-diagram.command writes two files. docs/architecture.render.log is the Terminal transcript and stays in .gitignore. docs/architecture.render.json is a four-field stamp — source path, source bytes, image bytes, timestamp — and is committed. scripts/check-diagram-current.mjs reads the stamp and never the log.

  89. D-021August 4, 2026

    The Salesforce adapter creates no Lead, and the PRD asked for one

    an approved quote syncs to Salesforce as an Account plus a Closed-Won Opportunity, and never as a Lead. The PRD's task 9 asks for "a Lead and an Opportunity". This is a deliberate deviation and it is recorded here rather than quietly satisfied, because the cheapest way to close the gap — create a Lead too — would ship a defect into every tenant's pipeline report.

  90. D-020August 4, 2026

    A secret is verified by its shape before it is saved, never by its content

    when a secret value is pasted into a form by the account owner, the value is checked for length and a leading fragment before the form is submitted, and the clipboard is seeded with a sentinel string beforehand so that a failed copy cannot masquerade as a successful one.

  91. D-019August 3, 2026

    tsx strips types; it does not check them

    flip the tamper byte with writeUInt8/readUInt8 rather than a compound index assignment, and reorder backend's verify so that environment-dependent steps run last.

  92. D-018August 3, 2026

    A security test that fails one run in four is worse than no test

    tamper with an encryption envelope by flipping bits in the decoded bytes, never by editing a base64url character. Two tests in the AES-256-GCM suite — "refuses an envelope whose ciphertext has been altered" and "refuses an envelope whose authentication tag has been altered" — mutated the envelope by replacing the final base64url character, "A" for anything else and "B" for an "A". That is not a mutation. base64 packs six bits per character, so when a payload's byte length is not a multiple of three, the final character carries two or four low-order bits that decode to nothing. Both fields land in exactly that case: the test's ciphertext is 22 bytes and a GCM tag is always 16, and 22 mod 3 = 16 mod 3 = 1, which is the four-dead-bit case. Whenever the original final character and its replacement agreed on their two significant bits, the "altered" envelope decoded to byte-for-byte the original, open() correctly succeeded, and the assertion failed with Missing expected exception.

  93. D-017August 3, 2026

    The service board is the union, and aliases decide coverage only

    serviceBoardRows() renders every check the monitor reports plus every declared component no check appears to cover — not one or the other.

  94. D-016August 3, 2026

    The claims register reports; it does not edit

    Roughly a hundred assertions rendered on this site are not true of this company. They are now written down — every one with a file, a line, a severity and the evidence that would make it true — in docs/legal/FLAGGED-CLAIMS.md. Not one of them was changed.

  95. D-015August 3, 2026

    A grid of times is not availability

    When NEXT_PUBLIC_SCHEDULING_PROVIDER and NEXT_PUBLIC_SCHEDULING_URL are set and the URL survives validation, the demo CTA is a plain link to the vendor's public booking page. When they are not, it opens the site's own modal, and that modal now says at the point of choice — not only on the receipt — that the times shown are business hours rather than a calendar.

  96. D-014August 3, 2026

    An email is not a ticket

    A submission through the form on /support is only described as a ticket when a helpdesk has returned an identifier for it. lib/helpdesk.ts files against Freshdesk or Zammad and returns a discriminated union; the filed arm carries the vendor's id, and nothing else in the codebase can produce one.

  97. D-013August 3, 2026

    "We don't know" is a variant, not a number

    Availability is read from an external monitor at request time, and when no monitor has measured it the site publishes no figure at all. readUptime() returns a discriminated union whose unmeasured arm carries a reason and no number, so there is no value for a page to accidentally render.

  98. D-012August 3, 2026

    The integrations layer is built to the consent screen and stops there

    Stripe, QuickBooks Online and Salesforce are implemented end to end — schema, migration, RLS policies, OAuth, encrypted credential storage, idempotent sync records, an approval fan-out, an inbound webhook, six HTTP routes, OpenAPI entries and seventy-nine tests — and every one of them is dark. The last step in each case is an action no repository can take on its own behalf: Stripe needs a secret key issued to an account, QuickBooks and Salesforce need a tenant's administrator to click Allow on a screen hosted by the provider.

  99. D-011August 3, 2026

    The published URL is the host that answers, and the spec is generated from the schemas

    Two things, which are really one thing. The API is documented at https://novel-systems-backend.vercel.app, not at https://api.novelsystems.ca. The vanity subdomain is not provisioned. It has no DNS record and no Vercel assignment, and until it has both, publishing it is publishing a hostname that does not resolve.

  100. D-010August 3, 2026

    Snapping is a control affordance, not geometry

    scripts/check-cutlist-parity.mjs was passing. The two engines declared TUBE_DEDUCTION 1.25 and TUBE_DEDUCTION_INCHES "1.25", and every other allowance agreed too. They still cut the same window to two different sizes.

  101. D-009August 3, 2026

    The commercial schema, and why the margin floor stayed in code

    Migration 0003 adds the tables a quote needs before it can be a commercial document rather than a calculation: customers, rate_cards, rate_card_items, margin_rules, quote_lines, and three nullable columns on quotes. It is purely additive — no drops, no type changes, no backfill — and that is a constraint the migration was written to satisfy, not a description of how it happened to turn out. The entries below are the places where the obvious choice was the wrong one.

  102. D-008August 3, 2026

    What the bug sweep fixed, and what it deliberately left alone

    A sweep of the codebase for defects, starting from a known OpenAPI mismatch and widening from there. Four findings were verified against source rather than taken on report. Three were fixed. One was investigated and deliberately not fixed, which is the entry most worth reading.

  103. D-007August 3, 2026

    The link checker asks what it can answer without the network

    The link audit checked three things: internal routes resolve, anchors exist, and the sitemap is backed by pages. It now checks six. The three additions were each chosen because a real defect in this repository passed all three original checks while being broken.

  104. D-006August 3, 2026

    The SSRF guard is exported, and the IPv6 half of it never ran

    assertSafeDestination is exported, and each blocked range has its own test case. The export is the part a reviewer will object to, so: this is a security control that was previously reachable only through dispatchWebhook. Testing it that way costs a live socket, a seeded subscription and a stubbed fetch per case, which is enough friction that the blocklist ended up covered by one case instead of twenty — and three bypasses sat in the file. Ten ranges asserted individually is what found this. The alternative on offer was "keep it private", and that is precisely what was in place while the bugs were there.

  105. D-005August 3, 2026

    Integration tests run against a fake data layer; RLS stays with Postgres

    Two tiers, and the split is on what each can honestly prove. The HTTP suite boots the real Express app on an ephemeral port and drives it with fetch, against an in-memory stand-in for Prisma. It runs anywhere in about six seconds, with no database. It covers what a controller-level test structurally cannot: helmet, CORS, the body-size limit, the rate limiters, Express's own routing and 404 handling, and every middleware in the chain in the order it actually runs. Two of the defects it was written to catch — a malformed query string answering 500 instead of 422, and a malformed JSON body doing the same — were live in main and invisible to every existing test, because neither fault is inside a controller.

  106. D-004August 3, 2026

    One validation path, and schemas live at module scope

    Every input to these routers is validated in the route table, by middleware, and every schema is declared at module scope and exported.

  107. D-003August 3, 2026

    Caller errors are translated in one place, and two of them were 500s

    Translation happens in the error handler, centrally, and nowhere else. The alternative — try/catch at each call site — puts the translation next to the code that failed, which reads well and is wrong: it has to be repeated at every site, it is silently missing at any site nobody thought about, and "was this handled here?" becomes a question you answer by reading every controller.

  108. D-002August 3, 2026

    Cut sizes are shared; prices are not

    The server engine gains the cut-size arithmetic as additional output. It keeps its own price. This is option (i) of open question A in PRD.md, and the reasoning is that the two engines are not two implementations of one thing that drifted. They are two products. lib/cpq-engine.ts prices shop wholesale — fabric, tube, hembar, aluminium surcharge, fabrication labour — and doubles it to retail. backend/src/services/cpq-engine.ts prices supply and install — resolved SKU costs from the tenant's own rate card, plus an installation labour model with a regional tier multiplier. A contractor buying a made blind and a building owner buying a hung blind are buying different things and the two prices should differ.

  109. D-001August 3, 2026

    Margin is a floor, not a target, and the floor is 50%

    One policy, both engines: MARGIN_FLOOR = 0.50, enforced as a floor.

Novel Systems publishes this in full rather than as highlights because a selected decision log is a testimonial. If an entry contradicts something else on this site, the entry is the record and the other page is the defect.

Back to the engineering evidence