From d2d9659ace31cbe198f6c9e770749cd2182cd558 Mon Sep 17 00:00:00 2001 From: nish3451 <257724087+nish3451@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:57:59 +0530 Subject: [PATCH] feat(measurement): add Website Correction conversion signal intended-outcome: make reviewed-service source handoff measurable without analytics or auto-send\nverify: node scripts/test-public-conversion-signal.mjs (120 checks passed); npm test and npm run ci retain only the pre-existing operator-surface failure; sgscan findings unchanged; git diff --check passed\n\nCo-Authored-By: Claude --- docs/measurement/public-conversion-signal.md | 108 +++++++++++++ package.json | 4 +- public/contact/index.html | 42 +++++ scripts/test-public-conversion-signal.mjs | 158 +++++++++++++++++++ 4 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 docs/measurement/public-conversion-signal.md create mode 100644 scripts/test-public-conversion-signal.mjs diff --git a/docs/measurement/public-conversion-signal.md b/docs/measurement/public-conversion-signal.md new file mode 100644 index 00000000..6cadddcf --- /dev/null +++ b/docs/measurement/public-conversion-signal.md @@ -0,0 +1,108 @@ +# Public conversion signal contract (The Website Correction application route) + +Scope: read-only measurement of which public page pointed a visitor to The +Website Correction application route. This layer does not deploy, send, or +collect anything. It does not change pricing or legal copy, does not touch +auth, payments, or leads, and does not depend on PR #10's public copy. + +## Signal source + +A Website Correction CTA is any public link that routes to the application +route (the contact page application section or the application email route) +or carries a `data-measure-source` marker. Every such CTA must carry an +explicit stable source tag naming its placement. + +Canonical stable source names: + +| Name | Placement | State in this checkout | +| --- | --- | --- | +| `homepage-hero` | Website Correction CTA in the homepage hero rail | Defined; no live CTA (PR #10 copy not merged) | +| `homepage-service` | Website Correction CTA in the homepage managed-service section | Defined; no live CTA (PR #10 copy not merged) | +| `homepage-footer` | Website Correction CTA in the homepage footer | Defined; no live CTA | + +Names are used only when they correspond to a real Website Correction CTA; +a name with no live CTA is dormant, not attached to unrelated links. In this +checkout the homepage has no Website Correction CTA: the "Ask about 0509" and +footer "0509" links are product CTAs (0509 is a product, not The Website +Correction) and must never carry a Website Correction source name. Any new +placement needs a new registered name, added here, to the allowlist in +`public/contact/index.html`, and to `scripts/test-public-conversion-signal.mjs`. + +## Offer naming + +The application route names the offer exactly as the public copy does: +The Website Correction. It is never presented as a generic "reviewed +service"; the human-reviewed nature of the offer is described in words +("reviewed by a human"), not used as a substitute offer name. The application +subject line is the operator-visible measurement signal: it names The +Website Correction and, when a marker is present, the source tag, so the +operator reads the signal directly in the received application email. The +default subject is exactly `The Website Correction application`; with a +marker present it becomes `The Website Correction application — from +(internal measurement marker)`. + +## Marker mechanics + +- CTA side: `data-measure-source=""` on the anchor, routing to + `/contact/?source=` or to the application mailto route with the + source name visible in the subject line. +- Endpoint side: `public/contact/index.html` reads the `?source=` parameter, + accepts only registered names, and prefills the application email subject + as `The Website Correction application — from (internal measurement + marker)`. The prefilled subject is the human-readable, manual measurement + signal; it is visible in the email client before sending, and the visitor + can edit or remove it. +- Nothing auto-sends or auto-submits: the route is a plain `mailto:` link the + visitor opens and sends themselves. There is no form, no hidden field, no + script-triggered navigation. + +## Owner + +The Tiny Studio operator (the repo operator) owns this signal contract and is +the only reader of the measurement signal. Interpretation happens only during +manual review of received application emails; no automation reads, tallies, or +acts on the signal. + +## Retention + +The static site stores nothing: this layer adds no cookie, no server log, no +client-side storage, and no third-party request. The only retention is what an +applicant voluntarily includes in the subject line of the email they send to +`support@tinystudio.in`; that email is governed by the studio inbox policy and +is not processed automatically. + +## Privacy boundary + +- No analytics provider, no cookies, no fingerprinting, no hidden fields, no + message-content collection. +- The marker is a first-party URL parameter and email subject prefill the + visitor can see, edit, or remove before sending. Removing it never blocks + the application. +- The disclosure about the marker sits on the contact page next to the + application route, where the visitor actually encounters the marker, and + states that it is for internal measurement only and that nothing is sent + until the visitor sends the email themselves. +- `public/privacy-choices/index.html` needs no change: this layer collects + nothing and offers no data choices, so there is nothing to opt out of there. + The privacy-choices page documents app-related data requests; this layer + does not create app data or choices, and the contact-page disclosure already + covers the only visitor-visible behavior this layer introduces. + +## Honesty + +Static-site source tagging is not proof of completed applications; only received human messages count as completion evidence. +A tag being present or a link being opened proves nothing about completion. + +## Falsifiable decision rules + +1. The tag pipeline is falsified iff any Website Correction CTA exists on the + public homepage without a registered `data-measure-source` tag, or the + contact endpoint fails to propagate an accepted source into the visible + prefilled subject line. `scripts/test-public-conversion-signal.mjs` + enforces this rule deterministically and fails when it breaks. +2. A completed application is counted only when a received human message + arrives in the studio inbox. If an application email arrives whose subject + carries no registered source name, the propagation rule is falsified and + the contact wiring must be re-verified before any conversion conclusion is + drawn. Under this rule, "no tagged clicks" and "no applications" are both + observable, and either direction can be disproven by a single counterexample. diff --git a/package.json b/package.json index 42cf2aa3..3f1e8f9c 100644 --- a/package.json +++ b/package.json @@ -85,8 +85,8 @@ "product:truth": "node scripts/check-product-truth.mjs", "mobbin:run": "node scripts/run-design-system-proving-lab.mjs", "mobbin:check": "node scripts/run-design-system-proving-lab.mjs --dry-run", - "ci": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check", - "test": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check" + "ci": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check", + "test": "node scripts/test-service-engine.mjs && node scripts/test-sales-intake-contract.mjs && node scripts/test-active-offer-projection.mjs && node scripts/test-active-operator-surfaces.mjs && node scripts/test-client-readiness-contract.mjs && node scripts/test-validated-service-client.mjs && node scripts/test-client-acceptance-gates.mjs && node scripts/check-product-truth.mjs && node scripts/check-human-service-kit.mjs && node scripts/test-design-system-proving-lab.mjs && node scripts/check-retention-automation.mjs && node scripts/test-retention-automation.mjs && node scripts/check-agency-defaults.mjs && node scripts/check-outbound-claim-safety.mjs && node scripts/check-outbound-send-readiness.mjs && node scripts/test-outbound-send-readiness.mjs && node scripts/test-public-conversion-signal.mjs && find scripts -name '*.mjs' -print0 | xargs -0 -n1 node --check" }, "engines": { "node": ">=20" diff --git a/public/contact/index.html b/public/contact/index.html index a5b2b21f..7ea556bb 100644 --- a/public/contact/index.html +++ b/public/contact/index.html @@ -80,6 +80,48 @@

A direct line to Tiny Studio.

+
+
+
+

The Website Correction

+

Apply by email, with a visible measurement marker.

+

+ If you arrived from a Tiny Studio page, the link may have + carried an internal measurement marker (for example + ?source=homepage-hero) so the studio can learn + which page pointed you here. The marker is for internal + measurement only: there is no analytics provider, no cookie, + and nothing is sent until you press send in your own email + app. You can remove the marker before sending. +

+ +
+
+

What the marker contains

+

+ Only a stable page name from the measurement contract, never + any text you type and never anything from your device. When a + marker is present it appears in the email subject line, so it + stays visible to you before you send. Without a marker the + email opens with a plain subject and the application works + the same. +

+
+
+ +
+

Support

diff --git a/scripts/test-public-conversion-signal.mjs b/scripts/test-public-conversion-signal.mjs new file mode 100644 index 00000000..9a65b327 --- /dev/null +++ b/scripts/test-public-conversion-signal.mjs @@ -0,0 +1,158 @@ +import { readFileSync, existsSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { dirname, join } from "node:path" + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") +const read = (p) => readFileSync(join(ROOT, p), "utf8") + +let failures = 0 +let checks = 0 +const ok = (cond, msg) => { + checks++ + if (cond) console.log(` ok ${msg}`) + else { + failures++ + console.error(` FAIL ${msg}`) + } +} + +const OFFER_NAME = "The Website Correction" +const SIGNAL_REGISTRY = [ + { name: "homepage-hero", meaning: "Website Correction CTA in the homepage hero rail" }, + { name: "homepage-service", meaning: "Website Correction CTA in the homepage managed-service section" }, + { name: "homepage-footer", meaning: "Website Correction CTA in the homepage footer" } +] +const REGISTRY_NAMES = SIGNAL_REGISTRY.map((s) => s.name) + +const DOCS_PATH = "docs/measurement/public-conversion-signal.md" +const INDEX_HTML = "public/index.html" +const CONTACT_HTML = "public/contact/index.html" +const PRIVACY_CHOICES_HTML = "public/privacy-choices/index.html" + +const forbidden = [ + "document.cookie", "localStorage", "sessionStorage", "navigator.sendBeacon", "sendBeacon(", + "fetch(", "XMLHttpRequest", + "googletagmanager", "google-analytics", "gtag(", "plausible.io", "fathom.js", + "posthog", "mixpanel", "amplitude", "window.analytics", "hotjar", "clarity.ms", "fbq(", + "connect.facebook.net", + "toDataURL", "hardwareConcurrency", "deviceMemory", "navigator.plugins", "FingerprintJS", + "window.fingerprint" +] + +const anchorsOf = (html) => [...html.matchAll(/]*>/gi)].map((m) => m[0]) +const hrefOf = (a) => { + const m = a.match(/href\s*=\s*"([^"]*)"/i) + return m ? m[1] : "" +} +const decoded = (s) => s.replace(/%20/gi, " ").replace(/%3D/gi, "=").replace(/%26/gi, "&").toLowerCase() + +const isWebsiteCorrectionCta = (a) => { + const href = decoded(hrefOf(a)) + return ( + /data-measure-source\s*=\s*"/i.test(a) || + (/^mailto:support@tinystudio\.in\?subject=/.test(href) && href.includes("website correction")) || + (href.includes("/contact/") && /[?&]source=/.test(href)) + ) +} + +console.log("test-public-conversion-signal: public conversion signal (The Website Correction application route)") + +console.log("A. signal definitions") +ok(REGISTRY_NAMES.length === new Set(REGISTRY_NAMES).size, "registry names are unique") +ok(REGISTRY_NAMES.every((n) => /^[a-z][a-z0-9-]*$/.test(n)), "registry names are stable lowercase slugs") + +console.log("B. docs contract") +ok(existsSync(join(ROOT, DOCS_PATH)), `${DOCS_PATH} exists`) +if (existsSync(join(ROOT, DOCS_PATH))) { + const docs = read(DOCS_PATH) + for (const name of REGISTRY_NAMES) ok(docs.includes(name), `docs document signal source ${name}`) + ok(docs.includes(OFFER_NAME), "docs name the offer truthfully as The Website Correction") + ok(!docs.includes("reviewed service"), "docs never name the offer as a generic reviewed service") + ok(docs.includes("prefilled subject"), "docs state the prefilled application subject is the operator-visible signal") + ok(docs.includes("The Website Correction application — from"), "docs pin the operator-visible subject format with the exact product name") + ok(docs.includes("(internal measurement marker)"), "docs pin the full propagated subject format") + ok(/owner/i.test(docs), "docs name the signal owner") + ok(docs.includes("retention"), "docs state retention") + ok(/privacy boundary/i.test(docs), "docs state the privacy boundary") + ok(docs.includes("privacy-choices"), "docs explain where the privacy disclosure lives") + ok(/falsif/i.test(docs), "docs contain a falsifiable decision rule") + ok(docs.includes("not proof of completed applications"), "docs are honest that tags are not application proof") + ok(docs.includes("only received human messages count"), "docs count only received human messages as completion evidence") +} + +console.log("C. homepage Website Correction CTA source tags") +const indexHtml = read(INDEX_HTML) +const ctaTags = anchorsOf(indexHtml).filter(isWebsiteCorrectionCta) +if (ctaTags.length === 0) { + console.log(" note: no Website Correction CTA present in this checkout (PR #10 public copy not merged); tag rule stays armed") +} +for (const a of ctaTags) { + const m = a.match(/data-measure-source\s*=\s*"([^"]*)"/i) + ok(m !== null, "every Website Correction CTA carries data-measure-source") + if (m) { + const name = m[1] + ok(REGISTRY_NAMES.includes(name), `source tag ${name} is a registered stable name`) + const href = decoded(hrefOf(a)) + const hasContext = + href.includes(`source=${name}`) || (href.startsWith("mailto:") && href.includes(name)) + ok(hasContext, `CTA ${name} routes to the application endpoint with source context`) + } +} + +console.log("D. contact endpoint source propagation") +const contactHtml = read(CONTACT_HTML) +ok( + /href\s*=\s*"mailto:support@tinystudio\.in\?subject=[^"]*"/i.test(contactHtml), + "contact page has a Website Correction application mailto route" +) +ok( + contactHtml.includes("?subject=The%20Website%20Correction%20application"), + "contact page default subject is exactly The Website Correction application" +) +ok( + decoded(contactHtml).includes("website correction"), + "contact page application route names the offer as Website Correction" +) +ok(contactHtml.includes(OFFER_NAME), "contact page names the offer truthfully as The Website Correction") +ok(!contactHtml.includes("reviewed service"), "contact page never names the offer as a generic reviewed service") +ok(contactHtml.includes("internal measurement"), "contact page states the signal is for internal measurement only") +ok(contactHtml.includes("nothing is sent"), "contact page states nothing is sent automatically") +ok(contactHtml.includes("remove the marker") || contactHtml.includes("remove it"), "contact page states the marker can be removed before sending") +ok(contactHtml.includes("URLSearchParams") && contactHtml.includes('get("source")'), "contact page reads the ?source= parameter") +ok( + REGISTRY_NAMES.every((n) => contactHtml.includes(n)), + "contact page allowlists every registered source name" +) +ok( + contactHtml.includes('"The Website Correction application — from " + source + " (internal measurement marker)"'), + "contact page propagates the exact The Website Correction application — from (internal measurement marker) subject" +) +ok( + contactHtml.includes("indexOf(source)") || contactHtml.includes("includes(source)"), + "contact page rejects unregistered source values" +) +for (const token of ["