Skip to content

M2/E4/#14 — Tier 2 manifest schema - #17

Merged
Rexy-5097 merged 1 commit into
mainfrom
feat/m2-e4-issue-14-manifest
Jul 30, 2026
Merged

M2/E4/#14 — Tier 2 manifest schema#17
Rexy-5097 merged 1 commit into
mainfrom
feat/m2-e4-issue-14-manifest

Conversation

@Rexy-5097

Copy link
Copy Markdown
Owner

Canonical identifier: Milestone M2 · Epic E4 · Issue #14 — Tier 2 manifest schema
Dependencies: #11 ✔ (merged as 6c26b33)
Selection: eligibility recomputed by parsing origin/main:docs/tis/TIS-v1.0.md §15 — complete {1…13}, eligible {#14 Order 14, #15 Order 15}; lowest Order selects #14.

This completes Epic E4.

What this adds

contracts/manifest.schema.json — the twelfth contract, and the one git actually carries. A Tier 2 manifest is what the repository holds in place of the ~600 MB it deliberately does not (ADR-0023, E6 §19).

Almost everything this schema does is a refusal

ADR-0023 defines three tiers with three different homes, and each must forbid the others. Expressed as prose, "a Tier 0 manifest must not carry a deposition" is a sentence someone has to read. Expressed as if tier = 0 then not deposition, a manifest that would redistribute another organisation's raw archive cannot be written.

Tier Required Structurally impossible
0 — raw source retrieval descriptor deposition, path — STD-23, E6 §11(iii): identified, never stored, never redistributed
1 — canonical release deposition, and retention: permanent retrieval, path — a prunable Tier 1 release makes a DOI resolve to nothing
2 — derived, in git path, constrained so it cannot leave the repo root retrieval, deposition

Retention is unrepresentable when it would be wrong

E6 §11(iv): an artifact referenced by an Evidence Binding is never pruned. So prunable with a non-empty referenced_by is refused by the contract rather than discouraged in a comment — it is the one combination that would let a published claim lose the bytes underneath it.

A permanent entry likewise cannot carry a prune_after. Permitting both would let a manifest state a contradiction that a retention run would then arbitrate silently, the same way every time.

doi is required as a field and nullable in value

ADR-0023's fallback degrades citability and says the degradation is published rather than hidden. Optional would make "no DOI" and "nobody recorded whether there is one" the same document — the ingest_time reasoning of ADR-0022 applied to citability.

Feature-level integration — the digest is the same digest

tests/integration/test_manifest_with_platform.py checks the one thing no schema can: that the digest written into a manifest is the digest of the bytes it claims to describe. That needs four components at once —

Every guarantee downstream — citation, re-acquisition, supersession, evidence binding — rests on that equality, and nothing had ever checked it end to end. Also included: a manifest that is schema-valid and false, pointing at bytes the store has never seen, with the demonstration that the store refuses it while the schema cannot.

A finding about my own test, not the schema

tier: 1.0 validates, and that is correct. JSON has one number type and JSON Schema compares enum members by value, so 1.0 and 1 are the same number — a schema cannot distinguish them, so the alternative to accepting 1.0 is not rejecting it but pretending to. The first version of that test asserted rejection and was wrong; it now asserts acceptance and records why.

Scope — no producer, no parser, no registry, no ingest

The schema and its tests. contexts/curation/manifest is E6/#20's; registry/*/*.json instances are E6/#21, E7/#23, E8/#25–#26, E9/#27's; the retention gate is E6/#22's.

The manifest gets no domain entity, deliberately: row 14's Unit column reads "manifest validity" — a schema test, not entity round-trip — and an entity would be a producer.

Two existing assertions updated, because #14 changes the contract set: expected count 11 → 12, and manifest added to the documented no-domain-entity exclusions with its reason.

Verification

unit (kernel + domain)   322 passed
tests/architecture       562 passed   (+102)
tests/integration         71 passed   (+8)
research/tests/v2        188 passed
contracts gate: 12 schemas, 11 compared against origin/main, every change additive
links / imports gates    clean

clean export (git archive HEAD):
  12 schemas · unit 322 · architecture 562 · integration 47 passed
  + 24 pre-existing errors (IMPL-012, reported at #13, identical on origin/main)

CI impact

No workflow change. Both new files land in jobs that already run. Still eight required contexts.

🤖 Generated with Claude Code

Canonical: Milestone M2 · Epic E4 · Issue #14. Deps: #11 (complete).
Eligibility recomputed by parsing origin/main:docs/tis/TIS-v1.0.md §15 — complete
{1..13}, eligible {#14 order 14, #15 order 15}; lowest Order selects #14.

Completes Epic E4. The twelfth contract, and the one git actually carries: a Tier 2
manifest is what the repository holds in place of ~600 MB it deliberately does not
(ADR-0023, E6 §19).

ALMOST EVERYTHING THIS SCHEMA DOES IS A REFUSAL

ADR-0023 defines three tiers with three different homes, and each must forbid the
others. Expressed as prose, "a Tier 0 manifest must not carry a deposition" is a
sentence someone has to read. Expressed as `if tier = 0 then not deposition`, a manifest
that would redistribute another organisation's raw archive CANNOT BE WRITTEN.

  tier 0   retrieval descriptor required; deposition and path impossible
           (STD-23, E6 §11(iii) — identified, never stored, never redistributed)
  tier 1   deposition required; retrieval and path impossible; retention must be
           permanent, because a prunable Tier 1 release makes a DOI resolve to nothing
  tier 2   path required, and constrained so it cannot leave the repository root

RETENTION IS UNREPRESENTABLE WHEN IT WOULD BE WRONG

E6 §11(iv): an artifact referenced by an Evidence Binding is NEVER pruned. So
`prunable` with a non-empty `referenced_by` is refused by the contract rather than
discouraged in a comment — it is the one combination that would let a published claim
lose the bytes underneath it. A `permanent` entry likewise cannot carry a `prune_after`:
permitting both would let a manifest state a contradiction that a retention run would
then arbitrate silently, the same way every time.

DOI IS REQUIRED AS A FIELD AND NULLABLE IN VALUE

ADR-0023's fallback degrades citability, and says the degradation is published rather
than hidden. Optional would make "no DOI" and "nobody recorded whether there is one"
the same document — the ingest_time reasoning of ADR-0022 applied to citability.

FEATURE-LEVEL INTEGRATION — THE DIGEST IS THE SAME DIGEST

tests/integration/test_manifest_with_platform.py checks the one thing no schema can:
that the digest in a manifest is the digest of the bytes it claims to describe. It
needs four components at once — the kernel mints it (#10, the only thing permitted to),
the contract validates the release (#11), the domain holds the digest without being
able to compute one (#12), and the import rules say who may do all of it (#13).

Every guarantee downstream — citation, re-acquisition, supersession, evidence binding —
rests on that equality, and nothing had ever checked it end to end. Also included: a
manifest that is schema-valid and FALSE, pointing at bytes the store has never seen,
and the demonstration that the store refuses it while the schema cannot.

A FINDING ABOUT MY OWN TEST, NOT THE SCHEMA

`tier: 1.0` validates, and that is correct. JSON has one number type and JSON Schema
compares enum members by value, so 1.0 and 1 are the same number. The first version of
that test asserted rejection and was wrong; it now asserts acceptance and records why.

NO PRODUCER, NO PARSER, NO REGISTRY, NO INGEST

The schema and its tests. `contexts/curation/manifest` is E6/#20's; registry/*/*.json
instances are E6/#21, E7/#23, E8/#25-#26, E9/#27's; the retention gate is E6/#22's.
The manifest gets no domain entity, deliberately — row 14's Unit column reads "manifest
validity", a schema test, and an entity would be a producer.

Two existing assertions updated because #14 changes the contract set: the expected
count 11 -> 12, and manifest added to the documented no-domain-entity exclusions.

VERIFICATION

  unit (kernel + domain)   322 passed
  tests/architecture       562 passed  (+102)
  tests/integration         71 passed  (+8)
  research/tests/v2        188 passed
  contracts gate: 12 schemas, 11 compared, every change additive
  links / imports gates    clean

CI: no workflow change. Both new files land in jobs that already run. Still eight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Rexy-5097
Rexy-5097 merged commit 807bdf4 into main Jul 30, 2026
8 checks passed
@Rexy-5097
Rexy-5097 deleted the feat/m2-e4-issue-14-manifest branch July 30, 2026 12:32
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>
Rexy-5097 added a commit that referenced this pull request Jul 30, 2026
Canonical: Milestone M3 · Epic E5 · Issue #17. Deps: #12, #15 (both complete).
Eligibility recomputed by parsing origin/main:docs/tis/TIS-v1.0.md §15 — complete {1..16},
eligible {#17, #22, #23, #25, #26, #27, #39}; lowest Order selects #17.

Three parsers, one per SoLEXS product, each implementing its section of SPEC-parsers@r6.
That document is a CONTRACT: "a deviation requires a logged amendment, not a code change."

  lc   §2.1  total-band counts per second. NaN is data, not an error.
  pi   §2.2  340-channel PI spectra per second. Ordinal channels; keV is prohibited.
  gti  §2.3  inclusive second-marks; Σ(STOP−START+1) == EXPOSURE, exactly.

NO DEFAULT ON A MISSING KEY — THE RULE THE WHOLE SPEC OPENS WITH

§5: "No default-on-missing-key (header.get(K, default) is banned for any physically
meaningful key — that idiom is the direct cause of v1's thirty-sprint failure)."

The concrete failure was F-05: v1 defaulted MJDREF to 58484 where the archive declares
40587, a ~49-YEAR timestamp error nothing detected. So `_fits.py` has no `get`. Every
lookup raises. Five tests delete a keyword and require the parse to abort.

NaN IS DATA. ZERO IS A MEASUREMENT. THEY NEVER MEET.

§2.1 r2, binding: SoLEXS COUNTS uses NaN as the missing-data sentinel; NaN values pass
through unchanged, are never imputed, never converted to zero, never removed; zero
remains a valid physical count.

Mapped onto the domain exactly: NaN -> None (observed to be absent, ADR-0017/L-07),
0.0 -> 0.0. The property is quantified over every position and both directions, because
collapsing the two is the one error that would be undetectable in every downstream
artifact. No interpolate/smooth/fill/impute/resample/repair function exists in any of the
three modules, and a test asserts none appears.

F-19 is checked AFTER the NaN branch, which is what makes §2.1's note true that the rule
is "inherently NaN-safe (NaN < 0 is False)".

THE keV PROHIBITION, ENFORCED BY ABSENCE

§2.2: no RMF/ARF exists anywhere in the archive, so PI-channel -> keV is impossible from
archive contents alone, and "no v2 artifact may state a SoLEXS energy in keV until a
response file is acquired". There is no keV field, no conversion, no calibration constant
— a test scans the module and its three classes for any of them. A comment is not a
constraint; an absent field is.

EXACT MEANS EXACT

F-09 requires Σ(STOP−START+1) == EXPOSURE with tolerance 0 s. §5 says why: "the relation
is definitional, and a tolerance would re-admit the ambiguity that produced
CONTRADICTION-001". A one-second discrepancy — precisely what the superseded exclusive
reading produces — aborts. The convention is verified for 2024-05-14 SDD2 only (§8 A-8)
and is NOT widened here; §2.3 requires any deviation to terminate validation.

VERIFIED AGAINST THE REAL ARCHIVE — EVERY OBSERVED VALUE REPRODUCED

Re-measured from the 2024-05-14 SDD2 products, the X8.7 flare day:

  86,400 rows                                      §2.1 NAXIS2
  5 NaN at offsets [0, 5, 30072, 30078, 83951]     §2.1 OBSERVED, exactly
  86,395 finite                                    §2.1 == EXPOSURE
  5 GTI intervals, Σ(STOP−START+1) = 86395.0       §2.3, zero error
  GTI-excluded set == NaN set                      §2.1 OBSERVED, this day
  SDD1 GTI NAXIS2 = 0 -> detector_active=False     F-12, the one non-fatal rule
  TSTART 1715644800.0 = 2024-05-14T00:00:00Z       §2.1 epoch anchor
  DETCHANS 340, CHANTYPE PI, channel map 0..339    §2.2
  .pi TSTART[0] == .lc TSTART                      V-PI-3 / F-06

These are the specification's own OBSERVED claims, independently reproduced by this code.
A drift in either the parser or the archive is now a red test rather than a quiet
disagreement between a document and reality.

astropy GRANTED TO INGEST — THE LINE #13 PREDICTED

Reading FITS needs a FITS reader. M2/E4/#13 declined to pre-grant third-party roots and
said why: "a context that later needs `astropy` should add one reviewable line rather
than inherit a blanket permission nobody voted for (STD-11)."

Three existing tests pinned `contexts.ingest.allow == {contracts, domain, kernel}` and
went red — the mechanism working. They are updated to be STRICTER, not looser: internal
grants must still equal ADR-0026's exactly, and a new test enumerates every third-party
grant and refuses any that names a governed root of this repository.

SCOPE

.hk (§2.4, v1.1 only, 5 of 436 days) is NOT implemented: TIS E5 §5 lists this epic's
SoLEXS modules as parsers/solexs/{lc,pi,gti} and assigns no .hk module to any issue.
Reported as an Engineering Plan Defect rather than filled by guessing at scope.

No aggregation to the 1-minute grid — §3's T1/T2 contract is the write path's (#19). No
`NaN => GTI-excluded` evaluation — §2.1 places it at the day-assembly layer explicitly,
"not inside the single-file .lc parser". No HEL1OS, no curation, no registry.

VERIFICATION

  unit (kernel + domain + contexts)  470 passed  (+65)
  tests/architecture                 563 passed  (+1)
  tests/integration                  128 passed  (+14)
  research/tests/v2                  188 passed
  links / imports / contracts gates  clean
  imports gate: 46 modules, 211 imports, 9 policies

Eight real-corpus tests run where the archive is present and skip where it is not
(STD-12, E5 §17). The guard tests for .gz PRODUCTS, never a directory: five v1.1 archive
directories exist in a clean checkout because their quicklook PNGs are tracked.

CI: numpy + astropy added to the unit and integration jobs. No new job, no new context;
still eight.

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