فهرست مقاله
در یک خطِ آمادهسازیِ داده، دانستنِ اینکه کالیبراسیون پس از دریافت قرار دارد کافی نیست؛ باید روشن باشد چه زمانی دریافت را پایانیافته اعلام کردهایم. تحویلِ نامِ کار به مصرفکننده، هنوز رسیدِ پایانِ آن کار نیست. این درس با TopologicalSorter واقعیِ کتابخانهٔ استانداردِ پایتون، همین فاصله را مشاهده میکند. نامهایی مانند ADC و CAL مرحلههایِ ساختگیاند؛ برنامه هیچ حسگر، فایل، فرمانِ ساخت، نخ یا سامانهٔ یکپارچهسازیِ مداومی را اجرا نمیکند.
۱. گرافِ پیشنیاز، قراردادِ اجرایِ کار نیست

ورودی، نگاشتِ نامِ هر کار به پیشنیازهایِ آن است. CAL به ADC وابسته است، FILTER منتظرِ CAL میماند و SAVE پس از FILTER قرار دارد؛ HEALTH نیز مستقل است. جهتِ یال از پیشنیاز به کارِ وابسته میرود. این نامها مقدارِ ولتاژ، زمانِ نمونهبرداری یا شناسهٔ دستگاهِ تأییدشده نیستند. قرارداد فقط میگوید اعلامِ پایانِ کدام نام، اجازهٔ تحویلِ نامِ بعدی را فراهم میکند.
مرزِ ورودیِ خودمان حداکثر هشت کار، هشت مدخلِ پیشنیاز برای هر کار و شانزده مدخل در کل است. ظرفِ اصلی dict دقیق و ظرفِ پیشنیازها tuple دقیق است. نام با حرفِ بزرگِ ASCII آغاز میشود و حداکثر دوازده نویسه دارد؛ ادامه فقط حرفِ بزرگ، رقم یا زیرخط است. این سقفها انتخابِ آموزشیاند و از محدودیتِ خودِ الگوریتم یا ظرفیتِ یک دستگاه استخراج نشدهاند.
در مرجعِ رسمیِ graphlib، نامِ پیشنیازِ تازه میتواند خودکار به گراف افزوده شود. رابطِ ما چنین اجازهای نمیدهد: پیشنیاز باید از ابتدا کلیدِ همان نگاشت باشد. آزمایشِ A وابسته به MISSING، در مسیرِ مستقیمِ کتابخانه دو نام میسازد، اما در رابطِ ما خطایِ ورودی است. این تفاوت برای پیدا کردنِ غلطِ نامِ مرحله مفید است؛ مجوزِ دسترسی یا اعتبارِ واقعیِ مرحله را بررسی نمیکند.
۲. get_ready تحویل میدهد و done وابسته را آزاد میکند

در ردِ دستی، نخست prepare را فراخوانی میکنیم و سپس get_ready دو نامِ ADC و HEALTH را میدهد. فراخوانیِ دوباره، بدونِ هیچ done، tuple خالی برمیگرداند. آن دو نام دوباره تحویل نمیشوند، ولی is_active هنوز True است. برنامه آنها را از مجموعهٔ «تحویلنشده» خارج کرده است؛ خالیبودنِ جوابِ بعدی، گواهِ دریافتِ نتیجه یا تمامشدنِ کارها نیست.
اکنون فقط done مربوط به ADC را اعلام میکنیم. CAL قابلِ تحویل میشود، بااینکه HEALTH هنوز پایانیافته اعلام نشده است. با اعلامِ پایانِ CAL، نامِ FILTER و سپس با پایانِ FILTER، نامِ SAVE ظاهر میشود. پس لازم نیست همهٔ عضوهایِ نخستین دسته را یکجا پایانیافته حساب کنیم. معیارِ این شاخه، پیشنیازِ خودش است؛ ترتیبِ نامها، سرعت یا اولویتِ عملیاتیِ مرحلهها را تعیین نمیکند.
پس از done مربوط به SAVE نیز جوابِ آمادهٔ تازه خالی و is_active برابر True است، چون HEALTH همچنان تحویلشده و بدونِ اعلامِ پایان مانده است. تنها پس از done آن، حالتِ فعال False میشود. این رد هفت مشاهده دارد و هیچ مدتِ زمانی را اندازه نمیگیرد. در کاربردِ واقعی، فراموششدنِ رسیدِ یک کار میتواند انتظار را ادامه دهد؛ این نمونه سیاستِ مهلت، شکستِ کارگر، retry یا لغو را اضافه نکرده است.
۳. چرخه؛ ردِ کلِ طرح و پیشرویِ تشخیصیِ جدا

دو تابع با دو مقصدِ متفاوت داریم. strict_complete_order فقط ترتیبِ کامل میخواهد و iterator حاصل از static_order را همانجا مصرف میکند. اگر چرخه باشد، CycleError از تابع بیرون میآید و ترتیبِ موفق برگردانده نمیشود. صرفِ ساختنِ یک iterator را همارزِ مصرفِ موفقِ آن نمیگیریم. در این مسیر، نامهایِ مستقلِ گرافِ معیوب بهعنوانِ یک طرحِ کامل عرضه نمیشوند.
تابعِ جداگانهٔ partial_diagnostic خطایِ prepare را ثبت میکند و فقط پیشرویِ تشخیصیِ ممکن را ادامه میدهد. در فیکسچر، X و Y به یکدیگر وابستهاند و SAVE منتظرِ X است؛ ADC و HEALTH مستقلاند و CAL به ADC وابسته است. دستههایِ تشخیصی، ابتدا ADC و HEALTH و سپس CAL هستند. سه نامِ SAVE، X و Y مسدود میمانند و فیلدِ complete صریحاً False است.
در این تشخیص، هر دستهٔ تحویلشده بلافاصله با done تأییدِ منطقی میشود؛ هیچ کارِ متناظر اجرا نشده است. شاهدِ چرخه یک مسیرِ بستهٔ واقعی از همان یالهاست، نه فهرستِ همهٔ علتها یا همهٔ عضوهایِ چرخه. SAVE در همین مثال پاییندست است. برای مقایسه، زمانبندِ رویدادِ حسگر رکورد را طبقِ زمانِ مجازی انتخاب میکرد؛ اینجا وابستگی و اعلامِ پایان را میسنجیم و ساعت نداریم.
۴. کدِ کامل و مرجعِ مستقلِ وضعیتها

فایل را با نامِ topological_ready_done.py و دستورِ python topological_ready_done.py اجرا کنید. کتابخانهٔ بیرونی لازم ندارد؛ اجرایِ ثبتشده Python 3.14.5 است. رفتارِ تکرارِ prepare در این کد به شاخهٔ ۳٫۱۴ تعلق دارد: دو فراخوانی پیش از شروع انجام میشود، سپس پس از تحویلِ واقعیِ A، فراخوانیِ بعدی رد میشود. همچنین افزودنِ نام پس از prepare خطاست. این مشاهده را به نسخههایِ قدیمیتر تعمیم نمیدهیم؛ صفحهٔ مستندات هنگامِ مطالعه 3.14.8 بود.
"""Actual stdlib dependency handoff on supplied names; no jobs or threads run."""
from graphlib import CycleError, TopologicalSorter
from itertools import permutations, product
import json
import platform
import re
import sys
def checked_graph(graph):
if type(graph) is not dict or len(graph) > 8:
raise ValueError("graph must be exact dict with at most 8 jobs")
valid_name = lambda x: type(x) is str and re.fullmatch(r"[A-Z][A-Z0-9_]{0,11}", x) is not None
if any(not valid_name(node) for node in graph):
raise ValueError("invalid job identifier")
entries = 0
for node, predecessors in graph.items():
if type(predecessors) is not tuple or len(predecessors) > 8:
raise ValueError("predecessors must be exact tuples of at most 8 names")
entries += len(predecessors)
if any(not valid_name(p) or p not in graph for p in predecessors):
raise ValueError("all predecessor names must be explicitly declared jobs")
if entries > 16:
raise ValueError("at most 16 predecessor entries in this teaching profile")
return {node: tuple(sorted(set(graph[node]))) for node in sorted(graph)}
def strict_complete_order(graph):
# Consume the iterator here: cycle rejection is observed before returning.
return tuple(TopologicalSorter(checked_graph(graph)).static_order())
def partial_diagnostic(graph):
supplied = checked_graph(graph)
sorter = TopologicalSorter(supplied)
cycle = None
try:
sorter.prepare()
except CycleError as error:
cycle = tuple(error.args[1])
processed, batches = [], []
while sorter.is_active():
ready = tuple(sorted(sorter.get_ready()))
assert ready # This diagnostic acknowledges every issued batch below.
batches.append(ready)
processed.extend(ready)
sorter.done(*ready)
blocked = tuple(sorted(set(supplied) - set(processed)))
return dict(complete=cycle is None and not blocked, acknowledged=tuple(processed),
ready_batches=tuple(batches), blocked=blocked, one_cycle=cycle)
class EligibilityReference:
"""Valid small fixtures only; set predicates, no graphlib or in-degree state."""
def __init__(self, graph):
self.graph = graph
self.issued = set()
self.finished = set()
def ready(self):
result = tuple(sorted(node for node, predecessors in self.graph.items()
if node not in self.issued and all(p in self.finished for p in predecessors)))
self.issued.update(result)
return result
def done(self, node):
assert node in self.issued and node not in self.finished
self.finished.add(node)
def active(self):
return bool(self.issued - self.finished) or any(
node not in self.issued and all(p in self.finished for p in predecessors)
for node, predecessors in self.graph.items())
def whole_permutation_reference(graph):
# Feasibility definition on all complete permutations, no ready/done path.
valid = []
for order in permutations(tuple(sorted(graph))):
place = {node: i for i, node in enumerate(order)}
if all(place[p] < place[node] for node, ps in graph.items() for p in ps):
valid.append(order)
return tuple(valid)
def real_closed_cycle(graph, cycle):
return bool(cycle) and len(cycle) >= 2 and cycle[0] == cycle[-1] and all(
source in graph[target] for source, target in zip(cycle, cycle[1:]))
def rejects(exception, function, *args):
try:
function(*args)
except exception:
return True
return False
def hand_transcript():
graph = {"ADC": (), "CAL": ("ADC",), "FILTER": ("CAL",),
"SAVE": ("FILTER",), "HEALTH": ()}
sorter = TopologicalSorter(checked_graph(graph))
sorter.prepare()
transcript = []
def observe(step):
ready = tuple(sorted(sorter.get_ready()))
transcript.append(dict(step=step, ready=ready, active=sorter.is_active()))
observe("initial_handoff")
observe("request_again_without_done")
for name in ("ADC", "CAL", "FILTER", "SAVE", "HEALTH"):
sorter.done(name)
observe("after_done_" + name)
return graph, transcript
def run():
graph, trace = hand_transcript()
checks = {}
checks["initial_ready_is_handoff_not_completion"] = trace[0] == dict(
step="initial_handoff", ready=("ADC", "HEALTH"), active=True)
checks["empty_ready_can_coexist_with_outstanding_active_work"] = (
trace[1]["ready"] == () and trace[1]["active"] is True
and trace[5]["ready"] == () and trace[5]["active"] is True)
checks["dependent_branch_advances_before_unrelated_health_ack"] = (
tuple(x["ready"] for x in trace[2:5]) == (("CAL",), ("FILTER",), ("SAVE",))
and trace[-1] == dict(step="after_done_HEALTH", ready=(), active=False))
once = TopologicalSorter({"A": (), "B": ("A",)})
once.prepare(); first = once.get_ready(); again = once.get_ready()
once.done("A"); second = once.get_ready()
checks["ready_nodes_are_delivered_once_until_acknowledgment"] = (
first == ("A",) and again == () and second == ("B",) and once.get_ready() == ())
lifecycle_cases = 0
possible = tuple((a, b) for a in "ABC" for b in "ABC")
for flags in product((False, True), repeat=len(possible)):
fixture = {node: () for node in "ABC"}
for (source, target), present in zip(possible, flags):
if present:
fixture[target] += (source,)
sorter = TopologicalSorter(checked_graph(fixture)); model = EligibilityReference(fixture)
cyclic = False
try:
sorter.prepare()
except CycleError:
cyclic = True
assert cyclic == (not whole_permutation_reference(fixture))
while True:
ready = tuple(sorted(sorter.get_ready())); expected = model.ready()
assert ready == expected and sorter.is_active() == model.active()
assert sorter.get_ready() == model.ready() == ()
if not ready:
break
for node in reversed(ready):
sorter.done(node); model.done(node)
assert sorter.is_active() == model.active()
assert sorter.is_active() is False and model.active() is False
lifecycle_cases += 1
checks["independent_eligibility_sets_all512_three_job_graphs_ONE_group"] = lifecycle_cases == 512
order_cases = 0
edges = tuple((a, b) for a in "ABCD" for b in "ABCD" if a < b)
for flags in product((False, True), repeat=len(edges)):
fixture = {node: () for node in "ABCD"}
for (source, target), present in zip(edges, flags):
if present:
fixture[target] += (source,)
assert strict_complete_order(fixture) in whole_permutation_reference(fixture)
order_cases += 1
checks["static_order_membership_in_whole_permutation_oracle64_DAGs_ONE_group"] = order_cases == 64
cyclic_graph = {"ADC": (), "CAL": ("ADC",), "HEALTH": (),
"X": ("Y",), "Y": ("X",), "SAVE": ("X",)}
checks["strict_complete_order_rejects_entire_cyclic_graph"] = rejects(
CycleError, strict_complete_order, cyclic_graph)
diagnostic = partial_diagnostic(cyclic_graph)
checks["separate_partial_cycle_diagnostic_never_claims_complete"] = (
diagnostic["complete"] is False and diagnostic["acknowledged"] == ("ADC", "HEALTH", "CAL")
and diagnostic["blocked"] == ("SAVE", "X", "Y"))
self_loop = partial_diagnostic({"A": ("A",)})
checks["cycle_witness_is_real_closed_path_not_all_blocked_jobs"] = (
real_closed_cycle(cyclic_graph, diagnostic["one_cycle"])
and "SAVE" not in diagnostic["one_cycle"]
and self_loop["one_cycle"] == ("A", "A") and self_loop["acknowledged"] == ())
raw_implicit = tuple(TopologicalSorter({"A": ("MISSING",)}).static_order())
checks["own_unknown_predecessor_rejection_differs_from_stdlib_implicit_add"] = (
rejects(ValueError, strict_complete_order, {"A": ("MISSING",)})
and raw_implicit == ("MISSING", "A"))
wrong = TopologicalSorter({"A": (), "B": ("A",)})
before_prepare = (rejects(ValueError, wrong.get_ready), rejects(ValueError, wrong.is_active),
rejects(ValueError, wrong.done, "A"))
wrong.prepare()
unissued = rejects(ValueError, wrong.done, "A")
wrong.get_ready(); wrong.done("A")
checks["lifecycle_errors_before_prepare_unissued_unknown_and_repeated_done"] = (
all(before_prepare) and unissued and rejects(ValueError, wrong.done, "A")
and rejects(ValueError, wrong.done, "UNKNOWN") and rejects(ValueError, wrong.done, "B"))
repeated = TopologicalSorter({"A": ()}); repeated.prepare(); repeated.prepare()
add_blocked = rejects(ValueError, repeated.add, "B")
started = repeated.get_ready()
checks["python314_repeat_prepare_prestart_only_and_no_add_after_prepare"] = (
started == ("A",) and add_blocked and rejects(ValueError, repeated.prepare))
class DictChild(dict):
pass
class TupleChild(tuple):
pass
malformed = ([], DictChild({}), {"bad": ()}, {"": ()}, {"A" * 13: ()},
{True: ()}, {"A": []}, {"A": iter(())}, {"A": TupleChild(())},
{"A": (True,)}, {"A": ("B",)}, {"A": ("A",) * 9},
{"A": ("A",) * 8, "B": ("B",) * 8, "C": ("C",)},
{"N" + str(i): () for i in range(9)})
full = {"N" + str(i): () for i in range(8)}
checks["strict_supplied_graph_names_containers_entry_caps_empty_and_eight_jobs"] = (
all(rejects(ValueError, checked_graph, x) for x in malformed)
and strict_complete_order({}) == () and len(strict_complete_order(full)) == 8)
duplicate = {"A": (), "B": ("A", "A")}
before = tuple(duplicate.items()); first_result = checked_graph(duplicate); first_result["A"] = ("B",)
checks["duplicate_declarations_coalesce_without_mutating_input_or_sharing_result"] = (
strict_complete_order(duplicate) == ("A", "B") and tuple(duplicate.items()) == before
and checked_graph(duplicate) == {"A": (), "B": ("A",)})
assert len(checks) == 14 and all(type(x) is bool and x for x in checks.values()), checks
return dict(python=platform.python_version(), dependencies="standard library only",
graph_direction="mapping job -> predecessors; edge predecessor -> job",
hand_graph=graph, hand_transcript=trace, strict_cycle_rejected=True,
partial_cycle_diagnostic=diagnostic, raw_stdlib_implicit_job_order=raw_implicit,
independent_lifecycle_graphs_inside_one_group=lifecycle_cases,
independent_complete_order_graphs_inside_one_group=order_cases,
limits=dict(max_jobs=8, max_predecessor_entries_per_job=8, max_total_predecessor_entries=16,
identifiers="exact str ASCII [A-Z][A-Z0-9_]{0,11}"),
units="Job/edge counts and logical acknowledgment states; no time or electrical measurements.",
checks=checks, scope="No jobs, threads, network, clocks, CI system or callbacks executed; acknowledgments are authored logical events, not evidence of external success.")
if __name__ == "__main__":
sys.stdout.buffer.write((json.dumps(run(), ensure_ascii=True, indent=2) + "\n").encode("utf-8"))
مرجعِ وضعیت، دو مجموعهٔ مستقلِ issued و finished دارد. آمادهبودن را مستقیماً از تمامشدنِ پیشنیازها و تحویلنشدنِ نام محاسبه میکند؛ نه graphlib را فراخوانی میکند و نه شمارندهٔ ورودیِ داخلیِ آن را بازسازی میکند. برای هر یک از ۵۱۲ گرافِ سهنامیِ جهتدار، شاملِ خودحلقهها، دستهها، تحویلِ دوباره و حالتِ فعال با این مدل مقایسه شدهاند. این خانواده یک گروهِ بررسی است؛ همهٔ زمانبندیهایِ کارگرِ واقعی را پوشش نمیدهد.
مرجعِ دیگر با جایگشتهایِ کاملِ itertools، مستقیماً میسنجد هر پیشنیاز پیش از وابسته باشد. برای شصتوچهار گرافِ چهارنامی با یالهایِ روبهجلو، ترتیبِ کتابخانه باید یکی از پاسخهایِ معتبر باشد. کمترین ترتیبِ سراسری را شرط نکردهایم. مرتبکردنِ نامهایِ هر دسته در گزارش، سیاستِ نمایشِ خودمان است؛ درستیِ وابستگی را با ترتیبِ الفبایی جایگزین نمیکند.
هر چهارده گروهِ نامدار در اجرایِ ثبتشده موفق بودهاند. علاوه بر دو خانوادهٔ مرجع، پاسخِ دستی، چرخه و خودحلقه، نامِ ناشناخته، خطاهایِ چرخهٔ عمر، تکرارِ prepare، ورودیِ خالی و کرانِ هشت کار بررسی شدهاند. پیشنیازِ تکراری یک وابستگی حساب میشود و ورودی تغییر نمیکند. واحدهایِ خروجی، شمارِ نام و مدخل و وضعیتِ منطقیاند؛ هیچ کمیتِ الکتریکی یا سنجشِ کارایی گزارش نمیشود.
{
"python": "3.14.5",
"dependencies": "standard library only",
"graph_direction": "mapping job -> predecessors; edge predecessor -> job",
"hand_graph": {
"ADC": [],
"CAL": [
"ADC"
],
"FILTER": [
"CAL"
],
"SAVE": [
"FILTER"
],
"HEALTH": []
},
"hand_transcript": [
{
"step": "initial_handoff",
"ready": [
"ADC",
"HEALTH"
],
"active": true
},
{
"step": "request_again_without_done",
"ready": [],
"active": true
},
{
"step": "after_done_ADC",
"ready": [
"CAL"
],
"active": true
},
{
"step": "after_done_CAL",
"ready": [
"FILTER"
],
"active": true
},
{
"step": "after_done_FILTER",
"ready": [
"SAVE"
],
"active": true
},
{
"step": "after_done_SAVE",
"ready": [],
"active": true
},
{
"step": "after_done_HEALTH",
"ready": [],
"active": false
}
],
"strict_cycle_rejected": true,
"partial_cycle_diagnostic": {
"complete": false,
"acknowledged": [
"ADC",
"HEALTH",
"CAL"
],
"ready_batches": [
[
"ADC",
"HEALTH"
],
[
"CAL"
]
],
"blocked": [
"SAVE",
"X",
"Y"
],
"one_cycle": [
"X",
"Y",
"X"
]
},
"raw_stdlib_implicit_job_order": [
"MISSING",
"A"
],
"independent_lifecycle_graphs_inside_one_group": 512,
"independent_complete_order_graphs_inside_one_group": 64,
"limits": {
"max_jobs": 8,
"max_predecessor_entries_per_job": 8,
"max_total_predecessor_entries": 16,
"identifiers": "exact str ASCII [A-Z][A-Z0-9_]{0,11}"
},
"units": "Job/edge counts and logical acknowledgment states; no time or electrical measurements.",
"checks": {
"initial_ready_is_handoff_not_completion": true,
"empty_ready_can_coexist_with_outstanding_active_work": true,
"dependent_branch_advances_before_unrelated_health_ack": true,
"ready_nodes_are_delivered_once_until_acknowledgment": true,
"independent_eligibility_sets_all512_three_job_graphs_ONE_group": true,
"static_order_membership_in_whole_permutation_oracle64_DAGs_ONE_group": true,
"strict_complete_order_rejects_entire_cyclic_graph": true,
"separate_partial_cycle_diagnostic_never_claims_complete": true,
"cycle_witness_is_real_closed_path_not_all_blocked_jobs": true,
"own_unknown_predecessor_rejection_differs_from_stdlib_implicit_add": true,
"lifecycle_errors_before_prepare_unissued_unknown_and_repeated_done": true,
"python314_repeat_prepare_prestart_only_and_no_add_after_prepare": true,
"strict_supplied_graph_names_containers_entry_caps_empty_and_eight_jobs": true,
"duplicate_declarations_coalesce_without_mutating_input_or_sharing_result": true
},
"scope": "No jobs, threads, network, clocks, CI system or callbacks executed; acknowledgments are authored logical events, not evidence of external success."
}
۵. چهار پرسش دربارهٔ پایانِ اعلامشده

پرسش اول: چرا دومین get_ready خالی است، بااینکه دو کار پایان نیافتهاند؟ پاسخ: ADC و HEALTH قبلاً تحویل شدهاند و دوباره تحویل نمیشوند. هنوز هیچ پیشنیازِ تازهای پایانیافته اعلام نشده است؛ بنابراین نامِ آمادهٔ تازه نداریم. is_active همانجا True میماند و خالیبودنِ دسته را نباید با پایانِ کلِ وضعیت یکی گرفت.
پرسش دوم: آیا برای تحویلِ CAL باید HEALTH هم done شود؟ پاسخ: در گرافِ حاضر خیر؛ تنها پیشنیازِ CAL، نامِ ADC است. اعلامِ پایانِ ADC آن را آزاد میکند و HEALTH همچنان تعهدِ جداگانهٔ تحویلشده باقی میماند. این پاسخ از یالِ مشخصِ ورودی میآید، نه از تضمینِ عدالت، اجرایِ همزمان یا مدتِ پایان.
پرسش سوم: آیا خروجیِ تشخیصیِ ADC، HEALTH و CAL یعنی گرافِ چرخهدار پذیرفته شده است؟ پاسخ: خیر؛ complete برابر False است و SAVE، X و Y مسدودند. تابعِ ترتیبِ کامل برای همان ورودی CycleError میدهد. تشخیصِ جدا فقط شاخهٔ قابلِ پیشروی را نشان میدهد و نامِ SAVE نیز صرفاً به دلیلِ انتظارِ پاییندستی، در این مثال عضوِ چرخه نیست.
پرسش چهارم: چرا A وابسته به MISSING در دو مسیر نتیجهٔ متفاوت دارد؟ پاسخ: کتابخانه میتواند پیشنیازِ تازه را خودکار بیفزاید؛ رابطِ محدودِ ما همهٔ نامها را از پیش میخواهد و خطا میدهد. این سیاست، اشتباهِ املایی را پنهان نمیکند. برای عمرِ کارهایِ واقعاً ناهمگام، آزمونِ TaskGroup و پاکسازیِ خوانندهها موضوعِ دیگری دارد؛ اینجا done فقط اعلامِ منطقیِ نویسنده است و صحتِ خروجیِ بیرونی را ثابت نمیکند.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

