diff --git a/Zeta.sln b/Zeta.sln index 2914978a..22b48c73 100644 --- a/Zeta.sln +++ b/Zeta.sln @@ -29,6 +29,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "CrmSample", "samples\CrmSam EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FactoryDemo.Api.CSharp", "samples\FactoryDemo.Api.CSharp\FactoryDemo.Api.CSharp.csproj", "{E1F00C7B-E03C-4DA4-8B47-E872973529D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -183,11 +185,25 @@ Global {D44AB9CA-F491-41F4-96CE-B061238F3D6E}.Release|x64.Build.0 = Release|Any CPU {D44AB9CA-F491-41F4-96CE-B061238F3D6E}.Release|x86.ActiveCfg = Release|Any CPU {D44AB9CA-F491-41F4-96CE-B061238F3D6E}.Release|x86.Build.0 = Release|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Debug|x64.Build.0 = Debug|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Debug|x86.Build.0 = Debug|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Release|Any CPU.Build.0 = Release|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Release|x64.ActiveCfg = Release|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Release|x64.Build.0 = Release|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Release|x86.ActiveCfg = Release|Any CPU + {E1F00C7B-E03C-4DA4-8B47-E872973529D1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {D44AB9CA-F491-41F4-96CE-B061238F3D6E} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {40534D09-439E-4E5F-9A69-A73844DB674D} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {E1F00C7B-E03C-4DA4-8B47-E872973529D1} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} EndGlobalSection EndGlobal diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 10dbf4c2..491febf0 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -9542,6 +9542,51 @@ Aarav. --- +## P2 — FactoryDemo C# sample — deterministic smoke-test startup + +- [ ] **Smoke-test port allocation — replace random-in-range + with OS-assigned ephemeral port.** Codex P2 on PR #147: + `samples/FactoryDemo.Api.CSharp/smoke-test.sh` currently + picks a port from `5100-5499` via `RANDOM`, assumes it is + free, and fails with an address-in-use error if the port + is already occupied (parallel CI jobs, local services, + another smoke run). This creates flaky false negatives + unrelated to API correctness. **Scope:** two viable + approaches — (a) bind `--urls "http://127.0.0.1:0"` and + parse the chosen port from the Kestrel startup line in + the log file, or (b) pre-probe ports in the range with + `nc -z` / `/dev/tcp` and retry until one is free. + Approach (a) is preferred (truly deterministic; no race + window between probe and bind). Sibling + `samples/FactoryDemo.Api.FSharp/smoke-test.sh` has the + same issue and should be fixed in parallel. **Effort:** + S (shell-only, maybe 20 lines across both scripts plus a + log-line parser). **Owner:** devops-engineer. **Source:** + Codex thread `PRRT_kwDOSF9kNM59gdjf` on PR #147. + +## P2 — FactoryDemo C# sample — solution project-type GUID hygiene + +- [ ] **Align `FactoryDemo.Api.CSharp` project-type GUID + with other SDK-style C# projects.** Copilot finding on + PR #147: `Zeta.sln` lists `FactoryDemo.Api.CSharp` under + the legacy C# project-type GUID + `{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}` while every + other SDK-style C# project (`Core.CSharp`, + `Tests.CSharp`, `Core.CSharp.Tests`) uses the modern + SDK-style GUID `{9A19103F-16F7-4668-BE54-9A1E7A4F7556}`. + Mixing GUIDs can confuse solution-filter tooling and + Visual-Studio solution-node grouping; the `.csproj` + itself is already `` + so the legacy GUID is purely cosmetic drift from the + initial `dotnet new` template. **Scope:** change the + single GUID in `Zeta.sln` line 32; verify `dotnet + build -c Release` stays at 0W/0E. **Effort:** XS + (single-line GUID swap). **Owner:** devops-engineer or + msbuild-expert. **Source:** Copilot thread + `PRRT_kwDOSF9kNM59geKB` on PR #147. + +--- + ## Source of this backlog - `docs/MISSED-ITEMS-AUDIT.md` — per-round sweep diff --git a/docs/hygiene-history/live-lock-audit-history.md b/docs/hygiene-history/live-lock-audit-history.md index eeb5dba5..ad915758 100644 --- a/docs/hygiene-history/live-lock-audit-history.md +++ b/docs/hygiene-history/live-lock-audit-history.md @@ -37,3 +37,61 @@ The full memory context is | date (UTC) | window | EXT | INTL | SPEC | OTHR | smell? | notes | |---|---:|---:|---:|---:|---:|---|---| | 2026-04-23 | 25 | 0% | 72% | 16% | 12% | **FIRING** | Inaugural run. Last 25 merged commits on `origin/main` contain zero src/tests/samples/bench changes. Factory has been running purely on tick-history + BACKLOG + research output for weeks. Response arc: PR #141 (ServiceTitan CRM demo sample) is the pattern-breaker; once #141 merges, the next audit should show non-zero EXT. Audit script landed this run. | + +## Lessons integrated + +Per `memory/feedback_lesson_permanence_is_how_we_beat_arc3_and_dora_2026_04_23.md`, +every live-lock firing files a lesson here. Each lesson names the +**signature** (what pattern preceded the smell), the **mechanism** +(what caused it), and the **prevention** (what decisions avoid +re-occurrence). Consult this section before opening a speculative arc +— prevention is upstream of detection. + +### 2026-04-23 — tick-history-and-BACKLOG-dominance-with-zero-src + +- **Signature.** 25 consecutive merged commits on `origin/main` with + exactly zero changes to `src/`, `tests/`, `samples/`, or `bench/`. + Every commit was either a tick-history row, a BACKLOG row, a + research doc, a capability-map, or an ADR. No external-observable + product motion. +- **Mechanism.** The autonomous-loop cron fires every minute per + `docs/AUTONOMOUS-LOOP.md`. The standing never-idle discipline + (`memory/feedback_never_idle_speculative_work_over_waiting.md`) + says speculative work is valid non-idle. But there was no + counter-balancing force pulling the loop toward external-code + work. Every tick, the lowest-friction move was another + tick-history append or BACKLOG grooming. Compounded over dozens of + ticks, the factory drifted into pure meta-work without any agent + catching the drift. +- **Prevention (decisions to embed forward).** + 1. **External-priority stack is authoritative, agent-reorderable + only for internal priorities.** + `memory/project_aaron_external_priority_stack_and_live_lock_smell_2026_04_23.md` + names Aaron's stack (ServiceTitan+UI / Aurora / multi-algebra + DB / cutting-edge persistence) as externally-set; the agent + owns internal priorities but not the external stack's + ordering. Speculative work lives under internal; + external-priority work takes precedence when the ratio tilts. + 2. **Live-lock audit runs at round-close as a gate-not-a-report.** + `tools/audit/live-lock-audit.sh` exits 1 when EXT < 20%; + round-close should check this signal. A smell-firing + round-close must include at least one external-priority + increment in the next round's plan before the close ledger + accepts. + 3. **Speculative-work permit requires external-ratio check.** + Before opening a new speculative arc (research doc, large + BACKLOG row, capability map), agent reads the current audit + ratio. If smell is firing, no new speculative arcs open until + one external-priority increment lands. This is an *agent- + internal discipline*, not a blocking rule — but it gets cited + in the commit message ("audit EXT=X%, smell=not-firing, + speculative arc opens") so the discipline is visible. + 4. **Tick-history rows are NOT external work.** The tick-history + append is ledger-keeping, not product motion. Counting it as + "forward motion" was the silent-drift mechanism. Agents + should explicitly describe tick-history work as INTL and + pair it with EXT work in the same tick when the smell is + near firing. +- **Open carry-forward.** The round-close-ladder wiring is a + follow-up — the audit script is landed, but it is not yet + invoked automatically at round-close. BACKLOG P1 row filed. diff --git a/docs/pr-preservation/147-drain-log.md b/docs/pr-preservation/147-drain-log.md new file mode 100644 index 00000000..05156870 --- /dev/null +++ b/docs/pr-preservation/147-drain-log.md @@ -0,0 +1,156 @@ +# PR #147 review-drain log + +Git-native preservation of the 7 unresolved review threads +drained on `feat/servicetitan-factory-demo-api-csharp` at +2026-04-24. One entry per thread; each ends in a paired +`resolveReviewThread` mutation. Retained as part of the PR's +own commits so the audit trail survives regardless of GitHub +UI state (Otto-238 retractability + glass-halo). + +--- + +## Thread PRRT_kwDOSF9kNM59eE_k + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `.claude/commands/btw.md`:10 +- **Original comment**: P1: This command doc uses the human maintainer’s name directly (e.g., “(Aaron)”), but the repo’s operational standing rule is “No name attribution in code, docs, or skills” — use role references instead (docs/AGENT-BEST-PRACTICES.md:284-292). Please rewrite to “human maintainer” (and apply the same sweep to other new docs/skills in this PR that use the name). +- **Outcome**: fix-inline +- **Your reply**: Fixed — swept every name reference in `.claude/commands/btw.md` to "human maintainer" / "the maintainer". Checked the rest of the PR: only `btw.md` is a new doc/skill in this PR with name attribution; other new docs (`samples/FactoryDemo.Api.CSharp/**`, `tools/audit/live-lock-audit.sh`) already use role-refs. Fix in the drain commit. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59eFAE + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `tools/audit/live-lock-audit.sh`:54 +- **Original comment**: P2: `git diff-tree --no-commit-id --name-only -r` does not report paths for the repository’s root commit unless `--root` is provided. If the window includes the root commit (small repos / large windows), it will be misclassified as having no files. Consider adding `--root` to make classification correct for all commits. +- **Outcome**: fix-inline +- **Your reply**: Fixed — added `--root` to the `git diff-tree` invocation and expanded the comment to explain why. Otherwise the root commit silently misclassifies to OTHR (no files). Thanks. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59eFAU + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `samples/FactoryDemo.Api.CSharp/Program.cs`:21 +- **Original comment**: P1: Root endpoint parity issue — the F# sibling includes "/" in its advertised `endpoints` list, but this C# list omits it. That makes `/` JSON differ between implementations, contradicting the parity guarantee. Consider adding "/" to this list (and updating the smoke-test expected length accordingly), or removing it from the F# sibling so both match. +- **Outcome**: fix-inline +- **Your reply**: Fixed — added `"/"` at the head of the C# endpoints list (9 entries, matching F#'s list order), bumped `smoke-test.sh` expected length from 8 to 9, and updated the script header comment from "all 8 API endpoints plus the root `/`" to "all 9 endpoints". Parity restored. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59eFAn + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `samples/FactoryDemo.Api.CSharp/smoke-test.sh`:38 +- **Original comment**: P2: The comment claims `mktemp` "works on hosts without a writable /tmp", but `mktemp -t ...` still requires a writable temp directory (typically `$TMPDIR` or `/tmp`). Suggest rewording to avoid implying it works without any writable temp dir (or explicitly mention it uses the system temp dir). +- **Outcome**: fix-inline +- **Your reply**: Fixed — reworded the comment to "writes into the host's system temp dir (honouring `$TMPDIR` when set, falling back to `/tmp`)" so the behaviour is accurately described without the misleading "without a writable /tmp" phrasing. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59eFA2 + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `samples/ServiceTitanCrm/ServiceTitanCrm.fsproj`:5 +- **Original comment**: P1: Repo convention is that demos stay company-neutral (no `samples/ServiceTitan*/` dirs / namespaces). This project name and RootNamespace are ServiceTitan-specific, which the repo feedback explicitly calls out to avoid. Please rename this sample to a generic `FactoryDemo.*`-style name (folder, project, RootNamespace) per `memory/feedback_open_source_repo_demos_stay_generic_not_company_specific_2026_04_23.md:59-66`. +- **Outcome**: fix-inline +- **Your reply**: Fixed — renamed `samples/ServiceTitanCrm/` → `samples/FactoryDemo.Crm/`, `ServiceTitanCrm.fsproj` → `FactoryDemo.Crm.fsproj`, module `Zeta.Samples.ServiceTitanCrm.Program` → `Zeta.Samples.FactoryDemo.Crm.Program`, `RootNamespace` → `Zeta.Samples.FactoryDemo.Crm`. Updated `Zeta.sln` project entry, the ServiceTitan-CRM comment in `Program.fs`, and the `tests/Tests.FSharp/Operators/CrmScenarios.Tests.fs` doc comment. Also removed stray `samples/ServiceTitanFactoryApi/` bin+obj residue left over from an earlier rename. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59eFR4 + +- **Reviewer**: chatgpt-codex-connector +- **File**: `src/Core/SignalQuality.fs`:175 +- **Original comment**: P1: Avoid quarantining short text due gzip header overhead. `compressionRatio` clamps any ratio above `1.0` to `1.0`, and `compressionMeasure` maps that directly to severity, so short non-empty inputs (where gzip header/trailer overhead exceeds raw length) are deterministically scored as maximally suspicious. In practice this means many legitimate short payloads get `Quarantine`, which skews composite quality scoring and can trigger false failures even when content is fine. +- **Outcome**: fix-inline +- **Your reply**: Fixed — added a named `compressionMinInputBytes = 64` threshold. Inputs shorter than 64 bytes short-circuit to `0.0` (neutral Pass) in both `compressionRatio` and `compressionMeasure`, with explicit evidence noting the gzip-header-dominates regime. Rationale inline at the constant's docstring (gzip header 10 B + trailer 8 B + deflate overhead; 64 B leaves enough payload that honest incompressible data can still score high). Existing tests remain valid — the 26-char clamp test asserts `0.0 ≤ ratio ≤ 1.0` which `0.0` satisfies, and the end-to-end structured-vs-fluff test uses inputs well above threshold. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59eFR8 + +- **Reviewer**: chatgpt-codex-connector +- **File**: `samples/FactoryDemo.Api.CSharp/Program.cs`:24 +- **Original comment**: P2: Include root path in C# endpoint index for parity. The C# root metadata omits `"/"` from `endpoints`, while the F# sibling publishes it, so the advertised contract is no longer parity-compatible across the two sample APIs. Any consumer using root endpoint discovery (or parity checks) will see different endpoint sets despite the sample claiming interchangeable behavior. +- **Outcome**: fix-inline (same fix as PRRT_kwDOSF9kNM59eFAU above) +- **Your reply**: Fixed — added `"/"` to the C# endpoints list (same fix as Copilot's P1 on the same file). C# list is now 9 entries in F#-matching order; smoke-test length expectation updated from 8 → 9. Parity restored. +- **Resolution commit**: drain commit on `feat/servicetitan-factory-demo-api-csharp` + +--- + +All 7 threads (first pass): fix-inline. No BACKLOG rows owed. + +## 2026-04-24 — second pass (7 NEW post-merge threads) + +After the first-pass merge brought in 4 merge commits from +`origin/main`, Copilot + Codex re-ran and surfaced 7 new threads. +Draining them here with the same per-thread audit-trail shape. + +## Thread PRRT_kwDOSF9kNM59erUL + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `docs/operator-input-quality-log.md`:7 +- **Original comment**: P1: This new doc repeatedly uses the contributor name "Aaron" in narrative text. Repo rule is "No name attribution in code, docs, or skills"—names should appear only under memory/persona// and optionally docs/BACKLOG.md. Please rewrite to role refs like "human maintainer" / "human operator" (docs/AGENT-BEST-PRACTICES.md:284-292). +- **Outcome**: scope-bleed — file is NOT in PR #147 diff (landed via commit 204bbb6 on main before this PR's branch point). +- **Your reply**: Scope-bleed — `docs/operator-input-quality-log.md` is not in this PR's diff (landed via 204bbb6 on main). The name-attribution sweep is a main-line concern; tracking under `docs/FACTORY-HYGIENE.md` / BACKLOG "name-attribution sweep" rather than this PR. Resolving as out-of-PR-scope; the sweep will catch it. +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59erUu + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `samples/FactoryDemo.Api.CSharp/Seed.cs`:36 +- **Original comment**: P2: Seed data includes a person name that looks like it could be the maintainer ("Aaron Smith"). Given the repo's "no name attribution in code/docs/skills" discipline, it's safer to use a clearly fictional/company-neutral placeholder name here to avoid accidental attribution confusion. +- **Outcome**: fix-inline +- **Your reply**: Fixed — renamed row 13 seed entry from `"Aaron Smith (new contact)"` to `"Acme Contact (new lead)"` in both `samples/FactoryDemo.Api.CSharp/Seed.cs:34` and the F# sibling `samples/FactoryDemo.Api.FSharp/Seed.fs:63`, and updated the adjacent email-collision comment. Both siblings stay in parity and use a clearly fictional / company-neutral placeholder. +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59erVH + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `drop/README.md`:12 +- **Original comment**: The doc says this README is the only tracked file in `drop/`, but this PR also tracks `drop/.gitignore`. Please correct the wording so it matches the actual tracked sentinel design (README.md + .gitignore). +- **Outcome**: fix-inline +- **Your reply**: Fixed — reworded to "Together with the sibling `drop/.gitignore` it is one of the two tracked sentinel files in `drop/` — the README explains the shape and the `.gitignore` implements it (gitignore-except-sentinel)." Matches the actual tracked state (`README.md` + `.gitignore`). +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59erVa + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `tools/audit/live-lock-audit.sh`:11 +- **Original comment**: P1: This script comment attributes the directive to a specific contributor name ("Aaron's …"). Repo operational rule is "No name attribution in code, docs, or skills"—use role references like "human maintainer" instead (docs/AGENT-BEST-PRACTICES.md:284-292). +- **Outcome**: fix-inline +- **Your reply**: Fixed — rewrote the docstring attribution from `"Aaron's 2026-04-23 directive"` to `"Human-maintainer 2026-04-23 directive"`. Role ref per the rule. +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59erVr + +- **Reviewer**: copilot-pull-request-reviewer +- **File**: `docs/AUTONOMOUS-LOOP.md`:None +- **Original comment**: P1: This doc uses the contributor name "Aaron" in guidance text (e.g., "flag to Aaron"). Repo rule is to avoid contributor names in docs; use a role reference ("human maintainer") instead (docs/AGENT-BEST-PRACTICES.md:284-292). +- **Outcome**: already-addressed + scope-bleed. `docs/AUTONOMOUS-LOOP.md` is NOT in this PR's diff, and a repo-wide grep on HEAD shows zero `Aaron` occurrences in that file — the "flag to the human maintainer" line already uses the role reference (line 204-205). Copilot appears to be reviewing against stale context. +- **Your reply**: Already-addressed + scope-bleed. `docs/AUTONOMOUS-LOOP.md` is not in this PR's diff, and `grep -i aaron docs/AUTONOMOUS-LOOP.md` on HEAD returns zero matches — the "flag to the human maintainer" phrasing is already a role reference (line 204-205). Copilot flagged stale context. Resolving. +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59er9T + +- **Reviewer**: chatgpt-codex-connector +- **File**: `tools/audit/live-lock-audit.sh`:63 +- **Original comment**: P1: Handle merge commits when collecting changed paths. The classifier uses `git diff-tree --no-commit-id --name-only -r --root "$sha"`, which yields no file list for merge commits unless merge mode is requested. `git diff-tree -h` explicitly exposes merge-specific modes (`-m`, `-c`, `--cc`); in this repo `df1f2f1` returns 0 paths without `-m` but returns changed files with `-m`. Merge commits are currently mis-bucketed as `OTHR`, skewing EXT/INTL/SPEC percentages and potentially triggering or suppressing the live-lock smell incorrectly. +- **Outcome**: fix-inline +- **Your reply**: Fixed — added `-m` to the `git diff-tree` invocation and pipe through `sort -u` to fold duplicate paths across parents. Expanded the comment to explain the merge-commit pitfall (zero paths → mis-bucket as OTHR → skewed EXT/INTL/SPEC ratios → the live-lock gate can fire or suppress incorrectly). Re-ran `tools/audit/live-lock-audit.sh 10` against the current window and merges like `f1dc2bb` now bucket on their real paths. Thanks. +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +## Thread PRRT_kwDOSF9kNM59er9W + +- **Reviewer**: chatgpt-codex-connector +- **File**: `tests/Tests.FSharp/Algebra/SignalQuality.Tests.fs`:1 +- **Original comment**: P2: Include new SignalQuality/CRM tests in the test project. This new test file is not actually executed because `tests/Tests.FSharp/Tests.FSharp.fsproj` uses an explicit `` list and does not include `Algebra/SignalQuality.Tests.fs` (or `Operators/CrmScenarios.Tests.fs`). +- **Outcome**: already-addressed — both files ARE listed in `tests/Tests.FSharp/Tests.FSharp.fsproj` (line 26: `Algebra/SignalQuality.Tests.fs`; line 49: `Operators/CrmScenarios.Tests.fs`). `dotnet build -c Release` on current HEAD produces `Tests.FSharp.dll` with these test classes in it. Codex flagged stale context (likely the first iteration of the branch before the `` rows were added). +- **Your reply**: Already-addressed — both test files are in `tests/Tests.FSharp/Tests.FSharp.fsproj` (line 26 for `Algebra/SignalQuality.Tests.fs`, line 49 for `Operators/CrmScenarios.Tests.fs`). Build produces `Tests.FSharp.dll` with those test classes; CI enforces them. Codex appears to have flagged an earlier branch state. Resolving. +- **Resolution commit**: second-pass drain commit on `feat/servicetitan-factory-demo-api-csharp` + +--- + +Second-pass summary: + +- 4 fix-inline (Seed.cs + Seed.fs name, drop/README wording, live-lock-audit.sh attribution, live-lock-audit.sh `-m` flag). +- 3 scope-bleed / already-addressed (operator-input-quality-log out-of-PR, AUTONOMOUS-LOOP out-of-PR with zero Aaron on HEAD, SignalQuality/CRM tests already in fsproj). + +Build: `dotnet build -c Release` — 0W/0E. +Audit sanity: `tools/audit/live-lock-audit.sh 10` — still healthy with merge-commits now bucketed correctly. diff --git a/samples/FactoryDemo.Api.CSharp/Activity.cs b/samples/FactoryDemo.Api.CSharp/Activity.cs new file mode 100644 index 00000000..5cbc4a08 --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/Activity.cs @@ -0,0 +1,9 @@ +namespace Zeta.Samples.FactoryDemo.Api; + +public record Activity( + long Id, + long CustomerId, + long? OpportunityId, + string Kind, + string Notes, + DateTimeOffset OccurredAt); diff --git a/samples/FactoryDemo.Api.CSharp/Customer.cs b/samples/FactoryDemo.Api.CSharp/Customer.cs new file mode 100644 index 00000000..a9867a74 --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/Customer.cs @@ -0,0 +1,10 @@ +namespace Zeta.Samples.FactoryDemo.Api; + +public record Customer( + long Id, + string Name, + string Email, + string Phone, + string Address, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt); diff --git a/samples/FactoryDemo.Api.CSharp/FactoryDemo.Api.CSharp.csproj b/samples/FactoryDemo.Api.CSharp/FactoryDemo.Api.CSharp.csproj new file mode 100644 index 00000000..8aa884ac --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/FactoryDemo.Api.CSharp.csproj @@ -0,0 +1,10 @@ + + + net10.0 + Exe + Zeta.Samples.FactoryDemo.Api + enable + enable + true + + diff --git a/samples/FactoryDemo.Api.CSharp/Opportunity.cs b/samples/FactoryDemo.Api.CSharp/Opportunity.cs new file mode 100644 index 00000000..08abe1da --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/Opportunity.cs @@ -0,0 +1,9 @@ +namespace Zeta.Samples.FactoryDemo.Api; + +public record Opportunity( + long Id, + long CustomerId, + string Stage, + long AmountCents, + DateTimeOffset CreatedAt, + DateTimeOffset UpdatedAt); diff --git a/samples/FactoryDemo.Api.CSharp/Program.cs b/samples/FactoryDemo.Api.CSharp/Program.cs new file mode 100644 index 00000000..b91cfe96 --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/Program.cs @@ -0,0 +1,86 @@ +using Zeta.Samples.FactoryDemo.Api; + +// Minimal C# ASP.NET Core Web API serving the seed data for the +// factory-demo. Companion to the F# sibling at +// `samples/FactoryDemo.Api.FSharp/` — same 9 endpoints, same JSON +// shapes, same seed data. Any frontend consumes either one +// interchangeably. +// +// Why both F# and C# versions: the factory produces code in +// the target audience's stack. Many adopting teams run C# with +// no F# exposure; the C# version minimises adoption friction +// while the F# version stays the factory's reference-language +// baseline (F# looks closer to math, so theorems over the +// algebra are easier to express there). + +// Static endpoint list — extracted to satisfy CA1861 (avoid re-allocating +// the array on every request to the root endpoint). Advertises all 9 +// endpoints including `/` and the parameterised `{id}` routes, so the +// root is an honest index of what the API is actually serving. Matches +// the F# sibling's list exactly (parity guarantee: same 9 endpoints, +// same order) — any frontend consumes either implementation without +// seeing a different endpoint set at `/`. +string[] endpoints = +[ + "/", + "/api/customers", + "/api/customers/{id}", + "/api/customers/{id}/activities", + "/api/opportunities", + "/api/opportunities/{id}", + "/api/activities", + "/api/pipeline/funnel", + "/api/pipeline/duplicates", +]; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddEndpointsApiExplorer(); +var app = builder.Build(); + +app.MapGet("/", () => new +{ + name = "Factory-demo API (C#)", + version = "0.0.1", + endpoints, +}); + +app.MapGet("/api/customers", () => Seed.Customers); +app.MapGet("/api/customers/{id:long}", (long id) => + Seed.Customers.FirstOrDefault(c => c.Id == id) is { } c + ? Results.Ok(c) + : Results.NotFound()); + +app.MapGet("/api/customers/{id:long}/activities", (long id) => + Seed.Activities.Where(a => a.CustomerId == id)); + +app.MapGet("/api/opportunities", () => Seed.Opportunities); +app.MapGet("/api/opportunities/{id:long}", (long id) => + Seed.Opportunities.FirstOrDefault(o => o.Id == id) is { } o + ? Results.Ok(o) + : Results.NotFound()); + +app.MapGet("/api/activities", () => Seed.Activities); + +// MA0002 requires an explicit comparer for string GroupBy; ordinal is +// correct for our seed data — emails are already lowercased and ASCII. +app.MapGet("/api/pipeline/funnel", () => + Seed.Opportunities + .GroupBy(o => o.Stage, StringComparer.Ordinal) + .Select(g => new + { + Stage = g.Key, + Count = g.Count(), + TotalCents = g.Sum(o => o.AmountCents), + })); + +app.MapGet("/api/pipeline/duplicates", () => + Seed.Customers + .GroupBy(c => c.Email, StringComparer.Ordinal) + .Where(g => g.Count() > 1) + .Select(g => new + { + Email = g.Key, + CustomerIds = g.Select(c => c.Id).ToArray(), + })); + +app.Run(); diff --git a/samples/FactoryDemo.Api.CSharp/README.md b/samples/FactoryDemo.Api.CSharp/README.md new file mode 100644 index 00000000..b6512791 --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/README.md @@ -0,0 +1,90 @@ +# Factory-demo — JSON API (C#) + +**What this is:** The C# version of the factory-demo JSON API. +Identical 9 endpoints, identical JSON shapes, identical seed +data as the F# sibling at `samples/FactoryDemo.Api.FSharp/`. +Minimal ASP.NET Core, no heavy frameworks. + +**Why C# leads:** C# is the more popular language in the .NET +ecosystem by a wide margin; starting the factory demo in C# +meets the largest audience where they already are. The F# +sibling is the reference — F# looks closer to math, so +theorems over the algebra are easier to express — but the +demo path-of-least-friction is C#. + +**What this demonstrates about the factory:** The factory +produces code in the target audience's stack. Same CRM-shape, +two implementations, behavioural parity — a small but concrete +signal that the factory is language-independent where it needs +to be and language-opinionated where correctness matters. + +## How to run + +```bash +dotnet run --project samples/FactoryDemo.Api.CSharp/FactoryDemo.Api.CSharp.csproj +# API on http://localhost:5000 (or whatever ASP.NET picks) +curl http://localhost:5000/api/pipeline/funnel +``` + +## Endpoints (identical to the F# sibling) + +| Method | Path | Returns | +|---|---|---| +| GET | `/` | API metadata + endpoint list | +| GET | `/api/customers` | All customers | +| GET | `/api/customers/{id}` | Single customer, 404 if missing | +| GET | `/api/customers/{id}/activities` | Activities for one customer | +| GET | `/api/opportunities` | All opportunities | +| GET | `/api/opportunities/{id}` | Single opportunity, 404 if missing | +| GET | `/api/activities` | All activities | +| GET | `/api/pipeline/funnel` | Per-stage count + $ total | +| GET | `/api/pipeline/duplicates` | Customers sharing an email | + +## Parity guarantee + +Both versions return byte-identical JSON shapes for every +endpoint given the same seed. The only differences are: + +- `name` field at `/` — `"(F#)"` vs `"(C#)"` so the consumer + can tell which one is running +- JSON property ordering (both are valid JSON; order is + serializer-dependent) + +Otherwise: identical `customers`, `opportunities`, `activities`, +`funnel`, `duplicates`. Frontends can switch between them +without code changes. + +## Design notes (C# specifics) + +- **`Microsoft.NET.Sdk.Web` SDK.** ASP.NET Core via framework + reference; no NuGet package pin needed. +- **Records instead of classes.** `Customer`, `Opportunity`, + `Activity` are `record` types — immutable, value-equality, + trivially serializable. Matches the F# record semantics one + file over. +- **One type per file** — satisfies `MA0048`. F# allows + multiple types per file so the F# version is one-file; + C# convention is file-per-type. +- **`StringComparer.Ordinal` on GroupBy** — satisfies `MA0002`. + Ordinal is correct for our seed data (emails are ASCII / + lowercased); culture-aware comparison would introduce + unneeded overhead and non-determinism across locales. +- **Static endpoint array for `/`** — satisfies `CA1861`. + Declared once at startup, not on every request. +- **Nullable reference types + implicit usings.** Modern C# + defaults. Keeps the code short and idiomatic for C# + readers. + +## What this does NOT do + +Same as the F# sibling: no Postgres (v0), no writes, no auth, +no docker-compose. All are follow-up PRs. + +## Composes with + +- `samples/FactoryDemo.Api.FSharp/` — the F# sibling; reference + behaviour; use it as the algebraic truth when debugging +- A Postgres-backed sibling sample (schema + seed SQL) is a v1 + follow-up tracked in `docs/BACKLOG.md`; both API samples will + wire to it when it lands +- `docs/BACKLOG.md` — factory-demo scope rows diff --git a/samples/FactoryDemo.Api.CSharp/Seed.cs b/samples/FactoryDemo.Api.CSharp/Seed.cs new file mode 100644 index 00000000..7fc45f32 --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/Seed.cs @@ -0,0 +1,114 @@ +namespace Zeta.Samples.FactoryDemo.Api; + +/// +/// Deterministic in-memory seed data for the factory-demo. +/// The canonical seed source is the F# sibling's +/// samples/FactoryDemo.Api.FSharp/Seed.fs (same data, F# records); +/// this file mirrors that shape 1:1 in C# records. When the Postgres +/// backing sample lands (tracked in docs/BACKLOG.md under the +/// factory-demo rows), that sample's schema.sql / seed script +/// becomes the upstream truth and both language samples will mirror it. +/// +public static class Seed +{ + // Fixed clock so the seed is deterministic — reproducible across restarts. + private static readonly DateTimeOffset Now = new(2026, 4, 23, 0, 0, 0, TimeSpan.Zero); + private static DateTimeOffset Ago(int days) => Now.AddDays(-days); + + // Email collision #1: customers 1 and 13 share alice@acme.example. + // Email collision #2: customers 5 and 19 share bob@trades.example. + public static readonly IReadOnlyList Customers = new List + { + new(1, "Alice Plumbing LLC", "alice@acme.example", "555-0101", "123 Elm St, Portland OR", Ago(120), Ago(1)), + new(2, "Benson Roofing", "benson@roof.example", "555-0102", "45 Oak Ave, Seattle WA", Ago(120), Ago(1)), + new(3, "Crystal Electric", "crystal@sparks.example", "555-0103", "9 Pine Rd, Boise ID", Ago(120), Ago(1)), + new(4, "Delta HVAC & Mechanical", "delta@hvac.example", "555-0104", "700 Main St, Spokane WA", Ago(120), Ago(1)), + new(5, "Bob HVAC Services", "bob@trades.example", "555-0105", "12 Bay Blvd, Tacoma WA", Ago(120), Ago(1)), + new(6, "Evergreen Landscaping", "info@evergreen.example", "555-0106", "88 Forest Ln, Eugene OR", Ago(120), Ago(1)), + new(7, "Fairbanks Plumbing", "contact@fairbanks.example","555-0107", "5 River Rd, Anchorage AK", Ago(120), Ago(1)), + new(8, "Granite Pest Control", "hello@granite.example", "555-0108", "301 Stone Way, Boise ID", Ago(120), Ago(1)), + new(9, "Highland Roofing Co", "highland@roof.example", "555-0109", "22 Hill Dr, Bend OR", Ago(120), Ago(1)), + new(10, "Iron Tree Electric", "iron@tree.example", "555-0110", "17 Spruce St, Salem OR", Ago(120), Ago(1)), + new(11, "Jackson Pool Services", "jackson@pools.example", "555-0111", "600 Lake Rd, Reno NV", Ago(120), Ago(1)), + new(12, "Klein Garage Doors", "klein@doors.example", "555-0112", "44 4th Ave, Medford OR", Ago(120), Ago(1)), + new(13, "Acme Contact (new lead)", "alice@acme.example", "555-0113", "123 Elm St, Portland OR", Ago(120), Ago(1)), + new(14, "Lakeview Solar", "lakeview@solar.example", "555-0114", "250 Shore Dr, Bellevue WA", Ago(120), Ago(1)), + new(15, "Mountain Well Drilling", "mountain@wells.example", "555-0115", "12 Ridge Rd, Coeur dAlene ID", Ago(120), Ago(1)), + new(16, "Nightingale Security", "ngale@secure.example", "555-0116", "88 Watch Way, Vancouver WA", Ago(120), Ago(1)), + new(17, "Oak Hill Septic", "oak@septic.example", "555-0117", "14 Rural Rt 3, Gresham OR", Ago(120), Ago(1)), + new(18, "Prairie Window Cleaning", "prairie@windows.example", "555-0118", "66 Glass Rd, Kennewick WA", Ago(120), Ago(1)), + new(19, "Quincy Assistant (Bob HVAC)","bob@trades.example", "555-0119", "12 Bay Blvd, Tacoma WA", Ago(120), Ago(1)), + new(20, "Redwood Tree Service", "redwood@trees.example", "555-0120", "3 Canopy Ct, Hillsboro OR", Ago(120), Ago(1)), + }; + + public static readonly IReadOnlyList Opportunities = new List + { + new(1, 1, "Lead", 250000, Ago(30), Ago(2)), + new(2, 1, "Qualified", 800000, Ago(30), Ago(2)), + new(3, 2, "Lead", 180000, Ago(30), Ago(2)), + new(4, 3, "Proposal", 450000, Ago(30), Ago(2)), + new(5, 3, "Won", 120000, Ago(30), Ago(2)), + new(6, 4, "Lead", 2200000, Ago(30), Ago(2)), + new(7, 4, "Qualified", 600000, Ago(30), Ago(2)), + new(8, 5, "Proposal", 350000, Ago(30), Ago(2)), + new(9, 5, "Won", 900000, Ago(30), Ago(2)), + new(10, 6, "Lead", 150000, Ago(30), Ago(2)), + new(11, 7, "Qualified", 500000, Ago(30), Ago(2)), + new(12, 7, "Proposal", 700000, Ago(30), Ago(2)), + new(13, 8, "Won", 220000, Ago(30), Ago(2)), + new(14, 9, "Lead", 300000, Ago(30), Ago(2)), + new(15, 9, "Lead", 1800000, Ago(30), Ago(2)), + new(16, 10, "Qualified", 950000, Ago(30), Ago(2)), + new(17, 11, "Proposal", 1400000, Ago(30), Ago(2)), + new(18, 12, "Won", 380000, Ago(30), Ago(2)), + new(19, 13, "Lead", 50000, Ago(30), Ago(2)), + new(20, 14, "Proposal", 2500000, Ago(30), Ago(2)), + new(21, 14, "Qualified", 1100000, Ago(30), Ago(2)), + new(22, 15, "Won", 600000, Ago(30), Ago(2)), + new(23, 16, "Lead", 180000, Ago(30), Ago(2)), + new(24, 17, "Qualified", 270000, Ago(30), Ago(2)), + new(25, 18, "Lead", 80000, Ago(30), Ago(2)), + new(26, 19, "Proposal", 320000, Ago(30), Ago(2)), + new(27, 20, "Won", 450000, Ago(30), Ago(2)), + new(28, 20, "Lead", 210000, Ago(30), Ago(2)), + new(29, 2, "Lost", 90000, Ago(30), Ago(2)), + new(30, 6, "Lost", 400000, Ago(30), Ago(2)), + }; + + public static readonly IReadOnlyList Activities = new List + { + new(1, 1, 1, "Call", "Initial intake call — 3 units, basement finish", Ago(14)), + new(2, 1, 1, "Email", "Sent follow-up with rough estimate", Ago(13)), + new(3, 1, 2, "Call", "Scope expanded to full house repipe", Ago(6)), + new(4, 2, 3, "Email", "Insurance paperwork sent for roof claim", Ago(10)), + new(5, 3, 4, "Call", "Walkthrough scheduled for Tuesday", Ago(8)), + new(6, 3, 5, "Note", "Payment received — closed won", Ago(3)), + new(7, 4, 6, "Call", "Commercial HVAC replacement — 6 rooftop units", Ago(20)), + new(8, 4, 6, "Email", "Technical specs and load calcs sent", Ago(18)), + new(9, 4, 7, "Call", "Second opportunity — server-room cooling", Ago(5)), + new(10, 5, 8, "SMS", "Confirmed 10am arrival window", Ago(2)), + new(11, 5, 9, "Note", "Deposit received; scheduled for next week", Ago(7)), + new(12, 6, 10, "Email", "Initial inquiry from website", Ago(4)), + new(13, 7, 11, "Call", "Alaska project — remote site, flew tools in", Ago(30)), + new(14, 7, 12, "Email", "Proposal sent with permitting schedule", Ago(15)), + new(15, 8, 13, "Note", "Quarterly service contract signed", Ago(45)), + new(16, 9, 14, "Call", "Storm damage — needs quick turnaround", Ago(1)), + new(17, 9, 15, "Email", "Large hotel roof — sent credentials package", Ago(2)), + new(18, 10, 16, "Call", "Panel upgrade consult", Ago(11)), + new(19, 11, 17, "SMS", "Pool opening scheduled for May 1", Ago(5)), + new(20, 12, 18, "Note", "Installed — 3yr warranty registered", Ago(60)), + new(21, 13, 19, "Email", "Intro call tomorrow 2pm", Ago(1)), + new(22, 14, 20, "Call", "Roof assessment + solar compatibility check", Ago(12)), + new(23, 14, 21, "Email", "Federal tax credit paperwork sent", Ago(9)), + new(24, 15, 22, "Note", "Test-well results clean; contract signed", Ago(25)), + new(25, 16, 23, "Call", "Camera system walkthrough", Ago(6)), + new(26, 17, 24, "SMS", "Septic pump appointment confirmed", Ago(3)), + new(27, 18, 25, "Email", "Storefront window quote", Ago(7)), + new(28, 19, 26, "Call", "Coordinating with Bob HVAC on combined job", Ago(4)), + new(29, 20, 27, "Note", "Repeat customer — 2nd tree removal this year", Ago(40)), + new(30, 20, 28, "Email", "Quarterly pruning proposal", Ago(2)), + new(31, 2, 29, "Note", "Customer went with competitor on price", Ago(22)), + new(32, 6, 30, "Note", "Lost deal — decided to self-install", Ago(18)), + new(33, 1, null, "Email", "General follow-up — hope repipe went well", Ago(90)), + }; +} diff --git a/samples/FactoryDemo.Api.CSharp/smoke-test.sh b/samples/FactoryDemo.Api.CSharp/smoke-test.sh new file mode 100755 index 00000000..50830b30 --- /dev/null +++ b/samples/FactoryDemo.Api.CSharp/smoke-test.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Factory-demo C# API smoke test — exercises all 9 endpoints (`/` plus +# the 8 `/api/*` routes) and validates the JSON-shape contract. Exits 0 +# on pass, 1 on any failure. +# +# Usage: +# bash samples/FactoryDemo.Api.CSharp/smoke-test.sh +# +# Starts the API on a random free port, waits for /, hits each endpoint, +# verifies response shape + key invariants (row counts, duplicate-pair +# identity, funnel totals). Stops the API cleanly on exit. +# +# Dependencies on host: dotnet, curl, jq. All common dev tools; the demo +# does not ask for anything exotic. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT="$SCRIPT_DIR/FactoryDemo.Api.CSharp.csproj" + +for cmd in dotnet curl jq; do + if ! command -v "$cmd" >/dev/null; then + echo "Missing required tool: $cmd" >&2 + exit 2 + fi +done + +# Pick a high random port to avoid clashes with other dev services. +PORT=$(( 5100 + RANDOM % 400 )) +URL="http://localhost:${PORT}" + +echo "Building API..." +dotnet build "$PROJECT" -c Release --nologo -v quiet >/dev/null + +# Per-run server log — mktemp avoids collisions across concurrent smoke-test +# runs and writes into the host's system temp dir (honouring `$TMPDIR` when +# set, falling back to `/tmp`). The path is printed on both failure and +# success so the log is always discoverable. +# +# mktemp portability: GNU (Linux) and BSD (macOS) have incompatible +# invocations. GNU accepts `--tmpdir