Skip to content

Commit bd76e87

Browse files
committed
Audit logging ADR
1 parent a7d996a commit bd76e87

7 files changed

Lines changed: 1173 additions & 14 deletions

File tree

build.sbt

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ lazy val root = (project in file("."))
122122
qualityDeequ, // Removed empty quality module per v1.0-2 plan
123123
examples,
124124
compileFailTests,
125-
it,
125+
experimental
126126
)
127127
.settings(
128128
name := "flowforge",
@@ -308,16 +308,6 @@ lazy val maintenanceCli = moduleProject("maintenance-cli")
308308

309309
// ===== ADDITIONAL MODULES =====
310310

311-
lazy val it = (project in file("integration-tests"))
312-
.dependsOn(examples, connectorsGcs, enginesSpark)
313-
.settings(
314-
name := "integration-tests",
315-
description := "Flowforge Integration tests",
316-
publish / skip := true,
317-
Test / fork := true,
318-
Test / skip := !sys.props.get("withSparkIT").contains("true"),
319-
)
320-
321311
// ===== SBT ALIASES =====
322312
addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt")
323313
addCommandAlias("fmtCheck", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck")
@@ -425,3 +415,16 @@ lazy val contractsSdk = moduleProject("contracts-sdk")
425415
generated
426416
}.taskValue,
427417
)
418+
// Experimental Scala 3 module for capture checking demos (opt-in)
419+
lazy val experimental = moduleProject("experimental")
420+
.settings(
421+
description := "Experimental Scala 3 POCs",
422+
scalaVersion := Dependencies.Versions.scala3,
423+
crossScalaVersions := Seq(Dependencies.Versions.scala3),
424+
scalacOptions ++= Seq(
425+
"-explain",
426+
"-source:3.3"
427+
),
428+
Compile / mainClass := Some("com.flowforge.experimental.caprese.Main"),
429+
publish / skip := true,
430+
)

docs/adr/future/adr-004-caprese-pure-udfs-non-escaping-capabilities.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,188 @@ Introduce a small, opt-in module `experimental-caprese`:
3737
## References
3838
- Scala 3 Capture Checking reference. :contentReference[oaicite:37]{index=37}
3939
- “Capturing Types” paper excerpt. :contentReference[oaicite:38]{index=38}
40+
- Noel Welsh, “Direct‑style Effects Explained” (Scala 3): https://noelwelsh.com/posts/direct-style/
41+
- Nicolas Rinaudo, “Effects as Capabilities”: https://nrinaudo.github.io/articles/capabilities.html
42+
- Nicolas Rinaudo, “Hands‑on Capture Checking”: https://nrinaudo.github.io/articles/capture_checking.html
43+
44+
---
45+
46+
## Related Work & Alignment (Post‑Reading Notes)
47+
48+
- Capabilities for control flow (Nicolas Rinaudo): https://nrinaudo.github.io/articles/capabilities_flow.html
49+
- Takeaway: model special powers (labels/boundaries) as capabilities that must not escape their scope.
50+
- Relevance: identical non‑escape discipline applies to data/IO resources (connectors, sessions, handles). Our goal is to ensure such capabilities cannot leak into pure UDFs or lazy structures.
51+
52+
- Effects as Capabilities (N. Rinaudo): https://nrinaudo.github.io/articles/capabilities.html
53+
- Takeaway: express required effects/resources as context functions (direct‑style) rather than monads, keeping execution requirements explicit and composable.
54+
- Relevance: informs an optional Scala 3 facade to declare engine/connectors as required capabilities without changing core runtime.
55+
56+
- Direct‑style Effects (N. Welsh): https://noelwelsh.com/posts/direct-style/
57+
- Takeaway: direct‑style APIs with effect handlers/context functions can improve ergonomics while preserving separation of description vs. action.
58+
- Relevance: we can prototype a facade using context functions alongside our existing tagless‑final API; this remains optional and non‑blocking.
59+
60+
Conclusion: the article validates our direction — use Scala 3 capture checking to enforce non‑escaping capabilities and pure arrows (`A -> B`) for UDFs.
61+
62+
## ADR Amendments (No deletions; clarifications and additions)
63+
64+
### 1) Terminology & Surface Types
65+
66+
- Adopt Scala 3 terminology explicitly:
67+
- Pure functions use the pure arrow: `A -> B` (non‑capturing).
68+
- Tracked capabilities use the caret type: `C^`.
69+
- Keep existing aliases (for readability) but document their mapping:
70+
- `type PureFn[-A,+B] = A -> B` (already planned — reaffirmed).
71+
72+
### 2) API Shapes Informed by Capabilities Article
73+
74+
Add the following opt‑in APIs (names stable; semantics experimental):
75+
76+
```scala
77+
// Scope a capability so it cannot escape; pure by construction
78+
def withCapability[C, A](acquire: => C)(use: C^ => A): A
79+
80+
// Effectful variant for interop (scoped within F, but still non‑escaping in the "use" lambda)
81+
def withCapabilityF[F[_], C, A](acquire: F[C])(use: C^ => F[A]): F[A]
82+
83+
// Pipeline builder addition: enforce purity at the type level for internal transforms
84+
def pureTransform[A, B](name: String)(f: A -> B): PipelineBuilder[WithTransform, F, In, B]
85+
```
86+
87+
Design notes:
88+
- `withCapability` mirrors the boundary/break model: callers get a scoped power (`C^`), but cannot store or return it.
89+
- `pureTransform` makes “pure inside, effects at the edges” the default for UDFs.
90+
91+
### 3) Laziness & Non‑Escape (Caveats)
92+
93+
Pitfalls to prevent (mirroring the article’s Iterator example):
94+
- Returning closures that capture `C^` from a `withCapability` region.
95+
- Storing `C^` in a field of an object that outlives the region.
96+
- Building lazy collections/streams that reference `C^` (evaluation may occur after the region closes).
97+
98+
Mitigations (compile‑time):
99+
- Let capture checking reject escaping `C^` in the cases above.
100+
- Provide guidance to prefer eager, total transformations in `A -> B`; if laziness is required, ensure all use happens inside the capability scope.
101+
102+
### 4) Error Message Guidance (DX)
103+
104+
When capture checking rejects a program, aim for messages of the form:
105+
106+
```
107+
Caprese: capability C^ escapes its scope
108+
• captured in closure returned from `withCapability` at Foo.scala:42
109+
• referenced by lazy value `it` evaluated outside scope
110+
Hint: compute eagerly inside `withCapability { (c: C^) => ... }` and return plain values (A -> B).
111+
```
112+
113+
### 5) Interop & Incremental Adoption
114+
115+
- Interop: allow existing `A => B` transforms to coexist; `pureTransform` is opt‑in.
116+
- Escape hatch (temporary): an explicit, scoped suppression annotation (e.g., `@capreseUnsafeEscape`) for code that cannot be rewritten immediately. Not for production paths; tracked in CI.
117+
118+
### 6) Open Questions to Validate in POC
119+
120+
- False positives/negatives around laziness (Iterators, Streams, fs2/ZIO streams).
121+
- Ergonomics: can we keep ceremony low for common UDFs (does `A -> B` feel natural for teams)?
122+
- Tooling: scalafix lints to recommend `pureTransform` for obvious pure lambdas.
123+
124+
### 6.1) POC Hardening Plan (Realistic, Value‑Add)
125+
126+
- Tests that must fail compilation (ensure capture checking works in practice):
127+
1. Returning `C^` from `withCapability`.
128+
2. Storing `C^` in an object that outlives the scope (val/field).
129+
3. Creating lazy collections/streams (Iterator/Stream/fs2/ZIO) that capture `C^` then evaluate outside scope.
130+
4. Writing a `pureTransform` that closes over an IO handle (should be rejected).
131+
132+
- Tests that must pass:
133+
1. `A -> B` transforms with no captures; composition of multiple pure transforms.
134+
2. `withCapability` used to compute a plain `A` result that does not leak `C^`.
135+
3. Interop: `A => B` transforms continue to work (without purity guarantees).
136+
137+
- DX checks:
138+
- Error message clarity: include escape site and hint (see Error Message Guidance above).
139+
- Scalafix lint (advisory): suggest `pureTransform` when lambda is syntactically pure.
140+
141+
- Deliverables (time‑boxed):
142+
- Experimental Scala 3 module (exists: experimental‑caprese) with CI task to compile both “good” and “bad” examples.
143+
- Short migration note for authors (how to move `A => B` to `A -> B`).
144+
145+
### 7) Adoption Plan (Updated)
146+
147+
Phase 0 (branch):
148+
- Ship `experimental-caprese` module behind a flag; examples require `import language.experimental.captureChecking`.
149+
150+
Phase 1 (pilot):
151+
- Convert 1–2 inner transforms to `pureTransform`; wrap one connector with `withCapability`.
152+
- Measure compile errors and developer friction; refine error text.
153+
154+
Phase 2 (template):
155+
- Add a template switch that generates pipelines using `pureTransform` by default, with a documented interop path for `A => B`.
156+
157+
### 7.1) Optional Scala 3 Facade (Direct‑Style) — Exploratory Only
158+
159+
- Goal: improve ergonomics for some code by expressing required resources as capabilities via context functions, while keeping core runtime unchanged.
160+
- Sketch (non‑binding):
161+
162+
```scala
163+
// Requires a DataAlgebra capability in scope; still pure in the middle
164+
def runJob[A, B](using da: DataAlgebraCapability): A -> B = ???
165+
166+
// IO edges remain effect‑polymorphic (Cats‑Effect/ZIO) in existing APIs
167+
def read[F[_]: EffectSystem, A](src: DataSource): F[Dataset[A]]
168+
```
169+
170+
- Constraints:
171+
- Facade must not change engine adapters or distributed semantics.
172+
- Opt‑in; keep Tagless‑Final `F[_]` API as the stable default.
173+
- Ship only after POC proves value and zero regression in clarity/perf.
174+
175+
### 8) Non‑Goals (unchanged)
176+
177+
- No changes to engine adapters or distributed runtime semantics.
178+
- No promise to enforce purity across third‑party libraries; only at our API boundaries.
179+
180+
### 10) Bottom Line & Scope (Sign‑off Criteria)
181+
182+
- This ADR is about compile‑time guarantees that add real engineering value: pure UDFs (`A -> B`) and non‑escaping capabilities (`C^`).
183+
- Runtime stays the same: effect‑polymorphic edges (Cats‑Effect/ZIO), algebraic engine boundary, no engine rewrites.
184+
- Optional Scala 3 facade (direct‑style) is exploratory and strictly additive; we will not gate core features on it.
185+
- Success criteria:
186+
- Clear compile errors on escapes; zero runtime cost for purity.
187+
- Authors can adopt `A -> B` incrementally; interop remains smooth.
188+
- No regressions in performance or operator ergonomics.
189+
190+
### 9) Traceability
191+
192+
- This amendment was informed by: “Capabilities and Control Flow in Scala”, Nicolas Rinaudo (link above). The non‑escape discipline and laziness caveats map directly to our goals for pure UDFs and scoped resources.
193+
194+
---
195+
196+
## Appendix — Tiny Examples (for reviewers)
197+
198+
Scala 3 source files should include:
199+
```scala
200+
import language.experimental.captureChecking
201+
```
202+
203+
1) Rejected (capability escapes)
204+
```scala
205+
def withCapability[C, A](acquire: => C)(use: C^ => A): A = { val c = acquire; use(c) }
206+
final case class Connector(token: String)
207+
208+
// ERROR: C^ escapes the capability scope
209+
def unsafeEscape: Connector =
210+
withCapability(Connector("t")) { c =>
211+
c // <- reject: returning the capability
212+
}
213+
```
214+
215+
2) Accepted (pure transform and scoped capability)
216+
```scala
217+
type PureFn[-A,+B] = A -> B
218+
val pureUpper: PureFn[String, String] = s => s.toUpperCase
219+
220+
def safeUse: String =
221+
withCapability(Connector("t")) { c =>
222+
pureUpper("ok") // c is used but does not escape
223+
}
224+
```

docs/archive/brainstorming/brainstorming/flowforge/kyo-caprese.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ lazy val experimentalSettings = Seq(
376376
scalacOptions ++= Seq("-Xfatal-warnings")
377377
)
378378

379-
lazy val experimentalCaprese = (project in file("modules/experimental-caprese"))
379+
lazy val experimental = (project in file("modules/experimental-caprese"))
380380
.settings(name := "flowforge-experimental-caprese")
381381
.settings(experimentalSettings)
382382

@@ -393,7 +393,7 @@ lazy val experimentalKyo = (project in file("modules/experimental-kyo"))
393393
)
394394

395395
lazy val experimentalExamples = (project in file("modules/experimental-examples"))
396-
.dependsOn(experimentalCaprese, experimentalKyo, core /* your core module id */)
396+
.dependsOn(experimental, experimentalKyo, core /* your core module id */)
397397
.settings(experimentalSettings)
398398
```
399399

@@ -604,7 +604,7 @@ jobs:
604604
- uses: actions/setup-java@v4
605605
with: { distribution: 'temurin', java-version: ${{ matrix.java }} }
606606
- name: SBT test (experimental only)
607-
run: sbt "project experimentalCaprese" test "project experimentalKyo" test "project experimentalExamples" test
607+
run: sbt "project experimental" test "project experimentalKyo" test "project experimentalExamples" test
608608
```
609609
610610
Optionally keep it **non-blocking** at first (soft gate). Later, promote to hard gate.

docs/talks/Managers-Summary.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Manager’s Summary — Compile‑Time Contracts & Fiber‑Safe Pipelines (1‑pager)
2+
3+
- What it is
4+
- A design approach for data pipelines in Scala where schema contracts are enforced at compile time and orchestration respects effect/fiber safety at runtime.
5+
- Business logic stays pure and testable; IO is explicit and resource‑safe.
6+
7+
- Why it matters (outcomes)
8+
- Fewer incidents: Contract drift blocked before deployment (compile gate), reducing data quality outages.
9+
- Faster remediation: Clear, actionable compiler errors pinpoint Missing/Extra/Mismatched fields.
10+
- Higher developer velocity: Pure transformations unit‑test in milliseconds; fewer flaky E2E tests.
11+
- Portability: Same pipeline logic runs on multiple engines (e.g., Spark/Flink) via a trait‑based runner.
12+
- Compliance & governance: Typed contracts + policy variants encode intent and enforce via CI.
13+
14+
- How it works (high level)
15+
- Compile‑time: Case classes → Magnolia Shape → Schema AST → policy compare → compile success or fail.
16+
- Runtime: Pipelines are Kleisli graphs executed with a fiber‑aware effect system (Cats‑Effect/ZIO), with explicit resource safety.
17+
18+
- ROI levers (example targets over 6–12 months)
19+
- 50–80% reduction in schema‑related incidents in batch/stream pipelines.
20+
- 30–50% reduction in E2E test runtime by shifting to pure unit tests for inner transforms.
21+
- 20–40% faster onboarding due to templates and policy‑driven guardrails.
22+
- 25–40% fewer ad‑hoc hotfixes caused by unplanned contract changes.
23+
24+
- Costs and risks
25+
- Upfront learning: Team needs to learn the idioms (phantom types, type classes, Kleisli, effect systems).
26+
- Template/CI adoption: Requires build and CI wiring to enforce compile gates.
27+
- Integration work: Engine adapters (Kafka/Spark/Flink) and DQ preferences (native vs Deequ) must be chosen per team.
28+
29+
- Risk mitigations
30+
- Start with one golden path template; demonstrate red→green contract fixes in CI.
31+
- Pick a single effect system per service (IO or ZIO) to limit cognitive load.
32+
- Phase policies: begin with Exact for critical interfaces, use Backward/Forward during migrations.
33+
34+
- KPIs to track
35+
- Contract drift incidents per quarter; MTTR for data breakages; test runtime; percentage of pipelines on the template; change failure rate for schema‑touching PRs.
36+
37+
- Adoption plan (90 days)
38+
- Weeks 1–2: Pilot one pipeline; wire compile‑fail tests and CI policy gates.
39+
- Weeks 3–6: Migrate 2–3 critical pipelines; add DQ checks; define escalation paths.
40+
- Weeks 7–12: Roll out template; publish docs; set KPIs on the engineering scorecard.
41+
42+
- Sound bites
43+
- “If it compiles, contracts align.”
44+
- “Pure inside, effects at the edges.”
45+
- “Typed pipelines, portable engines, safer operations.”

0 commit comments

Comments
 (0)