Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions docs/measurement/public-conversion-signal.md
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +10 to +13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Define the direct application link as an exception.

Lines 10-13 require every application-endpoint link to carry data-measure-source. However, public/contact/index.html line 98 is the direct application link and intentionally has no marker. This makes the contract false for the current flow.

Define source CTAs as inbound placement links, or explicitly exempt the contact-page application link.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/measurement/public-conversion-signal.md` around lines 10 - 13, Update
the CTA contract in the documentation to define source CTAs as inbound placement
links, or explicitly exempt the direct contact-page application link from the
data-measure-source requirement. Preserve the requirement for stable source tags
on all other qualifying placement links.


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) |
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark the newly wired homepage sources as live

This checkout adds active homepage-hero and homepage-service CTAs in public/index.html, but the signal contract still labels both dormant and states that PR #10 is not merged. Since this table is the documented registry operators are told to update when placements change, it now gives the opposite state from the deployed markup and can mislead future attribution maintenance.

Useful? React with 👍 / 👎.

| `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
Comment on lines +33 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the validator-forbidden phrase.

Line 34 contains reviewed service. scripts/test-public-conversion-signal.mjs line 70 requires that the documentation does not contain this text. Therefore, npm test and npm run ci fail on this checkout.

Reword the negative policy without that exact phrase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/measurement/public-conversion-signal.md` around lines 33 - 36, Update
the documentation paragraph around “The Website Correction” to remove the exact
validator-forbidden phrase “reviewed service,” while preserving the intended
distinction between the offer name and its human-reviewed description.

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 <name>
(internal measurement marker)`.

## Marker mechanics

- CTA side: `data-measure-source="<name>"` on the anchor, routing to
`/contact/?source=<name>` 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 <name> (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.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the conversion check before the known failure

In the checked .github/workflows/codex-ci.yml, CI invokes npm run ci, but this && chain places the new conversion test after test-active-operator-surfaces.mjs. The commit explicitly records that the earlier test fails on clean main, so shell short-circuiting prevents CI from ever executing the new 120-check gate (and the subsequent syntax checks); move this gate ahead of the known failure or repair the baseline so it is actually enforced.

Useful? React with 👍 / 👎.

"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"
Expand Down
42 changes: 42 additions & 0 deletions public/contact/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,48 @@ <h1>A direct line to Tiny Studio.</h1>
</aside>
</section>

<section class="app-strip reveal delay-1" id="website-correction-application" aria-labelledby="website-correction-title">
<div class="app-strip-grid">
<article>
<p class="eyebrow">The Website Correction</p>
<h3 id="website-correction-title">Apply by email, with a visible measurement marker.</h3>
<p class="section-copy">
If you arrived from a Tiny Studio page, the link may have
carried an internal measurement marker (for example
<code>?source=homepage-hero</code>) 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.
</p>
<div class="action-row">
<a class="button" id="website-correction-apply" href="mailto:support@tinystudio.in?subject=The%20Website%20Correction%20application">Apply to The Website Correction</a>
</div>
</article>
<article>
<p class="eyebrow">What the marker contains</p>
<p class="section-copy">
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.
</p>
</article>
</div>
<script>
(function () {
var REGISTRY = ["homepage-hero", "homepage-service", "homepage-footer"];
var link = document.getElementById("website-correction-apply");
var source = new URLSearchParams(window.location.search).get("source");
if (link && source && REGISTRY.indexOf(source) !== -1) {
link.href = "mailto:support@tinystudio.in?subject=" + encodeURIComponent("The Website Correction application — from " + source + " (internal measurement marker)");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve source attribution when scripts are unavailable

When JavaScript is disabled or blocked, this assignment never runs and the static mailto at line 117 retains the untagged default subject even after a visitor follows either tagged homepage URL. Because the page provides a <noscript> stylesheet fallback and the application itself remains usable, these visitors silently lose the conversion signal; provide a non-JavaScript propagation fallback or explicitly narrow the signal contract to JavaScript-enabled visits.

Useful? React with 👍 / 👎.

}
})();
</script>
</section>

<section class="card-grid">
<article class="info-card reveal delay-1">
<p class="eyebrow">Support</p>
Expand Down
158 changes: 158 additions & 0 deletions scripts/test-public-conversion-signal.mjs
Original file line number Diff line number Diff line change
@@ -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(/<a\b[^>]*>/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))
Comment on lines +52 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect untagged Website Correction CTAs

When the forthcoming homepage copy adds an untagged CTA such as <a href="/contact/#website-correction-application">Apply to The Website Correction</a>, this predicate returns false because it only recognizes links that already contain a source marker or a Website Correction mailto subject. The anchor is therefore excluded before line 91 checks for data-measure-source, so the advertised regression gate reports zero CTAs and passes while source attribution is broken.

Useful? React with 👍 / 👎.

)
}

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 <source> (internal measurement marker) subject"
)
ok(
contactHtml.includes("indexOf(source)") || contactHtml.includes("includes(source)"),
"contact page rejects unregistered source values"
)
for (const token of ["<form", "<input", "<textarea", "location.href", "location.assign", "location.replace", "window.open", ".submit(", "sendBeacon(", "fetch("]) {
ok(!contactHtml.includes(token), `contact page has no auto-send/auto-submit mechanism (${token})`)
}

console.log("E. no analytics provider, cookies, fingerprinting, or message-content collection")
for (const file of [INDEX_HTML, CONTACT_HTML, PRIVACY_CHOICES_HTML]) {
const html = read(file)
for (const token of forbidden) {
ok(!html.includes(token), `${file} has no ${token}`)
}
}

console.log("F. npm test/ci wiring")
const pkg = JSON.parse(read("package.json"))
ok(
pkg.scripts.test.includes("test-public-conversion-signal.mjs"),
"npm test runs the public conversion signal test"
)
ok(
pkg.scripts.ci.includes("test-public-conversion-signal.mjs"),
"npm run ci runs the public conversion signal test"
)

console.log(`\n${checks} checks, ${failures} failures`)
process.exit(failures === 0 ? 0 : 1)
Comment on lines +157 to +158

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the stated 120-check result verifiable.

With no homepage CTA, this script performs 116 checks. Each future homepage CTA adds three checks. The script does not assert an expected count, so it cannot validate the PR claim of a deterministic 120-check regression test.

Either enforce the intended fixed count with a fixed fixture, or update the stated verification result to match the dynamic check model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-public-conversion-signal.mjs` around lines 157 - 158, Update the
final validation around checks and failures in the test script so the reported
result is verifiable: either use a fixed homepage CTA fixture and assert exactly
120 checks, or revise the stated verification result to reflect the dynamic
count of 116 checks plus three per CTA. Keep the existing failure exit behavior
intact.

Loading