Skip to content

Make the published Soufflé program idiomatic - #47

Open
afogel wants to merge 7 commits into
ir/v8from
ir/datalog-idioms
Open

afogel wants to merge 7 commits into
ir/v8from
ir/datalog-idioms

Conversation

@afogel

@afogel afogel commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Stacked on #46 (V8). Follow-up to the review of ir/dist/rules.dl: the compiled program now reads as a Soufflé author would write it, and the compiler checks what Soufflé checks.

What changed

  • Column domains as subtypes. Every vocabulary column has a domain, defaulting to its name; a column holding the same kind of value under another name declares it (request_seq is a seq, parent is a pid). The emitter declares each domain as .type Seq <: number and types every relation with them. The compiler refuses a rule that joins or compares two domains, so a rule that Soufflé would reject never reaches the .dl.
  • Constants in the head. A violation head position may hold a constant. The nine rules that bound one through Field = "request_hash" now write violation(Session, EntryId, "request_hash").
  • Facts, not vacuous rules. The §7.2 default-trust table is a static relation, default_trust, with six facts, replacing six rules with a :- trust(_, _, _) body.
  • One typed .output per provision. The three-column violation(provision, subject, witness) relation packed with nested cat and to_string is gone. The differential reads Soufflé's per-provision CSVs into the one-line form the fixtures pin (provision, subject values, witness values), so expected.tsv and the evaluator are unchanged in shape.
  • A lineage closure that terminates. ACS-REQ-0010's closure is over (descendant, ancestor) pairs. The path-carrying form did not terminate on a derived_from cycle in either engine, and a trace is untrusted input. The witness names the ancestor and both trust levels; the report's evidence lists the derived_from facts.

What the domain check found

ACS-REQ-0111 compared a step_id from entries_compacted with a provenance_id in derived_from. The spec defines the union in terms of provenance ids, but the payload lists step ids and nothing on the wire ties the two. The rule now reads the mapping from a new Guardian-state relation, step_provenance, and the provision is unevaluated when a Guardian does not supply it, instead of being approximated. Recorded as findings row 8 in the slices doc.

Checks

  • tsc, bun test ir (187 pass), every --check (census, markers apply, extract, render, compile, both marker patches), lint: all clean.
  • Differential with Soufflé 2.5 locally: conformant 0/0/0, violating 33/33/33 on both engines.
  • Soufflé's own type checker was tested against the domain rules before they were written: literals and head constants are accepted in subtype positions, a base-typed count compares with a subtype, and a join or comparison across two subtypes is an error.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm


Generated by Claude Code

The compiled program now reads as a Soufflé author would write it, and
the compiler checks what Soufflé checks:

- Every vocabulary column has a domain (default: its name; `request_seq`
  declares `domain: seq`). The emitter declares each as a subtype
  (`.type Seq <: number`) and the compiler refuses a rule that joins or
  compares two domains, so both engines reject the same rule.
- A violation head position may hold a constant. The nine rules that
  bound one through `Field = "..."` name it in the head.
- The §7.2 default-trust table is a static relation with facts instead
  of six rules with a vacuous body.
- Each provision's violation relation is its own typed `.output`. The
  packed three-column relation built with `cat`/`to_string` is gone; the
  differential reads the per-provision CSVs into the one-line form the
  fixtures pin.
- The ACS-REQ-0010 lineage closure is over (descendant, ancestor) pairs
  and terminates on a `derived_from` cycle. The witness names the
  ancestor and both levels; the evidence lists the derived_from facts.

The domain check found one modeling error: ACS-REQ-0111 compared a
step_id with a provenance_id. It now reads that mapping from a new
Guardian-state relation, `step_provenance`, and is unevaluated without
it. Recorded as findings row 8.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

Normative impact (ACS 0.1.2 at 6fce2a0)

Nothing added, changed, removed, or unmarked.

@afogel
afogel added this pull request to stack #48 September 14, 2026 14:18
…ions honestly

The affordance and requirement IDs from the shaping breadboard (U22,
N36, R4.6 and the like) no longer appear in rendered reports, the PR
comment, CLI errors, generated artifacts, the catalog notes that the
provision index renders, the ir README, or a CI step name. They stay in
code comments and the shaping documents, where they are traceability.

The PR comment's "added provisions with no conformance test" section
now lists only provisions a fixture could cite: Requirements with a
predicate of their own. Everything else added is listed by the reason
it takes no test (permission, non-testable, inexpressible, alias, or
not an obligation), so the count means what it says. On the V7 PR that
list would have read 63 rules, not 122 provisions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
The spec-lint workflow wrote an empty file when the base branch had no
ir/ tree, and the lint then failed parsing it instead of reporting an
empty baseline. The workflow now leaves the file absent, and the lint
reads an empty file as absent too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
The comment now leads with one line: how many rules were added without a
test, how many provisions changed and how many tests that puts under
review, what was removed without a tombstone, what normative text is
unmarked. Each non-empty section follows as a collapsible table with the
provision's title, actor, and a permalink to its line in the spec at
the pinned commit; a short table opens by default, a long one starts
collapsed. Empty sections are not rendered, and a change that touches
nothing produces one sentence.

The lint report carries what the renderer needs: titles from the
records, actor and place for each added provision, and the corpus's
remote URL for the permalinks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BX56nQPAsqYihopWfWy1xm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants