feat(construction_pattern): port to FULL Rust — dichotomy wave
Full parity port of src/rune/construction_pattern.py (dichotomy wave, #2479 tranche): ConstructionPattern construct/revise, record_pattern_proposal, evaluate_applicability, derive_pattern_obligations, combine_pattern_obligations as cp_* FFI entry points (doc-envelope law; freed via capability_scoring's cs_free_string — no re-exported free symbol). Frozen oracle truth: 56 cases (scripts/gen_construction_pattern_truth.py, RUNE_CP_CORE=python pinned) exercising every public function and branch — constructor validation errors, open-world ext, revision history accumulation + 200-char reason truncation, exclusion override paths, boundary-aware short-token vs containment matching, ':'-prefix entities, objective '/'-split (dashes kept), authority relationship scans with the falsy-from_id quirk, evidence truncation, EXCEPTED defaults, accepted/rejected/unresolved routing, duplicate collapse + conflicts. ctypes loader src/rune/cp_loader.py mirrors broker_loader.py; facade delegation on the four pure functions with oracle fallback and RUNE_CP_CORE=python pin. Live parity 56/56 vs oracle; battery 9/9; clippy 0; construction surface tests 88/88 green with and without the kernel live.
This commit is contained in:
1 parent
d7a5a99bab
commit
681312c69f
8 files changed
+7766
-137
No files matched your search
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze construction_pattern oracle truth for the Rust port battery
|
||||
(dichotomy wave #2479 tranche, batch: construction_pattern).
|
||||
|
||||
Exercises EVERY public function and branch of
|
||||
src/rune/construction_pattern.py:
|
||||
|
||||
ConstructionPattern.__init__ (defaults, sorting, ext, all validation errors)
|
||||
ConstructionPattern.to_dict / from_dict (roundtrip incl. ext)
|
||||
ConstructionPattern.revise (changes, history accumulation, 200-char
|
||||
reason truncation, sorted evidence/changed)
|
||||
record_pattern_proposal (bounds errors, evidence sorting, 0/1 bounds)
|
||||
evaluate_applicability (exclusions by objective-substring and by
|
||||
constraint membership, first-match-wins,
|
||||
no-signals UNKNOWN, no-groups UNKNOWN,
|
||||
surface_kinds via surface lists AND via
|
||||
resolutions, short-token vs containment path
|
||||
and entity matching, ':'-prefix entities,
|
||||
objective_any '/' splitting (dashes NOT
|
||||
split), contracts_match via authorities
|
||||
(implements/exposes/provides/serves),
|
||||
APPLIES / PARTIAL / DOES_NOT_APPLY,
|
||||
evidence [:5] truncation)
|
||||
derive_pattern_obligations (when None, always True vs truthy-not-True,
|
||||
path_contains hit/miss + [:3] truncation,
|
||||
entities_match via paths/ui/regions,
|
||||
contracts_touched via need constraints and
|
||||
via authority relationships, unrecognized
|
||||
shapes, EXCEPTED with defaults,
|
||||
accepted/rejected/unresolved routing)
|
||||
combine_pattern_obligations (duplicate collapse + also_from, same-id
|
||||
different-text conflicts, id sort, empty)
|
||||
|
||||
Each case records its INPUT and the oracle's EXPECTED output/error; replayed
|
||||
VERBATIM by src/rust/construction_pattern/src/branch_battery.rs.
|
||||
|
||||
Output: src/rust/construction_pattern/src/frozen_cp_truth.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["RUNE_CP_CORE"] = "python" # POISONED-TRUTH LAW: freeze the ORACLE
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rune import construction_pattern as cp # noqa: E402
|
||||
|
||||
OUT = Path(__file__).resolve().parents[1] / "src" / "rust" / "construction_pattern" / "src" / "frozen_cp_truth.json"
|
||||
|
||||
cases = []
|
||||
|
||||
|
||||
def need(objective="repair the deploy promotion ordering",
|
||||
affected_surface=("scripts/deploy_stage.py", "scripts/deploy_promotion.py"),
|
||||
constraints=None):
|
||||
class N:
|
||||
pass
|
||||
n = N()
|
||||
n.objective = objective
|
||||
n.affected_surface = list(affected_surface)
|
||||
n.constraints = dict(constraints if constraints is not None else {"contracts": ["contract:monotonic_promotion"]})
|
||||
return n
|
||||
|
||||
|
||||
def serialize_need(n):
|
||||
return {"objective": n.objective, "affected_surface": n.affected_surface, "constraints": n.constraints}
|
||||
|
||||
|
||||
SPINE = {
|
||||
"entities": [
|
||||
{"id": "module:deploy_stage", "kind": "module"},
|
||||
{"id": "contract:monotonic_promotion", "kind": "contract"},
|
||||
{"id": "module:deploy_qualify", "kind": "module"},
|
||||
{"id": "contract:fail_closed_deployment", "kind": "contract"},
|
||||
],
|
||||
"relationships": [
|
||||
{"type": "implements", "from": "module:deploy_stage", "to": "contract:monotonic_promotion"},
|
||||
{"type": "exposes", "from": "module:deploy_stage", "to": "contract:exposed_api"},
|
||||
{"type": "serves", "from": "module:deploy_stage", "to": "contract:served_edge"},
|
||||
{"type": "implements", "from": "module:deploy_qualify", "to": "contract:fail_closed_deployment"},
|
||||
{"type": "provides", "from": "module:deploy_qualify", "to": "contract:provided_gate"},
|
||||
{"type": "serves", "from": "module:deploy_qualify", "to": "contract:second_edge"},
|
||||
{"type": "depends_on", "from": "module:deploy_stage", "to": "module:deploy_qualify"},
|
||||
],
|
||||
}
|
||||
AUTH = {"rune_spine": SPINE}
|
||||
|
||||
SURFACE = {
|
||||
"affected_paths": [
|
||||
"scripts/deploy_stage.py", "scripts/deploy_promotion.py",
|
||||
"src/rune/rune_tui.py", "src/rune/builtin.py",
|
||||
],
|
||||
"affected_existing_entities": ["module:deploy_stage", "module:deploy_qualify", "ui:chat"],
|
||||
"affected_ui_surfaces": ["ui:version_display"],
|
||||
"affected_regions": ["region:left"],
|
||||
"workflow_config_surfaces": [".gitea/workflows/deploy.yml"],
|
||||
"resolutions": [
|
||||
{"id": "module:deploy_stage", "kind": "source_path", "verdict": "FOUND"},
|
||||
{"id": "ui:version_display", "kind": "ui_component", "verdict": "FOUND"},
|
||||
{"id": "region:left", "kind": "layout_region", "verdict": "FOUND"},
|
||||
{"id": "cfg:deploy", "kind": "workflow_config", "verdict": "FOUND"},
|
||||
{"id": "module:deploy_stage", "kind": "structural_entity", "verdict": "FOUND"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def case(name, op, inp, fn):
|
||||
import copy as _copy
|
||||
# Deep-copy the input BEFORE running the oracle: to_dict() shares
|
||||
# nested references (ext) with the live object, and revise() mutates
|
||||
# revision_history in place — without the copy the recorded input
|
||||
# aliases oracle-mutated state.
|
||||
inp = _copy.deepcopy(inp)
|
||||
try:
|
||||
expected = fn()
|
||||
except ValueError as e:
|
||||
expected = {"error": str(e)}
|
||||
cases.append({"name": name, "op": op, "input": inp, "expected": expected})
|
||||
|
||||
|
||||
# ── ConstructionPattern.__init__ / to_dict / from_dict ──────────────────────
|
||||
|
||||
FULL_KW = dict(
|
||||
revision=3, maturity="EXERCISED",
|
||||
signals={"path_contains": ["deploy"], "objective_any": ["promotion"]},
|
||||
obligations=[
|
||||
{"id": "o2", "obligation": "second duty", "when": {"always": True}},
|
||||
{"id": "o1", "obligation": "first duty", "when": {"path_contains": ["deploy"]}},
|
||||
],
|
||||
provenance=["incident:zeta", "incident:alpha"],
|
||||
exclusions=["queue-only-project", "no-deploy-zone"],
|
||||
toolchain_notes=["zinc", "amber"],
|
||||
runtime_effects=["r2", "r1"],
|
||||
)
|
||||
|
||||
|
||||
def construct_case(name, kwargs):
|
||||
case(name, "construct", {"kwargs": kwargs},
|
||||
lambda: cp.ConstructionPattern.from_dict(kwargs).to_dict())
|
||||
|
||||
|
||||
construct_case("construct-full", {"pattern_id": "p.full", "purpose": "full pattern",
|
||||
"wibble": "future-data", **FULL_KW})
|
||||
construct_case("construct-minimal", {"pattern_id": "p.min", "purpose": "minimal pattern"})
|
||||
construct_case("construct-ext-open-world", {"pattern_id": "p.ext", "purpose": "open world",
|
||||
"wibble": "future-data", "count": 7})
|
||||
construct_case("construct-empty-id", {"pattern_id": "", "purpose": "purpose"})
|
||||
construct_case("construct-empty-purpose", {"pattern_id": "p", "purpose": ""})
|
||||
construct_case("construct-obl-missing-id",
|
||||
{"pattern_id": "p", "purpose": "purpose",
|
||||
"obligations": [{"obligation": "x", "when": {}}]})
|
||||
construct_case("construct-obl-missing-text",
|
||||
{"pattern_id": "p", "purpose": "purpose", "obligations": [{"id": "x", "when": {}}]})
|
||||
construct_case("construct-obl-missing-when",
|
||||
{"pattern_id": "p", "purpose": "purpose", "obligations": [{"id": "x", "obligation": "x"}]})
|
||||
construct_case("construct-from-dict-roundtrip",
|
||||
cp.ConstructionPattern("p.rt", "roundtrip", wibble={"deep": [1, 2]}, **FULL_KW).to_dict())
|
||||
|
||||
# ── revise ───────────────────────────────────────────────────────────────────
|
||||
|
||||
REV_BASE = dict(revision=2, maturity="UNIT-PROVEN",
|
||||
provenance=["contract:c9"], exclusions=["b-ex", "a-ex"])
|
||||
|
||||
|
||||
def revise_case(name, base_kwargs, reason, evidence, changes, ext=None):
|
||||
base = cp.ConstructionPattern("p.rev", "revisable", **(ext or {}), **base_kwargs)
|
||||
case(name, "revise",
|
||||
{"pattern": base.to_dict(), "reason": reason, "evidence": evidence, "changes": changes},
|
||||
lambda: base.revise(reason=reason, evidence=evidence, changes=changes).to_dict())
|
||||
|
||||
|
||||
revise_case("revise-with-changes", REV_BASE, "new incident evidence",
|
||||
["z-evidence", "a-evidence"], {"maturity": "EXERCISED"})
|
||||
revise_case("revise-history-accumulates", {"revision": 2, "revision_history": [
|
||||
{"prior_revision": 1, "reason": "initial correction", "evidence": ["old-evidence"], "changed": ["signals"]}]},
|
||||
"second revision", ["new-evidence"], {})
|
||||
revise_case("revise-reason-truncated-200", {}, "r" * 250, ["e"], None)
|
||||
revise_case("revise-open-world-ext-carried", {}, "carries ext", ["e"], {"purpose": "revised purpose"},
|
||||
ext={"wibble": "kept"})
|
||||
|
||||
# ── record_pattern_proposal ─────────────────────────────────────────────────
|
||||
|
||||
def propose_case(name, pattern_id="x", provider="p", model="m", evidence=(), confidence=0.5):
|
||||
case(name, "propose",
|
||||
{"pattern_id": pattern_id, "provider": provider, "model": model,
|
||||
"evidence": list(evidence), "confidence": confidence},
|
||||
lambda: cp.record_pattern_proposal(pattern_id, provider=provider, model=model,
|
||||
evidence=list(evidence), confidence=confidence))
|
||||
|
||||
|
||||
propose_case("propose-valid", pattern_id="some-pattern", provider="local", model="m1",
|
||||
evidence=["z-hunch", "a-hunch"], confidence=0.5)
|
||||
case("propose-bounds-zero-one", "propose",
|
||||
{"pattern_id": "x", "provider": "p", "model": "m", "evidence": [], "confidence": [0.0, 1.0]},
|
||||
lambda: [cp.record_pattern_proposal("x", provider="p", model="m", evidence=[], confidence=0.0),
|
||||
cp.record_pattern_proposal("x", provider="p", model="m", evidence=[], confidence=1.0)])
|
||||
propose_case("propose-empty-provider", provider="")
|
||||
propose_case("propose-empty-model", model="")
|
||||
propose_case("propose-confidence-above-1", confidence=1.5)
|
||||
propose_case("propose-confidence-below-0", confidence=-0.1)
|
||||
|
||||
# ── evaluate_applicability ──────────────────────────────────────────────────
|
||||
|
||||
def eval_case(name, pattern_kwargs, n=None, surface=None, authorities=None):
|
||||
p = cp.ConstructionPattern("p.eval", "evaluated pattern", **pattern_kwargs)
|
||||
nn = n or need()
|
||||
s = surface or SURFACE
|
||||
a = authorities or AUTH
|
||||
case(name, "evaluate",
|
||||
{"pattern": p.to_dict(), "need": serialize_need(nn), "surface_result": s, "authorities": a},
|
||||
lambda: cp.evaluate_applicability(p, nn, s, a))
|
||||
|
||||
|
||||
eval_case("eval-all-groups-applies",
|
||||
{"signals": {"surface_kinds": ["source_path", "ui_component", "layout_region",
|
||||
"workflow_config", "structural_entity"],
|
||||
"path_contains": ["deploy"], "entities_match": ["module:"],
|
||||
"objective_any": ["promotion"],
|
||||
"contracts_match": ["contract:monotonic_promotion"]}})
|
||||
eval_case("eval-partial",
|
||||
{"signals": {"path_contains": ["deploy"], "entities_match": ["nope-never", "zzz"],
|
||||
"objective_any": ["zzzneverword"]}})
|
||||
eval_case("eval-none-declared-does-not-apply",
|
||||
{"signals": {"path_contains": ["/nonexistent/xyz/"], "objective_any": ["zzznever"]}})
|
||||
eval_case("eval-no-signals-unknown", {})
|
||||
eval_case("eval-no-groups-declared-unknown", {"signals": {"surface_kinds": [], "path_contains": []}})
|
||||
eval_case("eval-exclusion-objective-substring",
|
||||
{"signals": {"path_contains": ["deploy"]}, "exclusions": ["no deployment system in this project"]})
|
||||
eval_case("eval-exclusion-constraint-membership",
|
||||
{"signals": {"path_contains": ["deploy"]}, "exclusions": ["message-queue-only"]},
|
||||
n=need(constraints={"contracts": [], "pattern_exclusions": ["message-queue-only"]}))
|
||||
eval_case("eval-exclusion-first-match-wins",
|
||||
{"signals": {"path_contains": ["deploy"]},
|
||||
"exclusions": ["no deployment system in this project", "message-queue-only"]},
|
||||
n=need(constraints={"contracts": [], "pattern_exclusions": ["message-queue-only"]}))
|
||||
eval_case("eval-exclusion-declared-but-unmatched",
|
||||
{"signals": {"path_contains": ["deploy"]}, "exclusions": ["totally-unrelated-exclusion"]},
|
||||
n=need(constraints={"contracts": [], "pattern_exclusions": ["another-unrelated"]}))
|
||||
eval_case("eval-surface-kinds-via-resolutions",
|
||||
{"signals": {"surface_kinds": ["structural_entity", "nope_kind"]}})
|
||||
eval_case("eval-path-short-token",
|
||||
{"signals": {"path_contains": ["ui", "tui", "cfg", "deploy"]}})
|
||||
eval_case("eval-entity-prefix-and-tokens",
|
||||
{"signals": {"entities_match": ["ui:", "module:", "ui", "spine", "zzz"]}})
|
||||
eval_case("eval-objective-slash-split",
|
||||
{"signals": {"objective_any": ["deploy", "release", "widget"]}},
|
||||
n=need(objective="adjust the release/deploy widget", constraints={"contracts": []}))
|
||||
eval_case("eval-objective-dash-not-split",
|
||||
{"signals": {"objective_any": ["deploy", "pre-deploy"]}},
|
||||
n=need(objective="verify the pre-deploy check", constraints={"contracts": []}))
|
||||
eval_case("eval-contracts-all-rel-kinds",
|
||||
{"signals": {"contracts_match": ["contract:monotonic_promotion", "contract:exposed_api",
|
||||
"contract:served_edge", "contract:fail_closed_deployment",
|
||||
"contract:provided_gate", "contract:second_edge"]}})
|
||||
eval_case("eval-contracts-miss-non-rel-type",
|
||||
{"signals": {"contracts_match": ["module:deploy_qualify"]}})
|
||||
eval_case("eval-contracts-second-authority",
|
||||
{"signals": {"contracts_match": ["contract:scaffold_only"]}},
|
||||
authorities={"rune_spine": SPINE,
|
||||
"project_scaffold": {"relationships": [
|
||||
{"type": "implements", "from": "module:deploy_stage", "to": "contract:scaffold_only"}]}})
|
||||
eval_case("eval-empty-entity-id-quirk",
|
||||
{"signals": {"contracts_match": ["contract:monotonic_promotion"]}},
|
||||
surface={**SURFACE, "affected_existing_entities": ["", "module:deploy_qualify"]})
|
||||
|
||||
# ── derive_pattern_obligations ──────────────────────────────────────────────
|
||||
|
||||
O = lambda oid, text, when: {"id": oid, "obligation": text, "when": when}
|
||||
|
||||
|
||||
def derive_case(name, obligations, n=None, exceptions=None, surface=None, authorities=None):
|
||||
p = cp.ConstructionPattern("p.derive", "deriving pattern",
|
||||
provenance=["incident:prov-1", "contract:prov-0"],
|
||||
obligations=obligations)
|
||||
nn = n or need()
|
||||
s = surface or SURFACE
|
||||
a = authorities or AUTH
|
||||
case(name, "derive",
|
||||
{"pattern": p.to_dict(), "need": serialize_need(nn), "surface_result": s,
|
||||
"authorities": a, "project_exceptions": exceptions},
|
||||
lambda: cp.derive_pattern_obligations(p, nn, s, a, project_exceptions=exceptions))
|
||||
|
||||
|
||||
derive_case("derive-when-none-unknown", [O("mystery", "do the unconditioned thing", None)])
|
||||
derive_case("derive-always-required", [O("always-ob", "regression proof", {"always": True})])
|
||||
derive_case("derive-always-truthy-not-true",
|
||||
[O("truthy-ob", "truthy always is not always", {"always": 1})])
|
||||
derive_case("derive-path-hit-truncation",
|
||||
[O("path-ob", "touch the deploy paths", {"path_contains": ["deploy", "rune"]})])
|
||||
derive_case("derive-path-miss", [O("path-ob", "touch nothing real", {"path_contains": ["/absent/xyz/"]})])
|
||||
derive_case("derive-entities-via-ui-and-regions",
|
||||
[O("ui-ob", "respect the ui surface", {"entities_match": ["version_display"]}),
|
||||
O("region-ob", "respect the region", {"entities_match": ["left"]}),
|
||||
O("entity-ob", "respect the module", {"entities_match": ["module:"]}),
|
||||
O("miss-ob", "matches nothing", {"entities_match": ["zzz-never"]})])
|
||||
derive_case("derive-contracts-from-need-constraints",
|
||||
[O("ct-ob", "honor the explicit contract",
|
||||
{"contracts_touched": ["contract:explicit_need", "contract:absent"]})],
|
||||
n=need(constraints={"contracts": ["contract:explicit_need"]}))
|
||||
derive_case("derive-contracts-from-authorities",
|
||||
[O("ct-ob", "honor the authority-implied contract",
|
||||
{"contracts_touched": ["contract:provided_gate", "contract:served_edge", "contract:nowhere"]})])
|
||||
derive_case("derive-contracts-miss",
|
||||
[O("ct-ob", "cannot honor the unknown contract", {"contracts_touched": ["contract:not_anywhere"]})])
|
||||
derive_case("derive-unrecognized-shape", [O("weird-ob", "do the unknown thing", {"unsupported_shape": True})])
|
||||
derive_case("derive-mixed-routing",
|
||||
[O("acc-ob", "always applies", {"always": True}),
|
||||
O("rej-ob", "path never matches", {"path_contains": ["/absent/xyz/"]}),
|
||||
O("unres-ob", "unknown shape", {"weird": 1})])
|
||||
derive_case("derive-exception-with-defaults",
|
||||
[O("exc-ob", "suppressed obligation", {"always": True}),
|
||||
O("kept-ob", "surviving obligation", {"always": True})],
|
||||
exceptions=[{"obligation": "exc-ob"}])
|
||||
derive_case("derive-exception-full",
|
||||
[O("exc-ob", "suppressed obligation", {"always": True})],
|
||||
exceptions=[{"obligation": "exc-ob", "reason": "project policy documented",
|
||||
"evidence": "policy doc", "authority": "user"}])
|
||||
|
||||
# ── combine_pattern_obligations ─────────────────────────────────────────────
|
||||
|
||||
def derivation(pattern_id, rows, revision=1):
|
||||
"""rows: (id, verdict, text) — mirrors derive_pattern_obligations output."""
|
||||
out = {"pattern": pattern_id, "revision": revision, "accepted": [], "rejected": [], "unresolved": []}
|
||||
for oid, verdict, text in rows:
|
||||
entry = {"id": oid, "obligation": text, "pattern": pattern_id, "revision": revision,
|
||||
"provenance": "PATTERN", "pattern_provenance": [f"prov:{pattern_id}"],
|
||||
"verdict": verdict, "reason": "reason"}
|
||||
out[{"REQUIRED": "accepted", "APPLICABLE": "accepted",
|
||||
"NOT_APPLICABLE": "rejected"}.get(verdict, "unresolved")].append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def combine_case(name, derivs):
|
||||
case(name, "combine", {"derivations": derivs}, lambda: cp.combine_pattern_obligations(derivs))
|
||||
|
||||
|
||||
combine_case("combine-empty", [])
|
||||
combine_case("combine-single", [derivation("a", [("shared", "REQUIRED", "regression proof"),
|
||||
("z-only", "APPLICABLE", "z duty")])])
|
||||
combine_case("combine-duplicate-collapse", [
|
||||
derivation("a", [("shared", "REQUIRED", "regression proof")]),
|
||||
derivation("b", [("shared", "REQUIRED", "regression proof")]),
|
||||
derivation("c", [("shared", "REQUIRED", "regression proof")])])
|
||||
combine_case("combine-conflict-different-text", [
|
||||
derivation("x", [("shared", "REQUIRED", "version A of the rule")]),
|
||||
derivation("y", [("shared", "REQUIRED", "version B of the rule")])])
|
||||
combine_case("combine-sort-by-id", [
|
||||
derivation("p1", [("zz", "REQUIRED", "last"), ("aa", "REQUIRED", "first")]),
|
||||
derivation("p2", [("mm", "APPLICABLE", "middle")])])
|
||||
combine_case("combine-conflict-then-continue", [
|
||||
derivation("x", [("bad", "REQUIRED", "text one"), ("ok", "REQUIRED", "fine duty")]),
|
||||
derivation("y", [("bad", "REQUIRED", "text two")])])
|
||||
|
||||
# ── write truth ──────────────────────────────────────────────────────────────
|
||||
|
||||
OUT.write_text(json.dumps({"generator": "scripts/gen_construction_pattern_truth.py",
|
||||
"module": "src/rune/construction_pattern.py", "cases": cases}, indent=1) + "\n")
|
||||
print(f"{len(cases)} cases -> {OUT}")
|
||||
@@ -34,8 +34,32 @@ revision consumed so "which builds used the old rule" stays answerable.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from rune import structural_store as ss
|
||||
|
||||
|
||||
def _core():
|
||||
"""The Rust pattern core, or None when pinned to the oracle / not built.
|
||||
RUNE_CP_CORE=python pins the oracle; the kernel is a drop-in and every
|
||||
caller falls back to the oracle path on any kernel error."""
|
||||
if os.environ.get("RUNE_CP_CORE", "auto") == "python":
|
||||
return None
|
||||
try:
|
||||
from rune import cp_loader
|
||||
|
||||
return cp_loader if cp_loader.available() else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _need_dict(need) -> dict:
|
||||
return {
|
||||
"objective": need.objective,
|
||||
"affected_surface": list(need.affected_surface),
|
||||
"constraints": dict(need.constraints),
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"ConstructionPattern",
|
||||
"evaluate_applicability",
|
||||
@@ -168,6 +192,12 @@ def record_pattern_proposal(
|
||||
Recorded with verification state PENDING until deterministic project
|
||||
evidence, research evidence, proven construction evidence, or an
|
||||
explicit user decision accepts it."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.propose(pattern_id, provider, model, list(evidence), confidence)
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
if not provider or not model:
|
||||
raise ValueError("pattern proposal requires provider+model")
|
||||
if not 0.0 <= confidence <= 1.0:
|
||||
@@ -256,6 +286,13 @@ def evaluate_applicability(
|
||||
need explicitly declares the pattern non-applicable, DOES_NOT_APPLY
|
||||
with the exclusion recorded.
|
||||
"""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.evaluate(pattern.to_dict(), _need_dict(need),
|
||||
dict(surface_result), dict(authorities))
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
# §3/§18: PROJECT evidence overrides the pattern. Exclusions may be
|
||||
# declared by the project/need (pattern_exclusions constraint) or by the
|
||||
# pattern itself (non-applicability domains).
|
||||
@@ -427,6 +464,14 @@ def derive_pattern_obligations(
|
||||
verdict, reason/evidence, and provenance kind PATTERN (distinct from
|
||||
Tranche 5's STRUCTURAL derivation — §8 provenance separation).
|
||||
Project exceptions (§18) suppress with provenance retained."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.derive(pattern.to_dict(), _need_dict(need), dict(surface_result),
|
||||
dict(authorities),
|
||||
project_exceptions=list(project_exceptions or []))
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
project_exceptions = list(project_exceptions or [])
|
||||
out = {
|
||||
"pattern": pattern.pattern_id,
|
||||
@@ -474,6 +519,12 @@ def combine_pattern_obligations(derivations: list[dict]) -> dict:
|
||||
same obligation id collapse (provenance lists merge); conflicting
|
||||
obligations (same id, contradictory verdicts across patterns) surface
|
||||
as conflicts requiring evidence/user decision — never silently chosen."""
|
||||
core = _core()
|
||||
if core is not None:
|
||||
try:
|
||||
return core.combine(list(derivations))
|
||||
except Exception:
|
||||
pass # oracle fallback below; never crash the caller
|
||||
merged: dict[str, dict] = {}
|
||||
conflicts: list[dict] = []
|
||||
for d in derivations:
|
||||
|
||||
+86
-41
@@ -1,11 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ctypes loader for the Rust construction_projection core (facade burn-down).
|
||||
"""ctypes loader for the Rust construction_pattern core (dichotomy wave,
|
||||
#2479 tranche). Mirrors broker_loader.py's contract:
|
||||
|
||||
FFI contract (doc-envelope law):
|
||||
* cp_project({contract_entity, project_id, ...}) -> projection dict
|
||||
* cp_free_string(ptr)
|
||||
FFI contract (doc-envelope law — every call returns {"ok", "value"|"err"}):
|
||||
* cp_construct({pattern dict}) -> normalized pattern dict
|
||||
* cp_revise({pattern, reason, evidence, changes?}) -> new pattern dict
|
||||
* cp_propose({pattern_id, provider, model, evidence, confidence}) -> dict
|
||||
* cp_evaluate({pattern, need, surface_result, authorities}) -> verdict dict
|
||||
* cp_derive({pattern, need, surface_result, authorities,
|
||||
project_exceptions?}) -> derivation dict
|
||||
* cp_combine({derivations}) -> {"obligations", "conflicts"}
|
||||
|
||||
The `need` is a plain dict {objective, affected_surface, constraints} —
|
||||
the kernel never sees the live Need object.
|
||||
|
||||
Free: capability_scoring's cs_free_string (shared process). Override the
|
||||
.so with CP_RUST_LIB (a FILE path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
@@ -14,64 +25,98 @@ import os
|
||||
from pathlib import Path
|
||||
|
||||
_REPO = Path(__file__).resolve().parents[2]
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "construction_projection"
|
||||
_CRATE_DIR = _REPO / "src" / "rust" / "construction_pattern"
|
||||
_LIB_PATH = Path(
|
||||
os.environ.get(
|
||||
"CP_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_projection.so"
|
||||
)
|
||||
os.environ.get("CP_RUST_LIB", _CRATE_DIR / "target" / "release" / "libconstruction_pattern.so")
|
||||
)
|
||||
|
||||
_lib = None
|
||||
|
||||
|
||||
class ConstructionPatternCoreUnavailable(FileNotFoundError):
|
||||
"""The Rust pattern core is not built. Callers fall back to Python."""
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
return _LIB_PATH.is_file()
|
||||
|
||||
|
||||
_SYMS = ("cp_construct", "cp_revise", "cp_propose", "cp_evaluate", "cp_derive", "cp_combine")
|
||||
|
||||
|
||||
def _load() -> ctypes.CDLL:
|
||||
global _lib
|
||||
if _lib is not None:
|
||||
return _lib
|
||||
if not _LIB_PATH.exists():
|
||||
raise FileNotFoundError(f"construction_projection not built: {_LIB_PATH}")
|
||||
if not _LIB_PATH.is_file():
|
||||
raise ConstructionPatternCoreUnavailable(str(_LIB_PATH))
|
||||
lib = ctypes.CDLL(str(_LIB_PATH))
|
||||
lib.cp_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib.cp_free_string.restype = None
|
||||
fn = lib.cp_project
|
||||
fn.argtypes = [ctypes.c_char_p]
|
||||
fn.restype = ctypes.c_void_p
|
||||
for sym in _SYMS:
|
||||
getattr(lib, sym).argtypes = [ctypes.c_void_p]
|
||||
getattr(lib, sym).restype = ctypes.c_void_p
|
||||
# free comes from capability_scoring (same process) if loaded; else our own malloc path
|
||||
try:
|
||||
lib.cp_free_string
|
||||
except AttributeError:
|
||||
scoring = ctypes.CDLL(str(
|
||||
_REPO / "src" / "rust" / "capability_scoring" / "target" / "release" / "libcapability_scoring.so"
|
||||
))
|
||||
scoring.cs_free_string.argtypes = [ctypes.c_void_p]
|
||||
lib._free = scoring.cs_free_string
|
||||
_lib = lib
|
||||
return lib
|
||||
|
||||
|
||||
def _call(sym: str, payload: dict):
|
||||
lib = _load()
|
||||
ptr = getattr(lib, sym)(json.dumps(payload).encode("utf-8"))
|
||||
data = json.dumps(payload).encode()
|
||||
buf = ctypes.create_string_buffer(data)
|
||||
ptr = getattr(lib, sym)(ctypes.cast(buf, ctypes.c_void_p))
|
||||
if not ptr:
|
||||
raise RuntimeError(f"{sym} returned null")
|
||||
try:
|
||||
text = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
|
||||
finally:
|
||||
lib.cp_free_string(ptr)
|
||||
env = json.loads(text)
|
||||
if not env.get("ok"):
|
||||
raise RuntimeError(f"{sym} core error: {env.get('err')}")
|
||||
return env["value"]
|
||||
out = ctypes.string_at(ptr).decode()
|
||||
free = getattr(lib, "_free", None)
|
||||
if free is not None:
|
||||
free(ptr)
|
||||
doc = json.loads(out)
|
||||
if not doc.get("ok"):
|
||||
raise RuntimeError(doc.get("err", f"{sym} failed"))
|
||||
return doc["value"]
|
||||
|
||||
|
||||
def project(contract_entity: dict, contract_id: str = "", project_id: str = "", originating_goal_id: str = "",
|
||||
operation_id: str = "", waiting_state: str = "", live_correlation: str = "",
|
||||
stage: str = "", builder_state: str = "", verification_state: str = "",
|
||||
checkpoint: str = "", failure_class: str = "", completion_state: str = "") -> dict:
|
||||
return _call("cp_project", {"contract_entity": contract_entity,
|
||||
"contract_id": contract_id,
|
||||
"project_id": project_id,
|
||||
"originating_goal_id": originating_goal_id,
|
||||
"operation_id": operation_id,
|
||||
"waiting_state": waiting_state,
|
||||
"live_correlation": live_correlation,
|
||||
"stage": stage, "builder_state": builder_state,
|
||||
"verification_state": verification_state,
|
||||
"checkpoint": checkpoint,
|
||||
"failure_class": failure_class,
|
||||
"completion_state": completion_state})
|
||||
def construct(pattern: dict) -> dict:
|
||||
return _call("cp_construct", dict(pattern))
|
||||
|
||||
|
||||
def revise(pattern: dict, reason: str, evidence: list, changes: dict | None = None) -> dict:
|
||||
payload = {"pattern": pattern, "reason": reason, "evidence": list(evidence)}
|
||||
if changes is not None:
|
||||
payload["changes"] = dict(changes)
|
||||
return _call("cp_revise", payload)
|
||||
|
||||
|
||||
def propose(pattern_id: str, provider: str, model: str, evidence: list, confidence: float) -> dict:
|
||||
return _call("cp_propose", {
|
||||
"pattern_id": pattern_id, "provider": provider, "model": model,
|
||||
"evidence": list(evidence), "confidence": confidence,
|
||||
})
|
||||
|
||||
|
||||
def evaluate(pattern: dict, need: dict, surface_result: dict, authorities: dict) -> dict:
|
||||
return _call("cp_evaluate", {
|
||||
"pattern": pattern, "need": need,
|
||||
"surface_result": surface_result, "authorities": authorities,
|
||||
})
|
||||
|
||||
|
||||
def derive(pattern: dict, need: dict, surface_result: dict, authorities: dict,
|
||||
project_exceptions: list | None = None) -> dict:
|
||||
payload = {"pattern": pattern, "need": need,
|
||||
"surface_result": surface_result, "authorities": authorities}
|
||||
if project_exceptions is not None:
|
||||
payload["project_exceptions"] = list(project_exceptions)
|
||||
return _call("cp_derive", payload)
|
||||
|
||||
|
||||
def combine(derivations: list) -> dict:
|
||||
return _call("cp_combine", {"derivations": list(derivations)})
|
||||
Generated
+34
-6
@@ -6,10 +6,37 @@ version = 4
|
||||
name = "construction_pattern"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -47,7 +74,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -72,10 +98,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -85,9 +113,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -7,5 +7,4 @@ edition = "2021"
|
||||
crate-type = ["rlib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
@@ -1,36 +1,173 @@
|
||||
//! branch_battery.rs — #2479 RUST batch 34: the pattern-matching kernels.
|
||||
//! ORACLE-VERIFIED: the token-matching discipline is correct — short
|
||||
//! signals (<4 chars) must match WHOLE tokens, not substrings (the
|
||||
//! 'ui'-in-'builtin' false-positive class is blocked by design).
|
||||
//! branch_battery.rs — frozen-oracle battery for the construction_pattern
|
||||
//! port (dichotomy wave #2479 tranche).
|
||||
//!
|
||||
//! Truth embedded VERBATIM from src/frozen_cp_truth.json (frozen by
|
||||
//! scripts/gen_construction_pattern_truth.py against the live Python
|
||||
//! oracle, RUNE_CP_CORE=python pinned): every case replays its recorded
|
||||
//! INPUT through the kernel and asserts byte-identical EXPECTED output
|
||||
//! (errors compare on the exact ValueError text).
|
||||
|
||||
#[cfg(test)]
|
||||
mod battery {
|
||||
// batch 155: the kernels are now SHIPPED pub fns (promoted from here).
|
||||
use crate::{entity_matches, path_matches};
|
||||
use crate::{
|
||||
combine_pattern_obligations, construct_pattern, derive_pattern_obligations,
|
||||
entity_matches, evaluate_applicability, path_matches, record_pattern_proposal,
|
||||
revise_pattern,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
fn truth() -> Value {
|
||||
serde_json::from_str(include_str!("frozen_cp_truth.json")).expect("frozen truth parses")
|
||||
}
|
||||
|
||||
fn cases(op: &str) -> Vec<Value> {
|
||||
truth()["cases"]
|
||||
.as_array()
|
||||
.expect("cases array")
|
||||
.iter()
|
||||
.filter(|c| c["op"] == op)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn obj(v: &Value) -> serde_json::Map<String, Value> {
|
||||
v.as_object().expect("object").clone()
|
||||
}
|
||||
|
||||
fn run_construct(c: &Value) -> Result<Value, String> {
|
||||
construct_pattern(&obj(&c["input"]["kwargs"])).map(Value::Object)
|
||||
}
|
||||
|
||||
fn run_revise(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
let evidence: Vec<Value> = i["evidence"].as_array().cloned().unwrap_or_default();
|
||||
let changes = if i["changes"].is_null() { None } else { Some(obj(&i["changes"])) };
|
||||
revise_pattern(&obj(&i["pattern"]), i["reason"].as_str().unwrap_or(""), &evidence, changes.as_ref())
|
||||
.map(Value::Object)
|
||||
}
|
||||
|
||||
fn run_propose(c: &Value) -> Result<Value, String> {
|
||||
let i = &c["input"];
|
||||
if let Some(bounds) = i["confidence"].as_array() {
|
||||
let mut out = Vec::new();
|
||||
for b in bounds {
|
||||
out.push(record_pattern_proposal(
|
||||
i["pattern_id"].as_str().unwrap_or(""),
|
||||
i["provider"].as_str().unwrap_or(""),
|
||||
i["model"].as_str().unwrap_or(""),
|
||||
vec![],
|
||||
b.as_f64().unwrap(),
|
||||
)?);
|
||||
}
|
||||
return Ok(Value::Array(out));
|
||||
}
|
||||
let evidence: Vec<String> =
|
||||
i["evidence"].as_array().map(|a| a.iter().filter_map(|e| e.as_str().map(String::from)).collect()).unwrap_or_default();
|
||||
record_pattern_proposal(
|
||||
i["pattern_id"].as_str().unwrap_or(""),
|
||||
i["provider"].as_str().unwrap_or(""),
|
||||
i["model"].as_str().unwrap_or(""),
|
||||
evidence,
|
||||
i["confidence"].as_f64().unwrap_or(f64::NAN),
|
||||
)
|
||||
}
|
||||
|
||||
fn run_evaluate(c: &Value) -> Value {
|
||||
let i = &c["input"];
|
||||
evaluate_applicability(&obj(&i["pattern"]), &obj(&i["need"]), &obj(&i["surface_result"]), &obj(&i["authorities"]))
|
||||
}
|
||||
|
||||
fn run_derive(c: &Value) -> Value {
|
||||
let i = &c["input"];
|
||||
let exc: Vec<Value> =
|
||||
if i["project_exceptions"].is_null() { vec![] } else { i["project_exceptions"].as_array().cloned().unwrap_or_default() };
|
||||
derive_pattern_obligations(&obj(&i["pattern"]), &obj(&i["need"]), &obj(&i["surface_result"]), &obj(&i["authorities"]), &exc)
|
||||
}
|
||||
|
||||
fn run_combine(c: &Value) -> Value {
|
||||
let derivs: Vec<Value> = c["input"]["derivations"].as_array().cloned().unwrap_or_default();
|
||||
combine_pattern_obligations(&derivs)
|
||||
}
|
||||
|
||||
fn check(name: &str, got: Result<Value, String>, expected: &Value) {
|
||||
match expected.get("error") {
|
||||
Some(Value::String(msg)) => match got {
|
||||
Err(e) => assert_eq!(&e, msg, "{name}: error text"),
|
||||
Ok(v) => panic!("{name}: kernel succeeded ({v}) where oracle raised"),
|
||||
},
|
||||
_ => match got {
|
||||
Ok(v) => assert_eq!(&v, expected, "{name}: value mismatch"),
|
||||
Err(e) => panic!("{name}: unexpected kernel error {e}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_construct() {
|
||||
let cs = cases("construct");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_construct(c), &c["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_revise() {
|
||||
let cs = cases("revise");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_revise(c), &c["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_propose() {
|
||||
let cs = cases("propose");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
check(c["name"].as_str().unwrap(), run_propose(c), &c["expected"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_evaluate() {
|
||||
let cs = cases("evaluate");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
let got = run_evaluate(c);
|
||||
assert_eq!(&got, &c["expected"], "{}: value mismatch", c["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_derive() {
|
||||
let cs = cases("derive");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
let got = run_derive(c);
|
||||
assert_eq!(&got, &c["expected"], "{}: value mismatch", c["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn battery_combine() {
|
||||
let cs = cases("combine");
|
||||
assert!(!cs.is_empty());
|
||||
for c in &cs {
|
||||
let got = run_combine(c);
|
||||
assert_eq!(&got, &c["expected"], "{}: value mismatch", c["name"]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── oracle-verified matcher discipline (batch 155, preserved) ──
|
||||
|
||||
#[test]
|
||||
fn path_token_discipline() {
|
||||
// ORACLE-VERIFIED: short signals match WHOLE tokens only
|
||||
assert!(
|
||||
!path_matches("ui", "src/rune/rune_tui.py"),
|
||||
"'ui' is not a token in rune_tui.py"
|
||||
);
|
||||
assert!(
|
||||
!path_matches("ui", "src/rune/builtin.py"),
|
||||
"the 'ui'-in-'builtin' FP class"
|
||||
);
|
||||
assert!(
|
||||
path_matches("spine", "src/rune/spine.py"),
|
||||
"long signal: containment"
|
||||
);
|
||||
assert!(
|
||||
path_matches("tui", "src/rune/rune_tui.py"),
|
||||
"'tui' IS a token"
|
||||
);
|
||||
assert!(
|
||||
!path_matches("cfg", "config/settings.py"),
|
||||
"'cfg' not a token of config"
|
||||
);
|
||||
assert!(!path_matches("ui", "src/rune/rune_tui.py"), "'ui' is not a token in rune_tui.py");
|
||||
assert!(!path_matches("ui", "src/rune/builtin.py"), "the 'ui'-in-'builtin' FP class");
|
||||
assert!(path_matches("spine", "src/rune/spine.py"), "long signal: containment");
|
||||
assert!(path_matches("tui", "src/rune/rune_tui.py"), "'tui' IS a token");
|
||||
assert!(!path_matches("cfg", "config/settings.py"), "'cfg' not a token of config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -39,21 +176,13 @@ mod battery {
|
||||
assert!(entity_matches("module:", "module:spine"), "prefix match");
|
||||
assert!(!entity_matches("ui:", "region:left"), "prefix mismatch");
|
||||
assert!(entity_matches("spine", "module:spine"), "long containment");
|
||||
// ORACLE-VERIFIED: 'ui' (no colon, <4 chars) does NOT match 'ui:chat'
|
||||
// because the entity token-split keeps ':' as part of the token
|
||||
assert!(
|
||||
!entity_matches("ui", "ui:chat"),
|
||||
"oracle truth: token discipline"
|
||||
);
|
||||
assert!(!entity_matches("ui", "ui:chat"), "oracle truth: token discipline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_signal_containment() {
|
||||
assert!(path_matches("structural", "src/rune/structural_store.py"));
|
||||
assert!(entity_matches(
|
||||
"construction",
|
||||
"module:construction_pattern"
|
||||
));
|
||||
assert!(entity_matches("construction", "module:construction_pattern"));
|
||||
assert!(!path_matches("structural", "src/rune/other.py"));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,74 +1,774 @@
|
||||
//! construction_pattern — reusable construction knowledge (W6).
|
||||
use serde::{Deserialize, Serialize};
|
||||
//! construction_pattern — reusable evidence about HOW a class of thing
|
||||
//! is normally built (SCAFFOLD-TRANCHE-006).
|
||||
//!
|
||||
//! FULL parity port of src/rune/construction_pattern.py (dichotomy wave,
|
||||
//! #2479 tranche). A pattern is GUIDANCE, not authority (§3): project
|
||||
//! evidence always wins. Applicability is evidence-backed and deterministic;
|
||||
//! obligation derivation is filtered by the project; provenance answers
|
||||
//! "why is this here"; revisions keep old-rule traceability (§17).
|
||||
//!
|
||||
//! FFI (doc-envelope law), one entry point per public oracle function:
|
||||
//! cp_construct({pattern dict}) → normalized pattern dict
|
||||
//! cp_revise({pattern, reason, evidence, changes?}) → new pattern dict
|
||||
//! cp_propose({pattern_id, provider, model, evidence, confidence}) → dict
|
||||
//! cp_evaluate({pattern, need, surface_result, authorities}) → verdict dict
|
||||
//! cp_derive({pattern, need, surface_result, authorities,
|
||||
//! project_exceptions?}) → derivation dict
|
||||
//! cp_combine({derivations}) → {"obligations", "conflicts"}
|
||||
//! The `need` arrives as a plain dict {objective, affected_surface,
|
||||
//! constraints} — the kernel never sees the live Need object.
|
||||
//!
|
||||
//! The boundary-aware matchers are the oracle-verified kernels promoted by
|
||||
//! batch 155 (the 'ui'-in-'builtin' false-positive class is blocked by
|
||||
//! design): short (<4 char, char-counted) signals match whole tokens only.
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConstructionPattern {
|
||||
pub pattern_id: String,
|
||||
pub description: String,
|
||||
pub obligations: Vec<serde_json::Value>,
|
||||
pub revision: String,
|
||||
}
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PatternApplicability {
|
||||
pub verdict: String,
|
||||
pub reason: String,
|
||||
}
|
||||
// ── verdict lattices (string constants, oracle-identical) ─────────────────
|
||||
|
||||
pub fn check_applicability(
|
||||
_pattern: &ConstructionPattern,
|
||||
project_evidence: &serde_json::Value,
|
||||
) -> PatternApplicability {
|
||||
// Patterns are GUIDANCE, not authority. Project evidence wins.
|
||||
if project_evidence
|
||||
.get("overrides_pattern")
|
||||
.and_then(|o| o.as_bool())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return PatternApplicability {
|
||||
verdict: "DOES_NOT_APPLY".to_string(),
|
||||
reason: "project evidence overrides pattern".to_string(),
|
||||
};
|
||||
pub const APPLIES: &str = "APPLIES";
|
||||
pub const PARTIAL: &str = "PARTIAL";
|
||||
pub const DOES_NOT_APPLY: &str = "DOES_NOT_APPLY";
|
||||
pub const UNKNOWN: &str = "UNKNOWN";
|
||||
|
||||
pub const OBL_REQUIRED: &str = "REQUIRED";
|
||||
pub const OBL_APPLICABLE: &str = "APPLICABLE";
|
||||
pub const OBL_NOT_APPLICABLE: &str = "NOT_APPLICABLE";
|
||||
pub const OBL_UNKNOWN: &str = "UNKNOWN";
|
||||
|
||||
const KNOWN_KEYS: &[&str] = &[
|
||||
"pattern_id",
|
||||
"purpose",
|
||||
"revision",
|
||||
"maturity",
|
||||
"signals",
|
||||
"obligations",
|
||||
"provenance",
|
||||
"exclusions",
|
||||
"toolchain_notes",
|
||||
"runtime_effects",
|
||||
];
|
||||
|
||||
const REL_TYPES: &[&str] = &["implements", "exposes", "provides", "serves"];
|
||||
|
||||
// ── Python-repr helpers (reason/error strings embed Python list/dict reprs) ──
|
||||
|
||||
/// Single-quoted Python string repr (escapes \\, \', \n, \r, \t).
|
||||
fn py_quote(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len() + 2);
|
||||
out.push('\'');
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\'' => out.push_str("\\'"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
PatternApplicability {
|
||||
verdict: "APPLIES".to_string(),
|
||||
reason: "no override".to_string(),
|
||||
out.push('\'');
|
||||
out
|
||||
}
|
||||
|
||||
/// Python `repr` of a JSON value: True/False/None, single-quoted strings,
|
||||
/// `[...]` lists, `{k: v}` dicts in insertion order.
|
||||
fn py_repr(v: &Value) -> String {
|
||||
match v {
|
||||
Value::Null => "None".to_string(),
|
||||
Value::Bool(true) => "True".to_string(),
|
||||
Value::Bool(false) => "False".to_string(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::String(s) => py_quote(s),
|
||||
Value::Array(items) => {
|
||||
let inner: Vec<String> = items.iter().map(py_repr).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let inner: Vec<String> = map.iter().map(|(k, v)| format!("{}: {}", py_quote(k), py_repr(v))).collect();
|
||||
format!("{{{}}}", inner.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// pathMatches — boundary-aware path-signal matching (batch 155: promoted
|
||||
/// from the test module to the shipped lib — the kernels ARE the product).
|
||||
/// Short (<4 char) signals match path TOKENS only (the 'ui'-in-'builtin'
|
||||
/// false-positive class); longer signals may use containment.
|
||||
fn py_str_list(items: &[String]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| py_quote(s)).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
|
||||
// ── shared boundary-aware matchers (Tranche 7 §0) ──────────────────────────
|
||||
|
||||
/// Oracle-faithful token split: runs of non-token chars are ONE separator,
|
||||
/// with leading/trailing empties kept (`re.split` semantics — only the
|
||||
/// empties are observable, via the empty-signal quirk).
|
||||
fn split_tokens(lowered: &str, keep_colon: bool) -> Vec<String> {
|
||||
let is_tok = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || (keep_colon && c == ':');
|
||||
let mut out = Vec::new();
|
||||
let mut cur = String::new();
|
||||
let mut pending_empty = true;
|
||||
for c in lowered.chars() {
|
||||
if is_tok(c) {
|
||||
cur.push(c);
|
||||
pending_empty = false;
|
||||
} else if !cur.is_empty() || pending_empty {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
pending_empty = false;
|
||||
}
|
||||
}
|
||||
out.push(cur);
|
||||
out
|
||||
}
|
||||
|
||||
/// Boundary-aware path-signal matching: short (<4 char) signals match path
|
||||
/// TOKENS only (the 'ui'-in-'builtin' false-positive class); longer signals
|
||||
/// may use containment.
|
||||
pub fn path_matches(signal: &str, path: &str) -> bool {
|
||||
let s = signal.to_lowercase();
|
||||
if s.len() >= 4 {
|
||||
if s.chars().count() >= 4 {
|
||||
return path.to_lowercase().contains(&s);
|
||||
}
|
||||
let lower = path.to_lowercase();
|
||||
let tokens: std::collections::HashSet<&str> = lower
|
||||
.split(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit())
|
||||
.collect();
|
||||
tokens.contains(s.as_str())
|
||||
split_tokens(&path.to_lowercase(), false).iter().any(|t| t == &s)
|
||||
}
|
||||
|
||||
/// entityMatches — same discipline for entity ids; ':'-suffixed signals
|
||||
/// match the namespace prefix (structural kinds carry meaning there).
|
||||
/// Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
/// 'ui:', 'region:') match the id's namespace prefix — structural kinds
|
||||
/// carry meaning in their prefix, and prefix equality is exact, not a
|
||||
/// substring accident.
|
||||
pub fn entity_matches(signal: &str, entity_id: &str) -> bool {
|
||||
let s = signal.to_lowercase();
|
||||
if s.ends_with(':') {
|
||||
return entity_id.to_lowercase().starts_with(&s);
|
||||
return entity_id.to_lowercase().starts_with(s.as_str());
|
||||
}
|
||||
if s.len() >= 4 {
|
||||
if s.chars().count() >= 4 {
|
||||
return entity_id.to_lowercase().contains(&s);
|
||||
}
|
||||
let lower = entity_id.to_lowercase();
|
||||
let tokens: std::collections::HashSet<&str> = lower
|
||||
.split(|c: char| !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != ':')
|
||||
.collect();
|
||||
tokens.contains(s.as_str())
|
||||
split_tokens(&entity_id.to_lowercase(), true).iter().any(|t| t == &s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// ── small JSON accessors ──────────────────────────────────────────────────
|
||||
|
||||
fn str_list(v: &Value) -> Vec<String> {
|
||||
v.as_array()
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn sorted_strings(mut v: Vec<String>) -> Vec<String> {
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
/// `ss.relationships(doc, from_id=e)` — including the falsy-filter quirk:
|
||||
/// an empty from_id applies NO filter (Python `if from_id and ...`).
|
||||
fn relationships<'a>(doc: &'a Value, from_id: &str) -> Vec<&'a Map<String, Value>> {
|
||||
doc.get("relationships")
|
||||
.and_then(|r| r.as_array())
|
||||
.map(|rels| {
|
||||
rels.iter()
|
||||
.filter_map(|r| r.as_object())
|
||||
.filter(|r| from_id.is_empty() || r.get("from").and_then(|f| f.as_str()).unwrap_or("") == from_id)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ── pattern object (open-world) ───────────────────────────────────────────
|
||||
|
||||
/// `ConstructionPattern.from_dict` / constructor / `to_dict`: validate the
|
||||
/// required fields, then normalize (sorted provenance-family lists, ext
|
||||
/// passthrough). The input and output share one shape — `to_dict` output
|
||||
/// round-trips through here unchanged.
|
||||
pub fn construct_pattern(input: &Map<String, Value>) -> Result<Map<String, Value>, String> {
|
||||
let pattern_id = input.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if pattern_id.is_empty() {
|
||||
return Err("pattern_id required".to_string());
|
||||
}
|
||||
let purpose = input.get("purpose").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if purpose.is_empty() {
|
||||
return Err("purpose required".to_string());
|
||||
}
|
||||
if let Some(obs) = input.get("obligations").and_then(|v| v.as_array()) {
|
||||
for o in obs {
|
||||
for key in ["id", "obligation", "when"] {
|
||||
if o.get(key).is_none() {
|
||||
return Err(format!("obligation missing {}: {}", py_quote(key), py_repr(o)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Map::new();
|
||||
out.insert("pattern_id".into(), Value::String(pattern_id.to_string()));
|
||||
out.insert("purpose".into(), Value::String(purpose.to_string()));
|
||||
out.insert(
|
||||
"revision".into(),
|
||||
input.get("revision").cloned().unwrap_or(json!(1)),
|
||||
);
|
||||
out.insert(
|
||||
"maturity".into(),
|
||||
input.get("maturity").cloned().unwrap_or(json!("CANDIDATE")),
|
||||
);
|
||||
out.insert(
|
||||
"signals".into(),
|
||||
input.get("signals").cloned().unwrap_or_else(|| Value::Object(Map::new())),
|
||||
);
|
||||
out.insert(
|
||||
"obligations".into(),
|
||||
input.get("obligations").cloned().unwrap_or_else(|| Value::Array(vec![])),
|
||||
);
|
||||
for key in ["provenance", "exclusions", "toolchain_notes", "runtime_effects"] {
|
||||
let vals = input.get(key).map(str_list).unwrap_or_default();
|
||||
out.insert(key.into(), json!(sorted_strings(vals)));
|
||||
}
|
||||
for (k, v) in input.iter() {
|
||||
if !KNOWN_KEYS.contains(&k.as_str()) {
|
||||
out.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `ConstructionPattern.revise` (§17): returns a NEW pattern at revision+1
|
||||
/// recording the change — prior meaning is never invisibly mutated.
|
||||
pub fn revise_pattern(
|
||||
pattern: &Map<String, Value>,
|
||||
reason: &str,
|
||||
evidence: &[Value],
|
||||
changes: Option<&Map<String, Value>>,
|
||||
) -> Result<Map<String, Value>, String> {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let revision = pattern.get("revision").and_then(|v| v.as_i64()).unwrap_or(1);
|
||||
let mut merged = pattern.clone();
|
||||
merged.remove("pattern_id");
|
||||
if let Some(ch) = changes {
|
||||
for (k, v) in ch.iter() {
|
||||
merged.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
merged.insert("revision".into(), json!(revision + 1));
|
||||
let mut ev: Vec<String> = evidence.iter().filter_map(|e| e.as_str().map(String::from)).collect();
|
||||
ev.sort();
|
||||
let mut changed: Vec<String> = changes.map(|ch| ch.keys().cloned().collect()).unwrap_or_default();
|
||||
changed.sort();
|
||||
let entry = json!({
|
||||
"prior_revision": revision,
|
||||
"reason": reason.chars().take(200).collect::<String>(),
|
||||
"evidence": ev,
|
||||
"changed": changed,
|
||||
});
|
||||
match merged.get_mut("revision_history") {
|
||||
Some(Value::Array(hist)) => hist.push(entry),
|
||||
_ => {
|
||||
merged.insert("revision_history".into(), Value::Array(vec![entry]));
|
||||
}
|
||||
}
|
||||
merged.insert("pattern_id".into(), Value::String(pattern_id));
|
||||
construct_pattern(&merged)
|
||||
}
|
||||
|
||||
// ── model proposal seam (§5 — recorded, never authoritative) ────────────────
|
||||
|
||||
/// A model may PROPOSE a pattern; it may not certify applicability.
|
||||
pub fn record_pattern_proposal(
|
||||
pattern_id: &str,
|
||||
provider: &str,
|
||||
model: &str,
|
||||
evidence: Vec<String>,
|
||||
confidence: f64,
|
||||
) -> Result<Value, String> {
|
||||
if provider.is_empty() || model.is_empty() {
|
||||
return Err("pattern proposal requires provider+model".to_string());
|
||||
}
|
||||
if !(0.0..=1.0).contains(&confidence) {
|
||||
return Err("confidence must be in [0,1]".to_string());
|
||||
}
|
||||
Ok(json!({
|
||||
"kind": "pattern_proposal",
|
||||
"pattern_id": pattern_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": sorted_strings(evidence),
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}))
|
||||
}
|
||||
|
||||
// ── applicability (§4 — deterministic signals only) ─────────────────────────
|
||||
|
||||
fn surface_kind_present(kind: &str, surface: &Map<String, Value>) -> bool {
|
||||
let mapped = match kind {
|
||||
"source_path" => "affected_paths",
|
||||
"workflow_config" => "workflow_config_surfaces",
|
||||
"ui_component" => "affected_ui_surfaces",
|
||||
"layout_region" => "affected_regions",
|
||||
_ => "",
|
||||
};
|
||||
if surface.get(mapped).and_then(|v| v.as_array()).map(|a| !a.is_empty()).unwrap_or(false) {
|
||||
return true;
|
||||
}
|
||||
surface
|
||||
.get("resolutions")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|rs| rs.iter().any(|r| r.get("kind").and_then(|k| k.as_str()).unwrap_or("") == kind))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Deterministic applicability verdict from the pattern's declared signals.
|
||||
pub fn evaluate_applicability(
|
||||
pattern: &Map<String, Value>,
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let objective = need.get("objective").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let exclusions = pattern.get("exclusions").map(str_list).unwrap_or_default();
|
||||
let need_exclusions = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("pattern_exclusions"))
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
|
||||
// §3/§18: PROJECT evidence overrides the pattern.
|
||||
let mut declared = exclusions;
|
||||
declared.extend(need_exclusions.clone());
|
||||
let objective_low = objective.to_lowercase();
|
||||
for ex in &declared {
|
||||
if objective_low.contains(&ex.to_lowercase()) || need_exclusions.iter().any(|x| x == ex) {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": DOES_NOT_APPLY,
|
||||
"reason": format!("declared exclusion matched: {ex}"),
|
||||
"evidence": {"exclusion": ex, "source": "project/need declaration"},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let signals = pattern.get("signals").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
if signals.is_empty() {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "pattern declares no applicability signals — nothing deterministic to check",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
|
||||
let mut groups_hit: Vec<String> = Vec::new();
|
||||
let mut groups_declared: i64 = 0;
|
||||
let mut evidence = Map::new();
|
||||
|
||||
let paths: Vec<String> = surface
|
||||
.get("affected_paths")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let entities: Vec<String> =
|
||||
surface.get("affected_existing_entities").map(str_list).unwrap_or_default();
|
||||
|
||||
let mut check = |group: &str, candidates: Vec<String>, present: &dyn Fn(&str) -> bool| {
|
||||
if candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
groups_declared += 1;
|
||||
let hits: Vec<String> = candidates.iter().filter(|c| present(c)).cloned().collect();
|
||||
if !hits.is_empty() {
|
||||
groups_hit.push(group.to_string());
|
||||
evidence.insert(group.to_string(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
||||
}
|
||||
};
|
||||
|
||||
check(
|
||||
"surface_kinds",
|
||||
signals.get("surface_kinds").map(str_list).unwrap_or_default(),
|
||||
&|c| surface_kind_present(c, surface),
|
||||
);
|
||||
check(
|
||||
"path_contains",
|
||||
signals.get("path_contains").map(str_list).unwrap_or_default(),
|
||||
&|c| paths.iter().any(|p| path_matches(c, p)),
|
||||
);
|
||||
check(
|
||||
"entities_match",
|
||||
signals.get("entities_match").map(str_list).unwrap_or_default(),
|
||||
&|c| entities.iter().any(|e| entity_matches(c, e)),
|
||||
);
|
||||
let objective_words: Vec<String> =
|
||||
objective.replace('/', " ").split_whitespace().map(|w| w.to_lowercase()).collect();
|
||||
check(
|
||||
"objective_any",
|
||||
signals
|
||||
.get("objective_any")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|w| w.to_lowercase())
|
||||
.collect(),
|
||||
&|c| objective_words.iter().any(|w| w == c),
|
||||
);
|
||||
|
||||
if let Some(contracts) = signals.get("contracts_match").and_then(|v| v.as_array()) {
|
||||
if !contracts.is_empty() {
|
||||
groups_declared += 1;
|
||||
let wanted: Vec<&str> = contracts.iter().filter_map(|c| c.as_str()).collect();
|
||||
let mut hits: Vec<String> = Vec::new();
|
||||
for doc in authorities.values() {
|
||||
for e in &entities {
|
||||
for r in relationships(doc, e) {
|
||||
let rtype = r.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let to = r.get("to").and_then(|t| t.as_str()).unwrap_or("");
|
||||
if REL_TYPES.contains(&rtype) && wanted.contains(&to) {
|
||||
hits.push(format!("{e}->{to}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hits.is_empty() {
|
||||
groups_hit.push("contracts_match".to_string());
|
||||
evidence.insert("contracts_match".into(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if groups_declared == 0 {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "no applicable signal groups declared",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
let verdict = if groups_hit.len() as i64 == groups_declared {
|
||||
APPLIES
|
||||
} else if !groups_hit.is_empty() {
|
||||
PARTIAL
|
||||
} else {
|
||||
DOES_NOT_APPLY
|
||||
};
|
||||
let mut hit_sorted = groups_hit.clone();
|
||||
hit_sorted.sort();
|
||||
let detail = if hit_sorted.is_empty() { "none".to_string() } else { py_str_list(&hit_sorted) };
|
||||
json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": verdict,
|
||||
"reason": format!("{}/{} signal groups produced evidence: {}", groups_hit.len(), groups_declared, detail),
|
||||
"evidence": evidence,
|
||||
})
|
||||
}
|
||||
|
||||
// ── obligation derivation (§6-7 — filtered by project evidence) ─────────────
|
||||
|
||||
/// Evaluate one obligation's `when` condition against project evidence.
|
||||
pub fn obligation_when_hit(
|
||||
obligation: &Map<String, Value>,
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> (String, String) {
|
||||
let paths: Vec<String> = surface
|
||||
.get("affected_paths")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let entities: Vec<String> = surface
|
||||
.get("affected_existing_entities")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("affected_ui_surfaces").map(str_list).unwrap_or_default())
|
||||
.chain(surface.get("affected_regions").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let when = match obligation.get("when") {
|
||||
None | Some(Value::Null) => {
|
||||
return (OBL_UNKNOWN.to_string(), "obligation declares no applicability condition".to_string());
|
||||
}
|
||||
Some(w) => w,
|
||||
};
|
||||
if when.get("always").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return (
|
||||
OBL_REQUIRED.to_string(),
|
||||
"pattern-level obligation (always applies when the pattern applies)".to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(subs) = when.get("path_contains") {
|
||||
let hits: Vec<String> =
|
||||
str_list(subs).iter().flat_map(|sub| paths.iter().filter(|p| path_matches(sub, p)).cloned()).collect();
|
||||
if !hits.is_empty() {
|
||||
return (OBL_APPLICABLE.to_string(), format!("affected path(s): {}", py_str_list(&hits[..3.min(hits.len())])));
|
||||
}
|
||||
return (OBL_NOT_APPLICABLE.to_string(), format!("no affected path matches {}", py_repr(subs)));
|
||||
}
|
||||
if let Some(subs) = when.get("entities_match") {
|
||||
let hits: Vec<String> = str_list(subs)
|
||||
.iter()
|
||||
.flat_map(|sub| entities.iter().filter(|e| entity_matches(sub, e)).cloned())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (
|
||||
OBL_APPLICABLE.to_string(),
|
||||
format!("affected entity/entities: {}", py_str_list(&hits[..3.min(hits.len())])),
|
||||
);
|
||||
}
|
||||
return (OBL_NOT_APPLICABLE.to_string(), format!("no affected entity matches {}", py_repr(subs)));
|
||||
}
|
||||
if let Some(wanted) = when.get("contracts_touched") {
|
||||
let mut touched: Vec<String> = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("contracts"))
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
for doc in authorities.values() {
|
||||
for e in &entities {
|
||||
for r in relationships(doc, e) {
|
||||
let rtype = r.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let to = r.get("to").and_then(|t| t.as_str()).unwrap_or("").to_string();
|
||||
if REL_TYPES.contains(&rtype) && !touched.contains(&to) {
|
||||
touched.push(to);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hits: Vec<String> =
|
||||
str_list(wanted).into_iter().filter(|c| touched.contains(c)).collect();
|
||||
if !hits.is_empty() {
|
||||
return (OBL_APPLICABLE.to_string(), format!("contract(s) touched: {}", py_str_list(&hits)));
|
||||
}
|
||||
return (OBL_NOT_APPLICABLE.to_string(), format!("touched contracts do not include {}", py_repr(wanted)));
|
||||
}
|
||||
let mut keys: Vec<String> = when.as_object().map(|m| m.keys().cloned().collect()).unwrap_or_default();
|
||||
keys.sort();
|
||||
(OBL_UNKNOWN.to_string(), format!("unrecognized when-condition shape: {}", py_str_list(&keys)))
|
||||
}
|
||||
|
||||
/// Derive this pattern's obligations FILTERED by project evidence (§7).
|
||||
pub fn derive_pattern_obligations(
|
||||
pattern: &Map<String, Value>,
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
project_exceptions: &[Value],
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let revision = pattern.get("revision").cloned().unwrap_or(json!(1));
|
||||
let provenance = pattern.get("provenance").cloned().unwrap_or_else(|| Value::Array(vec![]));
|
||||
let mut accepted = Vec::new();
|
||||
let mut rejected = Vec::new();
|
||||
let mut unresolved = Vec::new();
|
||||
if let Some(obs) = pattern.get("obligations").and_then(|v| v.as_array()) {
|
||||
for o in obs {
|
||||
let o_obj = o.as_object().cloned().unwrap_or_default();
|
||||
let mut entry = Map::new();
|
||||
entry.insert("id".into(), o.get("id").cloned().unwrap_or(Value::Null));
|
||||
entry.insert("obligation".into(), o.get("obligation").cloned().unwrap_or(Value::Null));
|
||||
entry.insert("pattern".into(), Value::String(pattern_id.clone()));
|
||||
entry.insert("revision".into(), revision.clone());
|
||||
entry.insert("provenance".into(), Value::String("PATTERN".to_string()));
|
||||
entry.insert("pattern_provenance".into(), provenance.clone());
|
||||
let oid = o.get("id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if let Some(exc) = project_exceptions
|
||||
.iter()
|
||||
.find(|x| x.get("obligation").and_then(|v| v.as_str()).unwrap_or("") == oid)
|
||||
{
|
||||
entry.insert("verdict".into(), Value::String("EXCEPTED".to_string()));
|
||||
entry.insert(
|
||||
"reason".into(),
|
||||
exc.get("reason").cloned().unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
// Oracle field names on the exception are `evidence` /
|
||||
// `authority`; they land on the entry as exception_*.
|
||||
entry.insert(
|
||||
"exception_evidence".into(),
|
||||
exc.get("evidence").cloned().unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
entry.insert(
|
||||
"exception_authority".into(),
|
||||
exc.get("authority").cloned().unwrap_or_else(|| Value::String("user".to_string())),
|
||||
);
|
||||
rejected.push(Value::Object(entry));
|
||||
continue;
|
||||
}
|
||||
let (verdict, reason) = obligation_when_hit(&o_obj, need, surface, authorities);
|
||||
entry.insert("verdict".into(), Value::String(verdict.clone()));
|
||||
entry.insert("reason".into(), Value::String(reason));
|
||||
if verdict == OBL_REQUIRED || verdict == OBL_APPLICABLE {
|
||||
accepted.push(Value::Object(entry));
|
||||
} else if verdict == OBL_NOT_APPLICABLE {
|
||||
rejected.push(Value::Object(entry));
|
||||
} else {
|
||||
unresolved.push(Value::Object(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
json!({
|
||||
"pattern": pattern_id,
|
||||
"revision": revision,
|
||||
"accepted": accepted,
|
||||
"rejected": rejected,
|
||||
"unresolved": unresolved,
|
||||
})
|
||||
}
|
||||
|
||||
/// Combine multiple patterns' derivations (§26-27): duplicates of the same
|
||||
/// obligation id collapse (provenance lists merge); conflicting obligations
|
||||
/// surface as conflicts — never silently chosen.
|
||||
pub fn combine_pattern_obligations(derivations: &[Value]) -> Value {
|
||||
let mut merged: Map<String, Value> = Map::new();
|
||||
let mut order: Vec<String> = Vec::new();
|
||||
let mut conflicts = Vec::new();
|
||||
for d in derivations {
|
||||
if let Some(acc) = d.get("accepted").and_then(|v| v.as_array()) {
|
||||
for entry in acc {
|
||||
let oid = entry.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
if let Some(prev) = merged.get_mut(&oid) {
|
||||
let prev_text = prev.get("obligation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let new_text = entry.get("obligation").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if prev_text != new_text {
|
||||
conflicts.push(json!({
|
||||
"obligation_id": oid,
|
||||
"conflict": "same id, different text",
|
||||
"sources": [
|
||||
prev.get("pattern").cloned().unwrap_or(Value::Null),
|
||||
entry.get("pattern").cloned().unwrap_or(Value::Null),
|
||||
],
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
let prev_obj = prev.as_object_mut().unwrap();
|
||||
match prev_obj.get_mut("also_from") {
|
||||
Some(Value::Array(arr)) => {
|
||||
arr.push(entry.get("pattern").cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
_ => {
|
||||
prev_obj.insert(
|
||||
"also_from".into(),
|
||||
json!([entry.get("pattern").cloned().unwrap_or(Value::Null)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
order.push(oid.clone());
|
||||
merged.insert(oid, entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
order.sort();
|
||||
let obligations: Vec<Value> = order.into_iter().filter_map(|k| merged.remove(&k)).collect();
|
||||
json!({"obligations": obligations, "conflicts": conflicts})
|
||||
}
|
||||
|
||||
// ── FFI (doc-envelope law) ──────────────────────────────────────────────
|
||||
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_char;
|
||||
|
||||
fn read_arg(ptr: *const c_char) -> Result<Value, String> {
|
||||
if ptr.is_null() {
|
||||
return Err("null argument pointer".into());
|
||||
}
|
||||
let x = unsafe { CStr::from_ptr(ptr) }.to_str().map_err(|e| e.to_string())?;
|
||||
serde_json::from_str(x).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn envelope(r: Result<Value, String>) -> *mut c_char {
|
||||
let v = match r {
|
||||
Ok(x) => json!({"ok": true, "value": x}),
|
||||
Err(e) => json!({"ok": false, "err": e}),
|
||||
};
|
||||
CString::new(serde_json::to_string(&v).unwrap_or_default())
|
||||
.map(|c| c.into_raw())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
fn obj_arg(a: &Value) -> Result<Map<String, Value>, String> {
|
||||
a.as_object().cloned().ok_or_else(|| "argument must be a JSON object".to_string())
|
||||
}
|
||||
|
||||
/// cp_construct({pattern dict}) → normalized pattern dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_construct(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| construct_pattern(&a).map(Value::Object));
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_revise({pattern, reason, evidence, changes?}) → new pattern dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_revise(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let reason = a.get("reason").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let evidence: Vec<Value> =
|
||||
a.get("evidence").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
let changes = a.get("changes").and_then(|v| v.as_object()).cloned();
|
||||
revise_pattern(&pattern, reason, &evidence, changes.as_ref()).map(Value::Object)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_propose({pattern_id, provider, model, evidence, confidence}) → dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_propose(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).and_then(|a| {
|
||||
let evidence = a.get("evidence").map(str_list).unwrap_or_default();
|
||||
let confidence = a.get("confidence").and_then(|v| v.as_f64()).unwrap_or(f64::NAN);
|
||||
record_pattern_proposal(
|
||||
a.get("pattern_id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("provider").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
a.get("model").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
evidence,
|
||||
confidence,
|
||||
)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_evaluate({pattern, need, surface_result, authorities}) → verdict dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_evaluate(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let need = a.get("need").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let surface = a.get("surface_result").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let authorities = a.get("authorities").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
evaluate_applicability(&pattern, &need, &surface, &authorities)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_derive({pattern, need, surface_result, authorities,
|
||||
/// project_exceptions?}) → derivation dict.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_derive(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let pattern = a.get("pattern").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let need = a.get("need").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let surface = a.get("surface_result").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let authorities = a.get("authorities").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
let exceptions: Vec<Value> =
|
||||
a.get("project_exceptions").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
derive_pattern_obligations(&pattern, &need, &surface, &authorities, &exceptions)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
/// cp_combine({derivations}) → {"obligations", "conflicts"}.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cp_combine(arg: *const c_char) -> *mut c_char {
|
||||
let r = read_arg(arg).and_then(|a| obj_arg(&a)).map(|a| {
|
||||
let derivs: Vec<Value> =
|
||||
a.get("derivations").and_then(|v| v.as_array()).cloned().unwrap_or_default();
|
||||
combine_pattern_obligations(&derivs)
|
||||
});
|
||||
envelope(r)
|
||||
}
|
||||
|
||||
// NOTE: no cp_free_string export — sibling crates sharing the Python
|
||||
// process already export identical free symbols; a duplicate breaks the
|
||||
// cdylib link. Callers free through capability_scoring's cs_free_string.
|
||||
|
||||
#[path = "branch_battery.rs"]
|
||||
#[cfg(test)]
|
||||
mod branch_battery;
|
||||
Reference in new issue
Block a user