diff --git a/README.md b/README.md
index 9a3b675..9dad9e0 100644
--- a/README.md
+++ b/README.md
@@ -130,8 +130,8 @@ that is not one task (`taskgate`).
module's defaults by `task reference`, and a page shows one with ``.
- **A figure said in prose has a line** in `tools/refgen/figures.txt`: every duration, size and
count a hand-written page says outside ``, with the constant of spin's that decides it
- (`spin:# = 15m`), which `task reference:check` reads and holds the line to, or
- why nothing holds it. A figure with no line fails, and so does a line its page no longer says.
+ (`spin:# = 15m`), which `task reference:check` reads and holds the line to; with the text of a file
+ that decides it where no constant does (`spin: has ""`); or with why nothing holds it. A figure with no line fails, and so does a line its page no longer says.
- **What a release is made of**, `reference/release.md`, is written from spin's `versions.yaml` at
the release installations run (its spin-machine), images.json and the module's newest release.
- **Diagrams** are the ones spin and spin-terraform-aws draw beside their sources (`.dot`, `.d2`):
diff --git a/tools/refgen/figures.go b/tools/refgen/figures.go
index 550377b..1332e05 100644
--- a/tools/refgen/figures.go
+++ b/tools/refgen/figures.go
@@ -17,9 +17,14 @@ import (
//
// | | spin:# =
//
-// for a figure a constant of spin's decides, which refgen reads and compares - 15m, 12h, 300, 3 -
-// or, in the third place, why nothing holds it: a default of a file refgen cannot evaluate, or
-// words that are no figure of the code's. A page that showed a figure with has no line:
+// for a figure a constant of spin's decides, which refgen reads and compares - 15m, 12h, 300, 3;
+//
+// | | spin: has ""
+//
+// for one decided where no constant is - a field's default, a timer's unit, a template's line, a
+// Terraform local - held by the text that decides it still being in the file, of spin or of
+// spin-terraform-aws, whatever its spacing; or, in the third place, why nothing holds it: another
+// party's fact, or words that are no figure of the code's. A page that showed a figure with has no line:
// the component is the code's own number.
//
// Before it a page said "an hour" of a credential the code had made fifteen minutes, and "about
@@ -29,8 +34,12 @@ const figuresList = "tools/refgen/figures.txt"
// saidFigure is a duration, a size or a count of tries, in digits or in words.
var saidFigure = regexp.MustCompile(`(?i)\b(?:(?:\d+(?:[.,]\d+)?|an?|one|two|three|four|five|six|seven|eight|nine|ten|twelve|fourteen|fifteen|twenty|thirty|sixty|ninety|half an?|a quarter of an|a few|few)[ -](?:(?:second|minute|hour|day|week|month|year)s?|times|attempts)|\d+(?:\.\d+)? ?[KMGT]i?B)\b`)
-// held is a figure's third place when a constant decides it.
-var held = regexp.MustCompile(`^spin:(\S+)#(\w+) = (\S+)$`)
+// held is a figure's third place when a constant decides it, and has when a file's text does.
+var (
+ held = regexp.MustCompile(`^spin:(\S+)#(\w+) = (\S+)$`)
+ has = regexp.MustCompile(`^([\w-]+):(\S+) has "(.+)"$`)
+ gaps = regexp.MustCompile(`\s+`)
+)
type figureLine struct {
n int
@@ -38,10 +47,11 @@ type figureLine struct {
}
// checkFigures holds every figure in the prose of a hand-written page to a line of list, every
-// line to words its page still says, and every line that names a constant to that constant's
-// value in the checkout spin. spin is "" in a run without it, which leaves the constants to the
-// run that has it.
-func checkFigures(docs, list, spin string) ([]string, error) {
+// line to words its page still says, and every line that names a constant or a file's text to
+// the checkout that has it. from is the checkouts at hand: a line of a repository that is not
+// among them - spin's, in a run without spin - is left to the run that has it.
+func checkFigures(docs, list string, from map[string]checkout) ([]string, error) {
+ spin := from["spin"].root
raw, err := os.ReadFile(list)
if err != nil {
return nil, err
@@ -89,6 +99,20 @@ func checkFigures(docs, list, spin string) ([]string, error) {
bad = append(bad, fmt.Sprintf("%s:%d: %s does not say %q: take the line out", list, l.n, l.page, l.words))
continue
}
+ if m := has.FindStringSubmatch(l.what); m != nil {
+ c, ok := from[m[1]]
+ if !ok {
+ continue
+ }
+ raw, err := os.ReadFile(filepath.Join(c.root, m[2]))
+ switch {
+ case err != nil:
+ bad = append(bad, fmt.Sprintf("%s:%d: %s says %q, and %s no longer has %s: correct the page and the line", list, l.n, l.page, l.words, m[1], m[2]))
+ case !strings.Contains(gaps.ReplaceAllString(string(raw), " "), gaps.ReplaceAllString(m[3], " ")):
+ bad = append(bad, fmt.Sprintf("%s:%d: %s says %q, and %s's %s no longer says %q: correct the page and the line", list, l.n, l.page, l.words, m[1], m[2], m[3]))
+ }
+ continue
+ }
m := held.FindStringSubmatch(l.what)
if m == nil || spin == "" {
continue
diff --git a/tools/refgen/figures.txt b/tools/refgen/figures.txt
index e5a90b5..16d6788 100644
--- a/tools/refgen/figures.txt
+++ b/tools/refgen/figures.txt
@@ -4,26 +4,27 @@
# | | spin:# =
#
# where a constant of spin's decides the figure - task reference:check reads it and fails when it
-# is another value, naming the page - or, in the third place, why nothing holds it. A figure a
+# is another value, naming the page; or `: has ""`, where the text that decides
+# it is held to still being in the file; or, in the third place, why nothing holds it. A figure a
# page says that has no line fails, and so does a line whose words its page no longer says. A
# "not held" line is a debt: the figure is true only while somebody remembers it.
-admin/configuration.md | seven days | not held: struct field default SessionDurationHours: 168 in DefaultSecurity() of internal/controlplane/settings/security.go, overridden by the session_duration_hours setting
+admin/configuration.md | seven days | spin:internal/controlplane/settings/security.go has "SessionDurationHours: 168,"
admin/hosts.mdx | 24 hours | spin:internal/controlplane/server/runnertokenserver/runnertoken.go#registrationTokenExpiry = 24h
admin/hosts.mdx | 30 days | spin:internal/controlplane/server/runnertokenserver/runnertoken.go#maxRegistrationTokenExpiry = 720h
-admin/observability.md | fifteen minutes | not held: systemd timer OnUnitActiveSec=15min in internal/installation/install/units/spin-vmbackup.timer
-admin/observability.md | 30 days | not held: var DefaultRetention (PlatformLogs: 30) in internal/logstore/logstore.go, overridden by the logs_platform_retention_days setting
-admin/observability.md | 7 days | not held: var DefaultRetention (WorkspaceLogs: 7) in internal/logstore/logstore.go, overridden by the logs_workspace_retention_days setting
-admin/observability.md | 7 days | not held: var DefaultRetention (Traces: 7) in internal/logstore/logstore.go, overridden by the traces_retention_days setting
+admin/observability.md | fifteen minutes | spin:internal/installation/install/units/spin-vmbackup.timer has "OnUnitActiveSec=15min"
+admin/observability.md | 30 days | spin:internal/logstore/logstore.go has "PlatformLogs: 30,"
+admin/observability.md | 7 days | spin:internal/logstore/logstore.go has "WorkspaceLogs: 7,"
+admin/observability.md | 7 days | spin:internal/logstore/logstore.go has "Traces: 7}"
admin/observability.md | 30 days | spin:internal/metricstore/metricstore.go#RetentionDays = 30
-admin/observability.md | thirty seconds | not held: poll_frequency = "30s" in the Alloy template internal/installation/boot/files/alloy.alloy.tmpl
+admin/observability.md | thirty seconds | spin:internal/installation/boot/files/alloy.alloy.tmpl has "poll_frequency = "30s""
admin/recovery.md | five minutes | spin:internal/pgserver/server.go#ArchiveTimeoutSeconds = 300
admin/recovery.md | five minutes | spin:internal/pgserver/server.go#ArchiveTimeoutSeconds = 300
-admin/users.md | seven days | not held: struct field default SessionDurationHours: 168 in DefaultSecurity() of internal/controlplane/settings/security.go, overridden by the session_duration_hours setting
+admin/users.md | seven days | spin:internal/controlplane/settings/security.go has "SessionDurationHours: 168,"
admin/users.md | a year | spin:internal/controlplane/server/authserver/apikeys.go#maxAPIKeyLifetime = 8784h
aws/cost.mdx | Three months | not a figure: a heading naming the period the cost table prices
aws/cost.mdx | a month | not a figure: the unit of S3's price per GB a month
aws/cost.mdx | a month | not a figure: the unit of Route 53's price per hosted zone
-aws/network.mdx | ten-second | not held: Terraform local internal_record_ttl = 10 in modules/controlplane/dns.tf of spin-terraform-aws
+aws/network.mdx | ten-second | spin-terraform-aws:modules/controlplane/dns.tf has "internal_record_ttl = 10"
aws/setup.md | a few minutes | not a figure: an estimate of how long Let's Encrypt issuance takes
aws/setup.md | ten minutes | not a figure: an estimate ("About") of the first bring-up time
aws/setup.md | a few minutes | not a figure: an estimate of how long a first runner takes to come up
@@ -39,16 +40,16 @@ concepts/workspaces.md | 512 MB | spin:internal/controlplane/server/workspaceser
concepts/workspaces.md | 10 GB | spin:internal/controlplane/server/workspaceserver/workspace.go#DefaultDiskGB = 10
concepts/workspaces.md | three times | spin:internal/lifecycle/budget.go#GenerationAttempts = 3
concepts/workspaces.md | Ten minutes | spin:internal/controlplane/server/workspaceserver/idle.go#WarnMinutes = 10
-overview/evaluate.md | 30 GB | not held: flag default -free-gb 30 in hack/lanes/cmd/lanes/ci.go (the CI run's preflight); nothing checks it for the development stack
+overview/evaluate.md | 30 GB | spin:hack/lanes/cmd/lanes/ci.go has "fs.Int("free-gb", 30,"
overview/how-it-works.mdx | five minutes | spin:internal/pgserver/server.go#ArchiveTimeoutSeconds = 300
overview/how-it-works.mdx | a minute | spin:internal/controlplane/domain/reconcile/reconcile.go#Every = 1m
overview/how-it-works.mdx | three times | spin:internal/lifecycle/budget.go#GenerationAttempts = 3
overview/how-it-works.mdx | five minutes | spin:internal/controlplane/settings/settings.go#DefaultVolumeRPOSeconds = 300
-overview/one-machine.md | 30 GB | not held: flag default -free-gb 30 in hack/lanes/cmd/lanes/ci.go (the CI run's preflight); nothing checks it for the development stack
+overview/one-machine.md | 30 GB | spin:hack/lanes/cmd/lanes/ci.go has "fs.Int("free-gb", 30,"
quickstart.md | few minutes | not a figure: an estimate of how long a runner takes to come up
security/data.md | 64 KiB | spin:internal/storage/crypto/layer.go#LayerFrameBytes = 65536
security/data.md | fifteen minutes | spin:internal/controlplane/server/volumeserver/storagecreds.go#StorageCredentialTTL = 15m
-security/identity.md | seven days | not held: struct field default SessionDurationHours: 168 in DefaultSecurity() of internal/controlplane/settings/security.go, overridden by the session_duration_hours setting
+security/identity.md | seven days | spin:internal/controlplane/settings/security.go has "SessionDurationHours: 168,"
security/identity.md | a year | spin:internal/controlplane/server/authserver/apikeys.go#maxAPIKeyLifetime = 8784h
security/identity.md | fifteen minutes | spin:internal/installation/provider/aws/stsjoin/stsjoin.go#maxExpiry = 15m
security/identity.md | an hour | spin:internal/controlplane/server/runnertokenserver/runnertoken.go#runnerTokenExpiry = 1h
diff --git a/tools/refgen/figures_test.go b/tools/refgen/figures_test.go
index 8a391c1..e954339 100644
--- a/tools/refgen/figures_test.go
+++ b/tools/refgen/figures_test.go
@@ -19,6 +19,9 @@ func TestCheckFigures(t *testing.T) {
"listed with why nothing holds it": {"About 220 MB.", "# sizes\np.md | 220 MB | not held: the size of Microsoft's server\n", ttl, ""},
"a link's target is not the page's": {"[It stops]() and [reads](/a/30-days/).", "", ttl, ""},
"a flag's own value is the CLI's": {"Pass `--expires 30 days`.\n\n```bash\nsleep 5 minutes\n```", "", ttl, ""},
+ "held by a file's text": {"Lasts seven days.", "p.md | seven days | spin:p/p.go has \"Tries = 3\"\n", ttl, ""},
+ "the text is gone": {"Lasts seven days.", "p.md | seven days | spin:p/p.go has \"Tries = 4\"\n", ttl, `no longer says "Tries = 4"`},
+ "the text's file is gone": {"Lasts seven days.", "p.md | seven days | spin:q/q.go has \"Tries = 3\"\n", ttl, "spin no longer has q/q.go"},
"a figure with no line": {"Lasts an hour.", "", ttl, `says "an hour", which nothing holds`},
"the constant moved": {"Lasts fifteen minutes.", "p.md | fifteen minutes | spin:p/p.go#TTL = 15m\n", strings.Replace(ttl, "15", "10", 1), "TTL is now 10m0s, not 15m"},
"the count moved": {"Tried three times.", "p.md | three times | spin:p/p.go#Tries = 3\n", strings.Replace(ttl, "= 3", "= 5", 1), "Tries is now 5, not 3"},
@@ -42,7 +45,7 @@ func TestCheckFigures(t *testing.T) {
if err := os.WriteFile(list, []byte(c.list), 0o644); err != nil {
t.Fatal(err)
}
- bad, err := checkFigures(docs, list, spin)
+ bad, err := checkFigures(docs, list, map[string]checkout{"spin": {root: spin}})
if err != nil {
t.Fatal(err)
}
@@ -64,7 +67,7 @@ func TestCheckFiguresWithoutSpin(t *testing.T) {
if err := os.WriteFile(list, []byte("p.md | fifteen minutes | spin:gone.go#TTL = 15m\n"), 0o644); err != nil {
t.Fatal(err)
}
- if bad, err := checkFigures(docs, list, ""); err != nil || len(bad) > 0 {
+ if bad, err := checkFigures(docs, list, nil); err != nil || len(bad) > 0 {
t.Errorf("checkFigures without spin: %v, %v", bad, err)
}
}
diff --git a/tools/refgen/main.go b/tools/refgen/main.go
index 7044050..ad67cc0 100644
--- a/tools/refgen/main.go
+++ b/tools/refgen/main.go
@@ -174,7 +174,7 @@ func run() error {
func() ([]string, error) { return checkDomains(*docs, *tf) },
func() ([]string, error) { return checkTofu(*docs, *tf) },
func() ([]string, error) { return checkReads(*docs, from) },
- func() ([]string, error) { return checkFigures(*docs, figuresList, *spin) },
+ func() ([]string, error) { return checkFigures(*docs, figuresList, from) },
} {
b, err := c()
if err != nil {