@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,420 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for the GOAP planner's three fixed blockers (feature ha-slice-e-goap-blockers).
|
|
4
|
+
|
|
5
|
+
Blocker 1 — adaptive iteration budget: the package's own shipped high_stakes/STRICT
|
|
6
|
+
demo scenario must find a plan at the DEFAULT budget (no max_iterations override).
|
|
7
|
+
Blocker 2 — verdict split: GOAL_UNREACHABLE (proven impossible, decided promptly by a
|
|
8
|
+
reachability closure independent of the budget) vs SEARCH_EXHAUSTED(n) (budget ran
|
|
9
|
+
out; carries retry-with-higher-limit guidance). Two separate tests, one per branch
|
|
10
|
+
(NFR-1) — a combined test could pass by accident on either branch.
|
|
11
|
+
Blocker 3 — issuer_keys_available / source_class_verified split: the planner can no
|
|
12
|
+
longer fabricate a "verified" high_stakes plan with zero real key material
|
|
13
|
+
(configure_trusted_issuers is no longer a zero-precondition freebie), and both new
|
|
14
|
+
flags are structurally initial-state-only (preconditions somewhere, effects nowhere).
|
|
15
|
+
|
|
16
|
+
QE re-check additions (2026-08, findings GAP-2/5/6/7/8 of 08_qe_report.md):
|
|
17
|
+
honest SEARCH_EXHAUSTED wording pinned against the verification-blocked falsifier
|
|
18
|
+
(GAP-2); the `is None` identity hazard pinned as documented behavior (GAP-5);
|
|
19
|
+
max_seconds wall-clock ceiling bounds failure-path latency (GAP-6); PARANOID and
|
|
20
|
+
DEVELOPMENT default budgets exercised by shipped scenarios (GAP-7/GAP-8).
|
|
21
|
+
|
|
22
|
+
python3 -m unittest test_goap_planner -v
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
# Import NOTHING from this directory before this line. Running these tests used to
|
|
28
|
+
# leave a __pycache__ inside the skill directory, and this skill is vendored into 11
|
|
29
|
+
# copies kept byte-identical — so the stray directory read as canonical DRIFT and
|
|
30
|
+
# turned an unrelated repo test red. (Same guard as test_evidence_provenance.py.)
|
|
31
|
+
sys.dont_write_bytecode = True
|
|
32
|
+
|
|
33
|
+
import time
|
|
34
|
+
import unittest
|
|
35
|
+
|
|
36
|
+
from goap_planner import (
|
|
37
|
+
GOAPResearchPlanner,
|
|
38
|
+
PlanNotFound,
|
|
39
|
+
PlanVerdict,
|
|
40
|
+
RESEARCH_ACTIONS,
|
|
41
|
+
ResearchAction,
|
|
42
|
+
ResearchPlan,
|
|
43
|
+
VerificationMode,
|
|
44
|
+
_default_max_iterations,
|
|
45
|
+
_reachability_closure,
|
|
46
|
+
create_research_goal,
|
|
47
|
+
find_research_plan,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# A test double simulating a real signed-source integration: issuer key material
|
|
51
|
+
# genuinely held AND a source class capable of Ed25519-signed delivery. The
|
|
52
|
+
# package's honest default for the real health sources (PubMed/PMC/DOI/WHO) is
|
|
53
|
+
# that NEITHER fact holds.
|
|
54
|
+
_CAPABILITY_FLAGS = {"issuer_keys_available", "source_class_verified"}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _verification_unreachable_scenario():
|
|
58
|
+
"""The GAP-2 falsifier (QE re-check, 2026-08): a goal that is
|
|
59
|
+
CLOSURE-REACHABLE but permanently rejected by the STRICT unsigned-claims
|
|
60
|
+
gate. No action in this library can ever produce ``signed_facts`` or
|
|
61
|
+
``cryptographically_verified``, so every goal node carries unsigned
|
|
62
|
+
claims and STRICT rejects it at EVERY budget (measured: search_exhausted
|
|
63
|
+
at max_iterations=1000 in 0.06s AND at 200000 in 14.16s). The 25 noise
|
|
64
|
+
actions keep open_set non-empty past any practical cap."""
|
|
65
|
+
actions = [ResearchAction(name="extract", preconditions={"start"},
|
|
66
|
+
effects={"claims_identified"}, cost=1, description="x")]
|
|
67
|
+
actions += [ResearchAction(name=f"n{i}", preconditions={"start"},
|
|
68
|
+
effects={f"f{i}"}, cost=1, description="n")
|
|
69
|
+
for i in range(25)]
|
|
70
|
+
return {"start"}, {"claims_identified"}, actions
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Blocker2VerdictSplitTests(unittest.TestCase):
|
|
74
|
+
"""FR-2 / AC-2 / AC-3 / AM-2 — 'don't know' is a first-class verdict."""
|
|
75
|
+
|
|
76
|
+
def test_goal_unreachable_for_sentinel_key(self):
|
|
77
|
+
"""AC-2: a goal fact no action's effects ever produce -> GOAL_UNREACHABLE,
|
|
78
|
+
decided promptly and independently of max_iterations (AM-2 confirmation:
|
|
79
|
+
near-identical wall-clock at budget 100 vs 200000 proves the verdict comes
|
|
80
|
+
from a real reachability check, not a relabeled timeout)."""
|
|
81
|
+
initial = {"topic_defined"}
|
|
82
|
+
goal = {"sentinel_fact_no_action_produces"}
|
|
83
|
+
# Sanity: the sentinel really is produced by no action.
|
|
84
|
+
for action in RESEARCH_ACTIONS:
|
|
85
|
+
self.assertNotIn("sentinel_fact_no_action_produces", action.effects)
|
|
86
|
+
|
|
87
|
+
timings = {}
|
|
88
|
+
for budget in (100, 200_000):
|
|
89
|
+
t0 = time.perf_counter()
|
|
90
|
+
result = find_research_plan(
|
|
91
|
+
initial, goal,
|
|
92
|
+
verification_mode=VerificationMode.MODERATE,
|
|
93
|
+
max_iterations=budget,
|
|
94
|
+
)
|
|
95
|
+
timings[budget] = time.perf_counter() - t0
|
|
96
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
97
|
+
self.assertIs(result.verdict, PlanVerdict.GOAL_UNREACHABLE)
|
|
98
|
+
self.assertIsNone(result.iterations)
|
|
99
|
+
self.assertIn("sentinel_fact_no_action_produces", result.message)
|
|
100
|
+
|
|
101
|
+
# Promptness: both calls decided by the closure (plan budget: sub-10ms;
|
|
102
|
+
# asserted at 50ms for CI-noise margin), and budget-independent — a
|
|
103
|
+
# relabeled timeout at budget 200000 would burn >1s.
|
|
104
|
+
for budget, elapsed in timings.items():
|
|
105
|
+
self.assertLess(
|
|
106
|
+
elapsed, 0.05,
|
|
107
|
+
f"GOAL_UNREACHABLE at max_iterations={budget} took {elapsed:.4f}s — "
|
|
108
|
+
"not the prompt closure-based verdict the contract promises",
|
|
109
|
+
)
|
|
110
|
+
self.assertLess(abs(timings[100] - timings[200_000]), 0.05)
|
|
111
|
+
|
|
112
|
+
def test_search_exhausted_with_tiny_budget(self):
|
|
113
|
+
"""AC-3 (red half): a genuinely-reachable goal at max_iterations=1 ->
|
|
114
|
+
SEARCH_EXHAUSTED(1) with concrete retry guidance, NOT GOAL_UNREACHABLE."""
|
|
115
|
+
initial, goal = create_research_goal("quick", VerificationMode.MODERATE)
|
|
116
|
+
result = find_research_plan(
|
|
117
|
+
initial, goal,
|
|
118
|
+
verification_mode=VerificationMode.MODERATE,
|
|
119
|
+
max_iterations=1,
|
|
120
|
+
)
|
|
121
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
122
|
+
self.assertIs(result.verdict, PlanVerdict.SEARCH_EXHAUSTED)
|
|
123
|
+
self.assertEqual(result.iterations, 1)
|
|
124
|
+
# Actionable retry guidance: names the knob AND a concrete larger number.
|
|
125
|
+
self.assertIn("max_iterations", result.message)
|
|
126
|
+
self.assertIn("max_iterations=10", result.message)
|
|
127
|
+
|
|
128
|
+
def test_search_exhausted_retry_succeeds_with_larger_budget(self):
|
|
129
|
+
"""AC-3 (green half): the SAME goal succeeds at a larger budget — proving
|
|
130
|
+
the SEARCH_EXHAUSTED verdict's retry advice is real, not decorative."""
|
|
131
|
+
initial, goal = create_research_goal("quick", VerificationMode.MODERATE)
|
|
132
|
+
result = find_research_plan(
|
|
133
|
+
initial, goal,
|
|
134
|
+
verification_mode=VerificationMode.MODERATE,
|
|
135
|
+
max_iterations=100,
|
|
136
|
+
)
|
|
137
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
138
|
+
|
|
139
|
+
def test_plan_not_found_is_falsy(self):
|
|
140
|
+
"""Defense in depth: a legacy `if plan:` TRUTHINESS check must degrade
|
|
141
|
+
to the old None semantics, never treat 'search exhausted' as 'plan
|
|
142
|
+
found'."""
|
|
143
|
+
result = PlanNotFound(verdict=PlanVerdict.SEARCH_EXHAUSTED, iterations=1)
|
|
144
|
+
self.assertFalse(bool(result))
|
|
145
|
+
|
|
146
|
+
def test_plan_not_found_is_not_none_identity_hazard(self):
|
|
147
|
+
"""GAP-5 (QE re-check): the falsy safety net does NOT cover identity
|
|
148
|
+
checks — `result is None` is now always False, so a legacy
|
|
149
|
+
`if plan is not None:` caller reads a no-plan result as SUCCESS. This
|
|
150
|
+
test pins that hazard as documented (not accidental) behavior; the
|
|
151
|
+
loud warning lives in the PlanNotFound / find_research_plan / plan()
|
|
152
|
+
docstrings and in SKILL.md."""
|
|
153
|
+
initial, goal = create_research_goal("quick", VerificationMode.MODERATE)
|
|
154
|
+
result = find_research_plan(
|
|
155
|
+
initial, goal,
|
|
156
|
+
verification_mode=VerificationMode.MODERATE,
|
|
157
|
+
max_iterations=1,
|
|
158
|
+
)
|
|
159
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
160
|
+
self.assertIsNotNone(result) # the documented hazard: identity checks lie
|
|
161
|
+
self.assertFalse(bool(result)) # while truthiness checks stay safe
|
|
162
|
+
|
|
163
|
+
def test_search_exhausted_message_honest_when_verification_gate_blocks(self):
|
|
164
|
+
"""GAP-2 (QE re-check): the SEARCH_EXHAUSTED message must not assert
|
|
165
|
+
'the budget was the limiting factor' — for a closure-reachable goal
|
|
166
|
+
that the STRICT unsigned-claims gate permanently rejects, NO budget
|
|
167
|
+
finds a plan (measured falsifier: search_exhausted at both
|
|
168
|
+
max_iterations=1000, 0.06s, and 200000, 14.16s). The message must say
|
|
169
|
+
what is actually true: closure passed => not PROVABLY unreachable,
|
|
170
|
+
which is NOT a guarantee a larger budget succeeds."""
|
|
171
|
+
initial, goal, actions = _verification_unreachable_scenario()
|
|
172
|
+
|
|
173
|
+
# Sanity 1: the goal IS closure-reachable (the relaxed check passes).
|
|
174
|
+
self.assertTrue(goal.issubset(_reachability_closure(initial, actions)))
|
|
175
|
+
# Sanity 2: the STRICT rejection is permanent — no action in the
|
|
176
|
+
# library can ever produce the facts that clear unsigned claims.
|
|
177
|
+
for action in actions:
|
|
178
|
+
self.assertNotIn("signed_facts", action.effects)
|
|
179
|
+
self.assertNotIn("cryptographically_verified", action.effects)
|
|
180
|
+
|
|
181
|
+
result = find_research_plan(
|
|
182
|
+
initial, goal, actions=actions,
|
|
183
|
+
verification_mode=VerificationMode.STRICT,
|
|
184
|
+
max_iterations=1000,
|
|
185
|
+
)
|
|
186
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
187
|
+
self.assertIs(result.verdict, PlanVerdict.SEARCH_EXHAUSTED)
|
|
188
|
+
self.assertEqual(result.iterations, 1000)
|
|
189
|
+
|
|
190
|
+
msg = result.message
|
|
191
|
+
# The honest wording, pinned:
|
|
192
|
+
self.assertIn("not provably unreachable", msg)
|
|
193
|
+
self.assertIn("NOT a guarantee", msg)
|
|
194
|
+
self.assertIn("max_iterations", msg)
|
|
195
|
+
# The refuted over-claims, banned:
|
|
196
|
+
self.assertNotIn("budget was the limiting factor", msg)
|
|
197
|
+
self.assertNotIn("reachable in principle", msg)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Blocker1AdaptiveBudgetTests(unittest.TestCase):
|
|
201
|
+
"""FR-1 / AC-1 — the default budget covers the package's own demo scenarios."""
|
|
202
|
+
|
|
203
|
+
def test_default_budget_finds_high_stakes_demo_plan(self):
|
|
204
|
+
"""AC-1: the shipped high_stakes/STRICT demo scenario (with the demo's
|
|
205
|
+
signed-source capability flags) finds a plan with NO max_iterations
|
|
206
|
+
argument passed."""
|
|
207
|
+
initial, goal = create_research_goal("high_stakes", VerificationMode.STRICT)
|
|
208
|
+
result = find_research_plan(
|
|
209
|
+
initial | _CAPABILITY_FLAGS, goal,
|
|
210
|
+
verification_mode=VerificationMode.STRICT,
|
|
211
|
+
)
|
|
212
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
213
|
+
self.assertGreater(len(result.actions), 0)
|
|
214
|
+
|
|
215
|
+
def test_default_budget_finds_all_moderate_demo_scenarios(self):
|
|
216
|
+
"""FR-1 calibration breadth: the MODERATE-mode shipped scenarios also
|
|
217
|
+
succeed at the adaptive default (calibrated across scenarios, not
|
|
218
|
+
extrapolated from one point)."""
|
|
219
|
+
for goal_type in ("exploratory", "competitive", "quick"):
|
|
220
|
+
with self.subTest(goal_type=goal_type):
|
|
221
|
+
initial, goal = create_research_goal(goal_type, VerificationMode.MODERATE)
|
|
222
|
+
result = find_research_plan(
|
|
223
|
+
initial, goal, verification_mode=VerificationMode.MODERATE
|
|
224
|
+
)
|
|
225
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
226
|
+
|
|
227
|
+
def test_default_budget_formula(self):
|
|
228
|
+
"""The adaptive default is mode-dominant (the MEASURED dominant variable)
|
|
229
|
+
with a per-goal headroom term, and floors match the calibrated constants."""
|
|
230
|
+
self.assertEqual(
|
|
231
|
+
_default_max_iterations(set("abcde"), VerificationMode.STRICT), 100_000
|
|
232
|
+
)
|
|
233
|
+
self.assertEqual(
|
|
234
|
+
_default_max_iterations(set("abcde"), VerificationMode.PARANOID), 100_000
|
|
235
|
+
)
|
|
236
|
+
self.assertEqual(
|
|
237
|
+
_default_max_iterations(set("abcde"), VerificationMode.MODERATE), 5_000
|
|
238
|
+
)
|
|
239
|
+
# GAP-8 (QE re-check): DEVELOPMENT reaches the MODERATE floor via the
|
|
240
|
+
# else-branch — asserted, not assumed.
|
|
241
|
+
self.assertEqual(
|
|
242
|
+
_default_max_iterations(set("abcde"), VerificationMode.DEVELOPMENT), 5_000
|
|
243
|
+
)
|
|
244
|
+
# Per-goal term takes over past the floor.
|
|
245
|
+
big_goal = {f"g{i}" for i in range(300)}
|
|
246
|
+
self.assertEqual(
|
|
247
|
+
_default_max_iterations(big_goal, VerificationMode.STRICT), 150_000
|
|
248
|
+
)
|
|
249
|
+
self.assertEqual(
|
|
250
|
+
_default_max_iterations(big_goal, VerificationMode.MODERATE), 15_000
|
|
251
|
+
)
|
|
252
|
+
self.assertEqual(
|
|
253
|
+
_default_max_iterations(big_goal, VerificationMode.DEVELOPMENT), 15_000
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def test_default_budget_finds_high_stakes_paranoid_plan(self):
|
|
257
|
+
"""GAP-7 (QE re-check): the PARANOID floor is no longer a bare
|
|
258
|
+
extrapolation — this shipped scenario exercises it. PARANOID's search
|
|
259
|
+
behavior is identical to STRICT in this planner (require_verification
|
|
260
|
+
is the only search-relevant switch, shared by both; the 0.99 threshold
|
|
261
|
+
affects reporting), so high_stakes/PARANOID must find a plan at the
|
|
262
|
+
DEFAULT budget exactly as the STRICT twin does."""
|
|
263
|
+
initial, goal = create_research_goal("high_stakes", VerificationMode.PARANOID)
|
|
264
|
+
result = find_research_plan(
|
|
265
|
+
initial | _CAPABILITY_FLAGS, goal,
|
|
266
|
+
verification_mode=VerificationMode.PARANOID,
|
|
267
|
+
)
|
|
268
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
269
|
+
self.assertGreater(len(result.actions), 0)
|
|
270
|
+
|
|
271
|
+
def test_development_mode_plan_at_default_budget(self):
|
|
272
|
+
"""GAP-8 (QE re-check): DEVELOPMENT mode is a working mode, not a dead
|
|
273
|
+
enum member — a shipped scenario plans successfully at its (MODERATE-
|
|
274
|
+
floor) default budget, with no verification gate applied."""
|
|
275
|
+
initial, goal = create_research_goal("exploratory", VerificationMode.DEVELOPMENT)
|
|
276
|
+
result = find_research_plan(
|
|
277
|
+
initial, goal, verification_mode=VerificationMode.DEVELOPMENT
|
|
278
|
+
)
|
|
279
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
280
|
+
|
|
281
|
+
def test_explicit_override_still_respected(self):
|
|
282
|
+
"""Regression guard from the plan (task 2.1): an explicit
|
|
283
|
+
max_iterations=200000 caller (the original repro command) still finds
|
|
284
|
+
the high_stakes plan — the sentinel-default only changes behavior when
|
|
285
|
+
the caller omits the argument."""
|
|
286
|
+
initial, goal = create_research_goal("high_stakes", VerificationMode.STRICT)
|
|
287
|
+
result = find_research_plan(
|
|
288
|
+
initial | _CAPABILITY_FLAGS, goal,
|
|
289
|
+
verification_mode=VerificationMode.STRICT,
|
|
290
|
+
max_iterations=200_000,
|
|
291
|
+
)
|
|
292
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class FailurePathLatencyTests(unittest.TestCase):
|
|
296
|
+
"""GAP-6 (QE re-check) — the failure path must be boundable.
|
|
297
|
+
|
|
298
|
+
Raising the STRICT default budget 1,000 -> 100,000 made the FAILURE path
|
|
299
|
+
~100x slower (measured on this class of scenario: 9.46s at the default
|
|
300
|
+
budget vs 0.06s at the old cap): any closure-passing goal the search
|
|
301
|
+
cannot satisfy burns the full budget. The fix is an opt-in wall-clock
|
|
302
|
+
ceiling (max_seconds) independent of the iteration count; the DEFAULT
|
|
303
|
+
stays None so the default search remains deterministic across machines
|
|
304
|
+
(decision recorded in ADR 03_adr/001, 'wall-clock default' rejected
|
|
305
|
+
option)."""
|
|
306
|
+
|
|
307
|
+
def test_max_seconds_bounds_failure_path_wall_clock(self):
|
|
308
|
+
"""With max_seconds=0.5, the verification-blocked falsifier returns
|
|
309
|
+
SEARCH_EXHAUSTED well before burning the 100,000-iteration STRICT
|
|
310
|
+
default budget — bounded wall clock, honest verdict."""
|
|
311
|
+
initial, goal, actions = _verification_unreachable_scenario()
|
|
312
|
+
t0 = time.perf_counter()
|
|
313
|
+
result = find_research_plan(
|
|
314
|
+
initial, goal, actions=actions,
|
|
315
|
+
verification_mode=VerificationMode.STRICT, # default budget: 100,000
|
|
316
|
+
max_seconds=0.5,
|
|
317
|
+
)
|
|
318
|
+
elapsed = time.perf_counter() - t0
|
|
319
|
+
self.assertLess(
|
|
320
|
+
elapsed, 2.0,
|
|
321
|
+
f"failure path took {elapsed:.2f}s despite max_seconds=0.5 — "
|
|
322
|
+
"the wall-clock ceiling is not bounding latency",
|
|
323
|
+
)
|
|
324
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
325
|
+
self.assertIs(result.verdict, PlanVerdict.SEARCH_EXHAUSTED)
|
|
326
|
+
# The ceiling, not the iteration cap, stopped the search — and the
|
|
327
|
+
# message says so instead of pretending the budget ran out.
|
|
328
|
+
self.assertLess(result.iterations, 100_000)
|
|
329
|
+
self.assertIn("max_seconds", result.message)
|
|
330
|
+
self.assertIn("not provably unreachable", result.message)
|
|
331
|
+
|
|
332
|
+
def test_max_seconds_does_not_cut_off_a_findable_plan(self):
|
|
333
|
+
"""A generous ceiling never affects a scenario the budget satisfies:
|
|
334
|
+
the success path is unchanged."""
|
|
335
|
+
initial, goal = create_research_goal("quick", VerificationMode.MODERATE)
|
|
336
|
+
result = find_research_plan(
|
|
337
|
+
initial, goal,
|
|
338
|
+
verification_mode=VerificationMode.MODERATE,
|
|
339
|
+
max_seconds=30.0,
|
|
340
|
+
)
|
|
341
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class Blocker3CapabilityFlagSplitTests(unittest.TestCase):
|
|
345
|
+
"""FR-3 / AC-4 / AM-1 — no more free 'verified' plans without real keys."""
|
|
346
|
+
|
|
347
|
+
def test_high_stakes_reachable_with_keys_and_source_class(self):
|
|
348
|
+
"""AC-4a: with a test double supplying both capability facts (a real
|
|
349
|
+
signed-source integration), high_stakes/STRICT finds a plan — exercised
|
|
350
|
+
through the high-level planner so the constructor wiring is covered."""
|
|
351
|
+
planner = GOAPResearchPlanner(
|
|
352
|
+
verification_mode="strict",
|
|
353
|
+
issuer_keys_available=True,
|
|
354
|
+
source_class_verified=True,
|
|
355
|
+
)
|
|
356
|
+
result = planner.plan(goal_type="high_stakes", topic="test")
|
|
357
|
+
self.assertIsInstance(result, ResearchPlan)
|
|
358
|
+
|
|
359
|
+
def test_high_stakes_unreachable_with_default_no_keys(self):
|
|
360
|
+
"""AC-4b / AM-1 confirmation: the package's REAL default (no genuine
|
|
361
|
+
issuer keys) yields GOAL_UNREACHABLE — never a silently-fabricated
|
|
362
|
+
'verified' plan. Reverting configure_trusted_issuers.preconditions to
|
|
363
|
+
set() flips this test to a fabricated ResearchPlan (proven in the
|
|
364
|
+
discrimination cycle), so this test is the safeguard's tripwire."""
|
|
365
|
+
initial, goal = create_research_goal("high_stakes", VerificationMode.STRICT)
|
|
366
|
+
result = find_research_plan(
|
|
367
|
+
initial, goal, verification_mode=VerificationMode.STRICT
|
|
368
|
+
)
|
|
369
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
370
|
+
self.assertIs(result.verdict, PlanVerdict.GOAL_UNREACHABLE)
|
|
371
|
+
|
|
372
|
+
# Same honest default through the high-level planner: a bare issuer
|
|
373
|
+
# DOMAIN LIST is not key possession.
|
|
374
|
+
planner = GOAPResearchPlanner(
|
|
375
|
+
verification_mode="strict",
|
|
376
|
+
trusted_issuers=["pubmed.ncbi.nlm.nih.gov", "who.int"],
|
|
377
|
+
)
|
|
378
|
+
result = planner.plan(goal_type="high_stakes", topic="test")
|
|
379
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
380
|
+
self.assertIs(result.verdict, PlanVerdict.GOAL_UNREACHABLE)
|
|
381
|
+
|
|
382
|
+
def test_partial_flags_still_unreachable(self):
|
|
383
|
+
"""Two axes are genuinely two axes: neither flag alone unlocks
|
|
384
|
+
high_stakes — keys without a verifiable source class (and vice versa)
|
|
385
|
+
still yield GOAL_UNREACHABLE."""
|
|
386
|
+
initial, goal = create_research_goal("high_stakes", VerificationMode.STRICT)
|
|
387
|
+
for only in sorted(_CAPABILITY_FLAGS):
|
|
388
|
+
with self.subTest(only_flag=only):
|
|
389
|
+
result = find_research_plan(
|
|
390
|
+
initial | {only}, goal,
|
|
391
|
+
verification_mode=VerificationMode.STRICT,
|
|
392
|
+
)
|
|
393
|
+
self.assertIsInstance(result, PlanNotFound)
|
|
394
|
+
self.assertIs(result.verdict, PlanVerdict.GOAL_UNREACHABLE)
|
|
395
|
+
|
|
396
|
+
def test_new_flags_are_preconditions_never_effects(self):
|
|
397
|
+
"""FR-3 structural guard (layer-1 deterministic check): each new flag is
|
|
398
|
+
READ as a precondition somewhere (the split is not cosmetic — the dead
|
|
399
|
+
whitelist_available mistake is not repeated) and PRODUCED by no action
|
|
400
|
+
(no action may manufacture its own key/source-class credibility)."""
|
|
401
|
+
for flag in sorted(_CAPABILITY_FLAGS):
|
|
402
|
+
with self.subTest(flag=flag):
|
|
403
|
+
self.assertTrue(
|
|
404
|
+
any(flag in a.preconditions for a in RESEARCH_ACTIONS),
|
|
405
|
+
f"{flag} is never read as a precondition — the split is cosmetic",
|
|
406
|
+
)
|
|
407
|
+
producers = [a.name for a in RESEARCH_ACTIONS if flag in a.effects]
|
|
408
|
+
self.assertEqual(
|
|
409
|
+
producers, [],
|
|
410
|
+
f"{flag} is produced for free by {producers} — the "
|
|
411
|
+
"whitelist_available defect reintroduced",
|
|
412
|
+
)
|
|
413
|
+
# And the dead flag itself is gone from the action graph entirely.
|
|
414
|
+
for action in RESEARCH_ACTIONS:
|
|
415
|
+
self.assertNotIn("whitelist_available", action.preconditions)
|
|
416
|
+
self.assertNotIn("whitelist_available", action.effects)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if __name__ == "__main__":
|
|
420
|
+
unittest.main()
|