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
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"major": 1,
"minor": 9,
"patch": 0,
"patch": 1,
"prerelease": ""
}
2 changes: 1 addition & 1 deletion api-schema/tmi-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "TMI (Threat Modeling Improved) API",
"description": "A RESTful API for collaborative threat modeling with full X6 graph library compatibility. This API provides schemas that align with AntV X6 cell object models for seamless integration with modern diagramming libraries. Supports OAuth 2.0 authentication with client callback integration for seamless single-page application authentication flows.\n\n## API Design v1.1.0\n\n### Authorization Model\nTMI uses hierarchical authorization: access control is defined at the ThreatModel level via the authorization field (readers, writers, owners). All child resources (Assets, Diagrams, Documents, Notes, Repositories, Threats) inherit permissions from their parent ThreatModel. This simplifies permission management and ensures consistent access control.\n\n### Bulk Operations\nNotes and Diagrams do not support bulk operations due to their unique creation workflows and lack of valid bulk use cases. All other resources (Threats, Assets, Documents, Repositories) support full bulk operations: POST (create), PUT (upsert), PATCH (partial update), DELETE (batch delete).\n\nAll resources support bulk metadata operations regardless of resource-level bulk support.\n\n### List Response Strategy\n- ThreatModels return summary information (TMListItem) because they contain many child objects that can be large.\n- Diagrams return summary information (DiagramListItem) because diagram data (cells, images) can be large.\n- Notes return summary information (NoteListItem) because the content field can be large.\n- Threats, Assets, Documents, Repositories return full schemas as they are relatively small and static.\n\n### PATCH Support\nAll resources support PATCH for partial updates using JSON Patch (RFC 6902). This is particularly useful for:\n- Assets: Array field updates (affected_assets, trust_boundaries) ensuring no duplicates\n- Notes: Updating name/description without changing content field\n- All resources: Efficient updates without full object replacement\n",
"version": "1.9.0",
"version": "1.9.1",
"contact": {
"name": "TMI Development Team",
"url": "https://github.com/ericfitz/tmi",
Expand Down
2,166 changes: 1,083 additions & 1,083 deletions api/api.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion api/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var (
// Minor version number
VersionMinor = "9"
// Patch version number
VersionPatch = "0"
VersionPatch = "1"
// VersionPreRelease is the pre-release label (e.g., "rc.0", "beta.1"), empty for stable releases
VersionPreRelease = ""
// GitCommit is the git commit hash from build
Expand Down
134 changes: 67 additions & 67 deletions config-reference.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions docs/superpowers/specs/2026-08-22-config-model-redesign-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,51 @@ load-bearing. Every operational setting declares `Hot` or `RestartRequired`. The
admin API reports it, and a `PUT` to a restart-required setting returns 200 with
an explicit "takes effect on restart" signal rather than appearing to work.

**Amended 2026-08-23, after Phase A measured it.** The paragraph above was
written assuming operational settings are runtime-editable, with
restart-required as the declared exception. Phase A set `Mutability` per entry
by reading each setting's consuming code, and the result is the opposite:
**roughly 89 Static to 11 Hot** across the operational set. Restart-required is
the rule, not the exception.

The traced case is `features.saml_enabled`: `auth/service.go`'s `NewService`
builds the SAML manager only when the boot flag is true, so the database-backed
reader gates handlers that a nil manager has already sunk. Flipping the row at
runtime changes nothing until a restart. That shape — construct-once at boot,
read-a-flag later — recurs across most of the operational set.

**What this does and does not change.**

- It does **not** invalidate the design. Declaring restart semantics honestly is
the point of promoting `Mutability`, and a `Static` declaration is a correct
declaration, not a defect.
- It does **not** change Phases C-E, whose mechanism (export → import → restart
→ verify) already restarts the service at each step. See §6.
- It **does** retire the informal claim that motivated parts of this work —
"move it to the database and admins can edit it live". That is true for about
eleven settings. For the rest, the database becomes the single source of
truth and the templating path (goal 3) becomes the way a value changes; the
edit still lands, it just takes effect on restart.
- The honest statement of the benefit is therefore **one source of truth, one
delivery path, and a reproducible per-environment template** — not live
reconfiguration. Anything that needs to be genuinely `Hot` requires
converting its consumer to lazy construction, which is separate work and is
not scheduled here.

`config-reference.md` reports the declared value as of this amendment. It
previously printed `hot` for **all 70** operational rows, because
`GetMigratableSettings()` assigns `Class` via `classificationFor(key)` and so
never carried the per-entry `withMutability(...)` overrides; the reference
generator now resolves `Mutability` from the registry def via `DefFor(key)`.
That lookup is confined to the documentation generator, leaving
`GetMigratableSettings()`'s observable output untouched — making
`classificationFor()` itself consult the registry remains Phase E work per
Ruling 15.

Note the 70 rows there against ~100 operational settings: the reference is
generated from the same `OmitWhenEmpty`-filtered projection that #810 reports,
so it does not yet list every key.

---

## 2. One registry, one declaration per setting
Expand Down
19 changes: 17 additions & 2 deletions internal/config/reference_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,28 @@ func bootstrapRow(s MigratableSetting) string {
yesNo(s.Class.Required), yesNo(s.Class.Secret), precedenceCell(s), sanitizeCell(s.Description))
}

// SEM@e6cee63c3a07d38f471e0ebfb81722849f36085e: format an operational config setting as a Markdown table row (pure)
// SEM@422a0d3e8f849483ac9c195e4a2bc39c06fc9c6b: format an operational config setting as a Markdown table row (pure)
func operationalRow(s MigratableSetting) string {
return fmt.Sprintf("| `%s` | %s | %s | %s | %s | %s | %s | %s | %s |\n",
s.Key, codeOrDash(s.EnvVar), s.Type, defaultCell(s), s.Class.Mutability.String(),
s.Key, codeOrDash(s.EnvVar), s.Type, defaultCell(s), declaredMutability(s),
s.Class.Visibility.String(), yesNo(s.Class.Secret), precedenceCell(s), sanitizeCell(s.Description))
}

// declaredMutability reports the registry's declared restart semantics for a
// setting. GetMigratableSettings assigns Class via classificationFor(key),
// which does not carry the per-entry withMutability(...) overrides the
// registry audit set by reading each setting's consuming code. Reading
// s.Class.Mutability here would therefore print the pre-audit default rather
// than the declared value. Generated provider keys have no registry def and
// keep the projected value.
// SEM@422a0d3e8f849483ac9c195e4a2bc39c06fc9c6b: resolve a setting's declared restart semantics from the registry (pure)
func declaredMutability(s MigratableSetting) string {
if d, ok := DefFor(s.Key); ok {
return d.Class.Mutability.String()
}
return s.Class.Mutability.String()
}

// precedenceCell renders the terse per-row precedence summary; the full rule
// is spelled out once in precedenceSection rather than repeated per row.
// SEM@05517d8cb7bfbe65374f23c29bbc9bd51efe97e2: summarize which source wins for a setting's category as a table cell (pure)
Expand Down
63 changes: 63 additions & 0 deletions internal/config/reference_gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,66 @@ func TestConfigReferenceFile_MatchesRegistry(t *testing.T) {
t.Error("config-reference.md is stale — run `make generate-config-docs`")
}
}

// TestGenerateReferenceMarkdown_MutabilityComesFromRegistry pins the fix for
// the reference table reporting every operational setting as "hot".
//
// GetMigratableSettings assigns Class via classificationFor(key), which does
// not carry the per-entry withMutability(...) overrides. Rendering
// s.Class.Mutability directly therefore printed the pre-audit default for
// every row. The generator resolves the value from the registry def instead.
//
// Watched to fail: reverting declaredMutability to return
// s.Class.Mutability.String() makes this test report 0 static rows.
func TestGenerateReferenceMarkdown_MutabilityComesFromRegistry(t *testing.T) {
out, err := GenerateReferenceMarkdown()
if err != nil {
t.Fatalf("GenerateReferenceMarkdown: %v", err)
}

var static, hot int
for _, line := range strings.Split(string(out), "\n") {
if !strings.HasPrefix(line, "| `") {
continue
}
switch {
case strings.Contains(line, "| static |"):
static++
case strings.Contains(line, "| hot |"):
hot++
}
}

if static+hot == 0 {
t.Fatal("no operational rows rendered a mutability cell")
}
// The registry audit set most operational settings to Static. If the
// generator regresses to the projected classification, every row reads
// "hot" and static drops to zero.
if static == 0 {
t.Errorf("every operational row reports hot (%d rows) — the generator is "+
"rendering the projected classification instead of the registry "+
"declaration; see declaredMutability", hot)
}
if static <= hot {
t.Errorf("expected static to dominate per the registry audit, got static=%d hot=%d", static, hot)
}

// Cross-check one traced case end to end: features.saml_enabled gates a
// manager built once at boot, so it must render static.
d, ok := DefFor("features.saml_enabled")
if !ok {
t.Fatal("features.saml_enabled missing from the registry")
}
if d.Class.Mutability != MutabilityStatic {
t.Errorf("features.saml_enabled declared %s, want static", d.Class.Mutability)
}
if !strings.Contains(string(out), "`features.saml_enabled`") {
t.Fatal("features.saml_enabled not present in the reference table")
}
for _, line := range strings.Split(string(out), "\n") {
if strings.Contains(line, "`features.saml_enabled`") && !strings.Contains(line, "| static |") {
t.Errorf("features.saml_enabled row does not report static: %s", line)
}
}
}
Loading