Changelog
2024-05-06: Change the noun from "promise" to "commitment" 2024-05-06: Manifold market: manifold.markets/dreev/will-we-resurrect-commitsto-with-co 2023-02-17: Edit all references to "promises.to" since we dropped that domain 2023-02-17: Add this changelog and reconstruct some past entries 2021-04-03: Remove all date parsing from the spec 2020-12-16: Notes on killing date parsing 2020-12-15: Move it back here 2020-03-12: Move the spec to a GitHub wiki 2018-09-22: QS talk: blog.bmndr.co/commitstew 2018-08-29: (using the prototype pretty extensively around this time) 2018-01-06: Prototype officially functional 2017-11-07: Lots of work on this spec 2017-10-27: Building a prototype 2017-08-09: Original blog post with the seed of the idea: blog.bmndr.co/will
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?
It was really bugging me that my future-tense statements could sometimes be falsehoods. So I started building this system so that instead of making a potentially false statement about what I'll do, I can instead -- impeccably truthfully and more informatively -- always give an exact probability that I'll do the thing. My current reliability for doing what I say is ~94%.
Here's how it works in practice, assuming 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 commitment is created in the commits.to app and an entry is added to your calendar and ideally a datapoint is sent to Beeminder. The system lets you mark the commitment completed and keeps track of your reliability — the fraction of commitments you keep! — and shows it off to anyone who follows an alice.commits.to link.
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, hopefully, ninety-nine point something percent faith in. I started doing this manually on 2017 July 27, tracking my commitments 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 commitment by constructing a URL — URL as UI! — and you mark a commitment complete by surfing to that URL and checking a box or clicking a button. By counting up how many commitments 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’ve first deployed 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 commitment and have carte blanche on changing it in any way. We just store all the commitments 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.commits.to/do_a_thing_by_noon
jo.commits.to/do_a_thing_by_noon
shows a form with some of the commitment fields
(see “Marking Commitment Fulfilled”)jo.commits.to
you see Jo’s overall reliability score and a list of all her commitments,
sorted by urgency/recencyCreating 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 commitments. 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 commitment.
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 recipient 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 commitment. And by restricting the allowed URL format we are finding that rogue commitments 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.
First, if you want to skip to the bottom line, here are the characters you can and can’t use in a commitment URL:
WILL ALWAYS BE ALLOWED: a-z
A-Z
0-9
-
_
TENTATIVE YES BUT AVOID: :
+
!
$
~
*
@
NO: .
/
#
?
%
(all other characters)
And now the full story, with rationale for each character. The tricky part about this is that some special characters will cause confusion and even break things. So we need to educate users about exactly what characters are allowed. Our hope is that most special characters that a user may accidentally try to use will be rejected and they will be trained to avoid any special characters and not happen upon any that break things. Previously we intended to minimize that chance by rejecting all special characters except underscores and dashes. Currently we intend to allow a specific set of special characters that we know to not cause problems.
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 allow them, and what the considerations are.
Lower case ASCII letters (a-z) obviously.
Upper case ASCII letters (A-Z) — commits.to is fully case-sensitive so alice.commits.to/aBc and alice.commits.to/abc and alice.commits.to/ABC are all distinct commitments.
Digits (0-9). 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 the most common way to separate words.
Dashes (-
) are the other most common way to separate words.
Colons (:
).
There are currently a handful of existing commitments with colons — they’re useful for specifying times of day.
(Note: In practice it has turned out to never be necessary to specify an off-the-hour deadline, like 5:30pm as opposed to 5pm or 6pm.
Especially since you can fine-tune the deadline after the commitment is created.
So we’re avoiding colons but on the lookout for use cases.)
Plus signs (+
).
The argument against them is that they have special syntactic meaning in URLs: namely, they’re a shortcut for %20
— the percent-encoding of the space character.
This is confusing since we think of underscores as ersatz-spaces.
Exclamation marks (!
).
This is out of scope of the current spec but
you could imagine them having special meaning for indicating high priority or strict deadlines, like an all-or-nothing late penalty function.
Dollar signs ($
).
Maybe in the future they could have special meaning to indicate variables, like in Perl?
(Again, unclear in the current spec what that even would mean.)
Tildes (~
).
Traditionally tildes have been used in URLs to prefix usernames, kind of like @-signs are now in other contexts.
There’s also a Unix convention for tildes being shorthand for your home directory, whatever that might mean in a commits.to context.
Asterisks (*
).
I don’t know what special syntactic meaning they could have but they seem to be innocuous.
At signs (@
).
But we’re avoiding all these special characters until we decide for sure.
Dots (.
).
It’s especially 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 commitments for, have dots.
(Counterpoint: Dots and dashes go together like peas and carrots, and dashes are allowed.)
Slashes (/
).
There are currently hundreds of existing commitments with slashes because our original implementation assumed due dates in the URL would be prefixed with /by/
.
But slashes 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.
A related problem is that it’s not obvious that URLs like alice.commits.to/reply_to_bob/by/5pm
and alice.commits.to/reply_to_bob/by/4pm
are treated as entirely distinct.
Change those to alice.commits.to/reply_to_bob_by_5pm
and alice.commits.to/reply_to_bob_by_4pm
and it’s more clear that the system won’t try to do any magic to treat those as referring to the same underlying commitment.
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 commitment URL, we silently fail:
The first occurrence of #
and everything after it will be dropped from the URL the server sees.
Question marks (?
).
No commitment 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.
(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.)
Percents (%
).
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 commitment URL.
Everything else. Ampersands, parentheses, braces, equal signs, carets, quotes, 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.
Slashes are the only controversial case above, due to the 232 non-voided legacy URLs that currently use them. We currently have a few candidates for what to do about them.
The longer we go without using slashes in new commitments the more reasonable it may be to just let the ancient slashy URLs break. We’d just convert the existing URLs in the database using the following deslashing algorithm:
Find the first dash or underscore in the urtext and call it sep
.
I.e., sep==='-'
or sep==='_'
(default: underscore).
(And one dumb special case: if the urtext starts with schedule-k_tax_thing
then set sep
to be an underscore.)
Now simply globally replace slashes in the urtext with sep
.
This option would be more palatable if we could add manual custom redirects for certain legacy slashy URLs on a case-by-case basis.
If a URL contains a slash, it will 404, but also in that case it will display a huge “Did You Mean” with a link to the deslashed version of the URL. See Option 0 for the deslashing algorithm.
The migration plan is then as follows:
Only match up to the first slash. (This is how we originally spec’d the app.) We’d still parse the urtext after the first slash for setting the default due date upon commitment creation, it would just be optional advisory-only info, kind of like how query strings are often used to track referrers and such.
For example,
bob.commits.to/file_report/by_9pm
and
bob.commits.to/file_report/by_tuesday
and in fact
bob.commits.to/file_report/just_kidding_dont
would all get canonicalized/redirected to bob.commits.to/file_report
.
That way when bots or humans URL-hack a URL with slashes — going up a directory, so to speak — it doesn’t matter. Everything after the first slash is ignored (or used strictly to parse the default due date).
The advantage of this option is having unambiguous syntax for due dates.
This would normally be very in line with my design sense.
But we’re not willing to bite the bullet on fully unambiguous syntax, like bob.commits.to/foo/by/2018-05-01_17:00
or something.
We want to be able to say crazy human things like “in 2 hours” or “by tomorrow”.
And that’s fine as long as it’s clear to the user that the system is making a best guess that the user can then adjust.
There are also the following disadvantages of the slash blindness option:
But another advantage of slash blindness is easy migration: It would consist of manually dealing with the handful of collisions. I believe all such collisions currently can be resolved by deleting a rogue/voided commitment.
Note on URL format: We’re waiting for the implementation and this section of the spec to match each other before accepting more beta users.
The fundamental object in the commits.to app is of course the commitment, aka the commitment. The following fields comprise a Commitment object:
urtext
: full original text (URL) the user typed to create the commitment, also the primary keyuser
: who’s making the commitment, parsed as the subdomain in the URLnote
: optional additional notes or context for the commitmenttini
: unixtime that the commitment was madetdue
: unixtime that the commitment is due, aka the deadlinetfin
: unixtime that the commitment was fulfilled firm
: true when the due date is confirmed and can’t be edited again )void
: true if the commitment became unfulfillable or mootclix
: number of clicks a commitment has gottenbmid
: the id of the Beeminder datapoint for this commitment )(The ones in the parentheses we can ignore for the MVP.)
For example:
urtext
= “bob.commits.to/Foo_the_bar_by_noon_tomorrow”user
= “bob”note
= “committed in slack discussion about such-and-such”tini
= [unixtime of first GET request of the commitment’s URL]tdue
= [what “noon tomorrow” parsed to at time tini]tfin
= [unixtime that the user marked the commitment as fulfilled]firm
= falsevoid
= falseclix
= 0bmid
= 4f9dd9fd86f22478d3007007Currently we have two domains — commits.to and promises.to — and the latter simply redirects to the former. (UPDATE: Never mind the promises.to thing — we dropped that one.) It’s possible that in the future we’ll want “bob.commits.to/foo” and “bob.promises.to/foo” to be distinct commitments. If not, we may want to canonicalize a commitment’s key to strip out the domain. We could also choose to treat URLs as case-insensitive and store a canonicalized lowercase version. We could even choose to treat “foo-bar” and “foo_bar” as aliases to the same underlying commitment. If we support username aliases, we could want aliases to canonicalize, e.g., “bobby.commits.to/foo” would redirect to “bob.commits.to/foo”.
But currently we’re assuming there will be no canonicalization of anything and all those questions are moot. In this spec we treat the original urtext as the primary key and refer to it interchangeably with the commitment’s URL as the unique identifier for a commitment.
Here are some other ideas for fields, that we can worry about as the project evolves:
tini
to for the actual date the commitment was madeAnyone hitting a commitment’s URL can edit anything any time. There are no logins or restrictions at all in the MVP. When you visit a commitment’s URL you see an HTML form based on the Commitment data structure”):
urtext
, i.e., the URL is not editableuser
is shown at the top of the page with a
reliability score — not editablenote
is editabletini
defaults to the time of the first GET request and is editabletdue
defaults to what was parsed from the URL and is editabletfin
defaults to blank and is editablevoid
defaults to false and is editableclix
is shown at the bottom of the page — not editableThe user
and URL fields aren’t editable because they uniquely identify the commitment and we don’t want to have to validate uniqueness when submitting the form.
See “For Later: Changing URLs”.
The number of clicks, clix
, is automatically incremented.
Initially the due date is determined by parsing the URL (see “Parsing Dates and Commitment 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”).
As a shortcut, instead of the user filling in tfin
, they can instead click a button to mark the commitment complete, which simply sets tfin
equal to the current timestamp.
If it were a checkbox instead of a button, then unchecking it would set tfin
back to null.
For later: Whenever anything about the commitment changes it should be automatically mirrored in Beeminder (see “For Later: Beeminder Integration”).
UPDATE: I now believe that all date-parsing should be dropped from the spec/MVP entirely. Create commitments with URLs that don’t mention a date and the deadline defaults to Right Now. That way it’s immediately overdue which prompts you to set an actual deadline. You’re always allowed to do that when there’s no deadline set yet. (And of course you’re perfectly free to let the URL mention a date, like if that helps disambiguate, but that’s just part of the slug; Commits.to doesn’t try to understand it.)
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: Commitments are keyed only on the URL so those are entirely distinct commitments.
(This is pretty obvious now but was less so when we were using URLs like alice.commits.to/send_the_report/by/thu
.)
Which also means that if she uses exactly the same URL both weeks, the second time it will still resolve to the original commitment, even if it’s marked completed.
Also pretty obvious in retrospect despite a ton of early hand-wringing.
In practice it seems to be easy to make an unlimited number of unique names for commitments 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 commitment pointing at the new one instead. But for the MVP, commitment URLs are just necessarily unique.
What about the _by_
part of the URL?
If the commitment 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 commitment 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 Commitments Fulfilled”).
A big part of commits.to is tracking how reliable you are. Namely, what fraction of the commitments you logged did you actually fulfill? And there’s a fun twist: if you fulfill a commitment 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:
Being strictly monotone means that you always see your reliability score visibly ticking down second by second whenever you have an overdue commitment.
Approaching but never reaching zero just means you’ll always get some epsilon of credit for fulfilling a commitment 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. That’s a slippery slope to never finishing ever. 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. Being within 60 seconds of the deadline is noticeably better than being 61 seconds late. 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.
In short, we’re taking advantage of the following Schelling Fences:
(What about the fact that “1 month late” is commonly understood to be the same day of the month the next month and “1 year later” typically means the same calendar date the next year, regardless of leap years? Too bad, the late penalty function is complicated enough without having it depend on things like the calendar month of the deadline! And no one cares anyway — just go by what the late penalty function tells you. We do use 30 days rather than 30.4 and 365 instead of 365.25 though, so the Schelling deadlines are the same time of day as the original deadline.)
The following fully implements the late penalty function and is fully tested.
/* The main function is credit(t) which computes the fraction of full credit
you get for being t seconds late. It's roughly a continuous version of this:
credit(t) = 1 if t<=0s # not late so no penalty
credit(t) = .99999 if t<60s # seconds late (essentially no penalty)
credit(t) = .999 if t<1h # minutes late (baaaasically counts)
credit(t) = .99 if t<1d # hours late (no big deal, almost fully counts)
credit(t) = .9 if t<1w # days late (main thing is it's done)
credit(t) = .5 if t<30d # weeks late (half counts if this late)
credit(t) = .1 if t<1y # months late (mostly doesn't count)
credit(t) = .01 if t<10y # years late (better late than never, barely)
credit(t) = 0 otherwise # decades late (essentially zero credit)
*/
const cSecs = 0.99999 // how much credit you get if you're seconds late
const cMins = 0.999 // minutes
const cHrs = 0.99 // hours
const cDays = 0.9 // days
const cWks = 0.5 // weeks
const cMos = 0.1 // months
const cYrs = 0.01 // how much credit you get if you're years late
// Hand-picked magic h-values to give the lateness function sudden drops at all
// the focal lateness thresholds (a minute late, an hour late, etc) while still
// being continuous and strictly monotonically decreasing.
const hSecs = 300000 // h param for how steep the curve is if seconds late
const hMins = 3000 // minutes
const hHrs = 548 // hours
const hDays = 32 // days
const hWks = 5.4 // weeks
const hMos = 4.2 // months
const hYrs = 4 // h param for how steep the curve is if years late
const SID = 86400 // seconds in a day ( NB: treat 30d as 1mo & 365d as )
const SIW = 7 * SID // seconds in a week ( 1yr so that Schelling fence )
const SIM = 30 * SID // seconds in a month ( deadlines are the same time of )
const SIY = 365 * SID // seconds in a year ( day as the original deadline )
const exp = Math.exp // let's not ugly up all our pretty math
const log = Math.log // by littering it with "Math." prefixes
const pow = Math.pow // (actually new javascript can do x**y for exponents)
// Linearly interpolate to return u when x=a and v when x=b
// This is equivalent to hscale with h=-1 and isn't used for commits.to but it's
// nice for comparison:
// function lscale(x, a, b, u, v) { return (b*u - a*v + (v-u)*x)/(b-a) }
// Exponentially interpolate to return u when x=a and v when x=b
// This is standard exponential growth and is the limiting case of h=0 in hscale
// below. That function isn't defined at h=0 so we need this as a special case.
// We could also just never set h to exactly 0 and use .000001 or something
// which would be plenty close enough but let's do it right cuz math is fun!
function escale(x, a, b, u, v) {
return u*pow(u/v, a/(b-a))*exp(log(v/u)/(b-a)*x)
}
// Do an h-interpolation to return u when x=a and v when x=b.
// Special cases: h=-1 is linear, h=0 is exponential, h=1 is hyperbolic.
// As h approaches infinity this becomes a step function where hscale(a) = u
// but for x>a, hscale(x) = v (assuming u>v).
// For the derivation of this, see bonus.glitch.me
function hscale(h, x, a, b, u, v) {
if (h === 0) { return escale(x, a, b, u, v) }
return u*pow(1-(pow(v/u, -h)-1)/(a-b)*(x-a), -1/h)
// i.e.: u*pow(1-h*r*(x-a), -1/h) where r = (pow(v/u, -h)-1)/h/(a-b)
}
// Compute the credit you get for being t seconds late
function credit(t) {
return t <= 0 ? 1 : // not late at all or early => full credit
t < 60 ? hscale(hSecs, t, 0, 60, 1, cSecs) :
t < 3600 ? hscale(hMins, t, 60, 3600, cSecs, cMins) :
t < SID ? hscale(hHrs, t, 3600, SID, cMins, cHrs) :
t < SIW ? hscale(hDays, t, SID, SIW, cHrs, cDays) :
t < SIM ? hscale(hWks, t, SIW, SIM, cDays, cWks) :
t < SIY ? hscale(hMos, t, SIM, SIY, cWks, cMos) :
hscale(hYrs, t, SIY, 10*SIY, cMos, cYrs)
}
We’ll care about the following statistics initially:
The relevant fields (see “Commitment Data Structure”) are:
tfin
— when the commitment was fulfilledtdue
— commitment’s deadlineAnd 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 commitment as a function of how late you fulfill it.
Here we optimistically assume that any commitment you’re late on you’re going to fulfill in the next instant.
For a specific commitment, displayed prominently at the commitment’s URL, we compute the optimistic late penalty (fraction of credit lost so far by being late) and max credit (1 minus the late penalty so far) as follows. First a handy function to compute the most possible credit (least late penalty) a commitment will get, expressed as a fraction in (0,1]:
// The most possible credit (least late penalty) a commitment c can have
function rosycredit(c) {
if (c.tdue === null) { return 1 }
const ot = (c.tfin === null ? now() : c.tfin) // optimistic tfin is now
return credit(ot - c.tdue)
}
And then the key numbers to show in the UI:
maxcred = rosycredit(c) // show as "#{maxcred*100}%" in the UI
latepen = 1 - rosycredit(c) // show as "#{latepen*100}%" in the UI
The late penalty and max credit will change in real time for a pending commitment that’s past its deadline, and will update instantly when tfin
changes.
For the overall reliability score for a user, we assume unfulfilled commitments that are still pre-deadline don’t count for or against you. We call those pending commitments, where there’s still time to get full credit:
// A commitment c is pending if it's pre-deadline and not marked done
function isPending(c) {
return (c.tdue === null || now() < c.tdue) && c.tfin === null
}
And we optimistically assume that any commitment you’re late on you’re going to fulfill in the next instant. So a brute force implementation would iterate through a user’s commitments like so:
let pending = 0
let numerator = 0
let denominator = 0
user.commitments.forEach(c => {
if (isPending(c)) { pending++ }
else {
numerator += rosycredit(c)
denominator += 1
}
})
That’s it!
Now you can report that the user has made
{denominator+pending
} commitments
(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. (We recommend React for having numbers like that always updated in real time.)
Taking inspiration from Beeminder:
We refer to the page shown at alice.commits.to as Alice’s gallery because it shows the list of all her commitments.
We sort them as follows.
All incomplete commitments (no tfin
set) sort to the top.
Among incomplete commitments we sort by urgency, to be defined momentarily.
Among complete commitments we sort by decreasing completion date, tfin
.
So you see the most urgent things first, and then completed commitments, starting with the most recently completed.
The obvious way to sort by urgency is simply amount of time till the deadline. For overdue commitments that’s a negative number so the most overdue commitment would sort to the very top.
But just for fun, and maybe because it’s useful, we’ll use a more sophisticated definition of urgency. Namely, we’ll sort by least absolute distance to the nearest Schelling fence. If nothing is overdue then it’s the same as the obvious definition of urgency, since the due date will in fact be the nearest Schelling fence. But when some things are overdue, the most urgent is the one where you’re losing reliability the fastest or are about to start doing so. For example, if you’re already a few days late on something then you’re probably treating 1 week overdue as the new deadline. If you have something else that just passed the 24-hour overdue mark, that’s more urgent.
Putting all that together, we can define a single metric using a function that takes a time t
and a commitment and returns:
tini
, tdue
, or tfin
are set.tini - t
(typically negative unless tini
is in the future) if there’s no tdue
or tfin
set. So commitments without deadlines sort to the top, oldest first.
(If it has no tdue
, no tfin
, and tini
is in the future then it could sort below incomplete commitments, which is fine, and almost always moot.)t
to the nearest Schelling fence for incomplete commitments (no tfin
but tini
and tdue
are set).
These are in the middle, most urgent first.9e9 - tfin
, if it’s completed.
So completed commitments sort to the buttom but subtracting tfin
means commitments completed later yield a smaller number and sort first among completed commitments.Then we just sort everything by that single metric, smallest to biggest!
This is important enough and easy enough to be part of even the initial MVP. Namely, for each commitment, 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/”. For the event details: the whole URL. And for both the start and end date of the calendar event: the commitment’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 had it redirecting to commits.to for a while. Marcin Borkowski had the idea for URLs-as-UI for creating commitments. 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 commitment 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.
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 user beemind their total number of successes, where fractional successes count fractionally.
Specifically, the date on the Beeminder datapoint is the commitment’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 initially zero, and, when fulfilled, is 1 minus the late penalty.
As in the simple version, the datapoint’s comment should just contain the commitment’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 commitments per week. The way I (dreev) do this currently: I create a datapoint for each commitment (via IFTTT from Google Calendar) when I commit to 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 commitments like that may be cheating.)
The commits.to app’s interactions with Beeminder (via Beeminder API calls) are as follows:
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 commitment’s page as an audit log.
For example:
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 you just didn’t specify a deadline in the URL. I’m all for being super opinionated about things like not letting you edit deadlines but the public append-only changelog idea seems most general and flexible. In the meantime we can voluntarily log changes in the note field.)
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.
Let’s declare 6% per year to be reasonable.
So set a constant
R = 0.06
as well as
SIY = 31557600
for seconds in a year because we’ll need the discount rate per second.
So instead of summing up the scores for the commitments and dividing by how many there are, we take a weighted average of the scores.
A score’s weight is exp(-R*a/SIY)
where a
is the age of that commitment.
(Note that if a commitment’s age is zero then its weight is 1, and it takes about 12 years for a commitment to lose half its weight.)
We can compute the age of a commitment like so:
// Return seconds elapsed since a commitment's most recent milestone, where
// milestones include the commitment being made, being due, & being fulfilled.
// If any of those are in the future, then the age is zero.
function age(c) {
const t = Math.max(c.tini, c.tdue, c.tfin)
return Math.max(0, now() - t)
}
So we just multiply the scores by their weights, sum them up, and divide by the sum of the weights. Easy peasy.
(Note that 6% per year may take a long time to be perceptible. We could also try 36% per year — the basis of Beeminder’s Exquisitely Fair Pre-Pay Discounts.)
This was part of the original spec but it seems to never be needed in practice so we’ve demoted it to this “for later” section to be revisited if there’s demand for it.
In the above spec, we assume a commitment is fully fulfilled if the tfin
date is non-null.
Partial fulfillment means generalizing that so that tfin
gives the date that the commitment was fractionally fulfilled, even if that fraction is 0%, and xfin
gives the actual fraction.
If xfin
is always 1 whenever tfin
is non-null and 0 otherwise, then we have the special case that is what’s spec’d above.
In other words, the tfin
field in the Commitment object is replaced with two fields:
tfin
: unixtime that the commitment was (fractionally) fulfilled (even if 0%)xfin
: fraction fulfilled, default null to indicate still pendingFor example, if the user deemed a commitment half fulfilled then they’d set xfin = 0.5
.
To handle this, we need the following generalizations:
In “Marking Commitments Fulfilled”, the shortcut where you click a button to mark a commitment fulfilled, in addition to setting tfin
to now, sets xfin
to 1.
If it’s a box you can check and uncheck then unchecking it sets xfin
back to null.
xfin
Also xfin
would be an editable field in the HTML form for a commitment, settable to anything from 0% to 100%, or null.
Some combinations of tfin
and xfin
don’t make sense so we’ll consider each possibility:
tfin
null, xfin
nullThe commitment is unfulfilled. This is the default state.
tfin
specified, xfin
nullThis combination doesn’t make sense. We won’t prohibit it but will show this on the page:
Error: Commitment fulfilled at [
tfin
] but needs fraction fulfilled!
We also won’t worry about tfin
possibly being in the future, although that’s also weird.
tfin
null, 0 <= xfin < 1
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 the isPending()
function in
“Computing Statistics”
will count the commitment 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”.)
tfin
null, xfin
1Another 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: Commitment marked done but needs completion date!
In “Computing Statistics” this is treated optimistically as if the commitment will be completed in the next instant.
In “Computing Statistics”, the max credit is xfin
minus the late penalty so far instead of just 1 minus the late penalty so far.
Specifically:
latepen = 1 - rosycredit(p) // show as "#{latepen*100}%" in the UI
maxcred = (xfin === null ? 1 : xfin) * (1 - latepen) // also show as percent
The above code, including the rosycredit()
function, is robust to all the crazy combinations of tfin
and xfin
discussed in #2 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 — “commitment is 50% complete as of the 31st” — though the user could manually treat it that way.
The idea is to treat the commitment 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 every mention of tfin
changing, this generalizes to “tfin
or xfin
changing”.
The isPending()
function changes to:
// A commitment c is pending if it's pre-deadline and not marked totally done
function isPending(c) {
return (c.tdue === null || now() < c.tdue) && c.xfin !== 1
}
The brute force algorithm for iterating through a user’s commitments to compute their overall score changes to:
let pending = 0
let numerator = 0
let denominator = 0
user.commitments.forEach(c => {
if (isPending(c)) { pending++ }
else {
numerator += (c.xfin === null ? 1 : c.xfin) * rosycredit(c)
denominator += 1
}
})
In “Beeminder Integration”,
the datapoint value is xfin
minus the late penalty instead of 1 minus the late penalty.
Even Later:
For the MVP we just want to use the descriptions in the URL as given.
At most we can apply a humanize()
function to them when displaying the commitment 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 “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 commitments 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 commits.to to capture each calendar entry. (I’m doing that now with IFTTT to send commitments to Beeminder.)
And maybe it’d be fine for every calendar entry to get automatically added. Some of them wouldn’t be commitments but that’s fine — you could just mark them as non-commitments or delete them and they wouldn’t count. If they were commitments 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 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 commitments to be public. So the prankster would see a page that says Alice commits 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 commitment to be inactive if its tfin
and tdue
dates are both non-null and in the past.
So even if a commitment 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 commitments differently.
I sometimes dash out a commitment 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. Ok, you shouldn’t ever just assume your recipient won’t click the link but maybe you explicitly gave the new, better one. Or maybe you only made the commitment 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 commit to send_the_report again. The most un-can-of-worms-y way to do that is to rename the old commitment via a convention 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 commitment page for “foo_by_soon” would look up and display links to any “archived:1:foo_by_soon”, “archived:2:foo_by_soon”, etc commitments at the bottom, saying “looking for one of these previous incarnations of this commitment?”. The UI could help too: maybe commitments 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 commitment will create a new commitment but with a pointer to the archived version.)
Whatever the reason, you sometimes want to change a commitment’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 think that will be worth implementing 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.
On every commitment page we can link to the 3 commitments with the most similar URLs. PostgreSQL has built in functions for this.
A possibly silly idea:
“promises.to” and “commits.to” 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 — like dreev.es/might, etc.
So maybe verb
would make sense as one of the commitment data structure fields in the future?