-
Notifications
You must be signed in to change notification settings - Fork 20
Home
Can you hear yourself casually saying to a coworker, "I'll see if I can reproduce that bug"? Or to a friend, "I'll let you know if I can make it to the show" or "I'll send you the photos"? And can you see yourself flaking out and failing to follow through on those things?
Here's our solution to that problem! (We'll assume your name is Alice and your coworker or friend is Bob.) Any time you make any "I will" statement, let's say "I'll send you my edits tomorrow", you type a URL like so:
alice.commits.to/send_bob_edits/by/tomorrow_5pm
As in, you literally type that, on the fly, directly to Bob, manually, when you're making the commitment to him. When you or Bob click that URL a promise is created in the commits.to app and a calendar entry is added to your calendar and ideally a datapoint is sent to Beeminder. The system lets you mark the promise completed and keeps track of your reliability — the fraction of promises you keep! — and shows it off to anyone who follows an alice.commits.to link.
(We have both the "commits.to" and "promises.to" domain names with the latter redirecting to the former currently.)
My goal with this project is to have a way to say I'll do something in a way that friends and colleagues can have 99.97% faith in. Which is my actual reliability as of writing this, from having done this manually since the summer of 2017. I've tracked my promises in a spreadsheet and on Beeminder. And I've gotten the public accountability aspect by blogging about this.
The system is ridiculously powerful and satisfying. It's even weirdly relaxing. When you get a commitment logged and on your calendar you yourself have faith that it will happen so you can put it out of your head in the meantime. I'm excited for this to be something anyone can use!
You create a promise by constructing a URL (URL as UI!) and you mark a promise complete by surfing to that URL and checking a box. By counting up how many promises were made and how many were marked completed (and applying a fancy late penalty function) we show a real-time reliability percentage for each user.
We'll first deploy something that works for ourselves as the simplest possible CRUD app. No logins, no user accounts, no security, nothing. Anyone can surf to the URL for any promise and have carte blanche on changing it in any way. We just store all the promises and show the reliability statistics based on them.
Here's a walk-through of what needs to happen for a generic example of Jo promising to do a thing by noon:
- Jo surfs to
jo.commits.to/do_a_thing/by/noon
(see "Creation on GET") - The system checks if a promise with that URL exists yet (see "Parsing Dates and Promise Uniqueness")
- If not, create it (see "Promise Data Structure")
- The page served up for
jo.commits.to/do_a_thing/by/noon
shows a form with some of the promise fields (see "Marking Promises Fulfilled") - It also shows a big countdown to the deadline and any late penalty if the deadline has passed (see "Late Penalties")
- In the header or corner of the page should be Jo's overall reliability across all her promises (see "Computing Statistics")
- Also on the page: a link to create a calendar entry (see "Calendar Integration")
- (We're eager to add Beeminder Integration but will wait on that till we have user logins)
- Nothing else special happens when a promise is marked fulfilled other than the reliability percentage updates, and maybe the color changes
- If you go to just
jo.commits.to
orjo.promises.to
you see Jo's overall reliability score and a list of all her promises, sorted by due date would be nice
Creating an object in a database on the server in response to a GET request is not considered kosher. (Webdevs, please suppress your derisive snorts!) And, yes, it has practical disadvantages like crawlers creating rogue promises. The obvious way to solve that would be to have the GET request generate a page with a button which makes a POST request to confirm creation of the promise.
But we're treating it as a core design principle to make all tradeoffs in favor of lower friction, and removing a confirmation click removes friction. In some chat clients, URLs are prefetched to show inline previews and in that case create-on-GET means no clicks at all. Also we've found that a typical promisee who clicks on a URL won't click a confirmation button. It feels presumptuous or something. Or the page looked too intimidating in our early prototypes.
In any case, we're running with create-on-GET. We really like how every yourname.commits.to URL you type gets almost automatically logged as a promise. And by restricting the allowed URL format we are finding that rogue promises from crawlers can be a non-issue. As for possible abuse as we scale up, that's a bridge we'll cross when we get to it.
From seeing non-nerd confusion with this so far, we're viewing every possible simplification to the URL rules to be net positive.
Originally we planned to turn any valid URL into a promise.
Whatever the user sees in the browser address bar
(minus the http://
or https://
),
that's what we'd capture as the urtext.
That would be ideally simple in one sense.
But it turns out to be hard to match what browsers do
(if they're even consistent with each other or themselves over time)
and chat clients aren't consistent in how they linkify typed URLs.
Here be dragons, cans of worms, rabbitholes, and minefields.
With "allow anything" off the table, the way to be as simple as possible is to have the shortest possible list of allowed characters. For usability, choice equals friction. In observing real-world usage with a non-technical user, we've witnessed uncertainty about the URL format prevent them from creating promises. A list of allowed characters that feels natural to nerds can feel arbitrary and bewildering to non-nerds. Even people who are fine with the interface of typing URLs to create promises. Typing the URL just feels more stressful when they can't predict if they're doing it right.
And there's a more pragmatic reason to severely restrict the allowed characters in a promise URL. Namely, there are several characters (discussed below) that cause problems and that we don't have a good way to reject. By rejecting as many other special characters as possible, we train users to stick to the basics and minimize the chances that they'll stumble on a URL with a character that will break things.
(The logical extreme of this is to limit URLs to just letters, numbers, and dashes, and nothing else.
E.g., bob.commits.to/do-the-thing-by-5-30pm
.
Or to be ridiculously extreme, bob.commits.to/DoTheThingBy530pm
.
Each additional special character we allow is more thinking required of a new user to type a URL.
But we're not targetting that extreme yet.)
Without further ado, here are the exact rules for the URL format.
First, usernames are easy. They must be all lowercase, start with a letter, and contain only ASCII letters and numbers. No hyphens, even though those are common in subdomains. And no dots, i.e., no sub-subdomains. And no other characters that might technically be allowed in domain names.
For the part of the URL after the domain name (ok, after the slash after the domain name — turns out that slash isn't necessarily obvious to non-technical users), the following is the full list of possible characters, whether we currently allow them, and what the considerations are.
-
Lower case ASCII letters (a-z) are obviously always allowed.
-
Upper case ASCII letters (A-Z) are allowed, and case is preserved so you can create alice.commits.to/aBc but all variants like alice.commits.to/abc and alice.commits.to/ABC will redirect to the original alice.commits.to/aBc.
-
Digits (0-9) are always allowed. And unlike for usernames, there's no restriction that the path start with a letter. It's even allowed to use nothing but numbers (please don't actually do that).
-
Underscores (
_
) are allowed. -
Dashes (
-
) are allowed. -
Slashes (
/
) are currently allowed but deprecated. (Please avoid!) There are currently hundreds of existing promises with slashes. Slashes seem to cause problems because people, and maybe bots, when they see something like bob.commits.to/foo/by/soon will sometimes try hitting bob.commits.to/foo/by and bob.commits.to/foo. I've seen that happen often when giving out commits.to links publicly. -
Colons (
:
) are currently allowed but we're debating this. There are currently a handful of existing promises with colons. They're useful enough for specifying times of day that it's hard to argue against them on mere principle, nor do they seem to cause any practical problems. Though ironically they're one of the characters you might least expect to be allowed in a URL since they have special meaning for specifying a custom port when appended to the domain portion of the URL. -
Hashes (
#
) ought to be disallowed but this is technically hard! Normally URLs can contain hashes and the browser shows them fine in the address bar, but they don't get passed to the server. So we don't have an easy way to reject them. There's a way to solve this — capturing anchor links — with client-side javascript and passing it to the server, which will be nice to implement later. Currently if you use a hash in a promise URL, we silently fail: The first occurrence of#
and everything after it will be dropped from the URL the server sees. -
Question marks (
?
) are currently allowed but definitely deprecated. No promise URLs so far have question marks but it's slightly tricky for a route to reject them since they're not considered part of the URL path. Anything after the first question mark is technically the query string. So for now we're voluntarily avoiding question marks in URLs. (Another important problem with question marks: since our route matching doesn't see anything after the first question mark, all other characters that would normally be rejected will sneak through if they come anywhere after a question mark.) -
Ats (
@
) are currently allowed because there's one existing promise that uses one but it would be nice to reject these. -
Dots (
.
) are not allowed (UPDATE: allowed but deprecated; please don't use!). It's handy to disallow these because everything like robots.txt and apple-touch-icon-blahblah.png and various things that bots check for, sometimes maliciously, that we don't want to create promises for, have dots. -
Percents (
%
) are not allowed. If you use unicode characters or some special characters like^
then browsers will percent-encode them, like replacing^
with%5E
. We could allow URLs with percent-encodings fine but if a user types a%
in a URL that doesn't correspond to a valid percent-encoding then, well, the server freaks out and we need careful error-handling code to recover. But we're making it moot and rejecting anything with a percent in it. Whatever other characters we decide to allow, we should reject percents just because of the disparity between what the user typed and what the browser turns it into. In other words, if the URL contains a percent then we don't know if the user typed that or if they typed a special character that got percent-encoded. So that's a bad choice for a promise URL. -
Nothing else is allowed. Ampersands, exclamation marks, dollar signs, parentheses, etc etc, are all rejected. If you try to hit a URL with any of these characters you get a 404 page that also explains these rules.
Technical Quibble About Percent-Encodings |
---|
The above about percent-encoding is not quite true. There are some special characters that modern browsers consistently don't percent-encode. For example, en.wikipedia.org/wiki/Möbius_strip . So in theory it could be nice to allow those characters in promise URLs. As long as what the user typed and what the browser displays in the address bar are the same, then that's what we'd want as the urtext. If the browser changes what was typed — like with bob.commits.to/foo^bar — then we'd want to reject it. But this is a can of worms so we're going to disallow all non-ASCII characters. Which we do by just rejecting percent symbols because all non-ASCII characters get percent-encoded before being requested from the server. |
Metaquibble |
---|
If you use a percent symbol in such a way that it happens to encode a normal alphanumeric character then browsers will helpfully undo the pointless percent-encoding. For example, if you type alice.commits.to/%41 in the address bar, what the server will get is alice.commits.to/A (because 41 is the code for "A"). So a promise will be created despite the user having typed a URL with a percent in it! There's really no solution to that other than to train users not to use special characters. |
In summary:
YES: a-z
A-Z
0-9
-
_
OKFINE: :
AVOID: /
.
@
?
NO: #
%
(all other characters)
Slashes are the only controversial case above, due to the hundreds of legacy URLs that currently use them. Here are our possible courses of action!
- [UNDERBUS]
Throw existing promises with slashes under the bus and just reject slashes.
All existing URLs get converted to their non-slashy equivalents, like
foo-by-5pm
instead offoo/by/5pm
. - [LEGACYALL] Same but also make legacy redirects for all existing promises with slashes, and then reject slashes.
- [LEGACYFEW] Combo of UNDERBUS and LEGACYALL. Reject slashes and create legacy redirects on a case by case basis for URLs that are actually out in the wild (a small fraction of them) and that turn out to be embarrassing to have be broken (if we're lucky not more than a handful, and presumably decreasing over time as we care less about more and more ancient links).
- [SLASHBLIND] Only match up to the first slash. (This is how we originally spec'd the app.) We'd still parse the stuff after the first slash for setting the default due date upon promise creation, it would just be optional advisory-only info, kind of like how query strings are often used to track referrers and stuff.
- [PREFIXY] More radical: Any new URL that is a prefix of an existing URL redirects to the existing URL. So if there were an existing URL alice.commits.to/find_category_for_TPS_report and then you tried to create alice.commits.to/find_cat then it wouldn't work. You'd just get the original "find_category..." promise. This sounds stupid, and might be, but WordPress does this and it's rare to even notice it and is sometimes handy. It's also a bit like how GitHub handles long commit hashes. And StackOverflow's URLs work kind of like that. In any case, I wouldn't like this idea except that it solves the slashes problem. Or maybe it will grow on me...
- [IFORONE] Least radical: Just accept that slashes are an allowed character in promise URLs and sometimes that will mean rogue promises will get created but we need robust solutions for rogue promises anyway. ("I for one welcome our new slash overlords." But just kidding, I just added this for completeness.)
Note that we can't accept new beta users till we decide this.
The fundamental object in the commits.to app is of course the promise, aka the commitment. The following fields comprise a Promise object:
-
urtext
: full original text (URL) the user typed to create the promise -
urlx
: primary key, the canonicalized URL, with the ".commits.to" removed -
user
: who's making the promise, parsed as the subdomain in the URL -
note
: optional additional notes or context for the promise -
tini
: unixtime that the promise was made -
tdue
: unixtime that the promise is due, aka the deadline -
tfin
: unixtime that the promise was (fractionally) fulfilled (even if 0%) -
xfin
: fraction fulfilled, default null to indicate still pending - (
firm
: true when the due date is confirmed and can't be edited again ) - (
void
: true if the promise became unfulfillable or moot ) -
clix
: number of clicks a promise has gotten - (
bmid
: the id of the Beeminder datapoint for this promise )
(The ones in the parentheses we can ignore for the MVP.)
For example:
-
urtext
= "bob.commits.to/Foo_the_bar/by/noon_tomorrow" -
urlx
= "bob/Foo_the_bar/by/noon_tomorrow" -
user
= "bob" -
note
= "promised in slack discussion about such-and-such" -
tini
= [unixtime of first GET request of the promise's URL] -
tdue
= [what "noon tomorrow" parsed to at time tini] -
tfin
= [unixtime that the user marked the promise as fulfilled] -
xfin
= 0.5 [imagine the user decided to deem it half fulfilled] -
firm
= false -
void
= false -
clix
= 0 -
bmid
= 4f9dd9fd86f22478d3000007
For the urlx
field, as long as there's only one domain (as there is currently, since promises.to just redirects all URLs to the commits.to equivalent) it doesn't matter whether we strip the ".commits.to" out of the URL.
Whether we'd want to do that in the case of multiple domains depends on whether we'd want to treat, e.g., "bob.commits.to/foo" and "bob.promises.to/foo" as distinct promises.
If there's never a need to strip the ".commits.to" then urlx
and urtext
would be identical and we wouldn't need two separate fields for them.
In this spec we'll mostly assume that's the case and refer simply to the URL as the unique identifier for a promise.
How the variants are stored is an implementation detail.
Here are some other ideas for fields, that we can worry about as the project evolves:
- Public changelog for justifying things like changes to the due date
- Whether the promise was created by the actual user (if they were logged in and were the first to click on it) or by another logged-in user or by someone not logged in
- Information about the client (browser, geoIP, etc) that originally created the promise
There are two ways to do this — basic and advanced.
In both cases the underlying fields are the same (see "Promise Data Structure"). And again, no logins or restrictions at all. Anyone hitting a promise's URL can edit anything any time.
(Exceptions: The user
and URL fields won't be editable because they uniquely identify the promise and we don't want to have to validate uniqueness when submitting the form.
See "For Later: Changing URLs".)
In both basic and advanced mode the user can edit the tdue
field to pick any date/time as the deadline.
Initially the due date is determined by parsing the URL
(see "Parsing Dates and Promise Uniqueness")
but the user has free rein to change it.
Yes, it defeats the point if you can keep changing the deadline but for the MVP, honor system!
We have ideas for later for how to further discourage cheating
(see "For Later: Public Changelog").
Besides editing the due date, all the user gets in basic mode is a checkbox to mark the promise complete.
Checking the box sets tfin
to now and xfin
to 1.
Unchecking the box sets tfin
and xfin
both back to null.
In advanced mode the user can edit all of the following fields:
-
tini
(initially the timestamp of the first GET request) tdue
tfin
xfin
In this case marking a promise (partially) fulfilled just means editing the xfin
field and the tfin
field to be non-null.
Some combinations of tfin
and xfin
don't make sense so we'll consider each possibility:
The promise is unfulfilled. This is the default state.
This combination doesn't make sense. We won't prohibit it but will show this on the page:
Error: Promise fulfilled at [
tfin
] but needs fraction fulfilled!We also won't worry about
tfin
possibly being in the future, although that's also weird.This is just the user treating
xfin
like a progress bar. "I haven't marked it done but I'm 75% of the way there!" If it's before the deadline then theisPending()
function in "Computing Statistics" will count the promise as pending, meaning it won't count for or against your reliability score. If it's after the deadline then we optimistically assume you'll complete it in the next instant and show your remaining credit accordingly. (Again, see "Computing Statistics".)Another combination that doesn't make sense. If you're 100% done then there must be a date that that happened. So show this on the page:
Warning: Promise marked done but needs completion date!
In "Computing Statistics" this is treated optimistically as if the promise will be completed in the next instant.
For later: Whenever anything about the promise changes it should be automatically mirrored in Beeminder (see "For Later: Beeminder Integration").
First, what should happen if alice says
alice.commits.to/send_the_report/by/thu
one week and then says
alice.commits.to/send_the_report/by/fri
the next week?
Answer: Promises are keyed only on the URL so those are entirely distinct promises. Which also means that if she uses exactly the same URL both weeks, the second time it will still resolve to the original promise, even if it's marked completed.
In practice it seems to be easy to make an unlimited number of unique names for promises and if there is a collision it's perfectly clear to the user why and what to do about it. Namely, make up a new URL! Later we can consider letting the user change the existing URL if they're ok with any links to the old promise pointing at the new one instead. But for the MVP, promise URLs are just necessarily unique.
What about the /by/
part of the URL?
If the promise is first being created then we run it through a date parser and initialize the due date to whatever it says.
If there's no /by/...
part or we couldn't parse it as a date/time, tdue
defaults to a week from now.
If the promise already exists then the /by/
part doesn't matter.
It will never override a tdue
that's already set.
In short, the /by/...
part of the URL is strictly advisory and can be changed by the user any time
(see "Marking Promises Fulfilled").
Implementation Note |
---|
Chris Butler found the Sherlock date parser which seems excellent. |
Implementation Note |
---|
The implementation of this is written and tested. |
A big part of commits.to is tracking how reliable you are. Namely, what fraction of the promises you logged did you actually fulfill? And there's a fun twist: if you fulfill a promise late you get partial credit. That way we can always compute a single metric for your reliability at any moment in time.
The function we're using for late penalties is below. The idea is to have your reliability decrease strictly monotonically the moment the deadline hits, with sudden drops when you're a minute, an hour, a day, etc, late. Here's a plot of that function — technically the fraction of credit remaining as a function of lateness — first zoomed in to the first 60some seconds, and then zoomed out further and further:
For example, credit(0)
is 1 (no penalty) and credit(3600)
is 0.999 (most of the credit for being just an hour late).
See "Computing Statistics" for how to actually use this in the app or read on for more on why we like this weirdo function.
There are a few key constraints on the shape of this function:
- Strict monotonicity
- Asymptotically approaches zero
- Sudden drops at a minute/hour/day/week/month/year late
Being strictly monotone means that you always see your reliability score visibly ticking down second by second whenever you have an overdue promise.
Approaching but never reaching zero just means you'll always get some epsilon of credit for fulfilling a promise no matter how late you are.
The third constraint is for beehavioral-economic reasons. We don't want you to feel like, once you've missed the deadline, that another hour or day or week won't matter. So the second-order discontinuities work like this: If you miss the nominal deadline your credit drops to 99.999% within seconds. The next sudden drop is at the 1-minute mark. After that you can still get 99.9% credit if you're less than an hour late. And if you miss that, you can still get 99% credit if you're less than a day late. At 24 hours the credit drops again to 90%, etc. A minute, an hour, a day, a week, a month, all the way up to the one-year anniversary of the deadline. If you hit that then you still get 10% credit. After that it drops pretty quickly to 1% and asymptotically approaches 0%, without ever reaching it.
We'll care about the following statistics initially:
- Each promise's late penalty (0% if not yet late)
- Each promise's max credit (100% if not yet late)
- Each user's total number of promises made
- Each user's total number of promises pending
- Each user's overall reliability score
The relevant fields (see "Promise Data Structure") are:
-
xfin
— a promise's fulfilled fraction, between 0 and 1, default 0 -
tfin
— when the promise was (fractionally) fulfilled -
tdue
— promise's deadline
And we'll assume we can get the current unixtime in seconds with a now()
function.
See "Late Penalties"
where we define the credit()
function for how much credit you get for a promise as a function of how late you fulfill it.
Here we optimistically assume that any promise you're late on you're going to fulfill in the next instant.
Implementation Note |
---|
In Javascript you get current unixtime in seconds with Date.now()/1000 (just Date.now() returns it in milliseconds). |
For a specific promise, displayed prominently at the promise's URL, we compute the optimistic late penalty (fraction of credit lost so far by being late) and max credit (xfin
minus the late penalty so far) as follows.
First a handy function to compute the most possible credit (least late penalty) a promise will get, expressed as a fraction in (0,1]:
// The most possible credit (least late penalty) a promise p can have
function rosycredit(p) {
if (p.tdue === null) { return 1 }
const ot = p.tfin === null ? now() : p.tfin // optimistic tfin is now
return credit(ot - p.tdue)
}
And then the key numbers to show in the UI:
latepen = 1 - rosycredit(p) // show as "#{latepen*100}%" in the UI
maxcred = (xfin === null ? 1 : xfin) * (1 - latepen) // also show as percent
The late penalty and max credit will change in real time for a pending promise that's past its deadline, and will update instantly when xfin
or tfin
change.
See "Marking Promises Fulfilled"
for handling the cases that xfin
is specified but not tfin
or vice versa, but the above code is robust to that and just always shows the most optimistic numbers.
(To be clear, if, say, xfin
is 50% and tfin
is 2017-10-31 that isn't meant like a progress meter — "promise is 50% complete as of the 31st" — though the user could manually treat it that way.
The idea is to treat the promise as being as done as it's going to get on Oct 31 and the credit you're getting is 50% of what you'd normally get.
No optimism about an xfin
of 50%, only an xfin
that's null.
So you multiply that 50% by whatever the credit function says based on how much after the due date Oct 31 is and that's your max credit.)
For the overall reliability score for a user, we assume unfulfilled promises that are still pre-deadline don't count for or against you. We call those pending promises, where there's still time to get full credit:
// A promise p is pending if it's pre-deadline and not marked totally done
function isPending(p) {
return (p.tdue === null || now() < p.tdue) && p.xfin !== 1
}
And we optimistically assume that any promise you're late on you're going to fulfill in the next instant. So a brute force implementation would iterate through a user's promises like so:
let pending = 0
let numerator = 0
let denominator = 0
user.promises.forEach(p => {
if (isPending(p)) { pending++ }
else {
numerator += (p.xfin === null ? 1 : p.xfin) * rosycredit(p)
denominator += 1
}
})
That's it!
Now you can report that the user has made
{denominator+pending
} promises
(of which {pending
} are still in the future)
and has a reliability of
{denominator === 0 ? 0 : numerator/denominator*100
}%.
The user's overall realtime reliability score should be shown prominently next to the username wherever it appears or huge in the header or something. It's the most important number in the whole app. Especially cool is how it will tick down in real time when one of your deadlines passes. (Dreev recommends React for having numbers like that always updated in real time.)
This is important enough and easy enough to be part of even the initial MVP. Namely, for each promise, create a link the user can click on to add it to their Google calendar. Like this:
Just view the html for that button here to see how that's constructed. (Source: StackOverflow answer.) For the event text, use the part of the URL after "commits.to/" and before "/by/", if present. For the the event details: the promise's URL. And for both the start and end date of the calendar event: the promise's deadline. No Calendar API is needed that way — just construct the link and if the user is logged in to Google it will create the calendar entry when they click it and confirm.
Daniel Reeves wrote a blog post about the idea. Sergii Kalinchuk got the "promises.to" domain and has it redirecting to commits.to. Marcin Borkowski had the idea for URLs-as-UI for creating promises. Chris Butler implemented most of the MVP.
This is the first thing we'd like to add after the MVP spec'd above! I think it would even make sense to say that the only way to log in to commits.to is via oauth with your Beeminder account.
The idea for the integration is to send a datapoint to Beeminder for each promise you make. A Beeminder datapoint consists of a date, a value, and a comment. Beeminder plots those cumulatively on a graph for you and lets you hard-commit to a certain rate of progress.
There are two ways we could do the integration. We'll first implement a simple way and then consider a more advanced way.
- Create a standard Do More goal for the user on Beeminder or ask the user for the goalname of an existing goal. The rate that the user is (meta) committing to should be 3 promises per week.
- Simply send a +1 to that Beeminder goal every time a new promise is created.
- The datapoint's comment should just have the promise's URL since that's a link to all the data about a promise.
The simple version of the integration just has the user committing to making some number of commits.to URLs per week, regardless of how many are fulfilled.
The advanced version has the beemind their total number of successes, where fractional successes count fractionally.
Specifically, the date on the Beeminder datapoint is the promise's completion date, if non-null, otherwise the deadline, tdue
(even though it's in the future).
And the value of the Beeminder datapoint is the fulfilled fraction, xfin
(initially zero).
As in the simple version, the datapoint's comment should just contain the promise's URL.
Or something like "Auto-added by commits.to at 12:34pm -- " and then the URL.
(It's nice to use the timezone the user has set in Beeminder — available in the User resource in the Beeminder API — when showing a time of day.)
The Beeminder goal should be a do-more goal to fulfill, say, 8 promises per week. The way I (dreev) do this currently: I create a datapoint for each promise (via IFTTT from Google Calendar) when I promise it, and then change the datapoint to a 1 when I fulfill it (or something less than 1 if I fulfill it late).
So Beeminder is not enforcing a success rate, just an absolute number of successes.
Pro tip: Promise a friend some things from your to-do list that you could do any time. That way you're always ready for an I-will beemergency. (But if your Personal Rule for commits.to is that only natural utterances of "I will" count as loggable commitments then making contrived promises like that may be cheating.)
The commits.to app's interactions with Beeminder (via Beeminder API calls) are as follows:
- When a promise is created, create a datapoint
- When a promise is marked (partially) fulfilled, update the datapoint's value
- When a promise's due date changes, update the datapoint's date
- [LATER] When a promise is deleted, delete the datapoint
- [LATER] When a promise is voided maybe also delete the datapoint in Beeminder
- [LATER] Create the initial Beeminder goal when a user signs up for commits.to
- Gray: completed promises
- Hot pink or showing flames or something cute: past due promises
- Red: deadline in less than 24 hours
- Orange: deadline in less than 48 hours
- Blue: deadline in less than 72 hours
- Green: deadline in more than 72 hours
I think this is the most elegant and flexible solution to prevent cheating. You can change anything at any time but you have to publicly justify each change and it's all permanently displayed on the promise's page.
For example:
- 2017-10-31: due date changed from 11/14 to 11/30 with comment "the original promise URL didn't specify a date and defaulted to a week out but the end of the month is what I had in mind"
Some people will do things like "giving myself an extra day because my cat got sick" which completely defeats the point of the whole system (even for entirely unimpeachable excuses it defeats the point, unless you explicitly make the deadline conditional in the first place) but by having to make those justifications publicly you can see when someone is doing that and discount their supposed reliability percentage accordingly. I mean, people can cheat and game this in a million ways anyway so no restrictions we try to impose will ever really solve this kind of problem.
(An alternative we were hashing out before was allowing you to edit the due date exactly once in case the system initially parsed it wrong or whatever. I think we'll be in a better position to make these kinds of design decisions after seeing more real-world usage. And I'm all for being super opinionated about things like not letting you edit deadlines.)
If you were late in the past but are always on time now, your past sins should fade over time. In other words, we should apply a discount rate to reliability scores. This is super straightforward and I (dreev) can add the code to do it when we're ready. Candidate rates include 6% per year (the interest rate the Beeminder founders lend money to each other at) and 36% per year (the basis of Beeminder's Exquisitely Fair Pre-Pay Discounts).
- Username, used as a subdomain for the URL
- Beeminder access token
- Timezone (needed to parse the deadlines; but less important since you can change the deadline if it's misparsed)
Even Later:
- Pronoun (default "they/them/their/theirs")
- Display name, e.g., "Alice" as opposed to username "alice"
I don't actually like these, just brainstorming:
- Magic spaces: Whichever non-alphanumeric character is most common in the URL, that's what's treated as a space
- Less magical version: A non-alphanumeric character must follow "alice.commits.to/" and that character is taken as the ersatz space. Eg: alice.commits.to/_start_her_promises_with_underscores
- Zero magic: Never tamper with the description as written in the URL at all.
For the MVP we definitely just want to use the descriptions in the URL as given.
At most we can apply a humanize()
function to them when displaying the promise on the page that could, for example, replace underscores with spaces.
Or try to be smart and turn "do-the-thing" into "do the thing" but also display the slug "do_things_1-3" as "do things 1-3" and not "do things 1 3".
It's a can of worms so for the MVP we should pick something very simple and only do it in the display logic.
This is totally at odds with the current spec but before we had the URLs-as-UI idea we thought you'd create promises by creating calendar entries and use the calendar API to automatically capture those.
There are various ways to add calendar entries with very low friction already. Then that would need to automatically trigger promises.to to capture each calendar entry. (I'm doing that now with IFTTT to send promises to Beeminder.)
And maybe it'd be fine for every calendar entry to get automatically added. Some of them wouldn't be promises but that's fine — you could just mark them as non-promises or delete them and they wouldn't count. If they were promises then you'd need to manually mark them as fulfilled or not. Beeminder (plus the embarrassment of having your reliability percentage drop when a deadline passes) would suffice to make sure you remember to do that.
Again, this is moot for now while we work on the URL-as-UI version.
Alice's friends can troll her by making up URLs like alice.commits.to/kick_a_puppy but that's not a huge concern. Alice, when logged in, could have to approve promises to be public. So the prankster would see a page that says Alice promises to kick a puppy but no one else would.
In the MVP we can skip the approval UI and worry about abuse like that the first time it's a problem, which I predict will be after commits.to is a million dollar company.
Define a promise to be inactive if its tfin
and tdue
dates are both non-null and in the past.
So even if a promise is done early it's still active till the due date, and even if it's overdue it's still active till it's done.
(Or "done" — it could be marked 0% fulfilled.)
We might want to display active and inactive promises differently.
I sometimes dash out a promise URL on my phone but later would prefer a better URL. Maybe I'm sure no one is going to click on the original one (I continue to be surprised how infrequently people click on these URLs, especially once the novelty wears off) and would like to just change it and let the original link break. Actually, you should never just assume your promisee won't click the link but maybe you explicitly gave the new, better one. Or maybe you only made the promise verbally and logged it directly via the address bar of your browser.
Or maybe alice.commits.to/send_the_report was completed and everyone knows it and now you want to promise to send_the_report again. The most un-can-of-worms-y way to do that is to rename the old promise via a convetion like alice.commits.to/send_the_report-old or alice.commits.to/archived/send_the_report and then just start over with alice.commits.to/send_the_report like usual.
(That could even be an Official Convention so that any promise page for "foo/by/soon" would look up and display links to any "archived/1/foo/by/soon", "archived/2/foo/by/soon", etc promises at the bottom, saying "looking for one of these previous incarnations of this promise?". The UI could help too: maybe promises have an archive button which replaces the path part of the URL "foo/by/soon" with "archived/1/foo/by/soon", or "archived/2/foo/by/soon" if there's already a "/1/", etc. So you hit archive and then any old links to the promise will create a new promise but with a pointer to the archived version.)
Whatever the reason, you sometimes want to change a promise's URL. We could just let you do that, showing in real time as you edit it whether the new URL is taken. If it's not taken then let the user hit submit.
I don't think that's too onerous to implement and will be worth it soon after the MVP. At least the renaming, if not the whole archiving/reusing convention.
Finally, some pie in the sky for later still: What if the user could somehow add 301-redirects willy-nilly? Then you could change URLs without breaking links.
- dreev.es/will/ (for anyone who has a domain for their own name)
- alice.promises.to/ (sergii grabbed this one)
- alice.commits.to/ (dreev grabbed this one)
- alice.willdefinite.ly/ (kinda awkward)
- alice.willveri.ly/ (too cutesy?)
- alice.willprobab.ly/ (emphasizes the reliability percentage)
- alice.willresolute.ly (maybe it would grow on me?)
A possibly silly idea:
we currently have "promises.to" and "commits.to" which are pretty synonomous but if we had other domains, that could maybe affect the reliability score.
Like "promising" is one thing but if it's alice.intends.to (not that we have that domain) then maybe it doesn't fully count against you if you don't actually do it.
Also if we made this work for people's personal domain names, like dreev.es/will, then we could have arbitrary verbs — dreev.es/might, etc.
So maybe verb
would make sense as one of the promise data structure fields in the future?
(This is also too half-baked to do anything with.)
- Parse incoming promises so all the fields are stored on GET
- Anything not parseable yields an error that the user sees when clicking on the URL
- Let you mark a promise fulfilled (or fractionally fulfilled, including 0%)
- No privacy or security features; everything is public
- Easy: construct a link the user can click to create the calendar event
- Realtime reliability score! (Code is done; just needs hooking up)
From the creators of Beeminder