|
1 | | -# qyl engineering contract |
| 1 | +# qyl repository contract |
2 | 2 |
|
3 | | -This is the only editable agent/contributor instruction file in this repository. |
4 | | -`CLAUDE.md` is a symlink to it. Keep public explanation in `README.md`, released history in release notes, and |
5 | | -executable truth in code, schemas, generators, and tests. Do not add progress diaries, repair prompts, handoff |
6 | | -documents, or a second rules file — every duplicated statement is a future contradiction. |
| 3 | +Owns collector behavior, OTLP ingestion, normalization, DuckDB persistence, |
| 4 | +product API, embedded dashboard, CLI, Runner, and workflow observation. The |
| 5 | +normative cross-repository architecture is `ARCHITECTURE-1.0.0.md`. |
7 | 6 |
|
8 | | -## Where the law lives |
| 7 | +OTLP messages remain OpenTelemetry-owned. Public qyl HTTP/SSE/Runner/workflow |
| 8 | +models come from `Qyl.Api.Contracts`. Physical rows, SQL, retention, and |
| 9 | +checkpoints remain private. The collector must never export telemetry to its own |
| 10 | +ingest endpoint. NativeAOT is the collector delivery contract. |
9 | 11 |
|
10 | | -`ARCHITECTURE-1.0.0.md` in this repository is the normative architecture (last revised 2026-07-28; reality keeps moving — |
11 | | -divergence is a migration item, never a reason to doubt the document): |
12 | | -component taxonomy, boundary law, the exhaustive dependency-edge list, the two generated loops, and gates G1–G11. Every |
13 | | -other qyl repository points at it. |
14 | | -`docs/component-taxonomy.html` is a view of the same content plus the target ↔ today naming ledger; on any conflict the |
15 | | -Markdown wins. |
16 | | - |
17 | | -This file does not restate the architecture. If you find yourself needing a fact about boundaries, edges, loops, or |
18 | | -gates, read it there — a paraphrase here would be a second contract owner, which is the exact failure the architecture |
19 | | -forbids. |
20 | | - |
21 | | -## Definition of done |
22 | | - |
23 | | -The standing goal is complete when gates G1–G11 pass, the `Ci` target below is green, and every touched repository is |
24 | | -clean and pushed. Gates are the arbiter: a claim of completion without the corresponding gate output is not evidence. |
25 | | -Gates arbitrate only what they measure — keep them aligned with the task's goal (upstream and downstream adjusted in |
26 | | -the same change), and never count something impure by nature (network, registries, external state) as guaranteed merely |
27 | | -because a gate went green. |
28 | | - |
29 | | -Two classes of action are human-gated; everything else is autonomous. The uncertainty rule of thumb, and when an |
30 | | -agent-performed action must be cited in the completion report: |
31 | | - |
32 | | -- Pushed outside ANcpLua, or irreversible / permanently traceable → **uncertain**: name the action explicitly when |
33 | | - reporting the task done. |
34 | | -- Undoable, done before and known to work, or pure with no possible side effect → **not uncertain**: proceed with the |
35 | | - regular pipeline and its default checks, no extra ceremony. |
36 | | - |
37 | | -- **Registry-irreversible steps** — publishing to nuget.org/npm and unlisting package IDs. Prepare, verify, and stop |
38 | | - with the exact command ready. This gate is structural, not conventional: in any repo whose CI would publish on a push |
39 | | - to `main`, converting that trigger to a version tag **precedes every other push** to that repo — the tag is the human |
40 | | - act, and after the conversion a rename can land on `main` with zero registry effect. |
41 | | -- **Amending the decree** — changing what a gate *means* in |
42 | | - `ARCHITECTURE-1.0.0.md`. If a gate is defective, annotate the defect in place with measured evidence and stop; rewrite |
43 | | - only with explicit authorization in the goal. (Implementing a gate is normal work; redefining it is not.) |
44 | | - |
45 | | -When you deviate from an instruction, say so and say why — a reasoned deviation report is worth more than silent |
46 | | -compliance and far more than silent deviation. |
47 | | - |
48 | | -## When code contradicts the law |
49 | | - |
50 | | -You will find code that contradicts `ARCHITECTURE-1.0.0.md` — that is what a migration looks like from the inside, and |
51 | | -your measured evidence is not a misreading. The document states the target; the code states today; the ledger reconciles |
52 | | -the two. The protocol: |
53 | | - |
54 | | -1. Verify the contradiction with one command and keep the output. |
55 | | -2. Check the ledger — most contradictions are already registered as fold or rename rows. A registered gap needs no |
56 | | - further investigation. |
57 | | -3. A genuinely new gap gets a ledger row in the same commit as the evidence. |
58 | | -4. Close the gap if it's in the active work order; otherwise proceed past it. |
59 | | - |
60 | | -A contradiction never justifies stalling, a workspace-wide re-audit, or a silent edit to the law. **The ledger is the |
61 | | -audit** — extend it, don't re-derive it; discovering the full rename scope is not a prerequisite for starting the first |
62 | | -item of it. |
63 | | - |
64 | | -Scope words in goals mean this: code-level work — renames, folds, deletions, generator and test changes — is always |
65 | | -*execute* scope. Only the two registry-irreversible actions above are *prepare-and-stop* scope. "Prepare the rename" is |
66 | | -not an instruction to audit instead of act. |
67 | | - |
68 | | -## Fail-closed invariants (placement and reasoning) |
69 | | - |
70 | | -The architecture states the self-export invariant; these are the layers in this codebase and why each exists. A rename |
71 | | -must move them intact: |
72 | | - |
73 | | -- `CollectorSelfExportGuard.ThrowIfSelfExporting` — sits directly after |
74 | | - `AddQylCollectorCore` because that is where the ports become known and nothing is bound yet. It reads the explicit |
75 | | - endpoint, so it is silent when nothing set one. |
76 | | -- `EnableAutoDiscovery = false` in the collector's own service defaults — stops the process from *probing* for a |
77 | | - collector (itself). |
78 | | -- `RequireConfiguredEndpoint = true` on the hosting options — the layer the other two miss: without it the OTLP exporter |
79 | | - falls back to its `localhost` |
80 | | - default, which is this process's own ingest port. This flag makes "no endpoint" mean "do not export". |
81 | | -- `CollectorHealthGuard.ThrowIfHealthSurfaceUnwired` — separate concern, same fail-closed pattern. |
82 | | - |
83 | | -The collector consumes the producer stack for self-telemetry only, through the published hosting package like any other |
84 | | -application — never a private copy of the composition logic. `qyl/internal/qyl.instrumentation` (`UseQyl`) once was such |
85 | | -a copy; the architecture fold retired it on 2026-07-26 and the ledger carries that row as done. Re-measured 2026-07-28: |
86 | | -`ActivitySources` holds one constant and no inventory, `CollectorDiscovery` has no second implementation — the file sets |
87 | | -`EnableCollectorDiscovery` on the published options — and the collector's 55-library restored closure carries zero |
88 | | -agent-framework packages. What remains on that row is the rename, not architecture. |
89 | | - |
90 | | -What stays on this side of the wire is the part the hosting package cannot know: health checks and |
91 | | -endpoints, exception capture, Kestrel and JSON conventions, and which of this application's own endpoints are span noise |
92 | | -(`HealthProbeSpanFilter`) — a thin collector-defaults layer that *calls* the published composition. Re-deriving OTel |
93 | | -wiring, an ActivitySource inventory, or collector discovery here is how the fork started, so the prohibition outlives the |
94 | | -fork it describes — the testable consequence is |
95 | | -gate G6: the collector fully exercisable with a plain OTLP client and no qyl producer packages. |
96 | | - |
97 | | -## Delivery |
98 | | - |
99 | | -qyl launched 1.0.0 on 2026-07-28. Work directly on `main`, preserve unrelated user changes, run the repository gates, make one intentional |
100 | | -commit per coherent repository change, and push it. Generated files are changed through their schema or generator and |
101 | | -regenerated in the same commit. Unpublished surfaces may converge directly; published package versions are immutable and |
102 | | -move through new versions (architecture §6.1 owns the rename-versioning policy). |
103 | | - |
104 | | -On the wire, metrics are counted, discarded, and acknowledged with |
105 | | -`partial_success` — implement that behavior exactly; the product scope behind it is stated in the architecture (§1, §6). |
106 | | - |
107 | | -## Contract ownership — repo-specific consequences |
108 | | - |
109 | | -Loop 2 (architecture §4) owns the product-API rule. The consequences inside this repository: |
110 | | - |
111 | | -- Do not declare parallel public DTOs in the collector, the CLI runtime, the dashboard, or MCP code. If an internal |
112 | | - shape must cross an HTTP, gRPC, MCP, streaming, or generated-client boundary: change TypeSpec first, regenerate |
113 | | - `Qyl.Api.Contracts` and client artifacts, then map to the generated contract. Accessibility modifiers do not decide |
114 | | - contract status — anything serialized across a boundary is a contract. |
115 | | -- OTLP ingestion's wire contract is the official OpenTelemetry protobuf schema. Vendoring a pinned upstream `.proto` |
116 | | - input is allowed; redefining it as a qyl-owned DTO hierarchy is not. |
117 | | -- Storage rows, ingest batches, query models, and projections may be owned locally precisely because they never cross a |
118 | | - boundary. |
119 | | - |
120 | | -## Implementation rules |
121 | | - |
122 | | -- A public capability needs an executable owner: a product call path, an owned downstream consumer, or a conformance |
123 | | - application exercising the complete contract. Mock-only tests and imaginary consumers are not acceptance evidence. |
124 | | -- Reuse a released, AOT-compatible upstream implementation when it satisfies the contract. Implement a missing gap only |
125 | | - when qyl needs it, and prove it through a complete executable vertical. |
126 | | -- Tests and fixtures use real protocol types, valid programmatically generated data, or captured-and-sanitized |
127 | | - datasets — hand-shaped JSON, substring checks over binary payloads, and echo-mocks prove nothing about |
128 | | - interoperability. |
129 | | -- `Version.props` owns the product version and shared package-version properties; `Directory.Packages.props` owns the |
130 | | - central `PackageVersion` |
131 | | - entries that consume them. Hardcoded versions elsewhere will drift. |
132 | | -- The toolchain is the `global.json` SDK (`10.0.302`, `latestFeature`) and C# 14. Interceptors are supported on this |
133 | | - SDK; use the current Roslyn APIs. |
134 | | -- Native AOT is the collector's publish contract (`QylAot` defaults on; the Dockerfile publishes the native lane). |
135 | | - `eng/scripts/collector-aot-smoke.sh` |
136 | | - is that lane's executable owner; `-p:QylAot=false` is the JIT diagnostic build with full analyzer enforcement. |
137 | | -- HTTP header attributes are denied at the collector persistence boundary unless the exact span key is in the small |
138 | | - generated safe-header allowlist. Never persist `Authorization`, cookies, `Mcp-Param-*`, or arbitrary tool-defined |
139 | | - header names in spans, logs, resources, entities, fixtures, or exception evidence — a leaked credential in stored |
140 | | - telemetry is unrecoverable. |
141 | | -- Never hand-edit generated C#, protobuf output, TypeScript contracts, or generated reports. Analyzer release manifests |
142 | | - are maintained inputs and change with their analyzer rules. |
143 | | - |
144 | | -## MCP telemetry and protocol-era discipline |
145 | | - |
146 | | -qyl classifies and stores MCP telemetry, owns the collector semantic catalog, and hosts MCP code of its own. The |
147 | | -2026-07-28 protocol revision changes what several MCP fields mean; these rules bind the ingest and enrichment path and |
148 | | -qyl's MCP host: |
149 | | - |
150 | | -- Protocol era is the negotiated protocol version, never the presence of a |
151 | | - `_meta` envelope — the legacy-fallback probe also carries one. |
152 | | -- MCP client and server identity is per-request and self-reported. Never promote `clientInfo` / `serverInfo` to a |
153 | | - telemetry resource attribute, a routing dimension, or a behavior or security decision — display, logging, and |
154 | | - debugging only. |
155 | | -- A multi-round tool call is N linked requests correlated by an opaque, untrusted `requestState`. Render the rounds as |
156 | | - linked spans, never a synthesized parent-child tree, and trust `requestState` only after verification. |
157 | | -- Span and RPC status come from the JSON-RPC and tool outcome, never the HTTP status: a modern-path JSON-RPC error rides |
158 | | - HTTP 400, and an error can arrive in-band on a committed 200. Map from the tool `isError` result and the protocol |
159 | | - error-code family. |
160 | | -- Wire concepts upstream semconv has not defined — `requestState`, round index, `resultType`, `subscriptions/listen` |
161 | | - lifetime, cache hints — enter the collector semantic catalog under the experimental `qyl.mcp.*` staging namespace, |
162 | | - deletion-targeted on every semconv bump that lands an upstream equivalent. Never mint an `mcp.*` alias for an |
163 | | - unratified concept. |
164 | | - |
165 | | -## Verification |
166 | | - |
167 | | -Run the narrow tests for the changed component and finish repository-wide work with: |
168 | | - |
169 | | -```bash |
170 | | -dotnet run --project eng/build/build.csproj -- Ci |
171 | | -``` |
172 | | - |
173 | | -The `Ci` target builds and tests the backend, builds and tests the product dashboard, runs its embedded Release-product |
174 | | -Playwright smoke, verifies the exact generated contract package, and checks the collector semantic catalog. For |
175 | | -schema-boundary changes, also compile and test the owning `qyl-api-schema` |
176 | | -repository and restore the resulting `Qyl.Api.Contracts` package into a clean consumer. |
177 | | - |
178 | | -## Cloudflare skills |
179 | | - |
180 | | -`api.qyl.at` is Cloudflare-proxied, so zone behavior is part of this service's |
181 | | -delivery path — the **Network → gRPC** setting in particular, since turning it |
182 | | -off breaks OTLP/gRPC ingest while HTTP keeps working, which reads like a client |
183 | | -bug. Load `/cloudflare:cloudflare` before changing or diagnosing anything at |
184 | | -that layer; it retrieves from current Cloudflare docs rather than from memory. |
185 | | - |
186 | | -`/cloudflare:wrangler` and `/cloudflare:web-perf` do not apply here: this repo |
187 | | -deploys no Worker and owns no browser surface. Those belong to `qyl.at`. |
188 | | - |
189 | | -## Durable references |
190 | | - |
191 | | -- Product and local development: `README.md` |
192 | | -- API authority: `https://github.com/ANcpLua/qyl-api-schema` |
193 | | -- Automatic instrumentation evidence: |
194 | | - `https://github.com/ANcpLua/Qyl.OpenTelemetry.AutoInstrumentation` |
195 | | -- Semantic-convention generation: |
196 | | - `https://github.com/ANcpLua/Qyl.OpenTelemetry.SemanticConventions` |
197 | | - |
198 | | -When a claim can be derived from a manifest, generated report, public API baseline, or test, link that evidence instead |
199 | | -of copying it into another Markdown ledger. |
| 12 | +Change generated files through their owner and regenerate. Run focused tests, |
| 13 | +then `dotnet run --project eng/build/build.csproj -- Ci`. |
0 commit comments