@dzhechkov/p-replicator 1.5.17 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +173 -65
- package/CHANGELOG.md +135 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +2 -2
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/05_architecture.md +6 -2
- package/README/eng/README.md +1 -1
- package/README/ru/01_quickstart.md +2 -2
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +1 -1
- package/README/ru/html/index.html +7 -7
- package/README.md +41 -18
- package/bin/cli.js +0 -0
- package/package.json +11 -10
- package/sbom.json +347 -77
- package/src/utils.js +2 -0
- package/templates/.claude/agents/doc-validator.md +2 -1
- package/templates/.claude/agents/product-discoverer.md +1 -1
- package/templates/.claude/commands/next.md +16 -0
- package/templates/.claude/commands/replicate.md +126 -8
- package/templates/.claude/commands/start.md +19 -1
- package/templates/.claude/hooks/autocommit-insights.cjs +95 -10
- package/templates/.claude/hooks/autocommit-plans.cjs +95 -10
- package/templates/.claude/hooks/autocommit-roadmap.cjs +96 -13
- package/templates/.claude/hooks/check-ports.cjs +232 -0
- package/templates/.claude/hooks/session-insights.cjs +13 -1
- package/templates/.claude/hooks/state-update.cjs +13 -1
- package/templates/.claude/hooks/statusline.cjs +145 -18
- package/templates/.claude/rules/docker-ports.md +123 -0
- package/templates/.claude/rules/replicate-pipeline.md +5 -2
- package/templates/.claude/settings.json +5 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md +57 -14
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/02-analyze-map.md +9 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md +6 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md +1 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-suggestions.md +11 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +1 -1
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +340 -47
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +359 -3
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +386 -13
- package/templates/.claude/skills/goap-research-ed25519/scripts/fixture_legacy_v2_fact.json +23 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/fixtures_field_cases.json +133 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +314 -44
- package/templates/.claude/skills/goap-research-ed25519/scripts/learning_bridge.py +890 -303
- package/templates/.claude/skills/goap-research-ed25519/scripts/population_match.py +591 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/risk_statement.py +289 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +57 -2
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +969 -344
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_goap_planner.py +420 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_population_match.py +544 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_risk_absolute.py +239 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_signature_v3.py +554 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +90 -0
- package/templates/.claude/skills/requirements-validator/SKILL.md +7 -0
- package/templates/.claude/skills/requirements-validator/references/scoring-system.md +28 -0
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +143 -1
- package/tests/e2e/lifecycle.test.js +21 -10
- package/tests/snapshot/baseline.json +51 -34
- package/tests/snapshot/update-baseline.js +2 -1
- package/tests/unit/adr-decision-coverage.test.js +137 -0
- package/tests/unit/adr-scanner-contract.test.js +108 -0
- package/tests/unit/autocommit-deletion.test.js +242 -0
- package/tests/unit/check-ports.test.js +184 -0
- package/tests/unit/db-port-rule.test.js +216 -0
- package/tests/unit/detect-parse-anchor.test.js +109 -0
- package/tests/unit/external-dependency-check.test.js +209 -0
- package/tests/unit/growth-module-b2b-gate.test.js +104 -0
- package/tests/unit/hooks-project-anchored.test.js +223 -0
- package/tests/unit/hooks-report-failures.test.js +207 -0
- package/tests/unit/pipeline-file-ownership.test.js +95 -0
- package/tests/unit/roadmap-one-schema.test.js +179 -0
- package/tests/unit/sparc-reconciliation.test.js +117 -0
- package/tests/unit/spec-pseudocode-traceability.test.js +146 -0
- package/tests/unit/statusline-honest-labels.test.js +178 -0
- package/tests/unit/statusline-two-roots.test.js +237 -0
- package/tests/unit/sync-templates-guard.test.js +209 -0
- package/tests/unit/utils.test.js +2 -2
- package/tests/unit/validation-gate-teeth.test.js +158 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_provenance": {
|
|
3
|
+
"status": "CLAIMED, not independently re-derived",
|
|
4
|
+
"evidence_class": "ASSERTED",
|
|
5
|
+
"note": "The four field cases are taken verbatim from a field report; this pipeline did not open the sources. Their `verbatim` strings are paraphrases of what the field report said the sources said, NOT transcriptions checked against a paper, and no DOI/NCT identifier is invented to make them look more grounded than they are. These fixtures assert the MATCHER's behaviour, which does not depend on the citation being real.",
|
|
6
|
+
"patient": "male, BMI 25, triglycerides 236 mg/dL, cardiovascular risk moderate (the brief's patient)"
|
|
7
|
+
},
|
|
8
|
+
"patient_profile": {
|
|
9
|
+
"sex": "male",
|
|
10
|
+
"bmi": 25,
|
|
11
|
+
"triglycerides_mg_dl": 236,
|
|
12
|
+
"cv_risk": "moderate"
|
|
13
|
+
},
|
|
14
|
+
"cases": [
|
|
15
|
+
{
|
|
16
|
+
"id": "testosterone-weight-loss",
|
|
17
|
+
"kind": "real-field-reversal",
|
|
18
|
+
"claim": "Weight loss raises testosterone",
|
|
19
|
+
"expected_verdict": "partial",
|
|
20
|
+
"expected_discrepancies": [
|
|
21
|
+
{"field": "bmi", "kind": "baseline-out-of-range", "direction": "below"}
|
|
22
|
+
],
|
|
23
|
+
"why_not_none": "The patient could have enrolled; the effect was simply not measured from where his BMI stands. Saying 'none' here would be the mirror error of saying 'full'.",
|
|
24
|
+
"study_population": {
|
|
25
|
+
"description": "men with obesity (BMI >= 30) enrolled in a weight-loss intervention",
|
|
26
|
+
"locator": "[Methods, Participants]",
|
|
27
|
+
"criteria": {
|
|
28
|
+
"sex": {"op": "==", "value": "male", "kind": "eligibility",
|
|
29
|
+
"verbatim": "men", "locator": "[Methods, Participants]"},
|
|
30
|
+
"bmi_min": {"op": ">=", "value": 30, "kind": "baseline",
|
|
31
|
+
"verbatim": "men with obesity (BMI >= 30 kg/m2)",
|
|
32
|
+
"locator": "[Methods, Participants]"}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "ed-rct-bmi30",
|
|
38
|
+
"kind": "real-field-reversal",
|
|
39
|
+
"claim": "Erectile dysfunction is reversible (randomised controlled trial)",
|
|
40
|
+
"expected_verdict": "none",
|
|
41
|
+
"expected_discrepancies": [
|
|
42
|
+
{"field": "bmi", "kind": "eligibility-excluded", "direction": "below"}
|
|
43
|
+
],
|
|
44
|
+
"why_none": "BMI >= 30 was an ENROLMENT criterion — this patient would not have been in the trial at all.",
|
|
45
|
+
"study_population": {
|
|
46
|
+
"description": "participants with BMI >= 30 randomised to the intervention",
|
|
47
|
+
"locator": "[Methods, Eligibility]",
|
|
48
|
+
"criteria": {
|
|
49
|
+
"bmi_min": {"op": ">=", "value": 30, "kind": "eligibility",
|
|
50
|
+
"verbatim": "inclusion criteria: BMI >= 30 kg/m2",
|
|
51
|
+
"locator": "[Methods, Eligibility]"}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "traverse-cv-safety",
|
|
57
|
+
"kind": "real-field-reversal",
|
|
58
|
+
"claim": "TRAVERSE found no excess cardiovascular harm from testosterone therapy",
|
|
59
|
+
"expected_verdict": "none",
|
|
60
|
+
"expected_discrepancies": [
|
|
61
|
+
{"field": "cv_risk", "kind": "eligibility-excluded", "direction": "outside-set"}
|
|
62
|
+
],
|
|
63
|
+
"why_none": "A null-harm finding established in high-cardiovascular-risk men does not extend by default to a man who is not high-risk.",
|
|
64
|
+
"study_population": {
|
|
65
|
+
"description": "men at high cardiovascular risk with hypogonadism",
|
|
66
|
+
"locator": "[Methods, Participants]",
|
|
67
|
+
"criteria": {
|
|
68
|
+
"cv_risk": {"op": "==", "value": "high", "kind": "eligibility",
|
|
69
|
+
"verbatim": "men with high cardiovascular risk",
|
|
70
|
+
"locator": "[Methods, Participants]"}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "omega3-ldl",
|
|
76
|
+
"kind": "real-field-reversal",
|
|
77
|
+
"claim": "Omega-3 raises LDL by 44.5%",
|
|
78
|
+
"expected_verdict": "partial",
|
|
79
|
+
"expected_discrepancies": [
|
|
80
|
+
{"field": "triglycerides", "kind": "baseline-out-of-range", "direction": "below"}
|
|
81
|
+
],
|
|
82
|
+
"why_not_none": "Severe hypertriglyceridemia is a different metabolic regime; the magnitude is not established at 236 mg/dL, but the patient is not excluded from the population.",
|
|
83
|
+
"study_population": {
|
|
84
|
+
"description": "patients with severe hypertriglyceridemia (baseline triglycerides >= 800 mg/dL)",
|
|
85
|
+
"locator": "[Methods, Baseline characteristics]",
|
|
86
|
+
"criteria": {
|
|
87
|
+
"triglycerides_mg_dl_min": {"op": ">=", "value": 800, "kind": "baseline",
|
|
88
|
+
"verbatim": "baseline triglycerides >= 800 mg/dL",
|
|
89
|
+
"locator": "[Methods, Baseline characteristics]"}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"id": "synthetic-missing-patient-value",
|
|
95
|
+
"kind": "synthetic",
|
|
96
|
+
"claim": "Synthetic: the patient profile does not carry the axis the study used",
|
|
97
|
+
"expected_verdict": "unknown",
|
|
98
|
+
"expected_discrepancies": [
|
|
99
|
+
{"field": "bmi", "kind": "patient-value-missing", "direction": "absent"}
|
|
100
|
+
],
|
|
101
|
+
"patient_profile_override": {"sex": "male"},
|
|
102
|
+
"note": "01_requirements.md AC-5 called this `partial` under the three-verdict wording. ADR-001 §2 and AM-6 confirmed a FOURTH verdict: an unestablished criterion is not a milder kind of match, so this is `unknown`. The distinction is the whole of D-17's discipline — unevaluable is never a pass.",
|
|
103
|
+
"study_population": {
|
|
104
|
+
"description": "men with obesity (BMI >= 30)",
|
|
105
|
+
"locator": "[Methods]",
|
|
106
|
+
"criteria": {
|
|
107
|
+
"sex": {"op": "==", "value": "male", "kind": "eligibility",
|
|
108
|
+
"verbatim": "men", "locator": "[Methods]"},
|
|
109
|
+
"bmi_min": {"op": ">=", "value": 30, "kind": "eligibility",
|
|
110
|
+
"verbatim": "BMI >= 30 kg/m2", "locator": "[Methods]"}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "synthetic-full",
|
|
116
|
+
"kind": "synthetic",
|
|
117
|
+
"claim": "Synthetic: every stated criterion is satisfied by a known patient value",
|
|
118
|
+
"expected_verdict": "full",
|
|
119
|
+
"expected_discrepancies": [],
|
|
120
|
+
"patient_profile_override": {"sex": "male", "bmi": 33},
|
|
121
|
+
"study_population": {
|
|
122
|
+
"description": "men with obesity (BMI >= 30)",
|
|
123
|
+
"locator": "[Methods]",
|
|
124
|
+
"criteria": {
|
|
125
|
+
"sex": {"op": "==", "value": "male", "kind": "eligibility",
|
|
126
|
+
"verbatim": "men", "locator": "[Methods]"},
|
|
127
|
+
"bmi_min": {"op": ">=", "value": 30, "kind": "eligibility",
|
|
128
|
+
"verbatim": "BMI >= 30 kg/m2", "locator": "[Methods]"}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
@@ -16,8 +16,9 @@ Features:
|
|
|
16
16
|
import heapq
|
|
17
17
|
import json
|
|
18
18
|
import hashlib
|
|
19
|
+
import time
|
|
19
20
|
from dataclasses import dataclass, field, asdict
|
|
20
|
-
from typing import Dict, Set, List, Optional, Tuple, Any
|
|
21
|
+
from typing import Dict, FrozenSet, Set, List, Optional, Tuple, Any, Union
|
|
21
22
|
from datetime import datetime
|
|
22
23
|
from enum import Enum
|
|
23
24
|
|
|
@@ -98,15 +99,70 @@ class ResearchPlan:
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
class PlanVerdict(Enum):
|
|
103
|
+
"""
|
|
104
|
+
Verdict for a planning attempt that did not produce a plan.
|
|
105
|
+
|
|
106
|
+
Two fundamentally different situations that must never share one message
|
|
107
|
+
(precedented in this repo: INSUFFICIENT_DATA in harness-core/src/compounding.ts
|
|
108
|
+
and 'inconclusive' in harness-core/src/skills-verify.ts — "don't know" is a
|
|
109
|
+
first-class verdict, not a failure collapsed into the same bucket as
|
|
110
|
+
"definitely no"):
|
|
111
|
+
|
|
112
|
+
- GOAL_UNREACHABLE: proven — no sequence of available actions can ever
|
|
113
|
+
satisfy the goal from the initial state, regardless of budget.
|
|
114
|
+
- SEARCH_EXHAUSTED: not proven unreachable — the search simply did not find
|
|
115
|
+
a plan within its iteration budget. Retrying with a higher
|
|
116
|
+
``max_iterations`` may succeed.
|
|
117
|
+
"""
|
|
118
|
+
GOAL_UNREACHABLE = "goal_unreachable"
|
|
119
|
+
SEARCH_EXHAUSTED = "search_exhausted"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class PlanNotFound:
|
|
124
|
+
"""
|
|
125
|
+
First-class "no plan" result carrying WHY no plan was returned.
|
|
126
|
+
|
|
127
|
+
Callers must branch on ``isinstance(result, ResearchPlan)`` /
|
|
128
|
+
``isinstance(result, PlanNotFound)`` rather than truthiness. As defense in
|
|
129
|
+
depth, ``__bool__`` returns False, so legacy TRUTHINESS checks
|
|
130
|
+
(``if plan:`` / ``if not plan:``) keep behaving exactly as they did when
|
|
131
|
+
this function returned ``None``.
|
|
132
|
+
|
|
133
|
+
IDENTITY checks are NOT covered by that safety net: ``plan is None`` is
|
|
134
|
+
now ALWAYS False. A caller migrating from the old contract via
|
|
135
|
+
``if plan is not None:`` will read a no-plan result as success and then
|
|
136
|
+
raise ``AttributeError`` on ``.actions`` — or, worse, silently proceed.
|
|
137
|
+
Migrate every ``is None`` / ``is not None`` check to
|
|
138
|
+
``isinstance(result, ResearchPlan)``.
|
|
139
|
+
"""
|
|
140
|
+
verdict: PlanVerdict
|
|
141
|
+
iterations: Optional[int] = None # set for SEARCH_EXHAUSTED; None for GOAL_UNREACHABLE
|
|
142
|
+
message: str = "" # actionable text; carries retry guidance for SEARCH_EXHAUSTED
|
|
143
|
+
|
|
144
|
+
def __bool__(self) -> bool:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
|
|
101
148
|
# Extended research actions library with Ed25519 verification support
|
|
102
149
|
RESEARCH_ACTIONS = [
|
|
103
150
|
# Setup Actions
|
|
151
|
+
#
|
|
152
|
+
# NOTE on the two capability flags below (historical defect: `whitelist_available`):
|
|
153
|
+
# `issuer_keys_available` (real Ed25519 issuer key material genuinely held) and
|
|
154
|
+
# `source_class_verified` (this class of source is capable of signed delivery at all)
|
|
155
|
+
# are INITIAL-STATE-ONLY facts about the deployment. No action in this library may
|
|
156
|
+
# ever list either of them in `effects` — an action that could manufacture its own
|
|
157
|
+
# key/source-class credibility for free would repeat the `whitelist_available`
|
|
158
|
+
# defect, where a dead flag plus a zero-precondition `configure_trusted_issuers`
|
|
159
|
+
# let the planner fabricate "verified" plans backed by nothing.
|
|
104
160
|
ResearchAction(
|
|
105
161
|
name="configure_trusted_issuers",
|
|
106
|
-
preconditions=
|
|
162
|
+
preconditions={"issuer_keys_available"},
|
|
107
163
|
effects={"whitelist_active", "verification_ready"},
|
|
108
164
|
cost=0,
|
|
109
|
-
description="Initialize trusted issuer whitelist"
|
|
165
|
+
description="Initialize trusted issuer whitelist (requires real issuer key material)"
|
|
110
166
|
),
|
|
111
167
|
ResearchAction(
|
|
112
168
|
name="generate_research_keypair",
|
|
@@ -126,7 +182,7 @@ RESEARCH_ACTIONS = [
|
|
|
126
182
|
),
|
|
127
183
|
ResearchAction(
|
|
128
184
|
name="web_search_verified",
|
|
129
|
-
preconditions={"topic_defined", "whitelist_active"},
|
|
185
|
+
preconditions={"topic_defined", "whitelist_active", "source_class_verified"},
|
|
130
186
|
effects={"verified_candidates_found", "trusted_sources_identified"},
|
|
131
187
|
cost=2,
|
|
132
188
|
description="Search with priority to trusted issuer sources",
|
|
@@ -158,7 +214,7 @@ RESEARCH_ACTIONS = [
|
|
|
158
214
|
),
|
|
159
215
|
ResearchAction(
|
|
160
216
|
name="fetch_signed_source",
|
|
161
|
-
preconditions={"sources_identified", "whitelist_active"},
|
|
217
|
+
preconditions={"sources_identified", "whitelist_active", "source_class_verified"},
|
|
162
218
|
effects={"signed_content_retrieved", "signature_verified"},
|
|
163
219
|
cost=3,
|
|
164
220
|
description="Retrieve content with Ed25519 signature verification",
|
|
@@ -328,29 +384,122 @@ def heuristic(state: Set[str], goal: Set[str], unsigned_claims: int = 0) -> floa
|
|
|
328
384
|
return base_cost + verification_penalty
|
|
329
385
|
|
|
330
386
|
|
|
387
|
+
# Adaptive default iteration budgets, calibrated by MEASUREMENT (feature
|
|
388
|
+
# ha-slice-e-goap-blockers, 2026-08-04): binary search for the minimal
|
|
389
|
+
# max_iterations that finds a plan for each shipped demo scenario showed the
|
|
390
|
+
# dominant variable is verification_mode, not goal-set size — STRICT scenarios
|
|
391
|
+
# need ~20-100x the iterations of MODERATE ones at comparable goal counts,
|
|
392
|
+
# because the strict-mode unsigned-claims rejection narrows the acceptable goal
|
|
393
|
+
# nodes drastically. Measured minima: competitive/MODERATE 3-goal = 2,249;
|
|
394
|
+
# high_stakes/STRICT 5-goal = 55,128. Floors carry ~2x margin over the worst
|
|
395
|
+
# measured scenario of their mode class. PARANOID inherits the STRICT floor:
|
|
396
|
+
# its search behavior is identical to STRICT in this planner (the only
|
|
397
|
+
# search-relevant switch is require_verification, shared by both modes; the
|
|
398
|
+
# 0.99-vs-0.95 threshold affects reporting, not the search), and the floor is
|
|
399
|
+
# exercised by a shipped scenario (test_default_budget_finds_high_stakes_
|
|
400
|
+
# paranoid_plan runs high_stakes/PARANOID to a plan at the default budget) —
|
|
401
|
+
# so "PARANOID = STRICT floor" is a tested identity, not a bare extrapolation.
|
|
402
|
+
# DEVELOPMENT shares the MODERATE floor via the else-branch (also tested).
|
|
403
|
+
_MODERATE_ITERATIONS_FLOOR = 5_000 # ~2.2x margin over measured competitive/MODERATE minimum (2,249)
|
|
404
|
+
_STRICT_ITERATIONS_FLOOR = 100_000 # ~1.8x margin over measured high_stakes/STRICT minimum (55,128)
|
|
405
|
+
_ITERATIONS_PER_GOAL_MODERATE = 50 # secondary headroom term for large goal sets
|
|
406
|
+
_ITERATIONS_PER_GOAL_STRICT = 500
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _default_max_iterations(goal_state: Set[str], verification_mode: VerificationMode) -> int:
|
|
410
|
+
"""Adaptive default search budget (see calibration note above)."""
|
|
411
|
+
if verification_mode in (VerificationMode.STRICT, VerificationMode.PARANOID):
|
|
412
|
+
return max(_STRICT_ITERATIONS_FLOOR, _ITERATIONS_PER_GOAL_STRICT * len(goal_state))
|
|
413
|
+
return max(_MODERATE_ITERATIONS_FLOOR, _ITERATIONS_PER_GOAL_MODERATE * len(goal_state))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _reachability_closure(initial_state: Set[str], actions: List[ResearchAction]) -> FrozenSet[str]:
|
|
417
|
+
"""
|
|
418
|
+
Delete-free forward-chaining reachability closure (classic STRIPS
|
|
419
|
+
relaxed-planning-graph technique).
|
|
420
|
+
|
|
421
|
+
Because ``ResearchAction.apply()`` is a pure set-union (state only grows,
|
|
422
|
+
facts are never removed), forward-chaining preconditions -> effects to a
|
|
423
|
+
fixed point is bounded by ``len(actions)`` passes: each action can flip
|
|
424
|
+
from inapplicable to applicable at most once. Cost: O(len(actions)^2) set
|
|
425
|
+
operations — microseconds, and by construction independent of any
|
|
426
|
+
iteration budget.
|
|
427
|
+
|
|
428
|
+
SOUNDNESS NOTE: this closure is a *relaxed* check — it ignores the
|
|
429
|
+
unsigned-claims / verification-mode gate entirely. ``goal not <= closure``
|
|
430
|
+
is therefore a SOUND proof of unreachability (the closure is a superset of
|
|
431
|
+
everything the constrained search could ever reach). ``goal <= closure``
|
|
432
|
+
is NOT a guarantee the constrained A* search finds a plan within any given
|
|
433
|
+
budget — which is exactly why SEARCH_EXHAUSTED remains meaningful even
|
|
434
|
+
when the closure says "reachable in principle".
|
|
435
|
+
"""
|
|
436
|
+
state = set(initial_state)
|
|
437
|
+
changed = True
|
|
438
|
+
while changed:
|
|
439
|
+
changed = False
|
|
440
|
+
for action in actions:
|
|
441
|
+
if action.preconditions.issubset(state):
|
|
442
|
+
before = len(state)
|
|
443
|
+
state |= action.effects
|
|
444
|
+
if len(state) > before:
|
|
445
|
+
changed = True
|
|
446
|
+
return frozenset(state)
|
|
447
|
+
|
|
448
|
+
|
|
331
449
|
def find_research_plan(
|
|
332
450
|
initial_state: Set[str],
|
|
333
451
|
goal_state: Set[str],
|
|
334
452
|
actions: List[ResearchAction] = None,
|
|
335
453
|
verification_mode: VerificationMode = VerificationMode.MODERATE,
|
|
336
|
-
max_iterations: int =
|
|
337
|
-
|
|
454
|
+
max_iterations: Optional[int] = None,
|
|
455
|
+
max_seconds: Optional[float] = None
|
|
456
|
+
) -> Union[ResearchPlan, "PlanNotFound"]:
|
|
338
457
|
"""
|
|
339
458
|
A* search to find optimal research plan with verification support.
|
|
340
|
-
|
|
459
|
+
|
|
341
460
|
Args:
|
|
342
461
|
initial_state: Starting conditions
|
|
343
462
|
goal_state: Target conditions to achieve
|
|
344
463
|
actions: Available actions (defaults to RESEARCH_ACTIONS)
|
|
345
464
|
verification_mode: How strict to be about verification
|
|
346
|
-
max_iterations: Maximum search iterations
|
|
347
|
-
|
|
465
|
+
max_iterations: Maximum search iterations. ``None`` (the default)
|
|
466
|
+
selects an adaptive budget calibrated per verification mode and
|
|
467
|
+
goal-set size (see ``_default_max_iterations``); an explicit value
|
|
468
|
+
is respected unchanged.
|
|
469
|
+
max_seconds: Optional wall-clock ceiling for the search, independent
|
|
470
|
+
of the iteration count. ``None`` (the default) keeps the search
|
|
471
|
+
deterministic (bounded only by ``max_iterations``) — note that on
|
|
472
|
+
the FAILURE path this means the full iteration budget is burned
|
|
473
|
+
(measured: ~7-10s at the default STRICT budget of 100,000 for a
|
|
474
|
+
goal the verification gate never accepts). Latency-sensitive
|
|
475
|
+
callers should pass a ceiling (e.g. ``max_seconds=2.0``); hitting
|
|
476
|
+
it returns SEARCH_EXHAUSTED with the ceiling named in the message.
|
|
477
|
+
|
|
348
478
|
Returns:
|
|
349
|
-
ResearchPlan
|
|
479
|
+
ResearchPlan on success, otherwise a PlanNotFound whose ``verdict``
|
|
480
|
+
distinguishes:
|
|
481
|
+
- PlanVerdict.GOAL_UNREACHABLE — proven: no action sequence can ever
|
|
482
|
+
satisfy the goal (decided promptly by a reachability closure, or by
|
|
483
|
+
exhaustive exploration of the state space).
|
|
484
|
+
- PlanVerdict.SEARCH_EXHAUSTED — the iteration budget (or the
|
|
485
|
+
``max_seconds`` wall-clock ceiling) ran out before a plan was found;
|
|
486
|
+
``iterations`` carries the iterations spent. This verdict is an
|
|
487
|
+
honest "don't know": the relaxed closure passing means the goal is
|
|
488
|
+
not PROVABLY unreachable, but it is NOT a guarantee that any budget
|
|
489
|
+
will find a plan — the closure ignores the verification-mode gate,
|
|
490
|
+
which can permanently reject every path (see the
|
|
491
|
+
``_reachability_closure`` soundness note).
|
|
492
|
+
Branch on ``isinstance(result, ResearchPlan)`` — never on truthiness
|
|
493
|
+
alone (PlanNotFound is falsy only as a legacy safety net), and NEVER
|
|
494
|
+
on ``result is None`` (this function no longer returns ``None``; an
|
|
495
|
+
identity check reads a no-plan result as success).
|
|
350
496
|
"""
|
|
351
497
|
if actions is None:
|
|
352
498
|
actions = RESEARCH_ACTIONS
|
|
353
|
-
|
|
499
|
+
|
|
500
|
+
if max_iterations is None:
|
|
501
|
+
max_iterations = _default_max_iterations(goal_state, verification_mode)
|
|
502
|
+
|
|
354
503
|
# Check if goal already satisfied
|
|
355
504
|
if goal_state.issubset(initial_state):
|
|
356
505
|
return ResearchPlan(
|
|
@@ -361,7 +510,24 @@ def find_research_plan(
|
|
|
361
510
|
estimated_confidence=1.0,
|
|
362
511
|
unsigned_claims_count=0
|
|
363
512
|
)
|
|
364
|
-
|
|
513
|
+
|
|
514
|
+
# Prompt unreachability proof, independent of max_iterations: if the goal
|
|
515
|
+
# is not inside the delete-free closure, no budget can ever reach it.
|
|
516
|
+
closure = _reachability_closure(initial_state, actions)
|
|
517
|
+
if not goal_state.issubset(closure):
|
|
518
|
+
missing = sorted(goal_state - closure)
|
|
519
|
+
return PlanNotFound(
|
|
520
|
+
verdict=PlanVerdict.GOAL_UNREACHABLE,
|
|
521
|
+
message=(
|
|
522
|
+
"Goal is provably unreachable from the initial state: no sequence of "
|
|
523
|
+
f"available actions can ever produce {missing}. "
|
|
524
|
+
"Raising max_iterations cannot help. If the missing facts are "
|
|
525
|
+
"deployment capabilities (e.g. issuer_keys_available, "
|
|
526
|
+
"source_class_verified), they must be supplied in the initial state "
|
|
527
|
+
"by a real integration — the planner cannot manufacture them."
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
|
|
365
531
|
# Priority queue
|
|
366
532
|
start_h = heuristic(initial_state, goal_state)
|
|
367
533
|
open_set = [PlanNode(start_h, 0, frozenset(initial_state), [], 0)]
|
|
@@ -372,8 +538,17 @@ def find_research_plan(
|
|
|
372
538
|
# Verification settings
|
|
373
539
|
require_verification = verification_mode in [VerificationMode.STRICT, VerificationMode.PARANOID]
|
|
374
540
|
|
|
541
|
+
# Optional wall-clock ceiling (GAP-6): independent of the iteration count,
|
|
542
|
+
# so a latency-sensitive caller is not forced to burn the full (large)
|
|
543
|
+
# adaptive budget on a goal the verification gate never accepts.
|
|
544
|
+
deadline = None if max_seconds is None else time.perf_counter() + max_seconds
|
|
545
|
+
time_ceiling_hit = False
|
|
546
|
+
|
|
375
547
|
iterations = 0
|
|
376
548
|
while open_set and iterations < max_iterations:
|
|
549
|
+
if deadline is not None and time.perf_counter() >= deadline:
|
|
550
|
+
time_ceiling_hit = True
|
|
551
|
+
break
|
|
377
552
|
iterations += 1
|
|
378
553
|
|
|
379
554
|
current = heapq.heappop(open_set)
|
|
@@ -441,8 +616,54 @@ def find_research_plan(
|
|
|
441
616
|
unsigned_claims=new_unsigned
|
|
442
617
|
)
|
|
443
618
|
heapq.heappush(open_set, new_node)
|
|
444
|
-
|
|
445
|
-
|
|
619
|
+
|
|
620
|
+
# No plan found — distinguish WHY (never conflate the two exits):
|
|
621
|
+
if open_set:
|
|
622
|
+
# Budget (iterations or wall clock) hit with work remaining: an honest
|
|
623
|
+
# "don't know yet". The relaxed closure passing means the goal is NOT
|
|
624
|
+
# PROVABLY unreachable — it does NOT mean a larger budget will find a
|
|
625
|
+
# plan: the closure ignores the verification-mode unsigned-claims
|
|
626
|
+
# gate, which (especially in strict/paranoid mode) can permanently
|
|
627
|
+
# reject every path to the goal (see _reachability_closure's
|
|
628
|
+
# soundness note; this message must never over-claim past it).
|
|
629
|
+
if time_ceiling_hit:
|
|
630
|
+
stopped = (
|
|
631
|
+
f"Search stopped by the wall-clock ceiling (max_seconds={max_seconds}) "
|
|
632
|
+
f"after {iterations} iterations without finding a plan."
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
stopped = (
|
|
636
|
+
f"Search exhausted after {iterations} iterations without finding a plan."
|
|
637
|
+
)
|
|
638
|
+
return PlanNotFound(
|
|
639
|
+
verdict=PlanVerdict.SEARCH_EXHAUSTED,
|
|
640
|
+
iterations=iterations,
|
|
641
|
+
message=(
|
|
642
|
+
stopped + " The relaxed reachability closure passed, so the goal is "
|
|
643
|
+
"not provably unreachable — but that is NOT a guarantee that a larger "
|
|
644
|
+
"budget will find a plan: the closure ignores the verification-mode "
|
|
645
|
+
"unsigned-claims gate, which can permanently reject every path to the "
|
|
646
|
+
"goal (especially in strict/paranoid mode). A retry with a higher "
|
|
647
|
+
f"max_iterations (e.g. max_iterations={max(iterations, 1) * 10}) MAY "
|
|
648
|
+
"succeed; if repeated increases keep exhausting, treat the goal as "
|
|
649
|
+
"unreachable under the active verification constraints instead of "
|
|
650
|
+
"raising the budget further."
|
|
651
|
+
)
|
|
652
|
+
)
|
|
653
|
+
# open_set genuinely emptied: the search exhaustively explored every
|
|
654
|
+
# reachable state without accepting a goal node — a second, stronger,
|
|
655
|
+
# exhaustive proof of unreachability under the active verification
|
|
656
|
+
# constraints (the relaxed closure ignores those constraints, so both
|
|
657
|
+
# outcomes are consistent).
|
|
658
|
+
return PlanNotFound(
|
|
659
|
+
verdict=PlanVerdict.GOAL_UNREACHABLE,
|
|
660
|
+
message=(
|
|
661
|
+
"Search space exhaustively explored (open set emptied) without a plan "
|
|
662
|
+
"satisfying the active verification constraints — the goal is "
|
|
663
|
+
"unreachable under the current verification mode; raising "
|
|
664
|
+
"max_iterations cannot help."
|
|
665
|
+
)
|
|
666
|
+
)
|
|
446
667
|
|
|
447
668
|
|
|
448
669
|
def format_plan(plan: ResearchPlan, action_library: List[ResearchAction] = None) -> str:
|
|
@@ -607,14 +828,24 @@ class GOAPResearchPlanner:
|
|
|
607
828
|
def __init__(
|
|
608
829
|
self,
|
|
609
830
|
verification_mode: str = "moderate",
|
|
610
|
-
trusted_issuers: Optional[List[str]] = None
|
|
831
|
+
trusted_issuers: Optional[List[str]] = None,
|
|
832
|
+
issuer_keys_available: bool = False,
|
|
833
|
+
source_class_verified: bool = False
|
|
611
834
|
):
|
|
612
835
|
"""
|
|
613
836
|
Initialize planner.
|
|
614
|
-
|
|
837
|
+
|
|
615
838
|
Args:
|
|
616
839
|
verification_mode: 'development', 'moderate', 'strict', or 'paranoid'
|
|
617
|
-
trusted_issuers: List of trusted issuer domains
|
|
840
|
+
trusted_issuers: List of trusted issuer domains (informational — a
|
|
841
|
+
list of domain strings is NOT evidence of key possession)
|
|
842
|
+
issuer_keys_available: True only when real Ed25519 key material for
|
|
843
|
+
the configured issuers genuinely exists. Default False — the
|
|
844
|
+
package's honest default, since no real issuer keys exist for
|
|
845
|
+
PubMed/PMC/DOI/WHO.
|
|
846
|
+
source_class_verified: True only for source classes actually
|
|
847
|
+
capable of Ed25519-signed delivery. Default False for the same
|
|
848
|
+
reason.
|
|
618
849
|
"""
|
|
619
850
|
mode_map = {
|
|
620
851
|
"development": VerificationMode.DEVELOPMENT,
|
|
@@ -624,38 +855,56 @@ class GOAPResearchPlanner:
|
|
|
624
855
|
}
|
|
625
856
|
self.verification_mode = mode_map.get(verification_mode.lower(), VerificationMode.MODERATE)
|
|
626
857
|
self.trusted_issuers = trusted_issuers or []
|
|
858
|
+
self.issuer_keys_available = issuer_keys_available
|
|
859
|
+
self.source_class_verified = source_class_verified
|
|
627
860
|
self.verification_ledger: List[VerificationResult] = []
|
|
628
861
|
|
|
629
862
|
def plan(
|
|
630
863
|
self,
|
|
631
864
|
goal_type: str,
|
|
632
865
|
topic: str,
|
|
633
|
-
custom_goals: Optional[Set[str]] = None
|
|
634
|
-
|
|
866
|
+
custom_goals: Optional[Set[str]] = None,
|
|
867
|
+
max_seconds: Optional[float] = None
|
|
868
|
+
) -> Union[ResearchPlan, PlanNotFound]:
|
|
635
869
|
"""
|
|
636
870
|
Generate research plan for given goal type.
|
|
637
|
-
|
|
871
|
+
|
|
638
872
|
Args:
|
|
639
873
|
goal_type: Type of research (see create_research_goal)
|
|
640
874
|
topic: Research topic (for logging)
|
|
641
875
|
custom_goals: Override default goals
|
|
642
|
-
|
|
876
|
+
max_seconds: Optional wall-clock ceiling for the search (see
|
|
877
|
+
find_research_plan) — bounds failure-path latency, which
|
|
878
|
+
otherwise burns the full adaptive iteration budget.
|
|
879
|
+
|
|
643
880
|
Returns:
|
|
644
|
-
ResearchPlan or
|
|
881
|
+
ResearchPlan on success, or PlanNotFound with a verdict
|
|
882
|
+
(PlanVerdict.GOAL_UNREACHABLE — proven impossible; or
|
|
883
|
+
PlanVerdict.SEARCH_EXHAUSTED — budget ran out; a retry with a
|
|
884
|
+
higher max_iterations MAY succeed, but is not guaranteed to — the
|
|
885
|
+
verification gate can permanently reject every path). Branch on
|
|
886
|
+
``isinstance(result, ResearchPlan)`` — never on ``result is None``
|
|
887
|
+
(this method no longer returns ``None``; an identity check reads a
|
|
888
|
+
no-plan result as success).
|
|
645
889
|
"""
|
|
646
890
|
initial, goal = create_research_goal(goal_type, self.verification_mode)
|
|
647
|
-
|
|
891
|
+
|
|
648
892
|
if custom_goals:
|
|
649
893
|
goal = custom_goals
|
|
650
|
-
|
|
651
|
-
#
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
894
|
+
|
|
895
|
+
# Deployment capability facts are initial-state-only: no action can
|
|
896
|
+
# produce them (see the RESEARCH_ACTIONS setup note). Each is added
|
|
897
|
+
# only when the deployment genuinely provides it.
|
|
898
|
+
if self.issuer_keys_available:
|
|
899
|
+
initial.add("issuer_keys_available")
|
|
900
|
+
if self.source_class_verified:
|
|
901
|
+
initial.add("source_class_verified")
|
|
902
|
+
|
|
655
903
|
return find_research_plan(
|
|
656
904
|
initial_state=initial,
|
|
657
905
|
goal_state=goal,
|
|
658
|
-
verification_mode=self.verification_mode
|
|
906
|
+
verification_mode=self.verification_mode,
|
|
907
|
+
max_seconds=max_seconds
|
|
659
908
|
)
|
|
660
909
|
|
|
661
910
|
def format_plan(self, plan: ResearchPlan) -> str:
|
|
@@ -679,42 +928,63 @@ if __name__ == "__main__":
|
|
|
679
928
|
initial, goal = create_research_goal("exploratory", VerificationMode.MODERATE)
|
|
680
929
|
print(f"Initial State: {initial}")
|
|
681
930
|
print(f"Goal State: {goal}")
|
|
682
|
-
|
|
931
|
+
|
|
683
932
|
plan = find_research_plan(initial, goal, verification_mode=VerificationMode.MODERATE)
|
|
684
|
-
if plan:
|
|
933
|
+
if isinstance(plan, ResearchPlan):
|
|
685
934
|
print(format_plan(plan))
|
|
686
935
|
else:
|
|
687
|
-
print("No plan
|
|
688
|
-
|
|
936
|
+
print(f"No plan: {plan.verdict.value} — {plan.message}")
|
|
937
|
+
|
|
689
938
|
# Example 2: High-stakes verified research
|
|
690
939
|
print("\n\n[2] HIGH-STAKES VERIFIED RESEARCH (Strict Mode)")
|
|
691
940
|
print("-" * 50)
|
|
692
|
-
|
|
941
|
+
|
|
693
942
|
initial, goal = create_research_goal("high_stakes", VerificationMode.STRICT)
|
|
694
943
|
print(f"Initial State: {initial}")
|
|
695
944
|
print(f"Goal State: {goal}")
|
|
696
|
-
|
|
945
|
+
|
|
946
|
+
# Honest default: no real Ed25519 issuer keys exist for the package's
|
|
947
|
+
# actual health sources (PubMed/PMC/DOI/WHO), so high_stakes is provably
|
|
948
|
+
# unreachable — reported as a first-class verdict, not a fabricated plan.
|
|
949
|
+
print("\n[2a] Honest default (no issuer keys):")
|
|
697
950
|
plan = find_research_plan(initial, goal, verification_mode=VerificationMode.STRICT)
|
|
698
|
-
if plan:
|
|
951
|
+
if isinstance(plan, ResearchPlan):
|
|
699
952
|
print(format_plan(plan))
|
|
700
953
|
else:
|
|
701
|
-
print("No plan
|
|
702
|
-
|
|
954
|
+
print(f"No plan: {plan.verdict.value} — {plan.message}")
|
|
955
|
+
|
|
956
|
+
# With a real signed-source integration (issuer key material genuinely
|
|
957
|
+
# held AND a source class capable of signed delivery), the same goal is
|
|
958
|
+
# reachable at the DEFAULT adaptive budget:
|
|
959
|
+
print("\n[2b] With real signed-source integration (issuer_keys_available + source_class_verified):")
|
|
960
|
+
initial_with_keys = initial | {"issuer_keys_available", "source_class_verified"}
|
|
961
|
+
plan = find_research_plan(initial_with_keys, goal, verification_mode=VerificationMode.STRICT)
|
|
962
|
+
if isinstance(plan, ResearchPlan):
|
|
963
|
+
print(format_plan(plan))
|
|
964
|
+
else:
|
|
965
|
+
print(f"No plan: {plan.verdict.value} — {plan.message}")
|
|
966
|
+
|
|
703
967
|
# Example 3: Using the high-level planner interface
|
|
704
968
|
print("\n\n[3] HIGH-LEVEL PLANNER INTERFACE")
|
|
705
969
|
print("-" * 50)
|
|
706
|
-
|
|
970
|
+
|
|
707
971
|
planner = GOAPResearchPlanner(
|
|
708
972
|
verification_mode="strict",
|
|
709
|
-
trusted_issuers=["reuters.com", "nature.com", "arxiv.org"]
|
|
973
|
+
trusted_issuers=["reuters.com", "nature.com", "arxiv.org"],
|
|
974
|
+
# Test-double capability flags simulating a real signed-source
|
|
975
|
+
# integration; the honest default for both is False.
|
|
976
|
+
issuer_keys_available=True,
|
|
977
|
+
source_class_verified=True
|
|
710
978
|
)
|
|
711
|
-
|
|
979
|
+
|
|
712
980
|
plan = planner.plan(
|
|
713
981
|
goal_type="verified_exploratory",
|
|
714
982
|
topic="AI safety regulations 2025"
|
|
715
983
|
)
|
|
716
|
-
|
|
717
|
-
if plan:
|
|
984
|
+
|
|
985
|
+
if isinstance(plan, ResearchPlan):
|
|
718
986
|
print(planner.format_plan(plan))
|
|
719
987
|
print("\nJSON Export:")
|
|
720
988
|
print(planner.export_plan(plan))
|
|
989
|
+
else:
|
|
990
|
+
print(f"No plan: {plan.verdict.value} — {plan.message}")
|