Skip to content

M2/E4/#13 — The six bounded-context import rules - #16

Merged
Rexy-5097 merged 1 commit into
mainfrom
feat/m2-e4-issue-13-context-imports
Jul 30, 2026
Merged

Rexy-5097 merged 1 commit into
mainfrom
feat/m2-e4-issue-13-context-imports

Conversation

@Rexy-5097

Copy link
Copy Markdown
Owner

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 matched policy.root. For contexts.evaluation that root is contexts — so importing contexts.method counted as importing itself. 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 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

Context May import Source
ingest · curation · groundtruth · method contracts, domain, kernel ADR-0026 + each README
evidence contracts, domain, kernel TIS E11 §10 uses kernel.provenance.ancestors()
evaluation contracts, domain ADR-0026 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: "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 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:

  • every context declares populated=False, and the gate rejects that declaration the moment a module appears — the first .py file in any context goes red until that issue states what it may import
  • a seventh context added without a policy fails, because undeclared() finds packages by __init__.py and none of these directories has one

Without 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.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 contracts schema, and registers it in the kernel provenance store. The gate permits it — and 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, 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

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

clean export (git archive HEAD):
  unit 322 · architecture 460 · integration 39 passed + 24 pre-existing errors
  (identical 24 on origin/main before this change — see the report)

CI impact

No workflow change. Both new files land in jobs that already run — tests/architecture/ in architecture, tests/integration/ in integration. Still eight required contexts, no new job.

🤖 Generated with Claude Code

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
Rexy-5097 merged commit 6beeeaa into main Jul 30, 2026
8 checks passed
@Rexy-5097
Rexy-5097 deleted the feat/m2-e4-issue-13-context-imports branch July 30, 2026 12:08
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant