Merge pull request #188 from ArtemFilin1990/feature/everest-stage3-oem-engineering-20261021
EVEREST stage 3: wire official OEM codebook to analogue gate, add isolated PostgreSQL smoke
This commit is contained in:
9 files changed
+354
No files matched your search
@@ -0,0 +1,46 @@
|
||||
name: EVEREST engineering core Postgres integration
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'feature/everest-stage3-oem-engineering-20261021'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'sql/engineering_core/**'
|
||||
- 'src/oem_evidence_bridge.py'
|
||||
- 'src/engineering_core.py'
|
||||
- 'src/verified_designation_dictionary.py'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
test-postgres-migration:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_USER: tester
|
||||
POSTGRES_PASSWORD: ci_password
|
||||
POSTGRES_DB: everest_bearings_ci
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U tester -d everest_bearings_ci"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
env:
|
||||
PGHOST: localhost
|
||||
PGPORT: 5432
|
||||
PGUSER: tester
|
||||
PGPASSWORD: ci_password
|
||||
PGDATABASE: everest_bearings_ci
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Apply engineering schema to ephemeral PostgreSQL
|
||||
run: psql -X -v ON_ERROR_STOP=1 -f sql/engineering_core/001_engineering_core_postgres.sql
|
||||
- name: Run rollback-only SQL assertions
|
||||
run: psql -X -v ON_ERROR_STOP=1 -f sql/engineering_core/tests/smoke.sql
|
||||
- name: Verify Python gate and OEM bridge
|
||||
run: |
|
||||
python -m pip install pytest
|
||||
PYTHONPATH=. python -m pytest -q tests/test_engineering_core.py tests/test_verified_dictionary.py tests/test_oem_evidence_bridge.py tests/test_bearing_dimension_roles.py
|
||||
@@ -0,0 +1,2 @@
|
||||
gost_designation,iso_assembly,bearing_code,adapter_code,assembly_kind,bearing_bore_d_mm,shaft_seat_d1_mm,outside_D_mm,bearing_width_B_mm,product_compatibility,source_bearing,source_assembly,note
|
||||
11215,1217K+H217,1217K,H217,BEARING_WITH_ADAPTER,85,75,150,28,CANDIDATE_ONLY,https://skf-us-cad-download.skf.com/item/self-aligning-ball/aligning-ball-bearings-with-tapered-bore-1/1217k,https://www.irbis.ua/catalog/product/11215-1217kh217-sh00004877,"Bearing bore 85 is NOT the sleeve's shaft-seat 75; B=28 is bearing width, not mounted overall assembly width. No approved substitution."
|
||||
|
@@ -0,0 +1,22 @@
|
||||
# One-shot PostgreSQL migration test in an ephemeral Docker build layer.
|
||||
# No database credentials or product data, nothing persists to runtime.
|
||||
FROM postgres:16-alpine AS verify
|
||||
WORKDIR /opt/everest-smoke
|
||||
COPY --chown=postgres:postgres sql/engineering_core/001_engineering_core_postgres.sql ./migration.sql
|
||||
COPY --chown=postgres:postgres sql/engineering_core/tests/smoke.sql ./smoke.sql
|
||||
USER postgres
|
||||
RUN set -eu; \
|
||||
initdb -D /tmp/everest-pg16 --username=postgres --auth-local=trust >/dev/null; \
|
||||
pg_ctl -D /tmp/everest-pg16 -o "-c listen_addresses=''" -w start; \
|
||||
createdb -U postgres everest_bearings_ci; \
|
||||
psql -X -v ON_ERROR_STOP=1 -U postgres -d everest_bearings_ci -f migration.sql; \
|
||||
psql -X -v ON_ERROR_STOP=1 -U postgres -d everest_bearings_ci -f smoke.sql; \
|
||||
pg_ctl -D /tmp/everest-pg16 -m fast -w stop; \
|
||||
mkdir -p /tmp/everest-smoke-status; \
|
||||
printf '%s\n' 'EVEREST PostgreSQL 16 migration smoke: PASSED' > /tmp/everest-smoke-status/index.html; \
|
||||
rm -rf /tmp/everest-pg16
|
||||
FROM python:3.13-alpine
|
||||
COPY --from=verify /tmp/everest-smoke-status /status
|
||||
USER 10001
|
||||
EXPOSE 10000
|
||||
CMD ["python", "-m", "http.server", "10000", "--bind", "0.0.0.0", "--directory", "/status"]
|
||||
@@ -0,0 +1,28 @@
|
||||
# EVEREST — external source governance (21 October 2026)
|
||||
|
||||
1. Primary manufacturer catalogues: SKF Product Select, NTN Bearing Finder, NSK online catalogue, Timken Engineering. Verify *exact* OEM SKU, publication edition and product feature before granting attribute provenance `CATALOG_VERIFIED`.
|
||||
2. Secondary cross-references: IRBIS, Podshipnik.info, 2rs.com.ua, Podshipnikon.ru, Planeta Podshipnik, BearingWorks. Search candidates only. A listing of GOST=ISO is NOT an approved analogue.
|
||||
3. Further candidates for source review: KOYO/JTEKT, AST, RBC, Kugellager Express, Sacom.lt, GlobalSpec, Size.name, Technodrive.pro, Universal Bearings. Recheck official domain, product coverage, copyright, terms and robots rules before any automated import.
|
||||
4. Reject numeric zero dimensions, ambiguous width concept B/T/C/H, inferred tolerance equivalence and unsupported manufacturer suffixes. Keep the original supplier designation and source.
|
||||
5. Every accepted attribute requires a source ID, document version, date, SKU and source location. Unknown or conflicting attributes block auto-replacement.
|
||||
6. `IDT/MOD/NEQ` is a relationship of standard *editions*, not products.
|
||||
7. Only a reviewed `analogue_decision` with immutable passport evidence and valid application scope can authorize a substitute.
|
||||
|
||||
Verified public landing pages:
|
||||
- https://productselect.skf.com/
|
||||
- https://bearingfinder.ntnamericas.com/
|
||||
- https://www.nsk.com/catalogs-and-cad/
|
||||
- https://engineering.timken.com/engineering-tool/bearing-search/
|
||||
- https://www.rbcbearings.com/Catalogs-Industrial
|
||||
- https://www.irbis.ua/ru/production/podshipniki-optom/razmery-podshipnikov
|
||||
- https://podshipnik.info/catalog
|
||||
- https://2rs.com.ua/ru/select-bearings/
|
||||
- https://podshipnikon.ru/razmeri-podshipnikov
|
||||
- https://planetapodshipnik.ru/news/post/tablica-markirovki-podshipnikov-iso-i-gost
|
||||
- https://www.bearingworks.com/bearing-sizes/
|
||||
|
||||
Batch proof: 4,583 source rows; 874 matching dictionary brand tokens; 141 fully recognized suffix sequences; 4,583 auto-replacements denied because documented scoped approvals were absent. Locally validated with 42 pytest tests; no approval of product pairs.
|
||||
|
||||
## Обнаруженный конфликт данных (не импортировать автоматически)
|
||||
|
||||
`6208`: BearingWorks показывает B=15 мм в таблице серии 6200, тогда как официальная карточка NSK `6208` указывает B=18 мм при d=40 мм, D=80 мм. Вторичный источник — https://www.bearingworks.com/bearing-sizes/ ; первичный — https://www.nsk.com/engineering/products/bearings/ball-bearings/deep-groove-ball-bearings/single-row-deep-groove-ball-bearings/6208-apn.html . Занести в реестр ошибок источников, не копировать размеры из противоречащей таблицы.
|
||||
@@ -0,0 +1,80 @@
|
||||
\set ON_ERROR_STOP on
|
||||
-- Run only against a disposable PostgreSQL database.
|
||||
DO $$ BEGIN
|
||||
IF current_database() <> 'everest_bearings_ci' THEN
|
||||
RAISE EXCEPTION 'Refusing smoke outside everest_bearings_ci';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
BEGIN;
|
||||
DO $$ DECLARE n integer; BEGIN
|
||||
SELECT count(*) INTO n FROM information_schema.tables WHERE table_schema = 'everest_engineering';
|
||||
IF n <> 10 THEN RAISE EXCEPTION 'Expected ten engineering tables, found %', n; END IF;
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='everest_engineering' AND column_name='is_direct_analog') THEN
|
||||
RAISE EXCEPTION 'Legacy unsafe is_direct_analog field leaked to engineering schema';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
INSERT INTO everest_engineering.source_documents(id,document_type,publisher,title,source_version)
|
||||
VALUES('smoke-doc','OTHER','Test suite','Ephemeral verification','1');
|
||||
INSERT INTO everest_engineering.bearing_products
|
||||
(id,original_raw_designation,brand,standard_system,basic_designation,attribute_profile,origin_document_id)
|
||||
VALUES ('smoke-a','6205','SKF','ISO','6205','RADIAL_BALL_V1','smoke-doc'),
|
||||
('smoke-b','6305','SKF','ISO','6305','RADIAL_BALL_V1','smoke-doc');
|
||||
INSERT INTO everest_engineering.bearing_equivalences
|
||||
(id,source_bearing_id,candidate_bearing_id,discovery_source_id,candidate_basis)
|
||||
VALUES ('smoke-pair','smoke-a','smoke-b','smoke-doc','DIMENSIONS');
|
||||
INSERT INTO everest_engineering.attribute_provenance
|
||||
(bearing_id,attribute_name,origin,canonical_value)
|
||||
VALUES ('smoke-a','clearance','UNKNOWN',NULL);
|
||||
|
||||
-- Expected-failure tests must assert a CHECK error actually occurred.
|
||||
DO $$ DECLARE rejected boolean := FALSE; BEGIN
|
||||
BEGIN
|
||||
INSERT INTO everest_engineering.attribute_provenance
|
||||
(bearing_id,attribute_name,origin,canonical_value)
|
||||
VALUES ('smoke-a','precision','EXPLICIT','NORMAL');
|
||||
EXCEPTION WHEN check_violation THEN rejected := TRUE;
|
||||
END;
|
||||
IF NOT rejected THEN RAISE EXCEPTION 'BUG: Explicit provenance without source was accepted'; END IF;
|
||||
END $$;
|
||||
DO $$ DECLARE rejected boolean := FALSE; BEGIN
|
||||
BEGIN
|
||||
INSERT INTO everest_engineering.analogue_decisions
|
||||
(id,equivalence_id,status,approved_by,approval_date,valid_until,justification,
|
||||
restrictions,evidence_source_id,source_snapshot_digest,candidate_snapshot_digest)
|
||||
VALUES ('smoke-unsafe','smoke-pair','CONDITIONAL','engineer',date '2026-10-21',
|
||||
date '2027-01-01','unscoped','["limit"]'::jsonb,'smoke-doc',repeat('a',64),repeat('b',64));
|
||||
EXCEPTION WHEN check_violation THEN rejected := TRUE;
|
||||
END;
|
||||
IF NOT rejected THEN RAISE EXCEPTION 'BUG: CONDITIONAL without scope was accepted'; END IF;
|
||||
END $$;
|
||||
|
||||
INSERT INTO everest_engineering.analogue_decisions(id,equivalence_id,status)
|
||||
VALUES ('smoke-dimensional','smoke-pair','DIMENSIONAL');
|
||||
INSERT INTO everest_engineering.decision_audit(decision_id,operation,actor,allowed,reason_codes)
|
||||
VALUES ('smoke-dimensional','DENIED','smoke-suite',FALSE,'["NO_APPROVED_DECISION"]'::jsonb);
|
||||
DO $$ DECLARE rejected boolean := FALSE; msg text; BEGIN
|
||||
BEGIN
|
||||
UPDATE everest_engineering.decision_audit SET allowed=TRUE WHERE actor='smoke-suite';
|
||||
EXCEPTION WHEN raise_exception THEN
|
||||
GET STACKED DIAGNOSTICS msg = MESSAGE_TEXT;
|
||||
IF msg NOT LIKE 'Append-only%' THEN RAISE; END IF;
|
||||
rejected := TRUE;
|
||||
END;
|
||||
IF NOT rejected THEN RAISE EXCEPTION 'BUG: audit UPDATE was accepted'; END IF;
|
||||
IF (SELECT allowed FROM everest_engineering.decision_audit WHERE actor='smoke-suite') THEN
|
||||
RAISE EXCEPTION 'BUG: audit row was modified'; END IF;
|
||||
END $$;
|
||||
DO $$ DECLARE rejected boolean := FALSE; msg text; BEGIN
|
||||
BEGIN
|
||||
DELETE FROM everest_engineering.analogue_decisions WHERE id='smoke-dimensional';
|
||||
EXCEPTION WHEN raise_exception THEN
|
||||
GET STACKED DIAGNOSTICS msg = MESSAGE_TEXT;
|
||||
IF msg NOT LIKE 'Append-only%' THEN RAISE; END IF;
|
||||
rejected := TRUE;
|
||||
END;
|
||||
IF NOT rejected THEN RAISE EXCEPTION 'BUG: decision DELETE was accepted'; END IF;
|
||||
END $$;
|
||||
ROLLBACK;
|
||||
SELECT 'PASS: temporary PostgreSQL 16 engineering migration + fail-closed checks' AS result;
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Explicit dimensional roles for bearings and mounted adapter assemblies.
|
||||
|
||||
Does not infer sizes or substitution rights from a designation string.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
AssemblyKind = Literal["SINGLE_BEARING", "BEARING_WITH_ADAPTER"]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BearingGeometry:
|
||||
designation: str
|
||||
assembly_kind: AssemblyKind
|
||||
bearing_bore_d_mm: Decimal | None
|
||||
outside_d_mm: Decimal | None
|
||||
bearing_width_b_mm: Decimal | None
|
||||
shaft_seat_d1_mm: Decimal | None
|
||||
sleeve_designation: str | None
|
||||
bearing_source_url: str | None
|
||||
sleeve_source_url: str | None
|
||||
|
||||
def evidence_problems(self) -> tuple[str, ...]:
|
||||
issues: list[str] = []
|
||||
if any(value is not None and value <= 0 for value in
|
||||
(self.bearing_bore_d_mm, self.outside_d_mm, self.bearing_width_b_mm,
|
||||
self.shaft_seat_d1_mm)):
|
||||
issues.append("NONPOSITIVE_DIMENSIONS")
|
||||
if self.bearing_bore_d_mm is not None and self.outside_d_mm is not None and self.bearing_bore_d_mm >= self.outside_d_mm:
|
||||
issues.append("INVALID_BEARING_BORE_GEOMETRY")
|
||||
if self.assembly_kind == "BEARING_WITH_ADAPTER":
|
||||
if not self.sleeve_designation or self.shaft_seat_d1_mm is None:
|
||||
issues.append("ADAPTER_OR_SHAFT_SEAT_MISSING")
|
||||
if self.bearing_bore_d_mm == self.shaft_seat_d1_mm:
|
||||
issues.append("ADAPTER_ROLE_AMBIGUITY")
|
||||
if not self.bearing_source_url or not self.sleeve_source_url:
|
||||
issues.append("MISSING_COMPONENT_PROVENANCE")
|
||||
elif self.sleeve_designation is not None:
|
||||
issues.append("SLEEVE_WITHOUT_ASSEMBLY")
|
||||
return tuple(issues)
|
||||
|
||||
def ddb_for_single_bearing(self) -> tuple[Decimal, Decimal, Decimal] | None:
|
||||
"""No unqualified d/D/B entry for an adapter-bearing set."""
|
||||
if self.assembly_kind != "SINGLE_BEARING" or self.evidence_problems():
|
||||
return None
|
||||
if any(x is None for x in (self.bearing_bore_d_mm, self.outside_d_mm, self.bearing_width_b_mm)):
|
||||
return None
|
||||
return self.bearing_bore_d_mm, self.outside_d_mm, self.bearing_width_b_mm
|
||||
|
||||
def compare_geometry(left: BearingGeometry, right: BearingGeometry) -> tuple[str, ...]:
|
||||
"""Identify dimensional incompatibility, never grant analogue status."""
|
||||
issues = list(left.evidence_problems()) + list(right.evidence_problems())
|
||||
if left.assembly_kind != right.assembly_kind:
|
||||
issues.append("SINGLE_VS_ASSEMBLY")
|
||||
for field in ("bearing_bore_d_mm", "outside_d_mm", "bearing_width_b_mm"):
|
||||
a, b = getattr(left, field), getattr(right, field)
|
||||
if a is None or b is None:
|
||||
issues.append(f"UNKNOWN:{field}")
|
||||
elif a != b:
|
||||
issues.append(f"DIFFERENT:{field}")
|
||||
if left.assembly_kind == right.assembly_kind == "BEARING_WITH_ADAPTER":
|
||||
if left.shaft_seat_d1_mm is None or right.shaft_seat_d1_mm is None:
|
||||
issues.append("UNKNOWN:shaft_seat_d1_mm")
|
||||
elif left.shaft_seat_d1_mm != right.shaft_seat_d1_mm:
|
||||
issues.append("DIFFERENT:shaft_seat_d1_mm")
|
||||
if left.sleeve_designation != right.sleeve_designation:
|
||||
issues.append("DIFFERENT:adapter_sleeve")
|
||||
return tuple(dict.fromkeys(issues))
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Manufacturer code meaning is not per-SKU evidence or an analogue approval."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from src.engineering_core import (
|
||||
ApplicationContext,
|
||||
AnalogueDecision,
|
||||
BearingPassport,
|
||||
GateResult,
|
||||
evaluate_autoreplace,
|
||||
)
|
||||
from src.verified_designation_dictionary import VerifiedCodebook
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CodeObservation:
|
||||
code: str
|
||||
brand: str
|
||||
feature: str
|
||||
contact_type: str
|
||||
source_url: str
|
||||
status: str = "OFFICIAL_CODE_ONLY"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InspectionResult:
|
||||
observations: tuple[CodeObservation, ...]
|
||||
unknown_suffix_parts: tuple[str, ...]
|
||||
code_parse_status: str
|
||||
gate: GateResult
|
||||
|
||||
|
||||
def inspect_candidate(
|
||||
source: BearingPassport,
|
||||
candidate: BearingPassport,
|
||||
decision: AnalogueDecision | None,
|
||||
context: ApplicationContext,
|
||||
codebook: VerifiedCodebook | None = None,
|
||||
) -> InspectionResult:
|
||||
"""No mutation of source attributes, provenance or engineering approval."""
|
||||
book = codebook or VerifiedCodebook()
|
||||
decoded = book.decode_suffix(candidate.brand, candidate.suffix)
|
||||
observations = tuple(
|
||||
CodeObservation(c.code, c.manufacturer, c.feature, c.contact_type, c.source_url)
|
||||
for c in decoded.codes
|
||||
)
|
||||
gate = evaluate_autoreplace(source, candidate, decision, context)
|
||||
return InspectionResult(observations, decoded.unknown_parts, decoded.status, gate)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Regression for ГОСТ 11215 / 1217K+H217 and false 75 mm bearing bore."""
|
||||
from decimal import Decimal as D
|
||||
from src.bearing_dimension_roles import BearingGeometry, compare_geometry
|
||||
|
||||
SKF = "https://skf-us-cad-download.skf.com/item/self-aligning-ball/aligning-ball-bearings-with-tapered-bore-1/1217k"
|
||||
IRBIS = "https://www.irbis.ua/catalog/product/11215-1217kh217-sh00004877"
|
||||
|
||||
def assembly():
|
||||
return BearingGeometry("11215 / 1217K+H217", "BEARING_WITH_ADAPTER",
|
||||
D(85), D(150), D(28), D(75), "H217", SKF, IRBIS)
|
||||
|
||||
def test_bore_and_shaft_seat_are_not_same_diameter():
|
||||
a = assembly()
|
||||
assert a.evidence_problems() == ()
|
||||
assert (a.bearing_bore_d_mm, a.shaft_seat_d1_mm) == (D(85), D(75))
|
||||
assert a.ddb_for_single_bearing() is None
|
||||
|
||||
def test_false_75mm_bore_is_detected():
|
||||
bad = BearingGeometry("11215","BEARING_WITH_ADAPTER",D(75),D(150),D(28),D(75),"H217",SKF,IRBIS)
|
||||
assert "ADAPTER_ROLE_AMBIGUITY" in bad.evidence_problems()
|
||||
|
||||
def test_assembly_not_indexed_as_single_bearing():
|
||||
standalone = BearingGeometry("1217K","SINGLE_BEARING",D(85),D(150),D(28),None,None,SKF,None)
|
||||
assert "SINGLE_VS_ASSEMBLY" in compare_geometry(assembly(),standalone)
|
||||
assert standalone.ddb_for_single_bearing() == (D(85),D(150),D(28))
|
||||
|
||||
def test_assembly_component_sources_required():
|
||||
bad = BearingGeometry("11215","BEARING_WITH_ADAPTER",D(85),D(150),D(28),D(75),"H217",None,IRBIS)
|
||||
assert "MISSING_COMPONENT_PROVENANCE" in bad.evidence_problems()
|
||||
|
||||
def test_different_shaft_seat_not_equal_assembly():
|
||||
other = BearingGeometry("1217K+H???","BEARING_WITH_ADAPTER",D(85),D(150),D(28),D(80),"H217",SKF,IRBIS)
|
||||
assert "DIFFERENT:shaft_seat_d1_mm" in compare_geometry(assembly(),other)
|
||||
|
||||
def test_bearing_data_missing_blocks_diameter_comparison():
|
||||
other = BearingGeometry("unknown","SINGLE_BEARING",None,D(150),D(28),None,None,None,None)
|
||||
assert "UNKNOWN:bearing_bore_d_mm" in compare_geometry(assembly(),other)
|
||||
@@ -0,0 +1,21 @@
|
||||
from datetime import date
|
||||
|
||||
from src.engineering_core import ApplicationContext, AttributeFact, BearingPassport
|
||||
from src.oem_evidence_bridge import inspect_candidate
|
||||
|
||||
|
||||
def passport(ident, brand, code):
|
||||
return BearingPassport(
|
||||
ident, code, brand, "ISO", "", "6205", code.split("6205")[-1],
|
||||
{"clearance": AttributeFact(None, "UNKNOWN")},
|
||||
)
|
||||
|
||||
|
||||
def test_official_code_does_not_grant_equivalence():
|
||||
source = passport("a", "NSK", "6205DDU")
|
||||
candidate = passport("b", "SKF", "6205-2RSH/C3")
|
||||
result = inspect_candidate(source, candidate, None, ApplicationContext(date(2026, 10, 21), None))
|
||||
assert [o.code for o in result.observations] == ["2RSH", "C3"]
|
||||
assert not result.gate.allow_autoreplace
|
||||
assert result.gate.reasons == ("NO_APPROVED_DECISION",)
|
||||
assert all(o.status == "OFFICIAL_CODE_ONLY" for o in result.observations)
|
||||
Reference in new issue
Block a user