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
20 changes: 20 additions & 0 deletions .sdd/graph/2026/07/20-113534-s-tac-ufs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
type: signal
layer: tactical
kind: done
refs:
- id: 20260719-190740-d-tac-ptn
kind: addresses
desc: delivers the plan's metadata, authoring, inspection, and derived-read slice
participants:
- Christopher
confidence: high
topics:
- engine/base-facts
- implementation/engine
- type-system/kinds
- reliability/testing
summary: Slice 1 of the derived base-fact index plan ships in commit 60adf47, delivering strict nested metadata, serialization and validation, propagation from engine capture through CLI and show, and a pure current-project read restricted to active indexed facts with deterministic ordering and no supersession inheritance. Three independent reviews drove fail-loud malformed enrollment, centralized validation, strict YAML shape, removable optional state, value isolation, and stronger ordering coverage; focused tests, vet, diff checks, and targeted lint are green. This intermediate done signal addresses the plan without closing it.
---

Slice 1 of the derived base-fact index plan ships strict metadata and the pure derived read in commit 60adf47. The slice adds exact nested fact-index serialization and validation, carries the value through engine capture, application, command, CLI, and show surfaces, and derives only active indexed facts from the current project's merged graph with deterministic ordering and no supersession inheritance. Three independent reviews covered architecture, acceptance-criteria fidelity, and adversarial risk; their findings drove fail-loud malformed enrollment, centralized validation, strict YAML shape, removable optional state, value isolation, and stronger ordering coverage. Focused tests, vet, diff checks, and targeted lint are green. This is an intermediate completion record and does not close the plan.
7 changes: 7 additions & 0 deletions .sdd/graph/wip/20260719-191635-christopher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
entry: 20260719-190740-d-tac-ptn
participant: Christopher
exclusive: true
---

Implement derived base-fact index plan slice by slice
20 changes: 18 additions & 2 deletions application/session_runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"

sdd "github.com/networkteam/sdd/application"
"github.com/networkteam/sdd/internal/model"
localadapter "github.com/networkteam/sdd/local"
)

Expand Down Expand Up @@ -390,7 +393,8 @@ func TestPreparedTransitionRejectsEmptyTargetAndStructuredDivergence(t *testing.
}

func TestCreateEntryResolvesConcreteDefaultWithoutCWDAndReleasesAroundLLM(t *testing.T) {
graph, err := localadapter.NewFilesystemGraphStore(localadapter.FilesystemGraphStoreOptions{Project: "example", GraphDir: t.TempDir()})
graphDir := t.TempDir()
graph, err := localadapter.NewFilesystemGraphStore(localadapter.FilesystemGraphStoreOptions{Project: "example", GraphDir: graphDir})
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -432,7 +436,8 @@ func TestCreateEntryResolvesConcreteDefaultWithoutCWDAndReleasesAroundLLM(t *tes
binding := openBinding(t, sessions, identity.Subject, "create-default")
t.Chdir(t.TempDir())
created, err := application.CreateEntry(t.Context(), identity, "example", binding, sdd.EntryDraft{
Kind: "gap", Layer: "tactical", Body: "Concrete target resolution must remain independent of the process working directory.", Confidence: "high",
Kind: "fact", Layer: "tactical", Body: "Concrete target resolution must remain independent of the process working directory.", Confidence: "high",
Topics: []string{"implementation/engine"}, Index: &sdd.FactIndex{Title: "Concrete target resolution", Topic: "implementation/engine"},
})
if err != nil || created.EntryID == "" {
t.Fatalf("CreateEntry = %+v, %v", created, err)
Expand All @@ -450,6 +455,17 @@ func TestCreateEntryResolvesConcreteDefaultWithoutCWDAndReleasesAroundLLM(t *tes
if !sessionHasEvent(stored.Events, "mutation_intent", `"Target":{"project":"example","branch":"main"}`) {
t.Fatalf("ordinary capture did not persist its concrete default target: %+v", stored.Events)
}
rel, err := model.IDToRelPath(created.EntryID)
if err != nil {
t.Fatal(err)
}
written, err := os.ReadFile(filepath.Join(graphDir, rel))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(written), "index:\n title: Concrete target resolution\n topic: implementation/engine\n") {
t.Fatalf("written entry missing nested index:\n%s", written)
}
}

func TestPreparedRevalidationToleratesJSONRoundTripScalarTypes(t *testing.T) {
Expand Down
18 changes: 17 additions & 1 deletion application/workflow_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ func (w *WorkflowSession) registerWorkflowQueries(registry *engine.Registry) err
}); err != nil {
return err
}
if err := registry.RegisterQuery(engine.Query{
Doc: engine.FuncDoc{Name: "factIndex", Doc: "Active indexed facts from the current project graph, ordered for session discovery."},
ServeSafe: true,
Fn: func(ctx *engine.Context, _ map[string]any) (any, error) {
return ctx.Graph.IndexedFacts()
},
}); err != nil {
return err
}
if err := registry.RegisterQuery(engine.Query{
Doc: engine.FuncDoc{Name: "viewLayout", Doc: "Rendered `sdd view` pipeline result. Arg layout: the full pipeline syntax; may be a Go template over the store. Optional arg maxBytes: cap the rendered result on a line boundary (0 = uncapped), for a framing lane that must stay bounded."},
ServeSafe: true,
Expand Down Expand Up @@ -130,7 +139,7 @@ func (w *WorkflowSession) registerWorkflowWrites(registry *engine.Registry) erro
if err := registry.RegisterCommand(engine.Command{
Doc: engine.FuncDoc{
Name: "newEntry", Doc: "Creates the entry from the capture state fields (pre-flight inside; staged attachments materialized from handles; a recorded override skips pre-flight, durably logged).",
Reads: []string{"body", "entryKind", "layer", "refs", "topics", "confidence", "intent", "attachments", "participants", "supersedes", "closes", "preflightOverride"}, Writes: []string{"entryId", "findings"},
Reads: []string{"body", "entryKind", "layer", "refs", "topics", "index", "confidence", "intent", "attachments", "participants", "supersedes", "closes", "preflightOverride"}, Writes: []string{"entryId", "findings"},
},
MutatesGraph: true,
Fn: w.runWorkflowNewEntry,
Expand Down Expand Up @@ -241,6 +250,13 @@ func (w *WorkflowSession) runWorkflowNewEntry(ctx *engine.Context) error {
}
draft.Confidence, _ = workflowStoreString(ctx.Store, "confidence")
draft.Intent, _ = workflowStoreString(ctx.Store, "intent")
if value, ok := ctx.Store.Get("index"); ok {
index, valid := value.(engine.FactIndex)
if !valid {
return fmt.Errorf("newEntry: index has invalid stored type")
}
draft.Index = &FactIndex{Title: index.Title, Topic: index.Topic}
}
if override, ok := ctx.Store.Get("preflightOverride"); ok {
draft.SkipPreflight, _ = override.(bool)
}
Expand Down
51 changes: 51 additions & 0 deletions application/workflow_target_graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package application

import (
"context"
"strings"
"testing"

"github.com/networkteam/sdd/internal/engine"
Expand Down Expand Up @@ -120,6 +121,56 @@ func TestWorkflowContextUsesBranchTargetForSummaryAndPredicates(t *testing.T) {
}
}

func TestFactIndexQueryReturnsModelRowsWithoutDependencies(t *testing.T) {
localIndex, err := model.NewFactIndex("Local cue", "cli/view")
if err != nil {
t.Fatal(err)
}
topic, err := model.ParseTopicPath("cli/view")
if err != nil {
t.Fatal(err)
}
local := model.NewGraph([]*model.Entry{{
ID: "20260719-120000-s-tac-loc", Type: model.TypeSignal, Layer: model.LayerTactical,
Kind: model.KindFact, Topics: []model.TopicPath{topic}, Index: localIndex,
}})
remoteIndex, err := model.NewFactIndex("Remote cue", "cli/view")
if err != nil {
t.Fatal(err)
}
remote := model.NewGraph([]*model.Entry{{
ID: "20260719-120100-s-tac-rem", Type: model.TypeSignal, Layer: model.LayerTactical,
Kind: model.KindFact, Topics: []model.TopicPath{topic}, Index: remoteIndex,
}})
model.NewMultiGraph(local, []string{"example/remote"}, func(string) (*model.Graph, error) { return remote, nil })

workflow := &WorkflowSession{}
registry := engine.NewRegistry()
if err := workflow.registerWorkflowQueries(registry); err != nil {
t.Fatal(err)
}
factIndex, ok := registry.Query("factIndex")
if !ok || !factIndex.ServeSafe {
t.Fatalf("factIndex registration = %+v", factIndex)
}
value, err := factIndex.Fn(&engine.Context{Graph: local}, nil)
if err != nil {
t.Fatal(err)
}
rows, ok := value.([]model.FactIndexRow)
if !ok || len(rows) != 1 || rows[0].ID != "20260719-120000-s-tac-loc" {
t.Fatalf("factIndex rows = %#v", value)
}
malicious := model.NewGraph([]*model.Entry{{
ID: "20260719-120200-s-tac-bad", Type: model.TypeSignal, Layer: model.LayerTactical,
Kind: model.KindFact, Topics: []model.TopicPath{topic},
Index: &model.FactIndex{Title: "Cue\n\n## Injected block", Topic: topic},
}})
if _, err := factIndex.Fn(&engine.Context{Graph: malicious}, nil); err == nil || !strings.Contains(err.Error(), "single-line") {
t.Fatalf("factIndex accepted a block-injecting title: %v", err)
}
}

func workflowTargetSnapshot(t *testing.T, revision string, entries []EntryDocument) *Snapshot {
t.Helper()
snapshot, err := BuildSnapshot(t.Context(), SnapshotData{
Expand Down
15 changes: 14 additions & 1 deletion application/write_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ type EntryRef struct {
Desc string
}

type FactIndex struct {
Title string `json:"title"`
Topic string `json:"topic"`
}

type EntryDraft struct {
Target MutationTarget
Kind string
Expand All @@ -40,6 +45,7 @@ type EntryDraft struct {
Participants []string
Confidence string
Topics []string
Index *FactIndex
AttachmentHandles []string
SkipPreflight bool
}
Expand Down Expand Up @@ -122,10 +128,17 @@ func (a *Application) CreateEntry(ctx context.Context, identity RequestIdentity,
}
topics = append(topics, topic)
}
var index *model.FactIndex
if draft.Index != nil {
index, err = model.NewFactIndex(draft.Index.Title, draft.Index.Topic)
if err != nil {
return CreateEntryResult{}, err
}
}
entry := &model.Entry{
ID: id, Type: entryType, Kind: kind, Layer: layer, Intent: model.Intent(draft.Intent),
Content: draft.Body, Participants: append([]string(nil), draft.Participants...),
Confidence: draft.Confidence, Topics: topics, Time: runtime.options.Now(),
Confidence: draft.Confidence, Topics: topics, Index: index, Time: runtime.options.Now(),
}
if len(entry.Participants) == 0 {
if principal.Participant != "" {
Expand Down
33 changes: 33 additions & 0 deletions cmd/sdd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,10 @@ func newCmd() *cli.Command {
Name: "topics",
Usage: "Comma-separated topic labels (any kind) — inline topic membership written as `topics:` strings",
},
&cli.StringFlag{
Name: "index",
Usage: "Fact retrieval cue — JSON object {\"title\":\"<standalone title>\",\"topic\":\"<topic also in --topics>\"}",
},
&cli.StringSliceFlag{
Name: "topic",
Usage: "Topic cluster (kind: annotation only) — JSON object {\"label\":\"path\",\"members\":[\"id\",...]}; repeatable. A plain string in --topics works too for whole-refs membership.",
Expand Down Expand Up @@ -700,6 +704,10 @@ func newCmd() *cli.Command {
if err != nil {
return err
}
factIndex, err := parseFactIndexFlag(cmd.String("index"))
if err != nil {
return err
}
refs, err := parseRefFlags(cmd.StringSlice("refs"))
if err != nil {
return err
Expand All @@ -725,6 +733,7 @@ func newCmd() *cli.Command {
Class: strings.TrimSpace(cmd.String("class")),
Actor: strings.TrimSpace(cmd.String("actor")),
TopicLabels: splitCSV(cmd.String("topics")),
Index: factIndex,
AnnotationTopics: annotationTopics,
FocusActors: focusActors,
FocusWhen: focusWhen,
Expand Down Expand Up @@ -2203,6 +2212,30 @@ func parseWhenFlag(s string) (*model.FocusWhen, error) {
return &w, nil
}

func parseFactIndexFlag(s string) (*model.FactIndex, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, nil
}
decoder := json.NewDecoder(strings.NewReader(s))
decoder.DisallowUnknownFields()
var raw struct {
Title string `json:"title"`
Topic string `json:"topic"`
}
if err := decoder.Decode(&raw); err != nil {
return nil, fmt.Errorf("--index: invalid JSON object: %w", err)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return nil, fmt.Errorf("--index: expected one JSON object")
}
index, err := model.NewFactIndex(raw.Title, raw.Topic)
if err != nil {
return nil, fmt.Errorf("--index: %w", err)
}
return index, nil
}

// parseInvolvementFlags parses each --involvement JSON value into a
// model.Involvement, preserving the actors-omitted vs explicit-empty
// distinction (the latter declares pull-available involvement).
Expand Down
22 changes: 22 additions & 0 deletions cmd/sdd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,28 @@ func TestParseRefFlags_UnknownKindRejectedAtCapture(t *testing.T) {
}
}

func TestParseFactIndexFlag(t *testing.T) {
index, err := parseFactIndexFlag(`{"title":"How to compose graph views","topic":"cli/view"}`)
if err != nil {
t.Fatal(err)
}
if index.Title != "How to compose graph views" || index.Topic.String() != "cli/view" {
t.Fatalf("index = %+v", index)
}
for _, input := range []string{
`{"title":"Missing topic"}`,
`{"title":" ","topic":"cli/view"}`,
`{"title":"Title","topic":"cli view"}`,
`{"title":"Title\n## Injected","topic":"cli/view"}`,
`{"title":"Title","topic":"cli/view","extra":true}`,
`{"title":"Title","topic":"cli/view"} {}`,
} {
if _, err := parseFactIndexFlag(input); err == nil {
t.Errorf("parseFactIndexFlag(%q) accepted invalid object", input)
}
}
}

func TestParseRefFlags_EmptyInput(t *testing.T) {
got, err := parseRefFlags(nil)
if err != nil {
Expand Down
17 changes: 7 additions & 10 deletions internal/basefacts/basefacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func build(id, frontmatter, body string) (*model.Entry, error) {
if entry.Type != model.TypeSignal || entry.Kind != model.KindFact {
return nil, fmt.Errorf("base fact %s is %s %s — base facts ship as kind: fact signals", id, entry.Type, entry.Kind)
}
if err := entry.Index.ValidateForEntry(entry.Kind, entry.Topics); err != nil {
return nil, fmt.Errorf("base fact %s index: %w", id, err)
}
entry.Embedded = true
return entry, nil
}
Expand All @@ -70,6 +73,9 @@ confidence: high
topics:
- engine/base-facts
- cli/view
index:
title: 'How to compose graph views (view tool): layout grammar, filters, ranking, quoting, and examples'
topic: cli/view
summary: >-
The view layout language composes graph views from a colon-chained
pipeline — filters, then ranking, paging, and transforms, ending in a
Expand All @@ -78,15 +84,6 @@ summary: >-
quoting rule for multi-word, date, and duration arguments.
`

// viewGrammarBody renders the fact's body: a short neutral orientation line
// followed by the host-neutral grammar-and-vocabulary reference generated
// from the live executor vocabulary. Nothing here names a host command, so
// the fact stays host-neutral (d-cpt-476).
func viewGrammarBody(vocab viewlayout.Vocabulary) string {
return "Reference for the view layout language — the grammar for composing graph " +
"views by pipeline. Pull it when building or debugging a layout: it lists " +
"every filter, rank algorithm, transform, render terminator, and macro the " +
"executor accepts, and the quoting rule that trips up unquoted multi-word, " +
"date, and duration arguments.\n\n" +
viewlayout.ReferenceBody(vocab)
return viewlayout.Markdown(vocab)
}
Loading
Loading