Repository navigation
M2/E4/#13 — The six bounded-context import rules - #16
Merged
Merged
Conversation
Canonical: Milestone M2 · Epic E4 · Issue #13. Deps: #6 ✔, #12 ✔ (530a412). Eligibility recomputed by parsing origin/main:docs/tis/TIS-v1.0.md §15: eligible set was {#13 order 13, #14 order 14, #15 order 15}; lowest Order selects #13. ADR-0026's Verification asks for "a kernel-zero-imports test plus six import-direction tests, each with a deliberate-violation case". The kernel test landed with #10 and the domain stdlib-only rule with #12. This is the six. THE RULE WAS INEXPRESSIBLE, NOT MERELY UNENFORCED `check` permitted an import whose top-level root matched `policy.root`. For `contexts.evaluation` that root is `contexts` — so importing `contexts.method` counted as importing itself, and ADR-0026's fifth dependency rule could not be *stated*, let alone violated. Demonstrated before changing anything: policy.package : contexts.evaluation policy.root : contexts permitted : ['contexts', 'contracts', 'domain'] Fixed by matching self-imports against the policy's dotted package rather than its root, via a new `imported_paths` that keeps full dotted paths. `imported_roots` is retained — root granularity is still correct for classifying stdlib and third party. This is a strict tightening. #6's own 17 self-tests pass unchanged, and the real gate still exits 0. THE SIX RULES, ENCODED AS ADR-0026 WRITES THEM ingest · curation · groundtruth · method contracts, domain, kernel evidence contracts, domain, kernel (+ writes nothing) evaluation contracts, domain ← as written No policy grants the `contexts` root, so no context may import another. Third-party roots are not pre-granted: the ADR grants none, and a context needing astropy should add one reviewable line rather than inherit a permission nobody voted for (STD-11). ARCHITECTURE CONTRADICTION — REPORTED, NOT RESOLVED (AC-001) ADR-0026 says "Evaluation imports contracts and domain only". TIS E10 §7 requires an Evaluation to carry its own digest; ADR-0005 reserves minting to the kernel. Those three cannot all hold. Encoded exactly as frozen and reported for resolution by ADR before M7/#30. Quietly adding `kernel` to R5 would be weakening a gate to fit an unwritten need, which is the one thing this project has repeatedly refused to do. Separately, ADR-0026 states R5 twice and inconsistently: the context table says "Depends on contracts only" and the Decision says "contracts and domain only". The precise Decision wording is taken as operative, following the DR-006 precedent. EMPTY CONTEXTS, AND WHY THESE RULES ARE NOT DECORATION contexts/* holds six READMEs and no Python, so none of the 55 architecture tests assert over the real tree — each one builds a violating module on disk and asserts the analyser reports it. The verdict is about the analyser, which exists. Two tests make the rules bind on the day code arrives rather than the day someone remembers them. Every context declares populated=False, and the gate rejects that declaration the moment a module appears — so the first .py file in any context goes red until that issue states what it may import. And a seventh context added without a policy fails, because `undeclared` finds packages by __init__.py and these directories have none. FEATURE-LEVEL INTEGRATION Six rejection tests are all satisfied by a rule set that rejects everything. So tests/integration/test_context_rules_with_platform.py asks the complementary question, which needs three real subsystems at once: does the rule set PERMIT the composition the architecture calls for? A plausible Ingest module builds a domain Observation, validates it against the real contract, and registers it in the kernel's provenance store. The gate permits it; the same source then genuinely runs, producing a schema-valid, DAG-linked artifact with ingest_time null and nothing fabricated. Then the point of the whole gate, as a test: the violating variant differs by one import and EXECUTES PERFECTLY. Strip that line and the remainder is byte-identical. No test that runs the context would ever notice the coupling. And the same text is permitted under R1 and refused under R5 — the verdict changes with the context, which is what makes these rules directional rather than one allow-list. VERIFICATION unit (kernel + domain) 322 passed tests/architecture 460 passed (+55) tests/integration 63 passed (+6) research/tests/v2 188 passed links / imports / contracts gates clean imports gate: 30 modules, 134 imports, 9 policies CI: no workflow change. Both new files land in jobs that already run. Still eight contexts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rexy-5097
added a commit
that referenced
this pull request
Jul 30, 2026
Canonical: Milestone M3 · Epic E5 · Issue #15. Deps: #11 (complete). Eligibility recomputed by parsing origin/main:docs/tis/TIS-v1.0.md §15 — complete {1..14}, eligible {#15, #22, #23, #25, #26, #27, #39}; lowest Order selects #15. First issue of Milestone M3, and the first code in a bounded context. WHAT AN ACQUISITION CHANNEL MUST PROVIDE, AND MAY NEVER LET OUT SourceAdapter descriptor() -> SourceDescriptor acquire(selector) -> RawArtifact + AcquisitionProvenance verify_conformance what an adapter must pass before its data enters the platform boundary.stamp the single sanctioned clock read in the system Credential a secret that cannot be printed or serialised assert_credential_free refuse to let one cross the boundary No channel is implemented. The ISSDC-PRADAN adapter is #16; the SoLEXS and HEL1OS parsers are #17 and #18; the bitemporal write path is #19. A PROTOCOL, NOT A FRAMEWORK ADR-0003 is explicit that it does NOT authorise "a source-plugin registry, a dispatch layer, or a configurable adapter framework", and ADR-0025 lists generic base classes in the same category. So: a typing.Protocol that is inherited from by nothing, constructs nothing and dispatches nothing, plus a checker taking an adapter the caller already holds. A test asserts contract.py never grows register/REGISTRY/get_adapter/dispatch. Under ADR-0025's test this is a free seam — stating what a channel must provide would still be right with exactly one source forever, because the parser downstream has to trust its input regardless of which channel produced it. THREE RULES, EACH MADE STRUCTURAL RATHER THAN REMEMBERED Credentials never leave (STD-19, E5 §13). Credential redacts itself in repr, str AND format, closing the routes secrets actually escape by. The integration test writes to a real log file through six of them and greps the file — not the object, the file. A stronger result than expected fell out: SourceDescriptor has no field a credential could occupy. Every field is a typed domain value; there is no dict, no Any, no extras. A secret cannot be placed in a conforming descriptor at all, so the runtime scan is the backstop for the duck-typed case rather than the primary defence. The test that assumed otherwise was corrected to assert what actually holds. Tier 0 is referenced, never redistributed (ADR-0023, STD-23, E5 §11(iv), §12). RawArtifact has a digest, a size, a way to re-acquire, and NO field that could hold the bytes — a test pins the field set. An artifact that can hold a whole-day archive will hold one, and nobody would see it in review; the object would just be larger. Both times, the second never fabricated (ADR-0004, ADR-0022, E5 §11(i)(ii)). boundary.stamp is the exception TIS §0.4 grants, in one named module so that `grep -rn 'ingest.boundary'` enumerates every clock read in the repository. Nothing here takes a row and returns a time for it: __all__ is exactly {stamp, stamp_from_epoch, monotonic}, asserted, and E5 §11(ii) forbids the rest. THE #13 MECHANISM FIRED, EXACTLY AS BUILT Adding the first module to contexts/ingest turned the gate red on its own: contexts.ingest: declared unpopulated but contains 7 module(s). populated flipped False -> True. That is the staleness check M2/E4/#13 shipped doing the one job it was written for, one issue later, without anyone remembering it existed. FEATURE-LEVEL INTEGRATION — THE FIRST REAL DATA PATH tests/integration/test_ingest_contract_with_platform.py follows scientific data from a channel to a contract-valid, provenance-linked Observation across all six components: kernel mints the digest, ingest stamps the time, domain builds the row, contracts validate it, the manifest records where the Tier 0 bytes stay, and the import rules say who may do it. Both bitemporal paths are followed, because the difference between them is the whole of ADR-0022: a new acquisition carries both times; a historical row carries valid_time and a null ingest_time, has no acquisition to describe, and nothing anywhere would fill it. A fabricated one passes the schema and fails the invariant — the division of labour between the layers, on the case that motivated the ADR. VERIFICATION unit (kernel + domain + contexts) 372 passed (+50) tests/architecture 562 passed tests/integration 97 passed (+26) research/tests/v2 188 passed links / imports / contracts gates clean imports gate: 37 modules, 168 imports, 9 policies CI: `contexts` added to the unit job, named as a root so a new context's tests run the day they are written. Reported as EPD-007. No new job, no new context; still eight. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Rexy-5097
added a commit
that referenced
this pull request
Jul 30, 2026
Canonical: Milestone M3 · Epic E5 · Issue #16. Deps: #15 (complete, 1d65c63). Eligibility recomputed by parsing origin/main:docs/tis/TIS-v1.0.md §15 — complete {1..15}, eligible {#16, #17, #22, #23, #25, #26, #27, #39}; lowest Order selects #16. The first channel. Aditya-L1 SoLEXS daily archives, registered with latency class ~33d and granularity daily-archive (E5 §18). WHAT ACQUISITION ACTUALLY IS FOR THIS CHANNEL PRADAN is not an API. ISSDC's own published download script states the procedure — "Login to Pradan in your browser, select data of your interest and download script for the session" — and warns that "There are session download limits, request rate limit and session timeouts in place... Violations may lead to blocking." So the network step is performed by a person, outside this system, and what it leaves is an extracted archive tree. `acquire` is the step that brings that tree into the platform: locate the day, have the kernel digest every product, stamp the moment the system learned of it, return provenance for all of it. ADR-0023 already describes the result correctly — Tier 0, referenced not stored, an evictable cache that is never the system of record. NO HTTP IS PERFORMED, AND NONE IS SIMULATED Writing a fetch against a portal that requires an interactive login would mean inventing a transport this channel does not have and inventing responses to go with it. When a day's products are not on disk the adapter raises UnavailableResource — the true statement, that the data has not been retrieved. A test asserts the module imports nothing that could make a request. No retry loop and no timeout either. E5 §15 makes retry policy adapter-local, and there is no network call here to retry; a loop around a filesystem read would be fabricated behaviour. THIS ADAPTER HOLDS NO CREDENTIAL It cannot leak a secret because it has none — authentication is a browser login that happens before and outside this code. Its descriptor records requires_credentials=True because RE-ACQUIRING needs a session, which is what a future reader must know (ADR-0023). `test_the_adapter_holds_no_credential` asserts vars() is exactly {archive_root, _store}. That is a stronger form of STD-19 than confinement: there is nothing here to confine. THE STD-12 TRAP, FOUND IN THIS ARCHIVE FOR A SECOND REASON E5 §16 requires real-data guards to test for the FITS products, not for a directory, citing the legacy isdir guard that silently disabled 188 tests. The same trap exists here for a different reason, verified: the five v1.1 archives carry a TRACKED quicklook PNG, so AL1_SLX_L1_20241001_v1.1/ exists in every clean checkout while its 3.5 GB of .gz products do not. Guards test for .gz products. `products()` raises UnavailableResource naming STD-12 when a directory is present and empty, and a test reproduces exactly that condition. ARCHIVE IDENTITY IS A ROLLUP, NOT AN OPAQUE DIGEST One line per product — digest, size, relative path — sorted, digested by the kernel. Same reasoning as DatasetRelease rolling per-table digests to a release digest (ADR-0006): a changed byte anywhere changes the archive identity AND stays locatable to the product that changed. A digest over concatenated content would detect the change and lose its address. Sorted because the identity is computed over the sequence: filesystem ordering must not reach it, or two machines produce two identities for the same bytes. AppleDouble sidecars and the v1.1 quicklook PNG are excluded — neither is what ISSDC published, and including either would make the digest depend on which computer looked at the data. The product bytes are NOT registered with the store. Only the listing is. Registering them would copy Tier 0 into the store, which ADR-0023 forbids. VERIFIED AGAINST THE REAL CORPUS 2024-05-14, the X8.7 flare SALVAGE-001 records v1 reproducing to the minute from GOES — now the Aditya-L1 side of the same day, entering the platform with provenance: 4 products, 13,302,792 bytes SDD1 gti only; SDD2 gti + lc + pi exactly SPEC-parsers r6 §1.1 436 archives, 436 unique dates re-measured, matching the spec Both real-corpus tests skip rather than fail where the archive is absent (E5 §17). VERIFICATION unit (kernel + domain + contexts) 405 passed (+33) tests/architecture 562 passed tests/integration 114 passed (+17) research/tests/v2 188 passed links / imports / contracts gates clean CI: no workflow change. `contexts` is already a unit-job root and tests/integration already runs. Still eight contexts. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Canonical identifier: Milestone M2 · Epic E4 · Issue #13
Dependencies: #6 ✔, #12 ✔ (merged as
530a412)Selection: eligibility recomputed by parsing
origin/main:docs/tis/TIS-v1.0.md§15. Eligible set{#13 Order 13, #14 Order 14, #15 Order 15}; lowest Order selects #13.ADR-0026's Verification clause asks for "a kernel-zero-imports test plus six import-direction tests, each with a deliberate-violation case". The kernel test landed with #10, the
domain/stdlib-only rule with #12. This is the six.The rule was inexpressible, not merely unenforced
check()permitted any import whose top-level root matchedpolicy.root. Forcontexts.evaluationthat root iscontexts— so importingcontexts.methodcounted as importing itself. ADR-0026's fifth dependency rule could not be stated, let alone violated. Demonstrated before changing anything:Fixed by matching self-imports against the policy's dotted package rather than its root, via a new
imported_paths()that preserves full dotted paths.imported_roots()is retained — root granularity is still correct for classifying stdlib and third party.This is a strict tightening. #6's own 17 self-tests pass unchanged and the real gate still exits 0. No gate was weakened.
The six rules, encoded as ADR-0026 writes them
contracts,domain,kernelcontracts,domain,kernelkernel.provenance.ancestors()contracts,domainNo policy grants the
contextsroot, so no context may import another. Third-party roots are not pre-granted: the ADR grants none, and a context needingastropyshould add one reviewable line rather than inherit a permission nobody voted for (STD-11).Architecture Contradiction — reported, not resolved (AC-001)
ADR-0026: "Evaluation imports contracts and domain only." TIS E10 §7 requires an Evaluation to carry its own
digest. ADR-0005 reserves minting to the kernel. Those three cannot all hold.Encoded exactly as frozen, and reported for resolution by ADR before M7/#30. Quietly adding
kernelto R5 would be weakening a gate to fit an unwritten future need.Separately, ADR-0026 states R5 twice and inconsistently — the context table says "Depends on contracts only", the Decision says "contracts and domain only". The precise Decision wording is taken as operative, following the DR-006 precedent (DR-007).
Empty contexts, and why these rules are not decoration
contexts/*holds six READMEs and no Python. So none of the 55 architecture tests assert over the real tree — each builds a violating module on disk and asserts the analyser reports it. The verdict is about the analyser, which exists.Two tests make the rules bind on the day code arrives rather than the day someone remembers them:
populated=False, and the gate rejects that declaration the moment a module appears — the first.pyfile in any context goes red until that issue states what it may importundeclared()finds packages by__init__.pyand none of these directories has oneWithout these entries a new module under
contexts/would be governed by nothing at all.Feature-level integration
Six rejection tests are all satisfied by a rule set that rejects everything.
tests/integration/test_context_rules_with_platform.pyasks the complementary question, which needs three real subsystems at once: does the rule set permit the composition the architecture calls for?A plausible Ingest module builds a
domainObservation, validates it against the realcontractsschema, and registers it in thekernelprovenance store. The gate permits it — and the same source then genuinely runs, producing a schema-valid, DAG-linked artifact withingest_timenull and nothing fabricated.Then the point of the whole gate, stated as a test: the violating variant executes perfectly. It differs by one import; strip that line and the remainder is byte-identical. No test that runs the context would ever notice the coupling.
And the same text is permitted under R1 and refused under R5 — the verdict changes with the context, which is what makes these rules directional rather than one global allow-list.
Verification
CI impact
No workflow change. Both new files land in jobs that already run —
tests/architecture/inarchitecture,tests/integration/inintegration. Still eight required contexts, no new job.🤖 Generated with Claude Code