diff --git a/content/legal.txt b/content/legal.txt index 7df4b45..75244a7 100644 --- a/content/legal.txt +++ b/content/legal.txt @@ -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 diff --git a/data/publisher.json b/data/publisher.json index 8b115f6..4534da3 100644 --- a/data/publisher.json +++ b/data/publisher.json @@ -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" } } diff --git a/internal/site/install.go b/internal/site/install.go index 3f66568..8cdfd4a 100644 --- a/internal/site/install.go +++ b/internal/site/install.go @@ -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 { diff --git a/internal/site/legal.go b/internal/site/legal.go index 7a232b8..2c69eef 100644 --- a/internal/site/legal.go +++ b/internal/site/legal.go @@ -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 ( @@ -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 } @@ -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 @@ -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 @@ -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) } @@ -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) @@ -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) } } @@ -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) } @@ -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 } diff --git a/internal/site/legal_test.go b/internal/site/legal_test.go index bae60af..2b0a4d3 100644 --- a/internal/site/legal_test.go +++ b/internal/site/legal_test.go @@ -31,6 +31,8 @@ const legalTemplate = `