fix(brain): provenance as a post-condition on every model-reachable write
CI / Lint / Test / Vet (pull_request) Successful in 45s
CI / Mirror to GitHub (pull_request) Skipped

The PR #147 review forged origin: static as a JWT principal through
three tools the first version never touched (brain_edit, brain_ingest_raw,
ingest_raw + brain_promote), and through YAML spellings a key-name match
missed (quoted, escaped, tagged, flow-mapped, merge keys, lone CR).

- api.StampProvenance removes every spelling of origin/written_by from
  the frontmatter and appends the real values. A line is dropped if
  EITHER yaml.v3 (decoding quotes, escapes, tags, flow maps) or the
  brain's line-oriented readers would read it as provenance; complex
  keys, orphan values and merge keys are dropped outright. Lone CRs in
  frontmatter become LF. Principal values that are not bare words are
  quoted.
- Applied as the last step of writeHallNote, UpdateNote, PromoteNote
  (re-attributed to the promoter), brain_edit (re-attributed to the
  editor) and both ingests (every page written or merged). REST
  /write and /promote carry no principal and stamp unknown.
- Ingest: newlines in scalar fields fold to spaces; a page type must be
  a plain lower-case name (../raw wrote outside wiki/, pre-existing).
- Audit: outcome is ok|error, never the error text, which echoed
  argument values.

Tests: every escape from the review is a table case asserting the
invariant on the file (parses as YAML, yaml.v3 and brain_get agree,
exactly the stamped lines). Plus REST /write, REST capture AuthPath,
body-line provenance in search, and quoted principals; two of the
review's surviving flips now go red. Three tests renamed so names claim
only what they assert.

Refs #145 #144

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 22:29:46 +02:00
co-authored by Claude Opus 5.5
parent 32dedb318d
commit e615d6f144
16 changed files with 609 additions and 40 deletions
+21
View File
@@ -393,8 +393,10 @@ func (s *Server) brainPromote(ctx context.Context, args json.RawMessage) (json.R
if err := json.Unmarshal(args, &a); err != nil {
return nil, fmt.Errorf("parse args: %w", err)
}
origin, writtenBy := provenanceFrom(ctx)
relPath, err := api.PromoteNote(s.brainDir, api.PromoteOptions{
Filename: a.Filename, Wing: a.Wing, Hall: a.Hall, Slug: a.Slug,
Origin: origin, WrittenBy: writtenBy,
})
if err != nil {
return nil, err
@@ -510,6 +512,9 @@ func (s *Server) brainIngestRaw(ctx context.Context, args json.RawMessage) (json
warnings = []string{}
}
if !a.DryRun {
if err := s.stampPages(ctx, pages); err != nil {
return nil, err
}
for _, p := range pages {
s.indexInGraph(ctx, "brain_ingest_raw", p)
}
@@ -605,6 +610,9 @@ func (s *Server) runIngest(ctx context.Context, content, source string, dryRun b
pages = []string{}
}
if !dryRun {
if err := s.stampPages(ctx, pages); err != nil {
return nil, err
}
for _, p := range pages {
s.indexInGraph(ctx, "brain_ingest", p)
}
@@ -615,3 +623,16 @@ func (s *Server) runIngest(ctx context.Context, content, source string, dryRun b
}
return json.Marshal(map[string]any{"pages": pages, "warnings": warnings})
}
// stampPages attributes every page an ingest wrote (or merged into) to the
// calling principal (hyperguild#145). Ingest builds frontmatter itself, so
// without this its pages carried no provenance at all.
func (s *Server) stampPages(ctx context.Context, pages []string) error {
origin, writtenBy := provenanceFrom(ctx)
for _, p := range pages {
if err := api.StampProvenanceFile(s.brainDir, p, origin, writtenBy); err != nil {
return fmt.Errorf("stamp provenance: %w", err)
}
}
return nil
}