Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion content/legal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ for a page with this name.
Every answer below is read out of the tree rather than written into the page, so
an answer that changes is a change to one value and not an edit to prose. Where
an answer has not been taken, the line says so and says what it is waiting on,
rather than leaving an empty space that reads as an answer of nothing.
rather than leaving an empty space that reads as an answer of nothing. Where an
answer has been taken and the value it needs has not arrived yet, the line names
the record that carries the decision and what the value is waiting on, because
a decided question and an open one are different things to tell a reader.

Publishing a site under a name of one's own carries a provider identification
duty in some jurisdictions. Which one applies turns on where the publisher sits
Expand Down
15 changes: 9 additions & 6 deletions data/publisher.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
{
"publisher": {
"asks": "Who publishes this site",
"state": "undecided",
"waiting": "entry 8 of issue 7"
"state": "decided",
"record": "decisions/0017-what-the-site-says-about-who-publishes-it.md",
"waiting": "the imprint-address service being booked, which happens outside this repository"
},
"contact": {
"asks": "How to reach whoever publishes it",
"state": "undecided",
"waiting": "entry 8 of issue 7"
"state": "decided",
"record": "decisions/0017-what-the-site-says-about-who-publishes-it.md",
"waiting": "the rotatable contact alias being set up, which happens outside this repository alongside the service booking"
},
"postal": {
"asks": "Whether a postal address is published",
"state": "undecided",
"waiting": "entry 8 of issue 7"
"state": "decided",
"record": "decisions/0017-what-the-site-says-about-who-publishes-it.md",
"waiting": "the imprint-address service being booked, whose address is the one that will be published"
}
}
2 changes: 1 addition & 1 deletion internal/site/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const (
)

// catalogue is what the tree says about the address an operator pastes into a
// server. State is one of the two the legal notice declares, Address is the
// server. State is one of the first two the legal notice declares, Address is the
// value an answered one carries, and Waiting is what an undecided one is
// waiting on. Exactly one of the last two carries anything.
type catalogue struct {
Expand Down
96 changes: 79 additions & 17 deletions internal/site/legal.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@
// That is the whole reason for the closed set below. A value nobody has decided
// and a value somebody forgot are the same empty string, and they render as the
// same empty element on a page that looks finished. Here they cannot be the same
// thing: an entry says which of the two states it is in, an answered one with
// thing: an entry says which of the three states it is in, an answered one with
// nothing in it is refused, and an undecided one renders as a sentence naming
// what it waits on. A reader of the page can then tell a question that is open
// from an answer that went missing, which is the distinction the page exists to
// keep.
//
// The third state is the one between those two, and it exists because a decision
// and the value it produces can arrive weeks apart. The question of who publishes
// this site was decided on 2026-08-24 and the answer is a record in decisions/,
// and the name, the address and the contact route it decides on are booked
// outside this repository and had not arrived twelve days later. A page that
// went on saying the question was not decided was saying something false, and a
// page that showed an empty answer was the blank above. So a decided entry names
// the record that carries the decision and what the value waits on, carries no
// value, and renders as a sentence saying exactly that.
package site

import (
Expand Down Expand Up @@ -54,20 +64,31 @@ const (
// refuses a page missing the link compares against this.
var LegalAddress = addressOf(LegalPath)

// The two states an entry may be in, and there is no third. A page cannot show a
// value it does not have, so the only question is whether it says why.
// The three states an entry may be in, and there is no fourth. A page cannot
// show a value it does not have, so the only question is whether it says why,
// and there are two different whys: nobody has decided, or somebody has and the
// value has not arrived.
const (
Answered = "answered"
Undecided = "undecided"
Decided = "decided"
)

// RecordDir is where a decided entry's record has to live. It is the directory
// decisions/README.md sets the shape of, and an entry naming a file anywhere
// else is naming something that is not a decision of record.
const RecordDir = "decisions/"

// notice is one thing the page answers, or says it cannot. Asks is the question
// in the words a reader reads, Answer is what it is answered with, and Waiting is
// what an unanswered one is waiting on. Exactly one of the last two carries
// anything, which is what the loader refuses everything else for.
// in the words a reader reads, Answer is what it is answered with, Record is the
// decision record a decided one points at, and Waiting is what an unanswered one
// is waiting on. An answered notice carries Answer alone, an undecided one
// Waiting alone, and a decided one Record and Waiting together, which is what
// the loader refuses everything else for.
type notice struct {
Asks string
Answer string
Record string
Waiting string
}

Expand All @@ -76,10 +97,11 @@ type entry struct {
Asks string `json:"asks"`
State string `json:"state"`
Value string `json:"value"`
Record string `json:"record"`
Waiting string `json:"waiting"`
}

var entryFields = map[string]bool{"asks": true, "state": true, "value": true, "waiting": true}
var entryFields = map[string]bool{"asks": true, "state": true, "value": true, "record": true, "waiting": true}

// readPublisher reads what the tree says about who publishes this site, in the
// order the file gives, and returns every reason it will not rather than the
Expand All @@ -89,7 +111,11 @@ var entryFields = map[string]bool{"asks": true, "state": true, "value": true, "w
// sequence: who publishes comes before how to reach them. A map would lose that,
// so the keys are read out of the raw object and put back in the order they were
// written.
func readPublisher(name string) ([]notice, error) {
//
// root is the tree the file belongs to, and it is what a decided entry's record
// is looked for in. A record is a pointer a reader follows, and a pointer at a
// file that is not there is the exact failure this state was added to end.
func readPublisher(root, name string) ([]notice, error) {
body, err := os.ReadFile(filepath.Clean(name))
if err != nil {
return nil, err
Expand Down Expand Up @@ -128,6 +154,12 @@ func readPublisher(name string) ([]notice, error) {
reasons = append(reasons, reason)
continue
}
if n.Record != "" {
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(n.Record))); err != nil {
reasons = append(reasons, fmt.Sprintf("%q is %s and names %q as the record carrying the decision, and there is no such file in the tree, so the page would send a reader to a record that is not there", key, Decided, n.Record))
continue
}
}
out = append(out, n)
}

Expand All @@ -145,8 +177,15 @@ func readPublisher(name string) ([]notice, error) {
// one carrying a value is a value somebody believes is published and no reader
// will ever see, which is worse than a blank because whoever wrote it has stopped
// looking. An undecided one naming nothing to wait on reads as a question nobody
// is holding. And an answered one still naming something reads as open while
// showing an answer, which is the pair a reader cannot resolve.
// is holding. An answered one still naming something reads as open while showing
// an answer, which is the pair a reader cannot resolve. A decided one carrying a
// value is an answer that has arrived and is being withheld from the page, a
// decided one naming no record is a claim that something was decided with
// nothing a reader can check it against, a decided one naming a record outside
// the decisions directory is pointing at something that is not a decision of
// record, and a decided one naming nothing it waits on reads as decided for no
// reason the page can give. Whether the record is in the tree is decided by the
// caller, which is the one that knows the tree.
func readEntry(key string, e entry) (notice, string) {
if strings.TrimSpace(e.Asks) == "" {
return notice{}, fmt.Sprintf("%q asks nothing, and an entry with no question renders as an answer to a question the page never puts", key)
Expand All @@ -159,19 +198,39 @@ func readEntry(key string, e entry) (notice, string) {
if strings.TrimSpace(e.Waiting) != "" {
return notice{}, fmt.Sprintf("%q is %s and still names %q as what it waits on, which reads as open and shows an answer at the same time", key, Answered, e.Waiting)
}
if strings.TrimSpace(e.Record) != "" {
return notice{}, fmt.Sprintf("%q is %s and still names %q as the record carrying the decision, which is a pointer the page has no sentence for once the value is on it", key, Answered, e.Record)
}
return notice{Asks: e.Asks, Answer: e.Value}, ""
case Undecided:
if strings.TrimSpace(e.Value) != "" {
return notice{}, fmt.Sprintf("%q is %s and carries the value %q, which no reader will see and which whoever wrote it has stopped looking at", key, Undecided, e.Value)
}
if strings.TrimSpace(e.Record) != "" {
return notice{}, fmt.Sprintf("%q is %s and names %q as the record carrying the decision, and a question with a decision record behind it is not undecided", key, Undecided, e.Record)
}
if strings.TrimSpace(e.Waiting) == "" {
return notice{}, fmt.Sprintf("%q is %s and names nothing it waits on, which reads on the page as a question nobody is holding", key, Undecided)
}
return notice{Asks: e.Asks, Waiting: e.Waiting}, ""
case Decided:
if strings.TrimSpace(e.Value) != "" {
return notice{}, fmt.Sprintf("%q is %s and carries the value %q, which is an answer that has arrived and is being kept off the page; an entry whose value has arrived is %s", key, Decided, e.Value, Answered)
}
if strings.TrimSpace(e.Record) == "" {
return notice{}, fmt.Sprintf("%q is %s and names no record carrying the decision, which reads on the page as a decision a reader has no way to check", key, Decided)
}
if !strings.HasPrefix(e.Record, RecordDir) || strings.TrimPrefix(e.Record, RecordDir) == "" {
return notice{}, fmt.Sprintf("%q is %s and names %q as the record carrying the decision, which is not under %s and so is not a decision of record", key, Decided, e.Record, RecordDir)
}
if strings.TrimSpace(e.Waiting) == "" {
return notice{}, fmt.Sprintf("%q is %s and names nothing the value waits on, which reads on the page as decided and missing for no reason it can give", key, Decided)
}
return notice{Asks: e.Asks, Record: e.Record, Waiting: e.Waiting}, ""
case "":
return notice{}, fmt.Sprintf("%q declares no state, and %s and %s are the only two an entry may be in", key, Answered, Undecided)
return notice{}, fmt.Sprintf("%q declares no state, and %s, %s and %s are the only three an entry may be in", key, Answered, Undecided, Decided)
default:
return notice{}, fmt.Sprintf("%q declares the state %q, and %s and %s are the only two an entry may be in", key, e.State, Answered, Undecided)
return notice{}, fmt.Sprintf("%q declares the state %q, and %s, %s and %s are the only three an entry may be in", key, e.State, Answered, Undecided, Decided)
}
}

Expand Down Expand Up @@ -212,7 +271,7 @@ func writeLegal(root, out, label string, tmpl *template.Template, said descripti
if err != nil {
return nil, fmt.Errorf("reading the legal notice prose: %w", err)
}
p.Notices, err = readPublisher(values)
p.Notices, err = readPublisher(root, values)
if err != nil {
return nil, fmt.Errorf("reading who publishes this site: %w", err)
}
Expand All @@ -234,14 +293,17 @@ func writeLegal(root, out, label string, tmpl *template.Template, said descripti
}
slashed := path.Join(label, LegalPath)

answered := 0
answered, decided := 0, 0
for _, n := range p.Notices {
if n.Answer != "" {
switch {
case n.Answer != "":
answered++
case n.Record != "":
decided++
}
}
fmt.Fprintf(log, "wrote %s (%d bytes, %d of %d answered)\n",
slashed, rendered.Len(), answered, len(p.Notices))
fmt.Fprintf(log, "wrote %s (%d bytes, %d of %d answered, %d decided and waiting on a value)\n",
slashed, rendered.Len(), answered, len(p.Notices), decided)
return []string{slashed}, nil
}

Expand Down
Loading
Loading