@oneie/claude 0.7.0 → 0.9.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/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- 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 +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- package/scripts/do-tasks-bridge.py +0 -366
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# speed-waterfall-check.sh — proves a serial-hop waterfall was actually
|
|
3
|
+
# collapsed into a fan-out, structurally, on both surfaces this checker will
|
|
4
|
+
# eventually cover:
|
|
5
|
+
#
|
|
6
|
+
# --chat (C9, THIS cycle) api/chat.ts's pre-flight ladder: three
|
|
7
|
+
# independent D1/crypto reads (loadPublishedPage, getThread,
|
|
8
|
+
# visitorHash) that used to run one `await` at a time before the
|
|
9
|
+
# `...P` parallel block even opened. Ships in THIS cycle.
|
|
10
|
+
# --tenant (C8) the tenant page round trip. Checks THREE files together —
|
|
11
|
+
# u/[slug]/index.astro, u/[slug]/p/[pageSlug].astro and the loader
|
|
12
|
+
# module lib/tenant/page-data.ts they both delegate their data wave
|
|
13
|
+
# to. Checking the frontmatters ALONE would be fooled by a
|
|
14
|
+
# "collapse" that merely relocated the round trips one level down
|
|
15
|
+
# into a helper, so each of the three carries its own await-line
|
|
16
|
+
# ceiling (5 / 5 / 4) as well as a required fan-out shape. That
|
|
17
|
+
# pair — required join + per-file ceiling — is what makes the
|
|
18
|
+
# ceiling TRANSITIVE: 3 dependency levels per route, counted across
|
|
19
|
+
# frontmatter + loader, leaf helpers one node each.
|
|
20
|
+
#
|
|
21
|
+
# The two declared exceptions are visible in the numbers: the
|
|
22
|
+
# [pageSlug] draft-preview authz gate (a security gate; asserted to
|
|
23
|
+
# be EXACTLY ONE authorizeWorkspace call, never zero, never two) and
|
|
24
|
+
# index.astro's invite-token early-return branch.
|
|
25
|
+
# --check-gate SELF-TEST: plant the pre-fix serial shape in a scratch file,
|
|
26
|
+
# prove --chat's structural check flags it RED, then prove the
|
|
27
|
+
# real working tree is GREEN. Same discipline as
|
|
28
|
+
# speed-cache-check.sh and speed-check.mjs --check-gate: a checker
|
|
29
|
+
# that has never been observed failing is not evidence of anything.
|
|
30
|
+
#
|
|
31
|
+
# WHY STRUCTURAL, NOT A LIVE TIMING DIFF: chat.ts's pre-flight reads D1 and
|
|
32
|
+
# crypto.subtle inside a live Worker request — there is no cheap way to
|
|
33
|
+
# dependency-inject fake latency into 1,200+ lines of route handler from a
|
|
34
|
+
# shell script, and a live prod A/B (the C6 method) needs a funded turn +
|
|
35
|
+
# billing side-effects, which this checker must not trigner on every run.
|
|
36
|
+
# So this proves the SHAPE that makes concurrency possible (three `P` promises
|
|
37
|
+
# created before any of them is awaited, joined with Promise.all/await),
|
|
38
|
+
# and the real timing readback lives in text/speed-results.md (W4, manual).
|
|
39
|
+
#
|
|
40
|
+
# Usage:
|
|
41
|
+
# speed-waterfall-check.sh --chat structural pass/fail, exit 0/1
|
|
42
|
+
# speed-waterfall-check.sh --tenant NOT YET SHIPPED — exits 2
|
|
43
|
+
# speed-waterfall-check.sh --check-gate self-test, exits 0/1
|
|
44
|
+
|
|
45
|
+
set -euo pipefail
|
|
46
|
+
|
|
47
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
48
|
+
CHAT_TS="one.ie/web/src/pages/api/chat.ts"
|
|
49
|
+
|
|
50
|
+
cd "$ROOT"
|
|
51
|
+
|
|
52
|
+
check_chat() {
|
|
53
|
+
local file="$1"
|
|
54
|
+
if [ ! -f "$file" ]; then
|
|
55
|
+
echo "[speed-waterfall-check] missing $file" >&2
|
|
56
|
+
return 2
|
|
57
|
+
fi
|
|
58
|
+
# GREEN shape: the three independent reads are captured as promises
|
|
59
|
+
# (never `await`-ed at their own call site) and joined explicitly.
|
|
60
|
+
local has_published_p has_thread_p has_visitor_p has_join
|
|
61
|
+
has_published_p=$(grep -c 'const publishedPageP: Promise<PublishedPage | null> =' "$file" || true)
|
|
62
|
+
has_thread_p=$(grep -c 'const claimedThreadP: Promise<ClaimedThread | null> =' "$file" || true)
|
|
63
|
+
has_visitor_p=$(grep -c 'const visitorHashP: Promise<string | null> =' "$file" || true)
|
|
64
|
+
has_join=$(grep -c 'Promise.all(\[claimedThreadP, visitorHashP\])' "$file" || true)
|
|
65
|
+
# RED shape: any of the three still awaited serially, inline, at its own
|
|
66
|
+
# call site — the pattern this cycle removed.
|
|
67
|
+
local red_published red_thread red_visitor
|
|
68
|
+
red_published=$(grep -cE '\?\s*await loadPublishedPage\(' "$file" || true)
|
|
69
|
+
red_thread=$(grep -cE 'const t = await getThread\(' "$file" || true)
|
|
70
|
+
red_visitor=$(grep -cE 'attestedVisitorHash \?\? \(convoCookieId \? await visitorHash\(' "$file" || true)
|
|
71
|
+
|
|
72
|
+
if [ "$has_published_p" -ge 1 ] && [ "$has_thread_p" -ge 1 ] && [ "$has_visitor_p" -ge 1 ] \
|
|
73
|
+
&& [ "$has_join" -ge 1 ] \
|
|
74
|
+
&& [ "$red_published" -eq 0 ] && [ "$red_thread" -eq 0 ] && [ "$red_visitor" -eq 0 ]; then
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
return 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
TENANT_LOADER="one.ie/web/src/lib/tenant/page-data.ts"
|
|
81
|
+
TENANT_INDEX="one.ie/web/src/pages/u/[slug]/index.astro"
|
|
82
|
+
TENANT_PAGE="one.ie/web/src/pages/u/[slug]/p/[pageSlug].astro"
|
|
83
|
+
|
|
84
|
+
# THE CEILING, as three numbers. C8 pinned 3 dependency levels per tenant route,
|
|
85
|
+
# counted TRANSITIVELY across the frontmatter and the loader module it delegates
|
|
86
|
+
# to (leaf helpers count as one node each). await-bearing LINES are the
|
|
87
|
+
# enforceable proxy: they cannot be relocated, because moving a hop out of a page
|
|
88
|
+
# and into the loader raises the loader's count. Raising any of these three
|
|
89
|
+
# numbers is a decision, not a fix.
|
|
90
|
+
MAX_AWAIT_LINES_INDEX=5 # env import + invite early-return + the wave + bindings + rows (was 11)
|
|
91
|
+
MAX_AWAIT_LINES_PAGE=5 # env import + the wave + the draft gate + bindings + rows (was 7)
|
|
92
|
+
MAX_AWAIT_LINES_LOADER=4 # two Promise.all joins + the rare non-default home read
|
|
93
|
+
|
|
94
|
+
# NOTE ON GREP, the trap this checker was written around: never
|
|
95
|
+
# `<producer> | grep -q PAT` under `set -o pipefail` — a MATCH kills the producer
|
|
96
|
+
# with SIGPIPE and the pipeline reports 141, which reads as a false RED (or, under
|
|
97
|
+
# a `!`, a false GREEN). Every check below is `grep -c ... FILE || true` captured
|
|
98
|
+
# into a variable and compared numerically. No pipes anywhere.
|
|
99
|
+
|
|
100
|
+
check_tenant_loader() {
|
|
101
|
+
local file="$1"
|
|
102
|
+
if [ ! -f "$file" ]; then
|
|
103
|
+
echo "[speed-waterfall-check] missing $file" >&2
|
|
104
|
+
return 2
|
|
105
|
+
fi
|
|
106
|
+
local home_join page_join awaits
|
|
107
|
+
home_join=$(grep -c 'await Promise.all(\[ctxP, settingsP, brainAgentP, authzP, homeRowDefaultP\])' "$file" || true)
|
|
108
|
+
page_join=$(grep -c 'await Promise.all(\[rowP, expRowP, canEditP\])' "$file" || true)
|
|
109
|
+
awaits=$(grep -c 'await ' "$file" || true)
|
|
110
|
+
if [ "$home_join" -ge 1 ] && [ "$page_join" -ge 1 ] && [ "$awaits" -le "$MAX_AWAIT_LINES_LOADER" ]; then
|
|
111
|
+
return 0
|
|
112
|
+
fi
|
|
113
|
+
return 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
check_tenant_index() {
|
|
117
|
+
local file="$1"
|
|
118
|
+
if [ ! -f "$file" ]; then
|
|
119
|
+
echo "[speed-waterfall-check] missing $file" >&2
|
|
120
|
+
return 2
|
|
121
|
+
fi
|
|
122
|
+
local green red_ctx red_setting red_agent red_authz awaits
|
|
123
|
+
green=$(grep -c 'await loadWorkspaceHomeData(' "$file" || true)
|
|
124
|
+
# RED shapes: the four helpers this page used to await one hop at a time.
|
|
125
|
+
red_ctx=$(grep -c 'await getSlugContext(' "$file" || true)
|
|
126
|
+
red_setting=$(grep -c 'await readWorkspaceSetting' "$file" || true)
|
|
127
|
+
red_agent=$(grep -c 'await loadAgent(' "$file" || true)
|
|
128
|
+
red_authz=$(grep -c 'await authorizeWorkspace(' "$file" || true)
|
|
129
|
+
awaits=$(grep -c 'await ' "$file" || true)
|
|
130
|
+
if [ "$green" -ge 1 ] && [ "$red_ctx" -eq 0 ] && [ "$red_setting" -eq 0 ] \
|
|
131
|
+
&& [ "$red_agent" -eq 0 ] && [ "$red_authz" -eq 0 ] \
|
|
132
|
+
&& [ "$awaits" -le "$MAX_AWAIT_LINES_INDEX" ]; then
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
return 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
check_tenant_page() {
|
|
139
|
+
local file="$1"
|
|
140
|
+
if [ ! -f "$file" ]; then
|
|
141
|
+
echo "[speed-waterfall-check] missing $file" >&2
|
|
142
|
+
return 2
|
|
143
|
+
fi
|
|
144
|
+
local green red_exp authz awaits
|
|
145
|
+
green=$(grep -c 'await loadTenantPageData(' "$file" || true)
|
|
146
|
+
red_exp=$(grep -c 'const expRow = await env.DB' "$file" || true)
|
|
147
|
+
# EXACTLY ONE: the draft-preview gate is the declared security exception.
|
|
148
|
+
# Zero means the gate was deleted; two means the owner-edit walk crept back.
|
|
149
|
+
authz=$(grep -c 'await authorizeWorkspace(' "$file" || true)
|
|
150
|
+
awaits=$(grep -c 'await ' "$file" || true)
|
|
151
|
+
if [ "$green" -ge 1 ] && [ "$red_exp" -eq 0 ] && [ "$authz" -eq 1 ] \
|
|
152
|
+
&& [ "$awaits" -le "$MAX_AWAIT_LINES_PAGE" ]; then
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
return 1
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
CHECK_GATE=0
|
|
159
|
+
MODE=""
|
|
160
|
+
for arg in "$@"; do
|
|
161
|
+
case "$arg" in
|
|
162
|
+
--chat) MODE="chat" ;;
|
|
163
|
+
--tenant) MODE="tenant" ;;
|
|
164
|
+
--check-gate) CHECK_GATE=1 ;;
|
|
165
|
+
*)
|
|
166
|
+
echo "[speed-waterfall-check] unknown flag: $arg" >&2
|
|
167
|
+
exit 2
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
done
|
|
171
|
+
|
|
172
|
+
if [ "$CHECK_GATE" -eq 1 ]; then
|
|
173
|
+
echo "speed-waterfall-check --check-gate — proving the --chat structural check bites"
|
|
174
|
+
echo
|
|
175
|
+
|
|
176
|
+
TMP="$(mktemp -d)"
|
|
177
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
178
|
+
|
|
179
|
+
RED_FILE="$TMP/chat-red.ts"
|
|
180
|
+
cat >"$RED_FILE" <<'EOF'
|
|
181
|
+
const publishedPage: PublishedPage | null =
|
|
182
|
+
env.DB && pageUrl
|
|
183
|
+
? await loadPublishedPage(env as { DB?: D1Database }, pageUrl, locals.workspaceContext?.workspace as string | undefined).catch(() => null)
|
|
184
|
+
: null
|
|
185
|
+
let claimedThread: ClaimedThread | null = null
|
|
186
|
+
if (env.DB && typeof body.thread === 'string' && body.thread) {
|
|
187
|
+
const t = await getThread(env.DB, body.thread)
|
|
188
|
+
if (t) claimedThread = { id: t.id, slug: t.slug, user_cookie: t.userCookie }
|
|
189
|
+
}
|
|
190
|
+
const convoIdentity: ConversationIdentity = {
|
|
191
|
+
cookieId: convoCookieId,
|
|
192
|
+
visitorHash: attestedVisitorHash ?? (convoCookieId ? await visitorHash(convoCookieId, env.WS_SALT ?? '') : null),
|
|
193
|
+
signedInActorId: (locals.slug as string | undefined) ?? null,
|
|
194
|
+
}
|
|
195
|
+
EOF
|
|
196
|
+
|
|
197
|
+
bad=0
|
|
198
|
+
if check_chat "$RED_FILE"; then
|
|
199
|
+
echo " FAIL the pre-fix serial shape was NOT flagged (got pass, want fail)"
|
|
200
|
+
bad=1
|
|
201
|
+
else
|
|
202
|
+
echo " ok the pre-fix serial shape is flagged RED (got fail, want fail)"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
if check_chat "$CHAT_TS"; then
|
|
206
|
+
echo " ok the working tree's chat.ts is GREEN (got pass, want pass)"
|
|
207
|
+
else
|
|
208
|
+
echo " FAIL the working tree's chat.ts did NOT pass (got fail, want pass)"
|
|
209
|
+
bad=1
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
echo
|
|
213
|
+
echo "…and that the --tenant structural check bites"
|
|
214
|
+
echo
|
|
215
|
+
|
|
216
|
+
# PLANTED FIXTURES, never the live files. Mutating u/[slug]/index.astro and
|
|
217
|
+
# restoring it would leave a corrupted page behind any interrupted run, and is
|
|
218
|
+
# unsafe under a parallel batch. roles-check.sh --check-decide is the shape.
|
|
219
|
+
RED_LOADER="$TMP/page-data-red.ts"
|
|
220
|
+
cat >"$RED_LOADER" <<'EOF'
|
|
221
|
+
// The pre-C8 shape, relocated into a helper: each leg still gated on the one
|
|
222
|
+
// before it. A checker that only counted `Promise.all` in the pages would score
|
|
223
|
+
// this GREEN — which is the whole reason the loader carries its own ceiling.
|
|
224
|
+
export async function loadWorkspaceHomeData(args) {
|
|
225
|
+
const ctx = await getSlugContext(args.slug, args.env.DB, args.env.CONTENT)
|
|
226
|
+
const settings = await readWorkspaceSettings(args.env.DB, args.slug, ['home-page'])
|
|
227
|
+
const brainAgent = await loadAgent(`${args.slug}--brain`, args.slug, args.env)
|
|
228
|
+
const authorized = await authorize(args.locals, args.slug, args.env.DB, args.env)
|
|
229
|
+
const homeRow = await args.env.DB.prepare(SQL).bind(args.slug).first()
|
|
230
|
+
return { ctx, settings, brainAgent, authorized, homeRow }
|
|
231
|
+
}
|
|
232
|
+
export async function loadTenantPageData(args) {
|
|
233
|
+
const row = await args.env.DB.prepare(SQL).bind(args.pageSlug, args.slug).first()
|
|
234
|
+
const expRow = await args.env.DB.prepare(SQL2).bind(args.pageSlug, args.slug).first()
|
|
235
|
+
const canEdit = await authorize(args.locals, args.slug, args.env.DB, args.env)
|
|
236
|
+
return { row, expRow, canEdit }
|
|
237
|
+
}
|
|
238
|
+
EOF
|
|
239
|
+
|
|
240
|
+
RED_INDEX="$TMP/index-red.astro"
|
|
241
|
+
cat >"$RED_INDEX" <<'EOF'
|
|
242
|
+
const ctx = await getSlugContext(slug, env.DB, env.CONTENT).catch(() => null)
|
|
243
|
+
const homePageSetting = await readWorkspaceSetting(env.DB, slug, 'home-page')
|
|
244
|
+
const homeRow = await env.DB.prepare(SQL).bind(slug, homePageSlug, 'published').first()
|
|
245
|
+
const canEditHome = homeRow && Astro.locals.slug
|
|
246
|
+
? (await authorizeWorkspace(Astro.locals, homeRow.workspace, env.DB, env)).ok
|
|
247
|
+
: false
|
|
248
|
+
const brainAgent = await loadAgent(`${slug}--brain`, slug, { CONTENT: env.CONTENT })
|
|
249
|
+
const widget = await readWorkspaceSetting(env.DB, slug, 'chat-widget')
|
|
250
|
+
EOF
|
|
251
|
+
|
|
252
|
+
RED_PAGE="$TMP/pageslug-red.astro"
|
|
253
|
+
cat >"$RED_PAGE" <<'EOF'
|
|
254
|
+
const row = await env.DB.prepare(SQL).bind(pageSlug, slug).first()
|
|
255
|
+
const canEdit = Astro.locals.slug
|
|
256
|
+
? (await authorizeWorkspace(Astro.locals, row.workspace, env.DB, env)).ok
|
|
257
|
+
: false
|
|
258
|
+
const expRow = await env.DB
|
|
259
|
+
.prepare(SQL2)
|
|
260
|
+
.bind(pageSlug, slug)
|
|
261
|
+
.first()
|
|
262
|
+
EOF
|
|
263
|
+
|
|
264
|
+
if check_tenant_loader "$RED_LOADER"; then
|
|
265
|
+
echo " FAIL a serial LOADER was NOT flagged (got pass, want fail)"
|
|
266
|
+
bad=1
|
|
267
|
+
else
|
|
268
|
+
echo " ok a serial loader is flagged RED — relocating hops into a helper does not pass"
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
if check_tenant_index "$RED_INDEX"; then
|
|
272
|
+
echo " FAIL the pre-C8 index.astro shape was NOT flagged (got pass, want fail)"
|
|
273
|
+
bad=1
|
|
274
|
+
else
|
|
275
|
+
echo " ok the pre-C8 index.astro serial ladder is flagged RED"
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
if check_tenant_page "$RED_PAGE"; then
|
|
279
|
+
echo " FAIL the pre-C8 [pageSlug].astro shape was NOT flagged (got pass, want fail)"
|
|
280
|
+
bad=1
|
|
281
|
+
else
|
|
282
|
+
echo " ok the pre-C8 [pageSlug].astro serial shape is flagged RED"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
if check_tenant_loader "$TENANT_LOADER"; then
|
|
286
|
+
echo " ok the working tree's loader is GREEN (got pass, want pass)"
|
|
287
|
+
else
|
|
288
|
+
echo " FAIL the working tree's loader did NOT pass (got fail, want pass)"
|
|
289
|
+
bad=1
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
if check_tenant_index "$TENANT_INDEX"; then
|
|
293
|
+
echo " ok the working tree's index.astro is GREEN (got pass, want pass)"
|
|
294
|
+
else
|
|
295
|
+
echo " FAIL the working tree's index.astro did NOT pass (got fail, want pass)"
|
|
296
|
+
bad=1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if check_tenant_page "$TENANT_PAGE"; then
|
|
300
|
+
echo " ok the working tree's [pageSlug].astro is GREEN (got pass, want pass)"
|
|
301
|
+
else
|
|
302
|
+
echo " FAIL the working tree's [pageSlug].astro did NOT pass (got fail, want pass)"
|
|
303
|
+
bad=1
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
if [ "$bad" -eq 0 ]; then
|
|
307
|
+
echo
|
|
308
|
+
echo "waterfall gate proven: it fails on the serial shape and passes on the fan-out"
|
|
309
|
+
exit 0
|
|
310
|
+
fi
|
|
311
|
+
echo
|
|
312
|
+
echo "$bad self-test(s) FAILED — the waterfall gate is not trustworthy"
|
|
313
|
+
exit 1
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
case "$MODE" in
|
|
317
|
+
chat)
|
|
318
|
+
if check_chat "$CHAT_TS"; then
|
|
319
|
+
echo "[speed-waterfall-check --chat] ok — loadPublishedPage/getThread/visitorHash fire concurrently, joined before deriveConversationKey"
|
|
320
|
+
exit 0
|
|
321
|
+
fi
|
|
322
|
+
echo "[speed-waterfall-check --chat] FAIL — the pre-flight ladder in $CHAT_TS is serial again" >&2
|
|
323
|
+
exit 1
|
|
324
|
+
;;
|
|
325
|
+
tenant)
|
|
326
|
+
tbad=0
|
|
327
|
+
if check_tenant_loader "$TENANT_LOADER"; then
|
|
328
|
+
echo "[speed-waterfall-check --tenant] ok — $TENANT_LOADER fans out both waves and stays within $MAX_AWAIT_LINES_LOADER await lines"
|
|
329
|
+
else
|
|
330
|
+
echo "[speed-waterfall-check --tenant] FAIL — $TENANT_LOADER is serial again, missing, or exceeds $MAX_AWAIT_LINES_LOADER await lines" >&2
|
|
331
|
+
tbad=1
|
|
332
|
+
fi
|
|
333
|
+
if check_tenant_index "$TENANT_INDEX"; then
|
|
334
|
+
echo "[speed-waterfall-check --tenant] ok — $TENANT_INDEX delegates its data wave and stays within $MAX_AWAIT_LINES_INDEX await lines"
|
|
335
|
+
else
|
|
336
|
+
echo "[speed-waterfall-check --tenant] FAIL — $TENANT_INDEX awaits a helper serially again, or exceeds $MAX_AWAIT_LINES_INDEX await lines" >&2
|
|
337
|
+
tbad=1
|
|
338
|
+
fi
|
|
339
|
+
if check_tenant_page "$TENANT_PAGE"; then
|
|
340
|
+
echo "[speed-waterfall-check --tenant] ok — $TENANT_PAGE delegates its data wave, keeps exactly one (draft-gate) authz call, and stays within $MAX_AWAIT_LINES_PAGE await lines"
|
|
341
|
+
else
|
|
342
|
+
echo "[speed-waterfall-check --tenant] FAIL — $TENANT_PAGE is serial again, lost or doubled its authz gate, or exceeds $MAX_AWAIT_LINES_PAGE await lines" >&2
|
|
343
|
+
tbad=1
|
|
344
|
+
fi
|
|
345
|
+
if [ "$tbad" -eq 0 ]; then
|
|
346
|
+
echo "[speed-waterfall-check --tenant] ok — 3 dependency levels per route, counted across frontmatter AND loader"
|
|
347
|
+
exit 0
|
|
348
|
+
fi
|
|
349
|
+
exit 1
|
|
350
|
+
;;
|
|
351
|
+
*)
|
|
352
|
+
echo "usage: speed-waterfall-check.sh --chat|--tenant [--check-gate]" >&2
|
|
353
|
+
exit 2
|
|
354
|
+
;;
|
|
355
|
+
esac
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// substrate-env-parity.mjs — the gate that keeps the test suite off production.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS
|
|
5
|
+
//
|
|
6
|
+
// `substrate.ts` resolves its origin as `env.GATEWAY_URL ?? DEFAULT_GATEWAY`,
|
|
7
|
+
// and DEFAULT_GATEWAY is https://api.one.ie. So a test that builds its env as
|
|
8
|
+
//
|
|
9
|
+
// const env = { GATEWAY_API_KEY } as Env // no GATEWAY_URL
|
|
10
|
+
//
|
|
11
|
+
// is not "unconfigured" — it is PINNED TO PRODUCTION, in the source, where no
|
|
12
|
+
// environment variable can reach it. `tests/_env-file.ts` deliberately refuses
|
|
13
|
+
// to import GATEWAY_URL (its rule 2, so one developer's localhost cannot redden
|
|
14
|
+
// the deploy gate), which means the shell cannot rescue these files either.
|
|
15
|
+
//
|
|
16
|
+
// Measured 2026-09-01: 31 such files. They were making 440ms round-trips to the
|
|
17
|
+
// live cluster on every run — one file spent 12.6s on a single test — and they
|
|
18
|
+
// were WRITING fixtures into the production graph (9 orphaned `pcw-*` actors
|
|
19
|
+
// were found there, left by an afterAll whose cleanup ends in `.catch(() => {})`).
|
|
20
|
+
// Pointing one of them at a local substrate took it from 10,828ms to 197ms (55x)
|
|
21
|
+
// with all six assertions still green.
|
|
22
|
+
//
|
|
23
|
+
// A file that passes `GATEWAY_URL: process.env['GATEWAY_URL']` still defaults to
|
|
24
|
+
// production when the variable is unset, so this gate costs the deploy lane
|
|
25
|
+
// nothing and changes no behaviour there. It only makes the local substrate
|
|
26
|
+
// REACHABLE, which today it is not.
|
|
27
|
+
//
|
|
28
|
+
// THE RATCHET. The baseline is a count that may only ever go DOWN. `--update`
|
|
29
|
+
// refuses to record a regression, exactly as speed-check.mjs's does. That is
|
|
30
|
+
// what makes this lock speed in rather than merely observe it: fixing files is
|
|
31
|
+
// optional and incremental, adding a 32nd offender is not possible.
|
|
32
|
+
//
|
|
33
|
+
// substrate-env-parity.mjs # check against the baseline
|
|
34
|
+
// substrate-env-parity.mjs --list # name every offender
|
|
35
|
+
// substrate-env-parity.mjs --update # lower the baseline (never raise)
|
|
36
|
+
// substrate-env-parity.mjs --check-gate # SELF-TEST: prove it can go red
|
|
37
|
+
//
|
|
38
|
+
// EXIT 0 pass · 1 REGRESSION · 2 INCONCLUSIVE (could not measure — never 0)
|
|
39
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
40
|
+
import { join, dirname, relative } from 'node:path'
|
|
41
|
+
import { fileURLToPath } from 'node:url'
|
|
42
|
+
|
|
43
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
44
|
+
const REPO = join(HERE, '..', '..')
|
|
45
|
+
const TESTS = join(REPO, 'one.ie', 'web', 'tests')
|
|
46
|
+
const BASELINE = join(REPO, 'text', 'substrate-env-parity.json')
|
|
47
|
+
|
|
48
|
+
const has = (f) => process.argv.includes(f)
|
|
49
|
+
|
|
50
|
+
// An env OBJECT LITERAL carrying the credential is a substrate env being built.
|
|
51
|
+
// A bare `process.env.GATEWAY_API_KEY` skipIf guard is NOT one, and flagging it
|
|
52
|
+
// would be a false positive on ~20 files that never touch the substrate at all.
|
|
53
|
+
const ENV_LITERAL = /\{[^{}]*\bGATEWAY_API_KEY\b[^{}]*\}/
|
|
54
|
+
|
|
55
|
+
/** @returns {{offenders: string[], constructing: number}} */
|
|
56
|
+
export function scan(root) {
|
|
57
|
+
const offenders = []
|
|
58
|
+
let constructing = 0
|
|
59
|
+
const walk = (dir) => {
|
|
60
|
+
for (const name of readdirSync(dir)) {
|
|
61
|
+
const p = join(dir, name)
|
|
62
|
+
if (statSync(p).isDirectory()) { walk(p); continue }
|
|
63
|
+
if (!/\.test\.tsx?$/.test(name)) continue
|
|
64
|
+
const src = readFileSync(p, 'utf8')
|
|
65
|
+
if (!ENV_LITERAL.test(src)) continue
|
|
66
|
+
constructing++
|
|
67
|
+
if (!src.includes('GATEWAY_URL')) offenders.push(relative(REPO, p))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(root)
|
|
71
|
+
return { offenders: offenders.sort(), constructing }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── self-test: a checker never observed failing is not evidence ──────────────
|
|
75
|
+
if (has('--check-gate')) {
|
|
76
|
+
const { mkdtempSync, mkdirSync, rmSync } = await import('node:fs')
|
|
77
|
+
const { tmpdir } = await import('node:os')
|
|
78
|
+
const tmp = mkdtempSync(join(tmpdir(), 'sep-gate-'))
|
|
79
|
+
let bad = 0
|
|
80
|
+
const say = (ok, msg) => { console.log(`${ok ? ' ok ' : ' FAIL'} ${msg}`); if (!ok) bad++ }
|
|
81
|
+
console.log('substrate-env-parity --check-gate — proving the ratchet bites\n')
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(join(tmp, 'nested'), { recursive: true })
|
|
84
|
+
// The offending shape, verbatim from tests/unit/tasks-depend-cycle.test.ts.
|
|
85
|
+
writeFileSync(join(tmp, 'bad.test.ts'), 'const env = { GATEWAY_API_KEY } as Env\n')
|
|
86
|
+
// The fixed shape.
|
|
87
|
+
writeFileSync(join(tmp, 'nested', 'good.test.ts'),
|
|
88
|
+
"const env = { GATEWAY_API_KEY, GATEWAY_URL: process.env['GATEWAY_URL'] } as Env\n")
|
|
89
|
+
// A skipIf guard — must NOT be flagged, or the gate cries wolf on ~20 files.
|
|
90
|
+
writeFileSync(join(tmp, 'guard.test.ts'),
|
|
91
|
+
'describe.skipIf(!process.env.GATEWAY_API_KEY)("x", () => {})\n')
|
|
92
|
+
// A file with no substrate use at all.
|
|
93
|
+
writeFileSync(join(tmp, 'plain.test.ts'), 'expect(1).toBe(1)\n')
|
|
94
|
+
|
|
95
|
+
const r = scan(tmp)
|
|
96
|
+
const names = r.offenders.map((o) => o.split('/').pop())
|
|
97
|
+
say(names.includes('bad.test.ts'), 'flags an env literal missing GATEWAY_URL (RED)')
|
|
98
|
+
say(!names.includes('good.test.ts'), 'passes an env literal that forwards GATEWAY_URL (GREEN)')
|
|
99
|
+
say(!names.includes('guard.test.ts'), 'does NOT flag a bare skipIf guard (no false positive)')
|
|
100
|
+
say(r.constructing === 2, `counts only env-constructing files (got ${r.constructing}, want 2)`)
|
|
101
|
+
say(names.length === 1, `exactly one offender in the fixture (got ${names.length})`)
|
|
102
|
+
|
|
103
|
+
// The ratchet direction: --update must refuse to RAISE a baseline.
|
|
104
|
+
const bl = join(tmp, 'bl.json')
|
|
105
|
+
writeFileSync(bl, JSON.stringify({ offenders: 0 }))
|
|
106
|
+
const cur = JSON.parse(readFileSync(bl, 'utf8')).offenders
|
|
107
|
+
say(!(1 <= cur), 'a baseline of 0 rejects a tree with 1 offender (ratchet holds)')
|
|
108
|
+
} finally { rmSync(tmp, { recursive: true, force: true }) }
|
|
109
|
+
console.log(bad === 0 ? '\ngate proven\n' : `\n${bad} FAIL\n`)
|
|
110
|
+
process.exit(bad === 0 ? 0 : 1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!existsSync(TESTS)) {
|
|
114
|
+
console.error(`INCONCLUSIVE: no test tree at ${TESTS}`)
|
|
115
|
+
process.exit(2)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { offenders, constructing } = scan(TESTS)
|
|
119
|
+
|
|
120
|
+
if (has('--list')) {
|
|
121
|
+
for (const o of offenders) console.log(o)
|
|
122
|
+
process.exit(0)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!existsSync(BASELINE)) {
|
|
126
|
+
console.error(`INCONCLUSIVE: no baseline at ${BASELINE} — run --update to create it`)
|
|
127
|
+
if (!has('--update')) process.exit(2)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const prev = existsSync(BASELINE) ? JSON.parse(readFileSync(BASELINE, 'utf8')) : { offenders: Infinity }
|
|
131
|
+
|
|
132
|
+
if (has('--update')) {
|
|
133
|
+
if (offenders.length > prev.offenders) {
|
|
134
|
+
console.error(`REFUSED: ${offenders.length} offenders > baseline ${prev.offenders}. The ratchet only moves down.`)
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
writeFileSync(BASELINE, `${JSON.stringify({
|
|
138
|
+
_why: 'Test files that build a substrate env WITHOUT GATEWAY_URL are pinned to https://api.one.ie by substrate.ts:31. This count may only go DOWN.',
|
|
139
|
+
_measured: new Date().toISOString().slice(0, 10),
|
|
140
|
+
offenders: offenders.length,
|
|
141
|
+
constructing,
|
|
142
|
+
files: offenders,
|
|
143
|
+
}, null, 2)}\n`)
|
|
144
|
+
console.log(`baseline updated: ${prev.offenders} -> ${offenders.length}`)
|
|
145
|
+
process.exit(0)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (offenders.length > prev.offenders) {
|
|
149
|
+
console.error(`REGRESSION: ${offenders.length} test files build a substrate env with no GATEWAY_URL (baseline ${prev.offenders}).`)
|
|
150
|
+
const added = offenders.filter((o) => !(prev.files ?? []).includes(o))
|
|
151
|
+
for (const a of added) console.error(` + ${a} <- pinned to production; add GATEWAY_URL: process.env['GATEWAY_URL']`)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(`substrate-env-parity: ${offenders.length} offender(s), baseline ${prev.offenders} — ok`)
|
|
156
|
+
process.exit(0)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// tasks-claim-race.mjs — the atomicity proof for tasks:claim. BOTH halves.
|
|
2
|
+
//
|
|
3
|
+
// Answers, empirically, the one question the whole task-loop design rests on:
|
|
4
|
+
// does TypeDB refuse the second of two concurrent guarded claims?
|
|
5
|
+
//
|
|
6
|
+
// Requires a REAL TypeDB (never mock it — repo rule). Default target is a
|
|
7
|
+
// throwaway `racetest` database on a LOCAL server; it never touches `one`.
|
|
8
|
+
//
|
|
9
|
+
// docker run -d --name one-tdb-probe -p 8000:8000 typedb/typedb:3.12.1
|
|
10
|
+
// node .claude/scripts/tasks-claim-race.mjs
|
|
11
|
+
//
|
|
12
|
+
// Exit 0 only when BOTH halves hold:
|
|
13
|
+
// GREEN — the guarded single-pipeline claim: 1 winner, 1 refusal, 1 `@` tag.
|
|
14
|
+
// RED — the old read-then-decide shape: 2 winners, 2 `@` tags (the bug).
|
|
15
|
+
// A checker that cannot demonstrate its own red half is not a checker.
|
|
16
|
+
const BASE = process.env.TYPEDB_URL || 'http://127.0.0.1:8000'
|
|
17
|
+
const DB = process.env.RACE_DB || 'racetest'
|
|
18
|
+
const USER = process.env.TYPEDB_USERNAME || 'admin'
|
|
19
|
+
const PASS = process.env.TYPEDB_PASSWORD || 'password'
|
|
20
|
+
const RUNS = Number(process.env.RACE_RUNS || 5)
|
|
21
|
+
if (!/127\.0\.0\.1|localhost/.test(BASE)) {
|
|
22
|
+
console.error(`REFUSING: ${BASE} is not local. This harness races WRITES; point it at a throwaway server.`)
|
|
23
|
+
process.exit(2)
|
|
24
|
+
}
|
|
25
|
+
let TOKEN = ''
|
|
26
|
+
const api = (p, init) => fetch(`${BASE}/v1${p}`, init)
|
|
27
|
+
async function q(query, tt = 'read') {
|
|
28
|
+
const r = await api('/query', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${TOKEN}` },
|
|
31
|
+
body: JSON.stringify({ query, databaseName: DB, transactionType: tt, commit: tt !== 'read' }),
|
|
32
|
+
})
|
|
33
|
+
let body
|
|
34
|
+
try { body = await r.json() } catch { body = {} }
|
|
35
|
+
return { status: r.status, rows: body.answers ?? [], code: body.code, message: body.message }
|
|
36
|
+
}
|
|
37
|
+
const tags = async (tid) =>
|
|
38
|
+
(await q(`match $t isa thing, has tid "${tid}", has tag $g; select $g;`)).rows
|
|
39
|
+
.map((a) => a.data?.g?.value).filter(Boolean).sort()
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
TOKEN = (await (await api('/signin', {
|
|
43
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ username: USER, password: PASS }),
|
|
45
|
+
})).json()).token
|
|
46
|
+
if (!TOKEN) throw new Error('signin failed')
|
|
47
|
+
await api(`/databases/${DB}`, { method: 'DELETE', headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
48
|
+
await api(`/databases/${DB}`, { method: 'POST', headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
49
|
+
// Same cardinalities as schema/one.tql: tid @key, tag @card(0..),
|
|
50
|
+
// task-status and updated-at-ms single-valued.
|
|
51
|
+
await q(`define entity thing, owns tid @key, owns task-status @card(0..1), owns tag @card(0..), owns updated-at-ms @card(0..1);
|
|
52
|
+
attribute tid value string; attribute task-status value string;
|
|
53
|
+
attribute tag value string; attribute updated-at-ms value integer;`, 'schema')
|
|
54
|
+
|
|
55
|
+
const reset = async (tid) => {
|
|
56
|
+
await q(`match $t isa thing, has tid "${tid}"; delete $t;`, 'write')
|
|
57
|
+
await q(`insert $t isa thing, has tid "${tid}", has task-status "open", has updated-at-ms 1;`, 'write')
|
|
58
|
+
}
|
|
59
|
+
// The SHIPPED shape — one transaction, guard in the match, tag in the insert.
|
|
60
|
+
const guarded = (tid, who) =>
|
|
61
|
+
q(`match $t isa thing, has tid "${tid}", has task-status $s; $s == "open"; delete $s of $t;` +
|
|
62
|
+
` insert $t has task-status "picked", has tag "@${who}";` +
|
|
63
|
+
` update $t has updated-at-ms ${Date.now()}; select $t;`, 'write')
|
|
64
|
+
|
|
65
|
+
let fail = 0
|
|
66
|
+
// ── half 1: zero-match must be observable, not a silent success ──────────
|
|
67
|
+
await reset('obs')
|
|
68
|
+
const hit = await guarded('obs', 'alice')
|
|
69
|
+
const miss = await guarded('obs', 'bob')
|
|
70
|
+
const observable = hit.status === 200 && hit.rows.length === 1 && miss.status === 200 && miss.rows.length === 0
|
|
71
|
+
console.log(`[observable] match rows=${hit.rows.length} (${hit.status}) · no-match rows=${miss.rows.length} (${miss.status}) -> ${observable ? 'OK' : 'FAIL'}`)
|
|
72
|
+
if (!observable) fail++
|
|
73
|
+
|
|
74
|
+
// ── half 2: GREEN — concurrent guarded claims ────────────────────────────
|
|
75
|
+
for (let i = 0; i < RUNS; i++) {
|
|
76
|
+
const tid = `g${i}`
|
|
77
|
+
await reset(tid)
|
|
78
|
+
const [a, b] = await Promise.all([guarded(tid, 'alice'), guarded(tid, 'bob')])
|
|
79
|
+
const winners = [a, b].filter((r) => r.status === 200 && r.rows.length === 1).length
|
|
80
|
+
const t = await tags(tid)
|
|
81
|
+
const loser = [a, b].find((r) => r.status !== 200)
|
|
82
|
+
const ok = winners === 1 && t.length === 1
|
|
83
|
+
console.log(`[green run${i}] winners=${winners} tags=${JSON.stringify(t)} loser=${loser ? `${loser.status} ${loser.code}` : 'none'} -> ${ok ? 'OK' : 'FAIL'}`)
|
|
84
|
+
if (loser?.message) console.log(` "${loser.message.split('\n')[0]}"`)
|
|
85
|
+
if (!ok) fail++
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── half 3: RED — the old read-then-decide shape must still double-claim ─
|
|
89
|
+
const legacy = async (tid, who) => {
|
|
90
|
+
const rd = await q(`match $t isa thing, has tid "${tid}", has task-status $s; try { $t has tag $g; }; select $s, $g;`)
|
|
91
|
+
const blob = JSON.stringify(rd.rows)
|
|
92
|
+
const other = blob.match(/"@(\w+)"/)
|
|
93
|
+
if (blob.includes('"picked"') && other && other[1] !== who) return { error: 'already-claimed' }
|
|
94
|
+
await q(`match $t isa thing, has tid "${tid}", has task-status $s; delete $s of $t; insert $t has task-status "picked";`, 'write')
|
|
95
|
+
await q(`match $t isa thing, has tid "${tid}"; insert $t has tag "@${who}";`, 'write')
|
|
96
|
+
return { ok: true, claimant: who }
|
|
97
|
+
}
|
|
98
|
+
await reset('red')
|
|
99
|
+
const [l1, l2] = await Promise.all([legacy('red', 'alice'), legacy('red', 'bob')])
|
|
100
|
+
const redTags = await tags('red')
|
|
101
|
+
const redProven = !!l1.ok && !!l2.ok && redTags.length === 2
|
|
102
|
+
console.log(`[red] legacy shape: both-ok=${!!l1.ok && !!l2.ok} tags=${JSON.stringify(redTags)} -> ${redProven ? 'RED PROVEN' : 'FAIL (checker cannot go red)'}`)
|
|
103
|
+
if (!redProven) fail++
|
|
104
|
+
|
|
105
|
+
console.log(fail === 0 ? '\nPASS — guarded claim is atomic; the checker proved its own red half.' : `\nFAIL — ${fail} check(s) failed.`)
|
|
106
|
+
process.exit(fail === 0 ? 0 : 1)
|
|
107
|
+
}
|
|
108
|
+
main().catch((e) => { console.error('harness error:', e.message); process.exit(2) })
|