@oneie/claude 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/abm-strategist.md +67 -1
- package/agents/ads-meta.md +67 -1
- package/agents/analyst.md +67 -1
- package/agents/animator.md +108 -0
- package/agents/architect.md +269 -20
- package/agents/brand-guardian.md +67 -1
- package/agents/brand-strategist.md +67 -1
- package/agents/campaign-content.md +67 -1
- package/agents/campaign-email.md +67 -1
- package/agents/campaign-sms.md +67 -1
- package/agents/campaign-social.md +67 -1
- package/agents/cco.md +83 -2
- package/agents/ceo.md +108 -11
- package/agents/chairman.md +197 -0
- package/agents/cmo.md +82 -2
- package/agents/community-greeter.md +67 -1
- package/agents/community-moderator.md +67 -1
- package/agents/compliance.md +67 -1
- package/agents/copywriter.md +67 -1
- package/agents/creative-strategist.md +67 -1
- package/agents/cro.md +81 -1
- package/agents/cto.md +266 -28
- package/agents/customer-interviewer.md +67 -1
- package/agents/customer-researcher.md +67 -1
- package/agents/customer-success-manager.md +67 -1
- package/agents/customer-trainer.md +67 -1
- package/agents/cxo.md +82 -1
- package/agents/demand-creator.md +67 -1
- package/agents/demo-mover.md +67 -1
- package/agents/demo-specialist.md +67 -1
- package/agents/demo-thai-family-law.md +67 -1
- package/agents/designer.md +67 -1
- package/agents/discovery-caller.md +67 -1
- package/agents/doctor.md +269 -0
- package/agents/educate-coach.md +67 -1
- package/agents/elevate-tutor.md +67 -1
- package/agents/email-lifecycle-marketer.md +67 -1
- package/agents/engage-specialist.md +67 -1
- package/agents/events-coordinator.md +67 -1
- package/agents/foundation-builder.md +67 -1
- package/agents/funnel-architect.md +67 -1
- package/agents/gift-creator.md +67 -1
- package/agents/google-ads.md +67 -1
- package/agents/guide.md +67 -1
- package/agents/helpdesk-dispatcher.md +67 -1
- package/agents/hook-specialist.md +67 -1
- package/agents/identify-optimizer.md +67 -1
- package/agents/implementer.md +313 -45
- package/agents/incident-commander.md +67 -1
- package/agents/insights-lead.md +87 -1
- package/agents/journey-runner.md +67 -1
- package/agents/linkedin-ads.md +67 -1
- package/agents/live-sales-chat.md +67 -1
- package/agents/market-researcher.md +67 -1
- package/agents/media-buyer.md +67 -1
- package/agents/memory-keeper.md +195 -0
- package/agents/movers-customer-researcher.md +67 -1
- package/agents/movers-foundation-builder.md +67 -1
- package/agents/movers-market-researcher.md +67 -1
- package/agents/movers-pricing-strategist.md +67 -1
- package/agents/nurture-architect.md +67 -1
- package/agents/offer-architect.md +67 -1
- package/agents/onboarder.md +67 -1
- package/agents/onboarding-specialist.md +67 -1
- package/agents/operations-dashboard.md +87 -1
- package/agents/perf-engineer.md +333 -37
- package/agents/playbook-writer.md +67 -1
- package/agents/plg-strategist.md +67 -1
- package/agents/positioning-architect.md +67 -1
- package/agents/press-officer.md +67 -1
- package/agents/pricing-strategist.md +67 -1
- package/agents/privacy-officer.md +67 -1
- package/agents/referral-manager.md +67 -1
- package/agents/refine-analyst.md +67 -1
- package/agents/release-manager.md +446 -39
- package/agents/renewals-upsell-rep.md +67 -1
- package/agents/review-engineer.md +319 -45
- package/agents/rewards-steward.md +67 -1
- package/agents/sales-call-coach.md +67 -1
- package/agents/sales-closer.md +67 -1
- package/agents/security-auditor.md +343 -48
- package/agents/sell-closer.md +67 -1
- package/agents/share-amplifier.md +67 -1
- package/agents/social-media-manager.md +67 -1
- package/agents/storyteller.md +301 -0
- package/agents/strategist.md +67 -1
- package/agents/strategy-aligner.md +67 -1
- package/agents/support-agent.md +67 -1
- package/agents/tagger.md +327 -0
- package/agents/tech-writer.md +195 -22
- package/agents/test-engineer.md +398 -29
- package/agents/tiktok-ads.md +67 -1
- package/agents/tracking-engineer.md +67 -1
- package/agents/trailkeeper.md +181 -0
- package/agents/upsell-strategist.md +67 -1
- package/agents/voice.md +67 -1
- package/agents/w1-recon.md +1 -1
- package/agents/w2-decide.md +1 -1
- package/agents/w3-edit.md +8 -2
- package/agents/w4-verify.md +13 -0
- package/agents/workflow-optimiser.md +81 -1
- package/commands/close.md +916 -160
- package/commands/deploy.md +102 -724
- package/commands/do.md +58 -2
- package/commands/sweep.md +159 -0
- package/commands/tasks.md +222 -0
- package/hooks/scripts/dev-only.sh +135 -0
- package/hooks/scripts/git-add-guard.sh +37 -2
- package/hooks/scripts/session-start.sh +32 -4
- package/package.json +1 -1
- package/rules/scripts.md +85 -0
- package/scripts/CLAUDE.md +315 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agent-actor-parity.sh +129 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/chrome.mjs +18 -0
- package/scripts/close-metrics.sh +587 -0
- package/scripts/close-owner.sh +326 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-emit.sh +311 -0
- package/scripts/deploy-gate-check.sh +155 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy-schema-check.sh +58 -0
- package/scripts/deploy.sh +393 -243
- package/scripts/do-auto.sh +127 -26
- package/scripts/do-board.sh +429 -0
- package/scripts/do-close.sh +1184 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-fleet.sh +8 -2
- package/scripts/do-plan-json.mjs +110 -12
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +86 -10
- package/scripts/do-rank.py +200 -3
- package/scripts/do-reconcile.sh +73 -12
- package/scripts/do-signal.sh +101 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-w4-gates.sh +11 -1
- package/scripts/do-world-check.sh +153 -0
- package/scripts/download-stats.sh +172 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +18 -1
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +71 -0
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/flywheel-outcome.sh +63 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +9 -0
- package/scripts/gate-watchdog.sh +619 -0
- package/scripts/gc-content-check.sh +142 -0
- package/scripts/gh-traffic-capture.sh +153 -0
- package/scripts/govern-order-check.sh +202 -0
- package/scripts/governor-doors-check.sh +86 -5
- package/scripts/health.sh +448 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/incident.sh +212 -0
- package/scripts/land.sh +755 -45
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/livekit-ratchet.sh +18 -1
- package/scripts/machine-check.sh +1 -1
- package/scripts/memory-index-budget.sh +79 -0
- package/scripts/npm-downloads.sh +109 -0
- package/scripts/one-agents.mjs +204 -8
- package/scripts/one-resume.sh +31 -3
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/redirect-lint.sh +169 -0
- package/scripts/release.sh +40 -6
- package/scripts/resume-lost-sessions.sh +68 -0
- package/scripts/shoot-pages.mjs +140 -0
- package/scripts/signal-meta-backfill.ts +451 -0
- package/scripts/signal-watch.sh +63 -6
- package/scripts/speed-cache-check.sh +12 -2
- package/scripts/sweep.sh +426 -0
- package/scripts/task-titles-dump.ts +101 -0
- package/scripts/test-cached.sh +47 -10
- package/scripts/test-lanes.sh +14 -0
- package/scripts/thread-name-backfill.ts +215 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +155 -8
- package/scripts/typedb-flake-check.sh +3 -1
- package/scripts/urls-lint.sh +8 -0
- package/scripts/verify-board-doors.sh +80 -0
- package/scripts/verify-fast.sh +159 -6
- package/scripts/worktree-up.sh +21 -3
- package/skills/astro/SKILL.md +9 -3
- package/skills/astro/optimize-performance.md +3 -2
- package/skills/cloudflare/SKILL.md +3 -2
- package/skills/cloudflare-security-audit/AI-AND-LLM.md +83 -0
- package/skills/cloudflare-security-audit/ATTACK-CLASSES.md +130 -0
- package/skills/cloudflare-security-audit/CLIENT-SIDE.md +83 -0
- package/skills/cloudflare-security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- package/skills/cloudflare-security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- package/skills/cloudflare-security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- package/skills/cloudflare-security-audit/HUNTING.md +251 -0
- package/skills/cloudflare-security-audit/LICENSE +21 -0
- package/skills/cloudflare-security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- package/skills/cloudflare-security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- package/skills/cloudflare-security-audit/PROVENANCE.md +78 -0
- package/skills/cloudflare-security-audit/RECONNAISSANCE.md +156 -0
- package/skills/cloudflare-security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- package/skills/cloudflare-security-audit/SKILL.md +192 -0
- package/skills/cloudflare-security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- package/skills/cloudflare-security-audit/VALIDATION-AND-REPORTING.md +186 -0
- package/skills/cloudflare-security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- package/skills/cloudflare-security-audit/report-schema.json +461 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.cjs +872 -0
- package/skills/cloudflare-security-audit/validate-coverage-ledger.test.cjs +740 -0
- package/skills/cloudflare-security-audit/validate-findings.cjs +773 -0
- package/skills/cloudflare-security-audit/validate-findings.test.cjs +652 -0
- package/skills/deploy/REFERENCE.md +713 -0
- package/skills/deploy/SKILL.md +140 -0
- package/skills/fleet-audit/SKILL.md +58 -0
- package/skills/meeting/SKILL.md +220 -0
- package/skills/planning/SKILL.md +256 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/typedb/SKILL.md +7 -0
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/templates/template-agent.md +7 -1
- package/templates/template-feature.md +9 -0
- package/templates/template-todo.md +29 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ad-copy-lint.sh — no forbidden sentence ships.
|
|
3
|
+
#
|
|
4
|
+
# Portability: monorepo-only. It hard-asserts against THIS tree — it reads the
|
|
5
|
+
# LIVE worker config at one.ie/web/wrangler.toml and refuses (exit 3) when a
|
|
6
|
+
# subject is absent. The classification of record is the manifest in
|
|
7
|
+
# factory-repo.sh; this line is the copy that travels with the script.
|
|
8
|
+
#
|
|
9
|
+
# ── THE SUBJECT IS THE COPY THAT REACHES A VISITOR ─────────────────────────
|
|
10
|
+
# Until 2026-09-08 the subject was text/ad-sovereign-keys.md, a 94-line file
|
|
11
|
+
# that arrived CLEAN in the same commit as this script and never contained the
|
|
12
|
+
# four claims. Production /ad meanwhile carried all of them. The accept string
|
|
13
|
+
# was green for a day while the page lied — a lint aimed at a mirror does not
|
|
14
|
+
# merely miss, it MANUFACTURES a green. The subject is now three surfaces, each
|
|
15
|
+
# one something a visitor actually receives:
|
|
16
|
+
#
|
|
17
|
+
# 1. one.ie/web/src/lib/chat/meta/commerce.ts, the '/ad' entry only. This
|
|
18
|
+
# ships from source in the worker; a visitor gets these sentences from the
|
|
19
|
+
# chat on /ad. Scoped to '/ad' because this is the ad lint — '/sell' and
|
|
20
|
+
# '/pricing' carry their own card claims and belong to whoever owns those
|
|
21
|
+
# pages.
|
|
22
|
+
# 2. text/ad.md, the fenced copy blocks under "## 4. The page, as shipped"
|
|
23
|
+
# plus the quoted paragraph under "### The cleared paragraph".
|
|
24
|
+
# EXTRACTED POSITIVELY, never grep-minus-exclusions: §2/§3 record
|
|
25
|
+
# withdrawn claims and §8 is a list of FORBIDDEN EXAMPLE sentences. Both
|
|
26
|
+
# are prose ABOUT copy, not copy, and a sweep over the whole file refuses
|
|
27
|
+
# the very records it is supposed to preserve. mask_subject blanks every
|
|
28
|
+
# line that is not shipped copy, so reported line numbers stay true.
|
|
29
|
+
# 3. --live: https://one.ie/ad as served. Production /ad canonicals to
|
|
30
|
+
# /u/one/p/ad and renders from the D1 `pages` row (workspace='one'
|
|
31
|
+
# slug='ad', copy in the `data` column — there is no `content` column).
|
|
32
|
+
# NOTHING IN THIS REPO IS THAT ROW'S SOURCE; it is edited in place. The
|
|
33
|
+
# served HTML is therefore the only readable subject for it, and a public
|
|
34
|
+
# GET reads no environment. Unreachable is exit 3 — no subject, no proof —
|
|
35
|
+
# never 0. It is OPT-IN: do-promise-settle.sh runs the accept string
|
|
36
|
+
# verbatim and a promise must not settle BROKEN on a network hiccup, so
|
|
37
|
+
# the no-arg run stays offline and files 2 and 1 are what it reads. Say
|
|
38
|
+
# plainly, then, what the default does NOT cover: the D1 row. Run --live
|
|
39
|
+
# after any edit to it.
|
|
40
|
+
#
|
|
41
|
+
# Deliverable 5 of text/bring-one-to-life.md. The launch copy may say only what
|
|
42
|
+
# was measured true. Four claims were measured FALSE on 2026-09-07 and are
|
|
43
|
+
# refused here:
|
|
44
|
+
#
|
|
45
|
+
# fee "free" as a fee claim. pay/backend/src/agent.ts:220 — the claim
|
|
46
|
+
# path is "Gas + 1% fee auto-deducted". Something is deducted, so
|
|
47
|
+
# nothing on this rail is free.
|
|
48
|
+
# chains "all major blockchains". There are FOUR, and they are named:
|
|
49
|
+
# Sui, Ethereum, Solana, Bitcoin
|
|
50
|
+
# (one.ie/web/tests/unit/vault/multichain.test.ts derives exactly
|
|
51
|
+
# those four addresses and asserts they are distinct).
|
|
52
|
+
# wallet "restore it in any wallet". sui keytool takes the phrase through
|
|
53
|
+
# BIP39's PBKDF2 and a BIP32 path; one.ie/web/src/lib/vault.ts:237
|
|
54
|
+
# takes mnemonicToEntropy STRAIGHT into HKDF (vault.ts:281). Same
|
|
55
|
+
# twenty-four words, different key. A stranger who follows that
|
|
56
|
+
# sentence lands on an empty address.
|
|
57
|
+
# card any card / Apple Pay / Google Pay claim while the surface under lint
|
|
58
|
+
# is not in live Stripe mode. This one is CONFIG-DERIVED: the mode is
|
|
59
|
+
# read from one.ie/web/wrangler.toml at run time, never hardcoded here.
|
|
60
|
+
#
|
|
61
|
+
# THE MODE IS PER-PAGE NOW, so this rule asks a per-page question. The worker
|
|
62
|
+
# holds both a test and a live Stripe pair, and STRIPE_LIVE_PAGES names the exact
|
|
63
|
+
# page paths permitted to charge a real card (one.ie/web/src/lib/stripe-mode.ts,
|
|
64
|
+
# resolveStripeForPage). Two questions, one answer each:
|
|
65
|
+
#
|
|
66
|
+
# no --page the GLOBAL question. Copy that is not tied to one page — an ad,
|
|
67
|
+
# a landing headline — may claim a card only if the whole worker is
|
|
68
|
+
# live (STRIPE_MODE = "live"). One page being armed does not license
|
|
69
|
+
# a sentence that appears everywhere. This is the default and it is
|
|
70
|
+
# unchanged, byte for byte, from before pages existed.
|
|
71
|
+
# --page P the PAGE question. Copy that ships on page P may claim a card if
|
|
72
|
+
# P is a member of STRIPE_LIVE_PAGES. Exact membership, no globs —
|
|
73
|
+
# the list is normalised the same way the resolver normalises it.
|
|
74
|
+
#
|
|
75
|
+
# The card rule is the point of the whole script. It goes green two ways and
|
|
76
|
+
# only two ways — the CONFIG changes (STRIPE_MODE = "live", or the page joins
|
|
77
|
+
# STRIPE_LIVE_PAGES) or the COPY changes (the claim comes out). Editing the lint
|
|
78
|
+
# is not one of them, and at 2am it is the only kind of check that does not get
|
|
79
|
+
# argued with.
|
|
80
|
+
#
|
|
81
|
+
# It reads NO environment. do-promise-settle.sh runs the accept string verbatim,
|
|
82
|
+
# so an opt-in flag would settle the promise BROKEN while every check is green.
|
|
83
|
+
#
|
|
84
|
+
# Usage:
|
|
85
|
+
# ad-copy-lint.sh lint every default subject against the real config
|
|
86
|
+
# ad-copy-lint.sh --page P [F] ask the PAGE question for path P (copy file F,
|
|
87
|
+
# default: every subject). Exit codes unchanged.
|
|
88
|
+
# ad-copy-lint.sh --live [URL] lint the SERVED page (default https://one.ie/ad),
|
|
89
|
+
# which is the only readable form of the D1 row.
|
|
90
|
+
# Unreachable is exit 3, never 0.
|
|
91
|
+
# ad-copy-lint.sh --self-test drive every rule RED against fixtures, both
|
|
92
|
+
# ways for the config branch, then assert the
|
|
93
|
+
# clean fixture is green
|
|
94
|
+
#
|
|
95
|
+
# Exit: 0 = no forbidden sentence · 1 = a forbidden claim ships · 3 = the
|
|
96
|
+
# SUBJECT is missing (the proof has no subject).
|
|
97
|
+
# A missing or unparseable config is NOT exit 3 — it ARMS the card rule
|
|
98
|
+
# and reports through the normal 0/1. A config the lint cannot read must
|
|
99
|
+
# never read as permission, and it must never read as an error either:
|
|
100
|
+
# copy with no rail sentence in it stays green whatever the toml says.
|
|
101
|
+
set -u
|
|
102
|
+
|
|
103
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
104
|
+
TOML_DEFAULT="$ROOT/one.ie/web/wrangler.toml"
|
|
105
|
+
LIVE_URL_DEFAULT="https://one.ie/ad"
|
|
106
|
+
|
|
107
|
+
# The default subjects, in order. Each is masked to its shipped copy by
|
|
108
|
+
# mask_subject before a single rule is applied.
|
|
109
|
+
subjects() {
|
|
110
|
+
printf '%s\n' \
|
|
111
|
+
"$ROOT/one.ie/web/src/lib/chat/meta/commerce.ts" \
|
|
112
|
+
"$ROOT/text/ad.md"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# The rules. One line per pattern: id %% extended-regex %% why.
|
|
117
|
+
# Matched case-insensitively over every line of the copy, including HTML
|
|
118
|
+
# comments — a claim that is hidden is still a claim that shipped.
|
|
119
|
+
# Word edges are written (^|[^[:alpha:]]) … ([^[:alpha:]]|$) rather than \b,
|
|
120
|
+
# which is not portable across the greps on this box.
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
rules() {
|
|
123
|
+
cat <<'RULES'
|
|
124
|
+
fee %% (^|[^[:alpha:]])(fee|gas)[- ]free([^[:alpha:]]|$) %% a fee denial. pay/backend/src/agent.ts:221 — "1% protocol fee per transaction. Paid by merchant. Gas also merchant-covered." Something is deducted per transaction, so no transaction is fee-free.
|
|
125
|
+
fee %% (no|zero|without)[[:space:]]+(any[[:space:]]+)?(fee|fees)([^[:alpha:]]|$) %% a no-fee claim. The protocol fee is 1% per transaction and gas is deducted on top (pay/backend/src/agent.ts:220-221).
|
|
126
|
+
fee %% costs?[[:space:]]+(you[[:space:]]+)?nothing %% an unqualified no-cost claim. Gas plus one percent is deducted per transaction (pay/backend/src/agent.ts:220-221). Name what is free instead of saying nothing costs.
|
|
127
|
+
fee %% (free|no[[:space:]]+cost)[^.]{0,50}(transaction|transfer|on-chain|gas|withdraw|payout|settle) %% "free" attached to the TRANSACTION rail. That rail is 1% plus gas (pay/backend/src/agent.ts:220-221). "Free" is true of the keys, the wallets, the shop and taking a payment — it is not true of a transaction.
|
|
128
|
+
fee %% (transaction|transfer|payout|withdrawal|settlement|gas)[^.]{0,50}(is|are|costs?)[[:space:]]+(free|nothing) %% the same claim as a sentence. 1% per transaction, gas on top (pay/backend/src/agent.ts:220-221).
|
|
129
|
+
fee %% unlimited[[:space:]]+free %% banned verbatim by text/ad.md §8 "Money and free". There is no overdraft: findFunder refuses rather than lends (one.ie/web/src/lib/credits.ts:122), and a free signup has 100 credits.
|
|
130
|
+
fee %% free[[:space:]]+forever[^.]{0,40}credits? %% "free forever" must never attach to credits (text/ad.md §8; free-offer.md §9). Credits are the priced product.
|
|
131
|
+
fee %% credits?[^.]{0,40}free[[:space:]]+forever %% the same claim, other order. "Free and sovereign forever" attaches to the keys and the wallets only.
|
|
132
|
+
fee %% first[[:space:]]+[a-z0-9,]+[[:space:]]+(messages|credits)[[:space:]]+are[[:space:]]+free %% banned verbatim by text/ad.md §8. There is no overdraft (credits.ts:122); a free signup has 100 credits and then buys more.
|
|
133
|
+
chains %% (all|any|every)[[:space:]]+(major[[:space:]]+)?(block)?chains? %% there are FOUR chains and they are named: Sui, Ethereum, Solana, Bitcoin (one.ie/web/tests/unit/vault/multichain.test.ts). Name them; do not generalise.
|
|
134
|
+
chains %% (all|any|every)[[:space:]]+(major[[:space:]]+)?(crypto|cryptocurrency|coin|token)s? %% the same over-claim wearing a different noun. Four chains, named.
|
|
135
|
+
wallet %% (any|another|other|standard|external|third[- ]party|hardware|your[[:space:]]+own)[[:space:]]+wallet %% the twenty-four words do NOT reconstruct this key elsewhere. A standard tool runs BIP39 PBKDF2 + BIP32; one.ie/web/src/lib/vault.ts:237 takes the entropy straight into HKDF (vault.ts:281). Same words, different key.
|
|
136
|
+
wallet %% (restore|recover|import|open)[a-z]*[[:space:]]+(it|them|the[[:space:]]+(key|keys|wallet|words|phrase))[[:space:]]+(in|into|with|on)[[:space:]]+(any|another|other|a[[:space:]]+standard) %% the same claim as a sentence. The words restore the key HERE.
|
|
137
|
+
card %% (^|[^[:alpha:]])cards?([^[:alpha:]]|$) %% a card claim, and the worker is not in live Stripe mode. A WITHDRAWAL is not a claim — see withdrawals() for the two exact sentences that may carry the word.
|
|
138
|
+
card %% (credit|debit)[- ]card %% a card claim, and the worker is not in live Stripe mode.
|
|
139
|
+
card %% apple[[:space:]]+pay %% an Apple Pay claim, and the worker is not in live Stripe mode.
|
|
140
|
+
card %% google[[:space:]]+pay %% a Google Pay claim, and the worker is not in live Stripe mode.
|
|
141
|
+
card %% (^|[^[:alpha:]])(stripe|visa|mastercard|amex)([^[:alpha:]]|$) %% a card-rail claim, and the worker is not in live Stripe mode.
|
|
142
|
+
RULES
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# WITHDRAWALS — the exact sentences in which a withdrawn claim may name itself.
|
|
147
|
+
#
|
|
148
|
+
# Prod's honest copy is "Card checkout is not live yet. Crypto checkout is."
|
|
149
|
+
# The word "card" is in it, and a bare pattern refuses the very sentence that
|
|
150
|
+
# tells the truth. So a card token is permitted ONLY inside one of the literals
|
|
151
|
+
# below, which are stripped from a line before the card rules see it.
|
|
152
|
+
#
|
|
153
|
+
# They are whole cells and whole sentences, never the token "Card checkout" on
|
|
154
|
+
# its own — an allowlisted token would permit "Card checkout is instant", and
|
|
155
|
+
# "Cards are not live yet, but Apple Pay is" must still go red (it does: the
|
|
156
|
+
# literal does not match, and Apple Pay has its own rule). Each is licensed by
|
|
157
|
+
# one.ie/web/wrangler.toml:110 STRIPE_MODE = "test"; when that says "live" the
|
|
158
|
+
# card rules are skipped wholesale and these strip to no effect.
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
withdrawals() {
|
|
161
|
+
cat <<'W'
|
|
162
|
+
Card checkout is not live yet. Crypto checkout is.
|
|
163
|
+
"feature":"Card checkout","us":"Not yet available","alt1":"Not yet available"
|
|
164
|
+
W
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# NEGATIVE EXAMPLES — a phrase quoted as a BAN is not a claim.
|
|
168
|
+
#
|
|
169
|
+
# commerce.ts's '/ad' system prompt tells the model which phrases to refuse, and
|
|
170
|
+
# it does that by naming them verbatim, which is the only way that instruction
|
|
171
|
+
# works. Linting them as claims would force the copy to stop naming what it
|
|
172
|
+
# refuses — the §8 failure one file over. Each literal below is stripped from
|
|
173
|
+
# every subject before any rule runs.
|
|
174
|
+
#
|
|
175
|
+
# The first four carry their QUOTE MARKS. That is the whole safety property:
|
|
176
|
+
# "no fees, ever" as a quoted ban is permitted, no fees, ever as a sentence
|
|
177
|
+
# still goes red. The fifth is a refusal instruction that cannot be read as a
|
|
178
|
+
# claim in any grammar. The sixth is the CONFIG VARIABLE the card rule reads:
|
|
179
|
+
# copy that cites `one.ie/web/wrangler.toml:110 is STRIPE_MODE = "test"` as its
|
|
180
|
+
# reason for withdrawing a claim is naming the guard, not naming a payment
|
|
181
|
+
# option, and the literal stops matching the day that line says "live" — at
|
|
182
|
+
# which point the card rules are skipped wholesale anyway.
|
|
183
|
+
#
|
|
184
|
+
# Adding a literal here is a reviewed act, not a regex
|
|
185
|
+
# tweak — it is the one place this script can be widened, and it is deliberately
|
|
186
|
+
# a list of exact strings so that widening it is legible in a diff.
|
|
187
|
+
negative_examples() {
|
|
188
|
+
cat <<'N'
|
|
189
|
+
"no fees, ever"
|
|
190
|
+
"get paid instantly"
|
|
191
|
+
"instant settlement"
|
|
192
|
+
"sell anything"
|
|
193
|
+
any card, Apple Pay or Google Pay claim whatsoever
|
|
194
|
+
STRIPE_MODE = "test"
|
|
195
|
+
N
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
strip_literals() { # $1 = producer fn name; stdin -> stdout, its literals removed
|
|
199
|
+
local w esc out
|
|
200
|
+
out="$(cat)"
|
|
201
|
+
while IFS= read -r w; do
|
|
202
|
+
[ -n "$w" ] || continue
|
|
203
|
+
esc="$(printf '%s' "$w" | sed -E 's/[][\\.*^$/&|?+(){}]/\\&/g')"
|
|
204
|
+
out="$(printf '%s' "$out" | sed "s/$esc//g")"
|
|
205
|
+
done <<L
|
|
206
|
+
$("$1")
|
|
207
|
+
L
|
|
208
|
+
printf '%s\n' "$out"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
strip_negatives() { strip_literals negative_examples; }
|
|
212
|
+
|
|
213
|
+
strip_withdrawals() { strip_literals withdrawals; }
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# mask_subject — POSITIVE extraction. Prints the file with every line that is
|
|
217
|
+
# NOT shipped copy blanked, so the line count (and therefore every reported
|
|
218
|
+
# line number) is the real one. An unrecognised file is passed through whole:
|
|
219
|
+
# --self-test's fixtures and an explicit file argument are linted entire.
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
mask_subject() { # $1 = path
|
|
222
|
+
case "$(basename "${1:-}")" in
|
|
223
|
+
ad.md) mask_ad_md "$1" ;;
|
|
224
|
+
commerce.ts) mask_commerce_ad "$1" ;;
|
|
225
|
+
*) cat "$1" ;;
|
|
226
|
+
esac
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# text/ad.md: the fenced blocks under "## 4. The page, as shipped", plus the
|
|
230
|
+
# blockquote under "### The cleared paragraph". Nothing else is copy.
|
|
231
|
+
mask_ad_md() {
|
|
232
|
+
awk '
|
|
233
|
+
/^## / { sec = $0; fence = 0 }
|
|
234
|
+
/^### The cleared paragraph/ { cleared = 1 }
|
|
235
|
+
/^### / { if ($0 !~ /^### The cleared paragraph/) cleared = 0 }
|
|
236
|
+
{
|
|
237
|
+
keep = 0
|
|
238
|
+
if (sec ~ /^## 4\. The page, as shipped/) {
|
|
239
|
+
if ($0 ~ /^```/) { fence = !fence; keep = 0 }
|
|
240
|
+
else if (fence) { keep = 1 }
|
|
241
|
+
}
|
|
242
|
+
if (cleared && $0 ~ /^>/) keep = 1
|
|
243
|
+
print keep ? $0 : ""
|
|
244
|
+
}
|
|
245
|
+
' "$1"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# commerce.ts: the "/ad" entry only, from its key to the next top-level key.
|
|
249
|
+
mask_commerce_ad() {
|
|
250
|
+
awk "
|
|
251
|
+
/^ '\/ad': \{/ { inad = 1; print; next }
|
|
252
|
+
inad && /^ '\// { inad = 0 }
|
|
253
|
+
{ print inad ? \$0 : \"\" }
|
|
254
|
+
" "$1"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# fetch_live — the served page. Public GET, no environment, no credentials.
|
|
259
|
+
# Entities are unescaped, then the COPY-BEARING props are extracted positively:
|
|
260
|
+
# a whole-page grep would refuse <meta name="twitter:card">, which is a protocol
|
|
261
|
+
# token and not a sentence anyone reads. Prints nothing on failure; the caller
|
|
262
|
+
# turns that into exit 3.
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
fetch_live() { # $1 = url
|
|
265
|
+
local html norm objs props tmp
|
|
266
|
+
html="$(curl -fsS --max-time 25 "$1" 2>/dev/null)" || return 1
|
|
267
|
+
[ -n "$html" ] || return 1
|
|
268
|
+
tmp="$(mktemp -d)"
|
|
269
|
+
|
|
270
|
+
# Unescape, then flatten Astro's island serialisation ("k":[0,"v"] -> "k":"v")
|
|
271
|
+
# so a copy object reads exactly as it does in text/ad.md's fenced blocks.
|
|
272
|
+
norm="$tmp/norm"
|
|
273
|
+
printf '%s' "$html" \
|
|
274
|
+
| sed -e 's/"/"/g' -e 's/'/'"'"'/g' -e 's/&/\&/g' -e 's/</</g' -e 's/>/>/g' \
|
|
275
|
+
| sed -E 's/"([a-zA-Z0-9_]+)":\[0,"([^"]*)"\]/"\1":"\2"/g' > "$norm"
|
|
276
|
+
|
|
277
|
+
# A COPY OBJECT STAYS ON ONE LINE. A pricing row is one claim, not four
|
|
278
|
+
# fragments: split into props, its cell reads "Card checkout" with the
|
|
279
|
+
# withdrawal that qualifies it on some other line, and the guard would refuse
|
|
280
|
+
# the honest sentence. Objects first, then the props that live outside one.
|
|
281
|
+
objs="$tmp/objs"; props="$tmp/props"
|
|
282
|
+
grep -oE '\{[^{}]*\}' "$norm" 2>/dev/null \
|
|
283
|
+
| grep -E '"(feature|question|label|title|eyebrow|heading|welcome|headline)":' > "$objs" || true
|
|
284
|
+
grep -oE '"(label|description|detail|note|feature|us|alt1|question|answer|title|subtitle|usLabel|eyebrow|heading|headline|body|sub|welcome|kicker|cta)":"[^"]*"' "$norm" 2>/dev/null \
|
|
285
|
+
| sort -u > "$props" || true
|
|
286
|
+
grep -oE '"(label|description|detail|note|feature|us|alt1|question|answer|title|subtitle|usLabel|eyebrow|heading|headline|body|sub|welcome|kicker|cta)":"[^"]*"' "$objs" 2>/dev/null \
|
|
287
|
+
| sort -u > "$tmp/inobj" || true
|
|
288
|
+
|
|
289
|
+
{ cat "$objs"; grep -Fxv -f "$tmp/inobj" "$props" 2>/dev/null || cat "$props"; } | grep -v '^$'
|
|
290
|
+
local rc=$?
|
|
291
|
+
rm -rf "$tmp"
|
|
292
|
+
return 0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# The live config read. FAILS CLOSED: anything that is not exactly one
|
|
297
|
+
# assignment whose value is "live" arms the card rule. A config the lint cannot
|
|
298
|
+
# parse must never read as permission. The same rule governs the page list — an
|
|
299
|
+
# absent, empty or unparseable STRIPE_LIVE_PAGES yields NO live pages, so every
|
|
300
|
+
# page question answers "not live".
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
read_stripe_mode() { # $1 = wrangler.toml path -> prints the mode, or a reason
|
|
303
|
+
local f="${1:-}"
|
|
304
|
+
if [ ! -f "$f" ]; then printf 'unreadable(no such file)'; return 0; fi
|
|
305
|
+
local hits
|
|
306
|
+
hits="$(grep -nE '^[[:space:]]*STRIPE_MODE[[:space:]]*=' "$f" 2>/dev/null || true)"
|
|
307
|
+
if [ -z "$hits" ]; then printf 'unreadable(no STRIPE_MODE assignment)'; return 0; fi
|
|
308
|
+
local seen="" v line
|
|
309
|
+
while IFS= read -r line; do
|
|
310
|
+
[ -n "$line" ] || continue
|
|
311
|
+
v="${line#*=}"
|
|
312
|
+
v="$(printf '%s' "$v" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//; s/[[:space:]]*#.*$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/; s/[[:space:]]*$//')"
|
|
313
|
+
case " $seen " in *" $v "*) ;; *) seen="${seen:+$seen }$v" ;; esac
|
|
314
|
+
done <<HITS
|
|
315
|
+
$hits
|
|
316
|
+
HITS
|
|
317
|
+
case "$seen" in
|
|
318
|
+
*" "*) printf 'conflicting(%s)' "$seen" ;;
|
|
319
|
+
"") printf 'unreadable(empty value)' ;;
|
|
320
|
+
*) printf '%s' "$seen" ;;
|
|
321
|
+
esac
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# One spelling of a page path, matching normalisePagePath() in
|
|
325
|
+
# one.ie/web/src/lib/stripe-mode.ts: lowercase, no query, no fragment, leading
|
|
326
|
+
# slash, no doubled or trailing slash. A path carrying whitespace or a wildcard
|
|
327
|
+
# normalises to nothing and is therefore a member of no list.
|
|
328
|
+
norm_page() { # $1 = raw path -> prints the normalised path, or nothing
|
|
329
|
+
local p="${1:-}"
|
|
330
|
+
p="$(printf '%s' "$p" | tr '[:upper:]' '[:lower:]' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
|
331
|
+
[ -n "$p" ] || return 0
|
|
332
|
+
case "$p" in *://*) p="/${p#*://}"; p="/${p#*/}" ;; esac
|
|
333
|
+
p="${p%%\?*}"; p="${p%%#*}"
|
|
334
|
+
case "$p" in /*) ;; *) p="/$p" ;; esac
|
|
335
|
+
p="$(printf '%s' "$p" | sed -E 's#/{2,}#/#g')"
|
|
336
|
+
[ "$p" = "/" ] || p="$(printf '%s' "$p" | sed -E 's#/+$##')"
|
|
337
|
+
case "$p" in *[[:space:]]*|*'*'*) return 0 ;; esac
|
|
338
|
+
printf '%s' "$p"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
read_stripe_live_pages() { # $1 = wrangler.toml -> prints normalised paths, one per line
|
|
342
|
+
local f="${1:-}" hits line v part n
|
|
343
|
+
[ -f "$f" ] || return 0
|
|
344
|
+
hits="$(grep -nE '^[[:space:]]*STRIPE_LIVE_PAGES[[:space:]]*=' "$f" 2>/dev/null || true)"
|
|
345
|
+
[ -n "$hits" ] || return 0
|
|
346
|
+
while IFS= read -r line; do
|
|
347
|
+
[ -n "$line" ] || continue
|
|
348
|
+
v="${line#*=}"
|
|
349
|
+
v="$(printf '%s' "$v" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*#.*$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/')"
|
|
350
|
+
v="$(printf '%s' "$v" | tr ',' ' ')"
|
|
351
|
+
for part in $v; do
|
|
352
|
+
n="$(norm_page "$part")"
|
|
353
|
+
[ -n "$n" ] && printf '%s\n' "$n"
|
|
354
|
+
done
|
|
355
|
+
done <<HITS
|
|
356
|
+
$hits
|
|
357
|
+
HITS
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
page_is_live() { # $1 = toml, $2 = page path -> exit 0 when the page may charge a card
|
|
361
|
+
local want p
|
|
362
|
+
want="$(norm_page "${2:-}")"
|
|
363
|
+
[ -n "$want" ] || return 1
|
|
364
|
+
while IFS= read -r p; do
|
|
365
|
+
[ "$p" = "$want" ] && return 0
|
|
366
|
+
done <<PAGES
|
|
367
|
+
$(read_stripe_live_pages "$1")
|
|
368
|
+
PAGES
|
|
369
|
+
return 1
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
lint() { # $1 = copy file, $2 = wrangler.toml, $3 = page path (optional), $4 = label -> 0 clean, 1 forbidden, 3 missing
|
|
374
|
+
local copy="$1" toml="$2" page="${3:-}" label="${4:-}"
|
|
375
|
+
local failed=0
|
|
376
|
+
[ -n "$label" ] || label="${copy#"$ROOT"/}"
|
|
377
|
+
|
|
378
|
+
if [ ! -f "$copy" ]; then
|
|
379
|
+
printf 'ad-copy-lint: MISSING SUBJECT — %s does not exist.\n' "$copy" >&2
|
|
380
|
+
printf ' The proof has no subject. Deliverable 5 of text/bring-one-to-life.md\n' >&2
|
|
381
|
+
printf ' names this file as the copy that carries the launch claims.\n' >&2
|
|
382
|
+
return 3
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
# The subject is the SHIPPED COPY inside this file, extracted positively.
|
|
386
|
+
# Masking blanks every other line rather than deleting it, so a reported line
|
|
387
|
+
# number is the real one in the real file.
|
|
388
|
+
local work work_card
|
|
389
|
+
work="$(mktemp)"; work_card="$(mktemp)"
|
|
390
|
+
mask_subject "$copy" | strip_negatives > "$work"
|
|
391
|
+
strip_withdrawals < "$work" > "$work_card"
|
|
392
|
+
|
|
393
|
+
# Two ways a card claim is permitted, and only two: the whole worker is live,
|
|
394
|
+
# or the PAGE under lint is a member of STRIPE_LIVE_PAGES. With no --page there
|
|
395
|
+
# is no page to be a member of anything, so only the global switch can permit —
|
|
396
|
+
# a sentence that is not tied to a page ships on every page, including the ones
|
|
397
|
+
# still taking 4242 4242 4242 4242.
|
|
398
|
+
local mode card_armed scope pages
|
|
399
|
+
mode="$(read_stripe_mode "$toml")"
|
|
400
|
+
if [ "$mode" = "live" ]; then
|
|
401
|
+
card_armed=0; scope="STRIPE_MODE=$mode (whole worker)"
|
|
402
|
+
elif [ -n "$page" ] && page_is_live "$toml" "$page"; then
|
|
403
|
+
card_armed=0; scope="STRIPE_LIVE_PAGES lists $(norm_page "$page")"
|
|
404
|
+
elif [ -n "$page" ]; then
|
|
405
|
+
card_armed=1; scope="STRIPE_MODE=$mode, and $(norm_page "$page") is not in STRIPE_LIVE_PAGES"
|
|
406
|
+
else
|
|
407
|
+
card_armed=1; scope="STRIPE_MODE=$mode, no --page (the global question)"
|
|
408
|
+
fi
|
|
409
|
+
pages="$(read_stripe_live_pages "$toml" | tr '\n' ' ' | sed -E 's/[[:space:]]+$//')"
|
|
410
|
+
|
|
411
|
+
printf 'ad-copy-lint\n'
|
|
412
|
+
printf ' copy: %s\n' "$label"
|
|
413
|
+
printf ' config: %s\n' "${toml#"$ROOT"/}"
|
|
414
|
+
printf ' page: %s\n' "${page:-<none — global question>}"
|
|
415
|
+
printf ' live pages: %s\n' "${pages:-<none>}"
|
|
416
|
+
printf ' verdict: %s -> card/Apple Pay/Google Pay claims %s\n' \
|
|
417
|
+
"$scope" "$( [ "$card_armed" -eq 1 ] && printf 'FORBIDDEN' || printf 'permitted' )"
|
|
418
|
+
printf '\n'
|
|
419
|
+
|
|
420
|
+
local id re why hits l lineno text
|
|
421
|
+
while IFS= read -r rule; do
|
|
422
|
+
[ -n "$rule" ] || continue
|
|
423
|
+
id="$(printf '%s' "$rule" | sed -E 's/^([^ ]+) %% .*$/\1/')"
|
|
424
|
+
re="$(printf '%s' "$rule" | sed -E 's/^[^ ]+ %% (.*) %% .*$/\1/')"
|
|
425
|
+
why="$(printf '%s' "$rule" | sed -E 's/^[^ ]+ %% .* %% (.*)$/\1/')"
|
|
426
|
+
if [ "$id" = "card" ] && [ "$card_armed" -eq 0 ]; then continue; fi
|
|
427
|
+
# A withdrawal is not a claim: the card rules never see the exact sentences
|
|
428
|
+
# in withdrawals(). Every other rule reads the copy as written.
|
|
429
|
+
local src; src="$work"; [ "$id" = "card" ] && src="$work_card"
|
|
430
|
+
hits="$(grep -nEi "$re" "$src" 2>/dev/null || true)"
|
|
431
|
+
[ -n "$hits" ] || continue
|
|
432
|
+
while IFS= read -r l; do
|
|
433
|
+
[ -n "$l" ] || continue
|
|
434
|
+
lineno="${l%%:*}"
|
|
435
|
+
text="$(printf '%s' "${l#*:}" | sed -E 's/^[[:space:]]+//')"
|
|
436
|
+
printf 'FORBIDDEN [%s] %s:%s\n' "$id" "$label" "$lineno"
|
|
437
|
+
printf ' line: %s\n' "$text"
|
|
438
|
+
printf ' why: %s\n' "$why"
|
|
439
|
+
printf '\n'
|
|
440
|
+
failed=$((failed + 1))
|
|
441
|
+
done <<HITS
|
|
442
|
+
$hits
|
|
443
|
+
HITS
|
|
444
|
+
done <<RULESIN
|
|
445
|
+
$(rules)
|
|
446
|
+
RULESIN
|
|
447
|
+
|
|
448
|
+
if [ "$failed" -gt 0 ]; then
|
|
449
|
+
rm -f "$work" "$work_card"
|
|
450
|
+
printf 'RED — %d forbidden claim(s) in %s.\n' "$failed" "$label" >&2
|
|
451
|
+
printf 'Go green by changing the COPY, or (for the card rule) the CONFIG. Not this file.\n' >&2
|
|
452
|
+
return 1
|
|
453
|
+
fi
|
|
454
|
+
rm -f "$work" "$work_card"
|
|
455
|
+
printf 'ok — no forbidden sentence. %d rule(s) applied, %d skipped as config-permitted.\n' \
|
|
456
|
+
"$( [ "$card_armed" -eq 1 ] && rules | grep -c . || rules | grep -vc '^card ' )" \
|
|
457
|
+
"$( [ "$card_armed" -eq 1 ] && printf 0 || rules | grep -c '^card ' )"
|
|
458
|
+
return 0
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# ---------------------------------------------------------------------------
|
|
462
|
+
# lint_all — every default subject, worst status wins. A missing subject is
|
|
463
|
+
# exit 3 and stays exit 3: it outranks a clean pass, because a proof with no
|
|
464
|
+
# subject is what this whole script was rebuilt to stop reporting as green.
|
|
465
|
+
# ---------------------------------------------------------------------------
|
|
466
|
+
lint_all() { # $1 = toml, $2 = page (optional) -> 0 clean · 1 forbidden · 3 missing
|
|
467
|
+
local toml="$1" page="${2:-}" worst=0 f rc
|
|
468
|
+
while IFS= read -r f; do
|
|
469
|
+
[ -n "$f" ] || continue
|
|
470
|
+
lint "$f" "$toml" "$page"; rc=$?
|
|
471
|
+
printf '\n'
|
|
472
|
+
case "$rc" in
|
|
473
|
+
3) worst=3 ;;
|
|
474
|
+
1) [ "$worst" -eq 3 ] || worst=1 ;;
|
|
475
|
+
esac
|
|
476
|
+
done <<SUBJ
|
|
477
|
+
$(subjects)
|
|
478
|
+
SUBJ
|
|
479
|
+
return "$worst"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
# --live — the served page, which is the only readable form of the D1 row.
|
|
483
|
+
lint_live() { # $1 = toml, $2 = url, $3 = page (optional)
|
|
484
|
+
local toml="$1" url="$2" page="${3:-}" tmp rc
|
|
485
|
+
tmp="$(mktemp)"
|
|
486
|
+
if ! fetch_live "$url" > "$tmp" || [ ! -s "$tmp" ]; then
|
|
487
|
+
rm -f "$tmp"
|
|
488
|
+
printf 'ad-copy-lint: MISSING SUBJECT — could not read the served copy at %s.\n' "$url" >&2
|
|
489
|
+
printf ' Production /ad renders from the D1 pages row and nothing in this repo\n' >&2
|
|
490
|
+
printf ' is its source, so the served page is the only subject there is. An\n' >&2
|
|
491
|
+
printf ' unreachable subject is exit 3 — no subject, no proof. Never a pass.\n' >&2
|
|
492
|
+
return 3
|
|
493
|
+
fi
|
|
494
|
+
lint "$tmp" "$toml" "$page" "$url (served copy — line N is the Nth extracted string)"; rc=$?
|
|
495
|
+
rm -f "$tmp"
|
|
496
|
+
return "$rc"
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
# ---------------------------------------------------------------------------
|
|
500
|
+
# --self-test — the red proofs, run against fixtures so the real copy is never
|
|
501
|
+
# touched. A lint that has never gone red proves nothing.
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
self_test() {
|
|
504
|
+
local tmp bad=0
|
|
505
|
+
tmp="$(mktemp -d)"
|
|
506
|
+
# expanded NOW, not at exit — $tmp is function-local and gone by then
|
|
507
|
+
trap "rm -rf '$tmp'" EXIT
|
|
508
|
+
|
|
509
|
+
printf 'STRIPE_MODE = "test"\n' > "$tmp/test.toml"
|
|
510
|
+
printf 'STRIPE_MODE = "live"\n' > "$tmp/live.toml"
|
|
511
|
+
printf 'PLATFORM_SLUG = "one"\n' > "$tmp/none.toml"
|
|
512
|
+
# The shipped shape: global mode still test, exactly one page armed.
|
|
513
|
+
printf 'STRIPE_MODE = "test"\nSTRIPE_LIVE_PAGES = "/shop/pay-playbook, /Credits/"\n' > "$tmp/pages.toml"
|
|
514
|
+
printf 'STRIPE_MODE = "test"\nSTRIPE_LIVE_PAGES = "/shop/*"\n' > "$tmp/glob.toml"
|
|
515
|
+
printf 'STRIPE_MODE = "test"\nSTRIPE_LIVE_PAGES = ""\n' > "$tmp/empty.toml"
|
|
516
|
+
|
|
517
|
+
clean() { cat > "$tmp/copy.md" <<'CLEAN'
|
|
518
|
+
# Sovereign keys
|
|
519
|
+
Gas plus one percent of the transaction is deducted (pay/backend/src/agent.ts:220).
|
|
520
|
+
Four chains derive from one key: Sui, Ethereum, Solana, Bitcoin.
|
|
521
|
+
Twenty-four words bring the key back here, on one.ie.
|
|
522
|
+
CLEAN
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
expect() { # $1 = want-exit, $2 = label, $3 = toml, [$4 = needle] [$5 = page] [$6 = subject file]
|
|
526
|
+
local want="$1" label="$2" toml="$3" needle="${4:-}" page="${5:-}" subj="${6:-$tmp/copy.md}" out rc
|
|
527
|
+
out="$(lint "$subj" "$toml" "$page" 2>&1)"; rc=$?
|
|
528
|
+
if [ "$rc" -ne "$want" ]; then
|
|
529
|
+
printf ' FAIL %s — exit %d, wanted %d\n' "$label" "$rc" "$want"; bad=1; return
|
|
530
|
+
fi
|
|
531
|
+
if [ -n "$needle" ]; then
|
|
532
|
+
case "$out" in *"$needle"*) ;; *) printf ' FAIL %s — output never named [%s]\n' "$label" "$needle"; bad=1; return ;; esac
|
|
533
|
+
fi
|
|
534
|
+
printf ' ok %s (exit %d)\n' "$label" "$rc"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
printf '== self-test: the clean fixture is green\n'
|
|
538
|
+
clean; expect 0 'clean copy, STRIPE_MODE=test' "$tmp/test.toml"
|
|
539
|
+
|
|
540
|
+
printf '\n== self-test: each forbidden claim is bitten\n'
|
|
541
|
+
clean; printf 'Every transaction is free.\n' >> "$tmp/copy.md"; expect 1 'plant "transaction is free"' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
542
|
+
clean; printf 'Sending money is free, with no cost on transfer.\n' >> "$tmp/copy.md"; expect 1 'plant "no cost on transfer"' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
543
|
+
clean; printf 'Sell with no fees at all.\n' >> "$tmp/copy.md"; expect 1 'plant "no fees"' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
544
|
+
clean; printf 'Gas-free claims, every time.\n' >> "$tmp/copy.md"; expect 1 'plant "gas-free"' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
545
|
+
clean; printf 'Unlimited free credits while you test.\n' >> "$tmp/copy.md"; expect 1 'plant "unlimited free"' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
546
|
+
clean; printf 'Credits are free forever.\n' >> "$tmp/copy.md"; expect 1 'plant "free forever" @ credits' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
547
|
+
clean; printf 'It costs you nothing to mint.\n' >> "$tmp/copy.md"; expect 1 'plant "costs you nothing"' "$tmp/test.toml" 'FORBIDDEN [fee]'
|
|
548
|
+
clean; printf 'One key, all major blockchains.\n' >> "$tmp/copy.md"; expect 1 'plant "all major blockchains"' "$tmp/test.toml" 'FORBIDDEN [chains]'
|
|
549
|
+
clean; printf 'Restore it in any wallet you like.\n' >> "$tmp/copy.md"; expect 1 'plant "any wallet"' "$tmp/test.toml" 'FORBIDDEN [wallet]'
|
|
550
|
+
clean; printf 'Pay with a card in one tap.\n' >> "$tmp/copy.md"; expect 1 'plant "card" @ test' "$tmp/test.toml" 'FORBIDDEN [card]'
|
|
551
|
+
clean; printf 'Apple Pay and Google Pay both work.\n' >> "$tmp/copy.md"; expect 1 'plant "Apple Pay" @ test' "$tmp/test.toml" 'FORBIDDEN [card]'
|
|
552
|
+
|
|
553
|
+
printf '\n== self-test: TRUE claims are not refused (the rule the old one broke)\n'
|
|
554
|
+
# These sentences ship on production /ad right now and every one of them is
|
|
555
|
+
# measured true. A "free" rule that refuses them is not strict, it is wrong —
|
|
556
|
+
# pay/backend/src/agent.ts:221 prices the TRANSACTION, not the key.
|
|
557
|
+
clean; printf '{"feature":"Your key and your wallets","us":"Free","alt1":"Free"}\n' >> "$tmp/copy.md"
|
|
558
|
+
expect 0 'true: the keys and wallets are Free' "$tmp/test.toml"
|
|
559
|
+
clean; printf 'Free and sovereign forever.\n' >> "$tmp/copy.md"
|
|
560
|
+
expect 0 'true: "Free and sovereign forever"' "$tmp/test.toml"
|
|
561
|
+
clean; printf 'Taking a payment never costs a credit, at any balance.\n' >> "$tmp/copy.md"
|
|
562
|
+
expect 0 'true: taking a payment costs 0 credits' "$tmp/test.toml"
|
|
563
|
+
clean; printf 'Switch your shop on and crypto checkout costs you 0%%.\n' >> "$tmp/copy.md"
|
|
564
|
+
expect 0 'true: crypto checkout is 0%' "$tmp/test.toml"
|
|
565
|
+
|
|
566
|
+
printf '\n== self-test: a WITHDRAWAL may name the rail; a claim wearing one may not\n'
|
|
567
|
+
clean; printf 'Card checkout is not live yet. Crypto checkout is.\n' >> "$tmp/copy.md"
|
|
568
|
+
expect 0 'the withdrawal sentence -> GREEN' "$tmp/test.toml"
|
|
569
|
+
clean; printf '{"feature":"Card checkout","us":"Not yet available","alt1":"Not yet available","note":"Card checkout is not live yet. Crypto checkout is."}\n' >> "$tmp/copy.md"
|
|
570
|
+
expect 0 'the withdrawn pricing row -> GREEN' "$tmp/test.toml"
|
|
571
|
+
# The discriminating test. A negation is not a licence: smuggling a live claim
|
|
572
|
+
# alongside a withdrawal must still go red, or the allowlist is a heuristic.
|
|
573
|
+
clean; printf 'Cards are not live yet, but Apple Pay is.\n' >> "$tmp/copy.md"
|
|
574
|
+
expect 1 'a claim smuggled beside a withdrawal -> RED' "$tmp/test.toml" 'FORBIDDEN [card]'
|
|
575
|
+
clean; printf 'Card checkout is not live yet. Crypto checkout is. Visa works today.\n' >> "$tmp/copy.md"
|
|
576
|
+
expect 1 'a claim appended to the withdrawal -> RED' "$tmp/test.toml" 'FORBIDDEN [card]'
|
|
577
|
+
clean; printf 'Card checkout is instant.\n' >> "$tmp/copy.md"
|
|
578
|
+
expect 1 'the allowlisted words in a NEW sentence -> RED' "$tmp/test.toml" 'FORBIDDEN [card]'
|
|
579
|
+
|
|
580
|
+
printf '\n== self-test: the card rule is CONFIG-derived, both ways\n'
|
|
581
|
+
clean; printf 'Pay with a card in one tap.\n' >> "$tmp/copy.md"
|
|
582
|
+
expect 1 'card copy + STRIPE_MODE=test -> RED' "$tmp/test.toml" 'FORBIDDEN [card]'
|
|
583
|
+
expect 0 'card copy + STRIPE_MODE=live -> GREEN' "$tmp/live.toml"
|
|
584
|
+
expect 1 'card copy + no STRIPE_MODE -> RED (fails closed)' "$tmp/none.toml" 'FORBIDDEN [card]'
|
|
585
|
+
clean
|
|
586
|
+
expect 0 'clean copy + no STRIPE_MODE -> green (card rule armed, nothing to bite)' "$tmp/none.toml"
|
|
587
|
+
|
|
588
|
+
printf '\n== self-test: the card rule is PAGE-derived\n'
|
|
589
|
+
clean; printf 'Pay with a card in one tap.\n' >> "$tmp/copy.md"
|
|
590
|
+
expect 0 'card copy on a LISTED page -> GREEN' "$tmp/pages.toml" '' '/shop/pay-playbook'
|
|
591
|
+
expect 0 'card copy on a listed page, spelt /Credits -> GREEN' "$tmp/pages.toml" '' '/credits/?ref=x'
|
|
592
|
+
expect 1 'card copy on an UNLISTED page -> RED' "$tmp/pages.toml" 'FORBIDDEN [card]' '/pricing'
|
|
593
|
+
expect 1 'card copy on a near-miss of a listed page -> RED' "$tmp/pages.toml" 'FORBIDDEN [card]' '/shop/pay-playbook-2'
|
|
594
|
+
expect 1 'card copy, pages armed but NO --page -> RED' "$tmp/pages.toml" 'FORBIDDEN [card]'
|
|
595
|
+
expect 1 'a GLOB in the config opens nothing -> RED' "$tmp/glob.toml" 'FORBIDDEN [card]' '/shop/pay-playbook'
|
|
596
|
+
expect 1 'a GLOB in the question matches nothing -> RED' "$tmp/pages.toml" 'FORBIDDEN [card]' '/shop/*'
|
|
597
|
+
expect 1 'an EMPTY list means no page is live -> RED' "$tmp/empty.toml" 'FORBIDDEN [card]' '/shop/pay-playbook'
|
|
598
|
+
expect 1 'no STRIPE_LIVE_PAGES at all -> RED' "$tmp/none.toml" 'FORBIDDEN [card]' '/shop/pay-playbook'
|
|
599
|
+
clean
|
|
600
|
+
expect 0 'clean copy on a listed page -> green' "$tmp/pages.toml" '' '/shop/pay-playbook'
|
|
601
|
+
|
|
602
|
+
printf '\n== self-test: POSITIVE extraction — copy is linted, records are not\n'
|
|
603
|
+
# The whole reason the subject is extracted rather than grepped. §2/§3 record
|
|
604
|
+
# withdrawn claims and §8 is a list of sentences that must never ship; a sweep
|
|
605
|
+
# over the file refuses the very records it exists to preserve.
|
|
606
|
+
mk_admd() { # $1 = a line to put in the SHIPPED block, $2 = a line for §8
|
|
607
|
+
cat > "$tmp/ad.md" <<ADMD
|
|
608
|
+
## 3. The offer
|
|
609
|
+
| 6 | Card checkout at Stripe plus 2% | migrations/0109 | **NO — withdrawn, wrangler.toml:110 is test** |
|
|
610
|
+
|
|
611
|
+
## 4. The page, as shipped
|
|
612
|
+
\`\`\`json
|
|
613
|
+
{"type":"Pricing","props":{"rows":[
|
|
614
|
+
{"feature":"Crypto checkout","us":"0%"}${1:+,}
|
|
615
|
+
${1:-}
|
|
616
|
+
]}}
|
|
617
|
+
\`\`\`
|
|
618
|
+
|
|
619
|
+
Prose outside a fence that mentions a card and Stripe is not copy.
|
|
620
|
+
|
|
621
|
+
## 8. Forbidden sentences
|
|
622
|
+
> ${2:-"nothing"}
|
|
623
|
+
ADMD
|
|
624
|
+
}
|
|
625
|
+
mk_admd '' ''
|
|
626
|
+
expect 0 'records in §3 + prose + a bare §8 are not copy' "$tmp/test.toml" '' '' "$tmp/ad.md"
|
|
627
|
+
mk_admd '' '"Pay with any card, no fees, ever, on all major blockchains"'
|
|
628
|
+
expect 0 'a forbidden EXAMPLE in §8 is not a claim' "$tmp/test.toml" '' '' "$tmp/ad.md"
|
|
629
|
+
mk_admd '{"feature":"Card checkout","us":"Stripe + 2%"}' ''
|
|
630
|
+
expect 1 'the same claim INSIDE a copy block -> RED' "$tmp/test.toml" 'FORBIDDEN [card]' '' "$tmp/ad.md"
|
|
631
|
+
|
|
632
|
+
printf '\n== self-test: a missing subject is exit 3, never a pass\n'
|
|
633
|
+
rm -f "$tmp/copy.md"
|
|
634
|
+
local rc; lint "$tmp/copy.md" "$tmp/test.toml" >/dev/null 2>&1; rc=$?
|
|
635
|
+
if [ "$rc" -eq 3 ]; then printf ' ok missing subject (exit 3)\n'; else printf ' FAIL missing subject — exit %d, wanted 3\n' "$rc"; bad=1; fi
|
|
636
|
+
|
|
637
|
+
printf '\n'
|
|
638
|
+
if [ "$bad" -eq 0 ]; then printf 'self-test: all red proofs bite.\n'; return 0; fi
|
|
639
|
+
printf 'self-test: FAILED — the lint cannot go red where it must.\n' >&2
|
|
640
|
+
return 1
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
case "${1:-}" in
|
|
644
|
+
--self-test) self_test ;;
|
|
645
|
+
-h|--help) sed -n '2,90p' "$0"; exit 0 ;;
|
|
646
|
+
# The SERVED page. Opt-in, because do-promise-settle.sh runs the accept string
|
|
647
|
+
# verbatim and a promise must not settle BROKEN on a network hiccup.
|
|
648
|
+
--live) lint_live "$TOML_DEFAULT" "${2:-$LIVE_URL_DEFAULT}" ;;
|
|
649
|
+
# The PAGE question. Note the default (no argument) is untouched: do-promise-settle.sh
|
|
650
|
+
# runs the accept string verbatim, so the global question must keep answering with
|
|
651
|
+
# no flag, no env and no page.
|
|
652
|
+
--page) [ -n "${2:-}" ] || { printf 'ad-copy-lint: --page needs a path\n' >&2; exit 2; }
|
|
653
|
+
if [ -n "${3:-}" ]; then lint "$3" "$TOML_DEFAULT" "$2"; else lint_all "$TOML_DEFAULT" "$2"; fi ;;
|
|
654
|
+
"") lint_all "$TOML_DEFAULT" ;;
|
|
655
|
+
*) printf 'ad-copy-lint: unknown argument %s (--page <path> [copy] · --live [url] · --self-test · nothing)\n' "$1" >&2; exit 2 ;;
|
|
656
|
+
esac
|