Establish the shared frontend editor package and make the Go resolvers the
single source of truth for variable labels.
Go — catalogue SSOT:
- VariableResolver gains Keys() []VariableKey; ResolverSet gains
Catalogue(). The 7 submission resolvers implement Keys() with the
bilingual labels ported from the TS VARIABLE_LABELS table (incl. the
legacy rule.* aliases). Keys() is entity-independent, so
SubmissionVariableCatalogue() builds a metadata-only ResolverSet.
- GET /api/docforge/variables serves the catalogue (auth-gated, static).
- Tests: docforge ResolverSet (BuildBag merge + Catalogue order) and the
submission catalogue integrity (no dupes, labels present, spot-checks).
Frontend — frontend/src/lib/docforge-editor/ (new shared package):
- dom.ts: escapeHtml + cssEscape (pure), with bun tests. Dedupes the two
identical escapeHtml/escapeHTML copies + the cssEscape copy that lived
in the submission editor.
- catalogue.ts: fetchVariableCatalogue() + labelMap() — the client for
the Go catalogue.
- submission-draft.ts now imports escapeHtml/cssEscape from the lib and
fetches the catalogue on boot into state.varLabels (labelFor reads it,
falling back to the raw key if the fetch fails — graceful degrade). The
hardcoded VARIABLE_LABELS table is removed; VARIABLE_GROUPS stays
(presentation: which keys to show + how to section them, legitimately
frontend).
Scope note: the DOM-coupled editor plumbing (wireDraftVars/focus
preservation/autosave debounce) is extracted in slice 6 alongside its first
reuse — the authoring page — rather than speculatively now (extract with the
consumer; same principle as slices 2-3). Slice 5 lands the pure utilities +
the catalogue, which the slice-6 authoring palette consumes.
Verification: go build/vet/test green (Go files gofmt-clean; handlers.go
pre-existing drift, added region clean); bun run build.ts clean;
bun test 274/274 (incl. 5 new docforge-editor tests).
m/paliad#157
90 lines
3.7 KiB
Go
90 lines
3.7 KiB
Go
package docforge
|
|
|
|
// VariableResolver populates one namespace of the placeholder bag.
|
|
//
|
|
// Each resolver owns a dotted namespace (e.g. "project", "parties") and
|
|
// pushes its keys into a shared PlaceholderMap. The push model — rather
|
|
// than a pull Resolve(key) — is deliberate: some namespaces emit a
|
|
// data-dependent set of keys (a multi-party suit produces
|
|
// parties.claimant.0.name, .1.name, … one per party), which a fixed
|
|
// key-by-key pull interface can't enumerate cleanly. Populate lets each
|
|
// resolver decide its own (possibly dynamic) key set in one pass.
|
|
//
|
|
// The consuming application implements concrete resolvers against its own
|
|
// data sources (paliad resolves project/party/deadline state from its
|
|
// Postgres database); docforge owns only the interface and the
|
|
// composition machinery (ResolverSet). This is the seam a second consumer
|
|
// (e.g. upc-commentary) plugs its own resolvers into without touching the
|
|
// engine.
|
|
type VariableResolver interface {
|
|
// Namespace returns the dotted prefix this resolver owns, e.g.
|
|
// "project". Informational — used for diagnostics and as the default
|
|
// group for this resolver's catalogue entries.
|
|
Namespace() string
|
|
|
|
// Populate writes this resolver's keys into bag. Resolvers own
|
|
// disjoint namespaces, so population order across resolvers does not
|
|
// affect the final bag.
|
|
Populate(bag PlaceholderMap)
|
|
|
|
// Keys returns the user-facing catalogue entries for this resolver —
|
|
// the variables an authoring palette can offer and a sidebar form can
|
|
// render, each with its bilingual label. This is the curated, static
|
|
// surface (e.g. the flat parties.claimant.name form), not the full
|
|
// possibly-dynamic key set Populate emits (e.g. the indexed
|
|
// parties.claimant.0.name). Go owns these labels so the frontend form
|
|
// and the authoring palette read one source of truth instead of a
|
|
// duplicated TS table.
|
|
Keys() []VariableKey
|
|
}
|
|
|
|
// VariableKey is one catalogue entry: the placeholder key plus its
|
|
// bilingual label and a group (the owning namespace by default). The
|
|
// frontend maps groups onto its own lawyer-facing presentation sections.
|
|
type VariableKey struct {
|
|
Key string `json:"key"`
|
|
LabelDE string `json:"label_de"`
|
|
LabelEN string `json:"label_en"`
|
|
Group string `json:"group"`
|
|
}
|
|
|
|
// ResolverSet composes an ordered list of VariableResolvers into a single
|
|
// PlaceholderMap. It is the replacement for hard-coded "call addFooVars,
|
|
// then addBarVars, …" sequencing: a consumer registers the resolvers that
|
|
// apply to a given render and calls BuildBag.
|
|
type ResolverSet struct {
|
|
resolvers []VariableResolver
|
|
}
|
|
|
|
// NewResolverSet builds a set from the given resolvers, in order.
|
|
func NewResolverSet(resolvers ...VariableResolver) *ResolverSet {
|
|
return &ResolverSet{resolvers: resolvers}
|
|
}
|
|
|
|
// Add appends a resolver to the set.
|
|
func (s *ResolverSet) Add(r VariableResolver) { s.resolvers = append(s.resolvers, r) }
|
|
|
|
// BuildBag runs every resolver's Populate into a fresh PlaceholderMap and
|
|
// returns it. Because resolvers own disjoint namespaces, the result is
|
|
// independent of resolver order.
|
|
func (s *ResolverSet) BuildBag() PlaceholderMap {
|
|
bag := PlaceholderMap{}
|
|
for _, r := range s.resolvers {
|
|
r.Populate(bag)
|
|
}
|
|
return bag
|
|
}
|
|
|
|
// Catalogue concatenates every resolver's Keys() in resolver order — the
|
|
// full set of user-facing variables for a palette or form, with bilingual
|
|
// labels. It does not require any per-call entity state, so a consumer can
|
|
// build a metadata-only ResolverSet (resolvers constructed with nil
|
|
// entities) purely to serve the catalogue.
|
|
func (s *ResolverSet) Catalogue() []VariableKey {
|
|
var out []VariableKey
|
|
for _, r := range s.resolvers {
|
|
out = append(out, r.Keys()...)
|
|
}
|
|
return out
|
|
}
|