Skip to content

Commit 59f05d6

Browse files
committed
Declare pyspssio capability boundary
1 parent cc49cf0 commit 59f05d6

6 files changed

Lines changed: 97 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ dictionary semantics cannot be reproduced, it stops until you pass the exact
5757
diagnostic code with `--allow-loss`. This avoids silent loss while making an
5858
intentional lossy export auditable.
5959

60+
The matrix is also available to Python callers as
61+
`openstatspec.capability_matrix()`. It distinguishes supported semantics from
62+
unobservable and fail-closed paths; see the SAV profile for the exact
63+
`pyspssio` 0.5.1 boundary.
64+
6065
See [the SAV profile](docs/sav-profile.md) for feature boundaries and
6166
[release readiness](docs/release-readiness.md) for the pre-tag checklist.
6267
Read [third-party notices](THIRD_PARTY_NOTICES.md) before distributing a

‎docs/release-readiness.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ diagnostics. An export with known loss fails unless the caller passes the
2828
relevant diagnostic codes through `allow_loss` or repeats `--allow-loss CODE`
2929
in the CLI.
3030

31-
Known boundaries include multiple-response sets, variable alignment, variable
32-
sets, and custom attributes. A caller that supplies consent receives the
33-
machine-readable loss report with the export result.
31+
The concrete pyspssio boundaries are documented in the [SAV profile](sav-profile.md):
32+
file labels/documents and independent write formats are unobservable;
33+
variable sets and legacy compatible names are fail-closed on export; only UTF-8
34+
has a source-encoding fidelity contract; and multi-value custom-attribute
35+
arrays are hard fail-closed. Scalar custom attributes, multiple-response sets,
36+
and variable alignment have verified paths. A caller that supplies consent for
37+
an available loss code receives the machine-readable loss report with the
38+
export result.
3439

3540
## Maintainer checks before tagging
3641

‎docs/sav-profile.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,28 @@ semantics fail explicitly; they never silently disappear. Encryption and
2828
byte-identical reproduction are outside the contract. The contract is semantic
2929
equivalence of supported values, order, and dictionary metadata.
3030

31+
## Engine capability boundary
32+
33+
The `openstatspec capabilities` command and the
34+
`openstatspec.capability_matrix()` function are the machine-readable declaration
35+
of this boundary. They deliberately distinguish a supported feature from a
36+
feature that the underlying engine cannot observe or write faithfully.
37+
38+
| SPSS semantic | pyspssio 0.5.1 status | Behaviour |
39+
| --- | --- | --- |
40+
| File label and document text | Unobservable | The public engine API exposes neither. Import records `file-label-and-documents-unobservable`; export requires that audited loss to be accepted. |
41+
| Print and write formats independently | Write format unobservable | The engine exposes one format and writes it as both values. Export requires `separate-write-format-unobservable`. |
42+
| Variable sets | Fail-closed on export | If a set cannot be inspected, or an inspected set cannot be written faithfully, it is recorded and export stops unless its exact loss is accepted. |
43+
| Legacy compatible variable names | Fail-closed on export | A compatible name that differs from the long source name is retained in the catalog, but cannot be set through the public writer. Export requires `compatible-variable-name-not-exportable`. |
44+
| Source encoding | UTF-8 fidelity only | UTF-8 is supported. A legacy code page is retained in metadata, but export requires `source-encoding-not-preserved` because the writer has no legacy-code-page preservation contract. |
45+
| Custom attributes with one value | Supported | File and variable scalar attributes round trip through the normalized attribute catalog. |
46+
| Custom-attribute value arrays | Fail-closed | Ordered arrays are preserved in the SQL catalog, but pyspssio accepts only one text value per attribute name. Export stops; it does not stringify, flatten, or silently drop values. |
47+
48+
The first four loss codes can be supplied through `allow_loss` only when a
49+
caller intentionally accepts the documented, machine-readable loss report.
50+
An attribute-value array has no corresponding writer representation, so it
51+
remains a hard fail rather than a lossy conversion option.
52+
3153
## Distribution
3254

3355
The required engine includes IBM I/O Module redistributables under terms that

‎src/openstatspec/__init__.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Public API for the OpenStatSpec Python reference implementation."""
22

3-
from .api import capabilities, export_sav, import_sav, inspect, validate
3+
from .api import capabilities, capability_matrix, export_sav, import_sav, inspect, validate
44
from .core import CapabilityDeclaration, LossReport, UnsupportedOperationError
55

6-
__all__ = ["CapabilityDeclaration", "LossReport", "UnsupportedOperationError", "capabilities", "export_sav", "import_sav", "inspect", "validate"]
6+
__all__ = ["CapabilityDeclaration", "LossReport", "UnsupportedOperationError", "capabilities", "capability_matrix", "export_sav", "import_sav", "inspect", "validate"]

‎src/openstatspec/api.py‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,36 @@
1212

1313

1414
def capability_matrix() -> Mapping[str, Any]:
15+
"""Return the pyspssio-backed SAV/ZSAV fidelity boundary.
16+
17+
supported means that the adapter has a tested faithful path.
18+
unobservable means that pyspssio's public reader API cannot expose the
19+
source semantic. fail-closed-on-export means an imported/catalogued value
20+
blocks export unless the documented audited loss route exists; a plain
21+
fail-closed feature has no faithful writer route at all.
22+
"""
1523
return {
1624
"spss": {
1725
"values": "supported", "variable_labels": "supported",
1826
"value_labels": "supported",
19-
"print_format": "supported", "write_format": "unobservable",
20-
"source_encoding": "preserved only when UTF-8",
27+
"print_format": "supported",
28+
"write_format": "unobservable",
29+
"file_label": "unobservable",
30+
"documents": "unobservable",
31+
"source_encoding": {
32+
"utf8": "supported",
33+
"legacy_code_pages": "fail-closed-on-export",
34+
},
2135
"measurement_level": "supported", "user_missing_rules": "supported",
22-
"documents": "unobservable", "multiple_response_sets": "supported",
23-
"variable_alignment": "supported", "variable_sets": "fail-closed",
24-
"custom_attributes": "supported", "variable_role": "supported",
36+
"multiple_response_sets": "supported",
37+
"variable_alignment": "supported",
38+
"variable_sets": "fail-closed-on-export",
39+
"compatible_variable_names": "fail-closed-on-export",
40+
"custom_attributes": {
41+
"scalar_values": "supported",
42+
"ordered_value_arrays": "fail-closed",
43+
},
44+
"variable_role": "supported",
2545
},
2646
"sql_profiles": declared_profiles(),
2747
}

‎tests/test_cli.py‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22

3+
import openstatspec
34
import openstatspec.cli
45
import pandas as pd
56
import pyspssio
@@ -33,4 +34,37 @@ def test_cli_import_inspect_validate_and_export_emit_json(tmp_path, capsys) -> N
3334
"--allow-loss", "file-label-and-documents-unobservable", "--allow-loss", "separate-write-format-unobservable",
3435
]) == 0
3536
assert json.loads(capsys.readouterr().out)["destination"] == str(output)
36-
assert pyspssio.read_sav(str(output), convert_datetimes=False)[0]["answer"].tolist() == [1.0]
37+
assert pyspssio.read_sav(str(output), convert_datetimes=False)[0]["answer"].tolist() == [1.0]
38+
39+
40+
def test_capability_matrix_is_public_and_cli_matches_engine_boundary(capsys) -> None:
41+
matrix = openstatspec.capability_matrix()
42+
43+
assert matrix["spss"] == {
44+
"values": "supported",
45+
"variable_labels": "supported",
46+
"value_labels": "supported",
47+
"print_format": "supported",
48+
"write_format": "unobservable",
49+
"file_label": "unobservable",
50+
"documents": "unobservable",
51+
"source_encoding": {
52+
"utf8": "supported",
53+
"legacy_code_pages": "fail-closed-on-export",
54+
},
55+
"measurement_level": "supported",
56+
"user_missing_rules": "supported",
57+
"multiple_response_sets": "supported",
58+
"variable_alignment": "supported",
59+
"variable_sets": "fail-closed-on-export",
60+
"compatible_variable_names": "fail-closed-on-export",
61+
"custom_attributes": {
62+
"scalar_values": "supported",
63+
"ordered_value_arrays": "fail-closed",
64+
},
65+
"variable_role": "supported",
66+
}
67+
68+
assert openstatspec.cli.main(["capabilities"]) == 0
69+
rendered = json.loads(capsys.readouterr().out)
70+
assert rendered == matrix

0 commit comments

Comments
 (0)