অধ্যায় 10 · প্যাকেজিং ও বিগিনার ক্যাপস্টোন
ডকুমেন্টেশন, রিলিজ ও পোর্টফোলিওর মান
- পৃষ্ঠা 20 / 22
- 26 মিনিট পড়া
ওসাকার আইকো একজন রিক্রুটারকে নিজের প্রজেক্টের লিংক পাঠালেন। রিপোজিটরিতে আছে কাজ করা একটা README, একটা ডেমো যেটা গোলমালের সময় কী ঘটে তাও দেখায়, সৎ সীমাবদ্ধতা, আর মানের একটা সংখ্যা; তাই রিক্রুটার পড়তে থাকেন। অন্য প্রার্থীর কোড হয়তো সমান ভালো, কিন্তু তাতে সেটা বোঝার কিছু নেই। কোড ঠিক করে একটা ফিচার কাজ করে কি না; ডকুমেন্টেশন ঠিক করে অন্যরা সেটা বুঝতে পারবেন কি না।
এখানে সবকিছু লোকাল ও অফলাইন। মডেলটা ছোট একটা বিকল্প, ভাষা মডেল নয়; আসল মডেল কম অনুমানযোগ্য, আর ঠিক সে কারণেই টেস্ট করতে হয়। এই পাতা তিনটা ছোট মডিউল আর একটা docs/ ফোল্ডার যোগ করে, আগে থেকে থাকা কিছুই বদলায় না।
যা শিখবেন
- সিনথেটিক ডেটা থেকে ডেমো রেকর্ড করা (একটা সফল, একটা প্রমাণ-না-থাকার ঘটনা, একটা সামলানো এরর), আর কোর অক্ষত রেখে নতুন অ্যাডাপ্টারসহ প্রোভাইডার-নিরপেক্ষ ইন্টারফেস নথিভুক্ত করা।
- মূল্যায়নের ফল ও সীমাবদ্ধতা সততার সঙ্গে নথিভুক্ত করা, যেখানে সংখ্যাগুলো আসে কোনো রান থেকে।
- সিক্রেট ও স্পর্শকাতর ফিক্সচার খোঁজা, নির্ভরতা আপডেট নীতি ঠিক করা, আর রিলিজ চেকলিস্টকে স্ক্রিপ্ট বানানো।
- ছোট কেস স্টাডি লেখা, প্রমাণ দেখে পরের ধাপ ঠিক করা, আর পরিষ্কার এনভায়রনমেন্টে রিলিজ ক্যান্ডিডেট রিভিউ করা।
docs/ ফোল্ডারে কী থাকে
ফোল্ডারটা একটা নিয়মে চলে: ডকুমেন্টের প্রতিটা সংখ্যা কোনো রান থেকে তৈরি, নইলে সেটা সেখানে নেই। হাতে লেখা সংখ্যা কোড বদলের দিনই পুরনো হয়ে যায়, যেমনটা আগের পাতার README টেস্ট দেখিয়েছে। তাই একটা স্ক্রিপ্ট ফলাফলের ডকুমেন্টগুলো লেখে, আর কোনোটা পুরনো হলে একটা টেস্ট ব্যর্থ হয়।
| ফাইল | কী নিয়ে | কীভাবে তৈরি |
|---|---|---|
DEMO.md | রেকর্ড করা ডেমো | রান থেকে তৈরি |
INTERFACE.md | অ্যাডাপ্টার যোগ করা | হাতে লেখা, adapter_example.py ও একটা টেস্ট দিয়ে সমর্থিত |
EVALUATION.md | ডেটাসেট, মেট্রিক, ব্যর্থতা | রান থেকে তৈরি |
LIMITATIONS.md | যা সমর্থিত নয় বা পরীক্ষা হয়নি | লেখা টেক্সটের ভেতরে তৈরি করা সংখ্যা |
UPDATE-POLICY.md, RELEASE-CHECKLIST.md | আপগ্রেড ও রিলিজ | হাতে লেখা; চেকলিস্টের একটা অংশ release.py চালায় |
CASE-STUDY.md, NEXT-STEP.md | পোর্টফোলিওর গল্প, পরের ধাপ | লেখা যুক্তির ভেতরে তৈরি করা সংখ্যা |
সিনথেটিক ডেটা দিয়ে ডেমো রেকর্ড করুন
ডেমো মানে সত্যিকারের আচরণের একটা রেকর্ড, যেটা যে কেউ আবার তৈরি করতে পারেন। আমাদেরটা স্ক্রিপ্ট, স্ক্রিন রেকর্ডিং নয়: সেটা প্যাকেজিং, কনফিগারেশন ও পুনরুৎপাদনযোগ্যতা পাতার অ্যাপকে ইন-প্রসেস কল করে এবং যা ফিরল তা লেখে। তিনটা ঘটনা দেখানো হয়, কারণ যে ডেমো শুধু সফল হয় সে আস্থা নিয়ে কিছুই বলে না: সাইটেশনসহ একটা উত্তর, এমন একটা প্রশ্ন যার উত্তর কোনো ডকুমেন্টে নেই, আর ক্র্যাশ করা একটা মডেল। ডেটা কাল্পনিক Kestrel Cycles হেল্প সেন্টারের; ব্যবহারকারীর আইডি mateo বানানো।
# excerpt of kit/evidencedesk/docgen.py
def demo_md() -> str:
class Crash: # a model that fails with text that must never reach the caller
def complete(self, request):
raise RuntimeError("password=hunter2 at /srv/app/db.py")
steps = [("1. Success", create_app(), Q_OK, "demo-1", "The answer cites the passage it used."),
("2. Missing evidence", create_app(), Q_NONE, "demo-2", "No document answers this, so the assistant refuses instead of guessing."),
("3. Handled error", create_app(client=Crash()), Q_OK, "demo-3",
"The model crashed with an internal message. The caller gets a plain failure: no path, no password, no stack trace.")]
from fastapi.testclient import TestClient
out = ["# Demo", "", "Synthetic data only (the fictional Kestrel Cycles help centre). Offline: a fake model, no key, no network.",
"Generated by `python -m evidencedesk.docgen` through the in-process test client; do not edit by hand.", ""]
for title, app, question, rid, note in steps:
with TestClient(app) as http:
r = http.post("/ask", json={"question": question}, headers={"x-user-id": "mateo", "x-request-id": rid})
out += [f"## {title}", "", note, "", "```text", f"POST /ask {_json({'question': question})}", f"-> {r.status_code} {_json(r.json())}", "```", ""]
return "\n".join(out)চালান, তারপর ডেমোটা পড়ুন।
python -m evidencedesk.docgen
sed -n '/^## 1/,$p' docs/DEMO.mdwrote docs/DEMO.md
wrote docs/EVALUATION.md
wrote docs/LIMITATIONS.md
wrote docs/NEXT-STEP.md
wrote docs/CASE-STUDY.md
## 1. Success
The answer cites the passage it used.
```text
POST /ask {"question": "How long do refunds take after inspection?"}
-> 200 {"message": "Refunds go back to the original payment method within 5 business days after the warehouse receives the item. [kc-returns-v3:c2]", "request_id": "demo-1", "status": "answered"}
```
## 2. Missing evidence
No document answers this, so the assistant refuses instead of guessing.
```text
POST /ask {"question": "Can I pay in instalments?"}
-> 200 {"message": "Not enough evidence in the help centre to answer this. Please contact Kestrel Cycles support.", "request_id": "demo-2", "status": "refused"}
```
## 3. Handled error
The model crashed with an internal message. The caller gets a plain failure: no path, no password, no stack
trace.
```text
POST /ask {"question": "How long do refunds take after inspection?"}
-> 500 {"message": "Something went wrong on our side. Please try again later.", "request_id": "demo-3", "status": "failed"}
```তিনটা জিনিস লক্ষ করুন। প্রত্যাখ্যানটা স্বাভাবিক 200 উত্তর: প্রমাণ না থাকা ঠিক আচরণ, এরর নয়। ক্র্যাশ সাধারণ বার্তাসহ একটা সাদামাটা 500 হয়; বানানো ক্র্যাশের টেক্সট (একটা পাসওয়ার্ড আর একটা পাথ) কলকারীর কাছে কখনো পৌঁছায় না। আর রিকোয়েস্ট আইডি হেডার দিয়ে ঠিক করা, তাই ফাইলটা প্রতিবার একই হয়।
প্রোভাইডার-নিরপেক্ষ ইন্টারফেস নথিভুক্ত করুন
চুক্তিটা একটাই মেথড, complete(request) -> response, আমাদের নিজস্ব রিকোয়েস্ট, রেসপন্স ও এরর টাইপসহ (দেখুন প্রোভাইডার-নিরপেক্ষ LLM ক্লায়েন্ট)। অ্যাডাপ্টার চারটা কাজ করে: আমাদের রিকোয়েস্ট প্রোভাইডারের ফরম্যাটে আনা, কল করা, উত্তর ফিরিয়ে আনা, আর ব্যর্থতাকে আমাদের এরর ক্লাসে আনা (দেখুন অ্যাডাপ্টার, কনফিগারেশন ও এরর হ্যান্ডলিং)। docs/INTERFACE.md এই ধাপগুলো তালিকা করে। কথা প্রমাণ নয়, তাই কিটে একটা কাল্পনিক প্রোভাইডারের জন্য পূর্ণ অ্যাডাপ্টার আছে: সিস্টেম টেক্সট ও ইনপুট আলাদা ফিল্ড, ইউসেজ ও ফিনিশ রিজনের নাম অন্যরকম, আর ব্যর্থতা মানে একটা স্ট্যাটাস। pretend_server নেটওয়ার্কের জায়গা নেয়। সে ওই ওয়্যার ফরম্যাটে কথা বলে কিন্তু উত্তর দেয় একটা ফেক ক্লায়েন্ট থেকে, তাই অ্যাডাপ্টারের ম্যাপিং কোড সত্যিই চলে।
# file: kit/evidencedesk/adapter_example.py
"""A third-party adapter written against evidencedesk.llm only. No core module imports this file."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from evidencedesk.errors import from_status
from evidencedesk.llm import LLMClient, LLMRequest, LLMResponse, Message, Usage
Transport = Callable[[dict[str, Any]], dict[str, Any]] # one JSON object in, {"status", "headers", "body"} out
STOP = {"stop": "end", "max_tokens": "length"} # their words -> ours; anything else is "other"
class JsonAdapter:
"""For an imaginary provider: system text and input are separate fields, usage and finish reason have their own names."""
def __init__(self, transport: Transport, model: str = "example-model") -> None:
self.transport, self.model = transport, model
def complete(self, request: LLMRequest) -> LLMResponse:
text = lambda role: "\n".join(m.content for m in request.messages if m.role == role)
reply = self.transport({"model": self.model, "system": text("system"), "input": text("user"), "max_output": request.max_tokens})
if reply["status"] != 200: # job 4: failures become our error classes, so retries and the safe layer work unchanged
raise from_status(reply["status"], reply.get("headers"), str(reply.get("body", "")))
body = reply["body"]
return LLMResponse(body["text"], self.model, Usage(body["tokens"]["in"], body["tokens"]["out"]), STOP.get(body["finish"], "other"))
def pretend_server(inner: LLMClient) -> Transport:
"""Stands in for the network: it speaks the imaginary wire format but answers from any LLMClient (here, a fake)."""
def transport(payload: dict[str, Any]) -> dict[str, Any]:
sent = LLMRequest((Message("system", payload["system"]), Message("user", payload["input"])), payload["model"], payload["max_output"])
got = inner.complete(sent)
finish = {v: k for k, v in STOP.items()}.get(got.stop_reason, "other")
return {"status": 200, "body": {"text": got.text, "tokens": {"in": got.usage.input_tokens, "out": got.usage.output_tokens}, "finish": finish}}
return transportএবার একই দুটো প্রশ্ন পুরো সার্ভিসের ভেতর দিয়ে দুবার পাঠান, একবার ডিফল্ট ক্লায়েন্টে আর একবার অ্যাডাপ্টারের মধ্য দিয়ে, আর যোগ করুন এমন একটা প্রোভাইডার যে ব্যর্থ হয়।
from fastapi.testclient import TestClient
from evidencedesk.adapter_example import JsonAdapter, pretend_server
from evidencedesk.api import OfflineClient, create_app
def ask(client, question):
with TestClient(create_app(client=client)) as http:
return http.post("/ask", json={"question": question}, headers={"x-user-id": "mateo"}).json()
adapted = JsonAdapter(pretend_server(OfflineClient()))
for q in ("How long do refunds take after inspection?", "Can I pay in instalments?"):
print(ask(adapted, q) == ask(None, q), ask(adapted, q)["status"])
down = JsonAdapter(lambda payload: {"status": 500, "body": "stack trace at /srv/app.py"})
print(ask(down, "How long do refunds take after inspection?"))True answered
True refused
{'status': 'failed', 'message': 'Something went wrong on our side. Please try again later.', 'request_id': 'req-1'}দুই ক্লায়েন্টেই একই উত্তর, তাই rag, safefail ও api বদলায়নি। ব্যর্থ প্রোভাইডার স্ট্যান্ডার্ড ব্যর্থতার আকারে শেষ হয় এবং তার টেক্সট কোথাও দেখা যায় না। নিচের টেস্টগুলো এটা পাকা করে, তার একটা ব্যর্থ হবে যদি কোনো কোর মডিউল কখনো অ্যাডাপ্টার ইমপোর্ট করে।
# excerpt of kit/tests/test_release.py
def test_core_logic_gives_the_same_answer_through_the_adapter():
question = "How long do I have to return a new bike?"
assert answer_question(JsonAdapter(pretend_server(FakeClient())), question) == answer_question(FakeClient(), question)
@pytest.mark.parametrize("question", [Q, "Can I pay in instalments?"])
def test_the_whole_service_answers_identically_through_the_adapter(question):
def ask(client):
with TestClient(create_app(client=client)) as http:
return http.post("/ask", json={"question": question}, **ASK).json()
assert ask(JsonAdapter(pretend_server(OfflineClient()))) == ask(None)
def test_no_core_module_imports_the_adapter():
for path in (KIT / "evidencedesk").glob("*.py"):
if path.name != "adapter_example.py":
imported = {n.module for n in ast.walk(ast.parse(path.read_text(encoding="utf-8"))) if isinstance(n, ast.ImportFrom)}
assert "evidencedesk.adapter_example" not in imported, path.namepython -m pytest tests/test_release.py -k "adapter or core or whole_service or stop_reasons or failing_provider" | sed 's/ in [0-9.]*s//'....... [100%]
7 passed, 7 deselectedএটা দেখায় যে অ্যাডাপ্টার চুক্তিতে খাপ খায় আর কোর জানে না সেটা আছে। আসল প্রোভাইডার কেমন আচরণ করে তা দেখায় না: আসল অ্যাডাপ্টারের জন্য অ্যাকাউন্ট লাগে, তাই সেগুলো প্রথম দুই পাতার মতো # not run here চিহ্নিত স্কেচ হয়ে থাকে।
মূল্যায়নের ফল নথিভুক্ত করুন
মূল্যায়নের অংশে চারটা জিনিস লাগে: ডেটাসেট (নাম, ভার্সন, হ্যাশ, আকার), অর্থসহ মেট্রিক, লুকানোর বদলে তালিকাভুক্ত ব্যর্থতা, আর মেট্রিক যা প্রমাণ করতে পারে না। EVALUATION.md লেখা হয় রুব্রিক, রিগ্রেশন ও রিলিজ গেট পাতার নির্দিষ্ট স্যুটের একটা লাইভ রান থেকে। আগে গেটের মেট্রিক, তারপর ডকুমেন্টের ব্যর্থতার অংশ।
python -m evidencedesk.gate | sed -n '4,9p;$p'
sed -n '/^## Failures/,$p' docs/EVALUATION.md recall_at_k 0.882
citation_validity 1.0
abstention_correct 0.773
tool_pass 1.0
format_pass 1.0
pass_rate 0.848
GATE: GO
## Failures
| Case | Question | Expected | What happened |
|---|---|---|---|
| lg03 | কিস্তিতে বাইক কেনা যায় কি? | abstain | a Bangla question no document answers; it was answered, not refused (the unsafe direction) |
| rc03 | RD2-24 | answer | an exact identifier on its own; the toy embedder matches letters and the match was too weak to answer |
| rc06 | How do I get my money back? | answer | a paraphrase ('money back' for 'refund'); the right document was not in the top results |
| rc08 | How long before a voucher lapses? | answer | a paraphrase ('voucher lapses' for 'gift card expires'); the top two results were too close to choose |
| rc13 | How long are my support chats stored? | answer | a paraphrase ('chats stored' for 'retention'); the top two results were too close to choose |
## What these numbers cannot show
- The answers come from recorded, hand-written replies, not from a language model. The suite checks retrieval,
thresholds, citation checks and the gate, not answer quality of any real model.
- 33 cases is a small sample: one case moves the pass rate by about 3.0 points, so a one-case difference is
noise.
- There is no held-out set: the same cases are used to judge and to tune, so the scores are optimistic for
unseen questions.
- A passing citation check shows the cited passage was shown to the model, not that it supports the claim.
- No latency, cost or load was measured here.রিভিউয়ারের চোখে ব্যর্থতাগুলো পড়ুন। ইংরেজির চারটা মিস ডকুমেন্টে উত্তর থাকা সত্ত্বেও প্রত্যাখ্যান করেছে; সেগুলো দেখায় টয় এমবেডার কী পারে না (একা একটা আইডেন্টিফায়ার, তিনটা প্যারাফ্রেজ)। বাংলার মিস lg03 ভিন্ন: যে প্রশ্নের উত্তর কোনো ডকুমেন্টে নেই, অ্যাসিস্ট্যান্ট তার উত্তর দিয়েছে, অর্থাৎ অনিরাপদ দিকে। বাংলা স্লাইসে মাত্র পাঁচটা কেস, তাই একটা কেসে সংখ্যা বিশ পয়েন্ট নড়ে। শেষের তালিকাটাই রিভিউয়াররা সবচেয়ে বেশি বিশ্বাস করেন, কারণ তাতে বোঝা যায় লেখক স্কোরের সীমা জানেন।
সৎ সীমাবদ্ধতার অংশ প্রকাশ করুন
একটা সীমাবদ্ধতা নির্দিষ্ট, প্রমাণের সঙ্গে বাঁধা, আর বলে দেয় কিছু সমর্থিত নয় (কাজ করে না বলে জানা) নাকি পরীক্ষা হয়নি (কেউ দেখেনি)। "সীমাবদ্ধতা থাকতে পারে" কোনো সীমাবদ্ধতা নয়। ভাষা, ডেটা, মডেল, সার্ভিস আর যে অবস্থা আপনি কখনো চালাননি, সবকিছুর কথা লিখুন। সংখ্যাগুলো তৈরি করা, তাই সেগুলো সরে যেতে পারে না।
cat docs/LIMITATIONS.md# Limitations
Generated by `python -m evidencedesk.docgen`; the counts come from the kit as it is now.
## Languages
- Supported by tests: English and Bangla only. The corpus has 11 English and 2 Bangla documents; the
evaluation has 5 Bangla and 3 mixed-script questions against 14 English ones.
- Bangla pass rate is 0.8 on 5 cases; failing: lg03. Any other language is untested.
## Models and retrieval
- The model is a fake with recorded replies. Real providers were never called for this release; their adapters
are untested sketches.
- The embedder is a toy (character trigrams). It matches letters, not meaning: 4 English questions fail (3
paraphrases, 1 exact identifier) and one test is a documented expected failure.
## Data
- Synthetic documents only (13 documents, 36 chunks), searched in memory. Large corpora, document updates and
real customer text are untested.
- 33 evaluation cases and 15 abuse cases: regression sets, not proof of quality or safety.
## Service and security
- `x-user-id` is a stand-in for authentication. Do not expose the service before real authentication exists.
- Prompt injection is limited by design (allowlists, owner filters, screening), not solved; the abuse set
shows known attacks are blocked, not that new ones are.
- Tested in-process with `TestClient` on Linux with Python 3.12.3. A real server, concurrency, load, macOS and
Windows were not tested.
## Packaging
- Data folders sit beside the package; a built wheel would not contain them. `gate.hand_written_client`
imports a test module (a known wart).দাবিটা লক্ষ করুন: শুধু Linux ও Python 3.12.3, যদিও README লক্ষ্য হিসেবে তিনটা অপারেটিং সিস্টেমের নাম করেছে। README লক্ষ্য বলতে পারে; সীমাবদ্ধতার ফাইল বলে কী চালানো হয়েছে।
সিক্রেট ও স্পর্শকাতর ফিক্সচার খুঁজে দেখুন
স্ক্রিপ্ট চেনা আকার ধরে; বাকিটা মানুষকে করতে হয়। ম্যানুয়াল চেকলিস্ট আছে RELEASE-CHECKLIST.md-এ: শুধু ওয়ার্কিং ট্রি নয়, git ইতিহাসও খুঁজুন (মুছে ফেলা সিক্রেট ইতিহাসে থেকে যায়, তাই সেটা বদলে ফেলুন), .gitignore এনভায়রনমেন্ট, .env ফাইল ও লগ ঢেকেছে কি না দেখুন, ফিক্সচার, লগ ও স্ক্রিনশটে আসল নাম বা ডেটা আছে কি না দেখুন, আর নিজে লেখেননি এমন প্রতিটা ফাইলের লাইসেন্স দেখুন।
স্বয়ংক্রিয় অংশ আপনার আগে বানানো জিনিসই ব্যবহার করে। safefail.personal_data ইমেইল, কার্ড ও ফোনের আকার ধরে (নিরাপদ ব্যর্থতা ও নিরাপত্তা রিগ্রেশন টেস্ট), আর API কি-র আকার ও বানানো একমাত্র API কি আসে security থেকে। নতুন ফাংশন যোগ করে প্রোভাইডার-ধাঁচের API কি, প্রাইভেট কি (private key) ব্লক, হার্ড-কোড করা অ্যাসাইনমেন্ট আর সিক্রেটের মতো ফাইলের নাম, এবং এনভায়রনমেন্ট ও ক্যাশ বাদ দেয় (scan_data সব ফোল্ডার ঘোরে, তাই প্রজেক্টের ভেতরের .venv-ও পড়ে ফেলত)।
# excerpt of kit/evidencedesk/release.py
ALLOWED = { # (file, kind) -> why a person decided this finding is fine; review it again whenever the file changes
("data/abuse/abuse_set.jsonl", "key-shaped"): "made-up bait that the key-shape control must block",
("tests/test_security_p17.py", "key-shaped"): "the same made-up bait, asserted by a test",
("tests/test_context_quality.py", "hard-coded secret"): "a reserved example address used as a fake secret",
}
def scan_secrets(root: Path = KIT, allowed: dict[tuple[str, str], str] | None = None) -> list[str]:
"""'path:line kind' for every secret-shaped string, secret-like file name or personal-data shape. Values are never returned.
Findings listed in `allowed` (default ALLOWED) are dropped; pass {} to see everything."""
allowed = ALLOWED if allowed is None else allowed
found = []
for path in project_files(root):
rel = path.relative_to(root)
found += [f"{rel} file name" for pattern in BAD_NAMES if path.match(pattern)]
if path.suffix not in TEXT:
continue
for number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
found += [f"{rel}:{number} {kind}" for kind, shape in SHAPES.items() if shape.search(line)]
found += [f"{rel}:{number} key-shaped" for m in KEY_SHAPE.finditer(line) if m[0] != SIMULATED_SECRET] # the one made-up key is allowed
found += [f"{rel}:{number} {kind}" for kind in personal_data(line)] if path.suffix in PERSONAL else []
return [f for f in found if (f.split(":")[0].split(" ")[0], f.split(" ", 1)[1]) not in allowed]তিনভাবে চালান: সে যা দেখে সব, রিভিউয়ের পরে, আর একটা ফোল্ডারে যেখানে আমরা একটা API কি, একটা পাসওয়ার্ড ও একটা ইমেইল বসিয়েছি (ব্লকের ভেতরেই বানানো, তাই এই পাতায় API কি-র মতো কোনো টেক্সট নেই)।
import tempfile
from pathlib import Path
from evidencedesk import release
print("everything the scan sees:", *release.scan_secrets(allowed={}), sep="\n ")
print("after review:", release.scan_secrets())
with tempfile.TemporaryDirectory() as folder: # a planted copy; the values are built here so this page holds no key-like text
(Path(folder) / "notes.md").write_text("key: sk-" + "ab" * 15 + "\npassword = \"" + "q" * 14 + "\"\nask ana@corp.test\n", encoding="utf-8")
(Path(folder) / ".env").write_text("MODE=demo\n", encoding="utf-8")
print("planted:", *release.scan_secrets(Path(folder)), sep="\n ")everything the scan sees:
data/abuse/abuse_set.jsonl:6 key-shaped
tests/test_context_quality.py:123 hard-coded secret
tests/test_security_p17.py:55 key-shaped
after review: []
planted:
.env file name
notes.md:1 provider-style key
notes.md:2 hard-coded secret
notes.md:3 emailকাঁচা তিনটা ফলাফলই ইচ্ছাকৃত: একটা বানানো API কি, যেটা কি-আকারের নিয়ন্ত্রণকে আটকাতেই হবে (অ্যাবিউজ সেটে আর একটা টেস্টে), আর একটা সংরক্ষিত উদাহরণ ঠিকানা যেটা নকল সিক্রেট হিসেবে ব্যবহার হয়। একজন মানুষ প্রতিটা পড়ে কারণ ALLOWED-এ লিখেছেন; অনুমতি ফাইল ও ধরনের নামে, লাইনের নামে নয়, তাই লাইন যোগ করলে ভাঙে না। স্ক্যান জানায় কোথায় আর কী ধরনের, মান কখনো জানায় না। তার সীমা: সে শুধু চেনা আকার পায় (সাধারণ বাক্যে লেখা পাসওয়ার্ড পার পেয়ে যায়) এবং git ইতিহাস পড়ে না। আলাদা স্ক্যানার ভালো সংযোজন, চেকলিস্টের বিকল্প নয়।
নির্ভরতা আপডেট নীতি ঠিক করুন
পিন হঠাৎ বদল ঠেকায়; নীতি ঠিক করে কখন ইচ্ছা করে একটা বদল নেওয়া হবে। UPDATE-POLICY.md বলে: একটা স্ক্র্যাচ কপি ও নতুন এনভায়রনমেন্টে টেস্ট করুন, pip freeze-কে লক ফাইলের সঙ্গে মেলান, স্যুট ও মূল্যায়ন গেট চালান, মেট্রিক বেসলাইনের সঙ্গে তুলনা করুন, রিলিজ নোট ও অ্যাডভাইজরি পড়ুন, আর একটা রিভিউ করা বদল হিসেবে গ্রহণ করুন। প্রকাশিত নিরাপত্তা ফিক্সে সঙ্গে সঙ্গে আপগ্রেড করুন; নইলে নির্ধারিত সময়ে এবং কোনো Python ভার্সনের সমর্থন শেষ হলে। এই হলো ড্রিল। যেদিন লিখেছি সেদিন কিছুই নতুন ছিল না, তাই প্রার্থী ধাপে numpy এক রিলিজ পিছনে যায়; আসল আপগ্রেডের ধাপ একই।
rm -rf ../try && cp -r . ../try && cd ../try
python -m venv .venv && source .venv/bin/activate
sed -i 's/^numpy==.*/numpy==2.5.3/' requirements.txt
python -m pip install -q --disable-pip-version-check -r requirements.txt
python -m pip freeze | diff - <(grep -v '^#' requirements.lock.txt)
python -m pytest | tail -1 | sed 's/ in [0-9.]*s//'
python -m evidencedesk.gate | tail -1
{ grep '^#' requirements.lock.txt; python -m pip freeze; } > lock.new && mv lock.new requirements.lock.txt
python -m pytest | tail -1 | sed 's/ in [0-9.]*s//'
python -m evidencedesk.release --tests | tail -110c10
< numpy==2.5.3
---
> numpy==2.5.4
1 failed, 468 passed, 1 xfailed
GATE: GO
469 passed, 1 xfailed
RESULT: READY (0 failed, 0 skipped)ডিফ হলো আসল বদল: একটা লাইন। স্যুট তারপর একটা ব্যর্থতা দেখায়, আর সেটাই ঠিক: যে টেস্ট চায় লক ফাইলে পিনগুলো থাকুক। লকও রিলিজের অংশ, তাই শেষ ধাপে সেটা নতুন করে তৈরি হয়। তারপর স্যুট পাস করে, গেট বলে GO আর রিলিজ চেক প্রস্তুত। মূল্যায়নের সংখ্যা নড়েনি; প্যাচ-স্তরের (patch-level) ভার্সন বদলে ঠিক এই প্রমাণটাই চাওয়া হয়।
ড্রিল যা ধরে না: যে আচরণ টেস্টে ওঠে না, আসল প্রোভাইডারের আচরণ (এখানে কিছুই আসল প্রোভাইডারকে কল করে না), আসল ডেটার পরিমাণ, কনকারেন্সি বা অন্য সিস্টেমের সমস্যা, আর ক্ষতিকর কিছু ঢুকে পড়া (compromised) রিলিজ, কারণ ভার্সন পিন মানে কনটেন্ট হ্যাশ নয়।
টুল নোট (২০২৬-১০-১২ অনুযায়ী): Python 3.12 2028-10 পর্যন্ত সমর্থিত বলে তালিকাভুক্ত, আর 3.10-এর জীবনকাল শেষ হয়েছে 2026-10-01 তারিখে, তাই কোনো Python ভার্সনের সমর্থন শেষ হওয়া এই নীতি চালানোর কারণ। ওই তারিখে একটা অ্যাডভাইজরি ডেটাবেসে pytest-এর একটা মাঝারি অ্যাডভাইজরি ছিল (9.0.3-এ ঠিক হয়েছে), আর প্যাকেজ ইনডেক্স pydantic, fastapi, numpy ও pytest-এর সর্বশেষ রিলিজের কোনো জানা দুর্বলতা তালিকাভুক্ত করেনি। pip-এর হ্যাশ-চেকিং মোডে
==পিন আর প্রতিটা প্যাকেজের হ্যাশ লাগে;uv.lockuv টুলের নিজস্ব, আরpylock.toml(PEP 751) একটা স্ট্যান্ডার্ড বিকল্প। অ্যাডভাইজরি প্রতিদিন বদলায়: বর্তমান ডকুমেন্টেশন দেখে নিন।
মৌলিক রিলিজ চেকলিস্ট ঠিক করুন
চেকলিস্ট কাজে লাগে যখন সেটা ছোট, হ্যাঁ-না ধাঁচের, আর যেখানে সম্ভব স্ক্রিপ্টে চলে। আমাদেরটা চায়: সমর্থিত Python, সঠিক ভার্সনের পিন, কনফিগার করা ও পাস করা টেস্ট, নথিভুক্ত কনফিগারেশন (README ও docs/), দরকারি ডেটা, ডিফল্ট হিসেবে অফলাইন মোড, নতুন রানের সঙ্গে মেলা তৈরি করা ডকুমেন্ট, পরিষ্কার সিক্রেট স্ক্যান, ব্লক না করা মূল্যায়ন গেট, আর কোনো ব্যর্থ কেস নেই এমন অ্যাবিউজ সেট। release.py সব চালায়; স্যুট ধীর বলে শুধু --tests দিলে চলে। প্রতিটা চেক একটা নাম, একটা স্ট্যাটাস আর একটা বিবরণ।
# excerpt of kit/evidencedesk/release.py
@dataclass(frozen=True)
class Check:
name: str
status: str # "ok", "FAIL" or "skip"
detail: str
def run_checks(root: Path = KIT, tests: bool = False) -> list[Check]:
checks: list[Check] = []
def add(name: str, problem: str | None, ok_detail: str) -> None:
checks.append(Check(name, "FAIL" if problem else "ok", problem or ok_detail))
...
add("offline by default", None if (s.offline, s.provider, s.api_key) == (True, "fake", None) else "defaults are not offline", "offline, fake provider, no key")
add("secrets and sensitive fixtures", f"{len(hits)} finding(s): {hits[:3]}" if hits else None, "no findings")
add("evaluation gate", None if decision.verdict != "block" else "; ".join(decision.blockers), f"{decision.verdict.upper()} against the stored baseline")
...
if tests:
... # run pytest in a subprocess and keep its last line
else:
checks.append(Check("test suite", "skip", "run with --tests"))
return checkspython -m evidencedesk.releaseRELEASE CHECK evidencedesk
[ok ] python version 3.12 (needs >=3.12)
[ok ] exact pins every line of requirements.txt uses ==
[ok ] tests configured testpaths ['tests'], 25 test files
[ok ] required data 5 of 5 paths present
[ok ] offline by default offline, fake provider, no key
[ok ] documentation 9 documents present
[ok ] generated docs current match a fresh run
[ok ] secrets and sensitive fixtures no findings
[ok ] evaluation gate GO against the stored baseline
[ok ] abuse set no failing case
[skip] test suite run with --tests
RESULT: READY (0 failed, 1 skipped)--tests ছাড়া স্যুট বাদ যায়, আর রিপোর্ট ভান না করে সেটা বলে। READY মানে স্বয়ংক্রিয় অংশ পাস করেছে, রিলিজ ভালো তা নয়: চেকলিস্টের ম্যানুয়াল অংশ (মানুষের চালানো পরিষ্কার-এনভায়রনমেন্ট রান, ইতিহাস খোঁজা, লাইসেন্স দেখা) তবু মানুষেরই কাজ।
ছোট পোর্টফোলিও কেস স্টাডি লিখুন
কেস স্টাডি এক পাতায় রিভিউয়ারের প্রশ্নের উত্তর দেয়: ব্যবহারকারীর সমস্যা, আপনি কী বানালেন, কী ছাড় দিলেন, কী মাপলেন, আর পরে কী করবেন। শুধু মাপা সংখ্যা ব্যবহার করুন, আর যে বাক্য প্রমাণ করতে পারেন না তা বাদ দিন। আমাদেরটা তৈরি করা, তাই এর সংখ্যাগুলো ওপরেরগুলোই।
cat docs/CASE-STUDY.md# Case study: EvidenceDesk
Numbers below are measured by `python -m evidencedesk.docgen`; nothing is estimated.
## The user problem
Customers of a fictional bike shop ask policy questions. A wrong answer about refunds or recalls is worse than
no answer, so the assistant must cite its source, say when it has none, and never leak private data or obey
text hidden in a document.
## Architecture
A request enters one FastAPI route, passes identity, task and size checks, retrieves evidence from a local
corpus, builds a context in which document text is marked untrusted, asks a provider-neutral client for a
reply, validates the reply, and returns one response shape. The model is replaceable behind a one-method
interface; the safety checks live outside the model.
## Trade-offs
- Offline fake model and toy embedder: every test is repeatable and free, but real quality is unmeasured.
- Refuse when evidence is weak: fewer wrong answers, more refusals. The threshold decides the balance.
- One public response shape with no internal codes: callers learn nothing from errors, and developers rely on
logs.
- Exact version pins and a lock file: repeatable installs, with the cost of deliberate upgrades.
## Measured results
- Evaluation (33 cases): recall 0.882, citation validity 1.0, abstention 0.773, pass rate 0.848.
- Failures: lg03, rc03, rc06, rc08, rc13; the release gate says GO against the stored baseline.
- Abuse set: 15 cases, checked by the release script.
## What I would do next
See `NEXT-STEP.md`: more Bangla refusal cases before any change to the threshold.পরের ইঞ্জিনিয়ারিং ধাপের পরিকল্পনা করুন
মূল্যায়নের পর লোভ হয় বড় কম্পোনেন্ট যোগ করার। শৃঙ্খলাবদ্ধ ধাপ শুরু হয় ব্যর্থতা থেকে: তালিকা করুন, কারণ ধরে ভাগ করুন, কোনটা ব্যবহারকারীকে সবচেয়ে বেশি ভোগায় ভাবুন, সবচেয়ে সস্তা পরীক্ষাটা বাছুন, আর বদলের আগে সাফল্যের টেস্ট লিখুন। এখানে ব্যর্থতা rc03, rc06, rc08, rc13 ও lg03। উত্তর থাকা প্রশ্ন প্রত্যাখ্যান করলে খরচ একটা সাপোর্ট যোগাযোগ; উত্তরহীন প্রশ্নের উত্তর দিলে খরচ আস্থা, তাই lg03 আগে। সবচেয়ে সস্তা উপায় (lever) কনফিডেন্স থ্রেশহোল্ড, তাই ডকুমেন্ট একই কেসগুলোর ওপর সেটার মান বদলে বদলে দেখে।
sed -n '/^| min_score/,/^$/p' docs/NEXT-STEP.md| min_score | Failing cases | Pass rate | Unanswerable answered |
|---|---|---|---|
| 0.25 | lg03, rc06, rc08, rc13 | 0.879 | 1 of 5 |
| 0.3 | lg03, rc03, rc06, rc08, rc13 | 0.848 | 1 of 5 |
| 0.35 | rc03, rc06, rc08, rc13 | 0.879 | 0 of 5 |
| 0.4 | rc02, rc03, rc06, rc08, rc13 | 0.848 | 0 of 5 |0.35-এ lg03 পাস করে আর নতুন কিছু ভাঙে না; 0.4-এ rc02 ভাঙে। 0.35 বসিয়ে এগিয়ে যেতে ইচ্ছা করে। করবেন না: মানটা যে কেসে স্কোর হয় সেগুলো দেখেই পাওয়া, আর একটা বাংলা কেসই সব প্রমাণ। NEXT-STEP.md-এর সিদ্ধান্ত হলো অন্তত পাঁচটা বাংলা প্রশ্ন যোগ করা যার উত্তর কোনো ডকুমেন্টে নেই, সুইপ আবার চালানো, তারপর বাছা। রিরেঙ্কার (reranker), আসল এমবেডিং বা দ্বিতীয় সার্চ পদ্ধতি অপেক্ষা করে যতক্ষণ না কোনো ব্যর্থতা দেখায় কী নেই; rc03 আর প্যারাফ্রেজগুলো কারণ লিখে রেখে তুলে রাখা হয়েছে।
মিনি-ল্যাব: রিলিজ ক্যান্ডিডেট রিভিউ
কিটটাকে অচেনা কারো চোখে রিভিউ করুন। kit ফোল্ডার কপি করে নাম দিন project, আর ব্লকটা তার পাশে lab.sh নামে রাখুন। আমরা চালিয়েছি env -i PATH=/usr/bin:/bin HOME=$PWD bash lab.sh, খালি এনভায়রনমেন্টে, তাই কোনো ভ্যারিয়েবল সাহায্য করেনি আর কোনো API ক্রেডেনশিয়াল ছিল না। এটা নতুন এনভায়রনমেন্ট বানায়, পিন করা ফাইল থেকে ইনস্টল করে, ইনস্টলকে লক ফাইলের সঙ্গে আর ডেমোকে README-র সঙ্গে মেলায়, ডেমো ও গেট চলার সময় যেকোনো নেটওয়ার্ক সংযোগকে স্পষ্টভাবে ব্যর্থ করে, আর পুরো স্যুটসহ রিলিজ চেক চালায়।
cd project
python3.12 -m venv .venv
source .venv/bin/activate
env | grep -ci api_key
python -m pip install -q --disable-pip-version-check -r requirements.txt
python -m pip freeze | diff - <(grep -v '^#' requirements.lock.txt) && echo "same as the lock file"
python -m evidencedesk.api | diff - <(sed -n '/^GET \/health/,/^POST \/ask -> 401/p' README.md) && echo "demo matches the README"
python - <<'EOF'
import socket
def blocked(*args, **kwargs):
raise RuntimeError("network call attempted")
socket.socket.connect = blocked # any outgoing connection now fails loudly
socket.getaddrinfo = blocked
from evidencedesk.api import demo
from evidencedesk.gate import run_gate_suite
print("offline demo lines:", len(demo().splitlines()), "| pass rate:", run_gate_suite()["metrics"]["pass_rate"])
EOF
python -m evidencedesk.release --tests0
same as the lock file
demo matches the README
offline demo lines: 5 | pass rate: 0.848
RELEASE CHECK evidencedesk
[ok ] python version 3.12 (needs >=3.12)
[ok ] exact pins every line of requirements.txt uses ==
[ok ] tests configured testpaths ['tests'], 25 test files
[ok ] required data 5 of 5 paths present
[ok ] offline by default offline, fake provider, no key
[ok ] documentation 9 documents present
[ok ] generated docs current match a fresh run
[ok ] secrets and sensitive fixtures no findings
[ok ] evaluation gate GO against the stored baseline
[ok ] abuse set no failing case
[ok ] test suite 469 passed, 1 xfailed
RESULT: READY (0 failed, 0 skipped)প্রতিটা লাইন দেখুন। ক্রেডেনশিয়ালের সংখ্যা 0, ইনস্টল লকের সঙ্গে মেলে, ডেমো README-র সঙ্গে মেলে। সংযোগ বন্ধ থাকা অবস্থাতেও ডেমো তার পাঁচ লাইন দিয়েছে আর মূল্যায়ন 0.848 স্কোর করেছে, তাই অফলাইন চলা ধরে নেওয়া নয়, দেখানো হয়েছে। রিলিজ চেক স্যুটসহ পাস করে: 469 পাস আর নথিভুক্ত একটা প্রত্যাশিত ব্যর্থতা। নথিভুক্ত সীমাবদ্ধতা একটা আবশ্যক আইটেম, আর সেটা আছে। যা এটা দেখাতে পারে না: এক Linux মেশিনে ইন-প্রসেস রান অন্য সিস্টেম, আসল প্রোভাইডার বা লোড সম্পর্কে কিছু বলে না, আর LIMITATIONS.md ঠিক সেটাই বলে।
কাজের জায়গায় এটা কোথায় দেখা যায়
কায়রোর ওমর এমন এক টিমে যোগ দিলেন যারা একটা প্রোটোটাইপ নিতে চায়। কেউ কোড পড়ার আগেই তারা ডেমো, মূল্যায়নের সংখ্যা, জানা সীমাবদ্ধতা আর রিলিজের পদ্ধতি চায়। এই চারটা থাকলে প্রোটোটাইপের সিদ্ধান্ত হয় একটা মিটিংয়ে; না থাকলে এক সপ্তাহের প্রশ্ন। একই ডকুমেন্ট অডিট, হস্তান্তর আর পরের চাকরিতে দেখানোর পোর্টফোলিওতে কাজে লাগে।
সাধারণ ভুল
- হাতে টাইপ করা সংখ্যা। সেদিন ঠিক থাকে, এক মাস পরে ভুল। রান থেকে তৈরি করুন আর ফাইলটা হালনাগাদ কি না টেস্ট করুন।
- যে ডেমো শুধু সফল হয়। আস্থা যার ওপর দাঁড়ায় তা লুকায়। একটা প্রত্যাখ্যান আর একটা সামলানো এরর দেখান।
- অস্পষ্ট সীমাবদ্ধতা। "প্রোডাকশন-প্রস্তুত নয়" কারো কাজে আসে না। ভাষা, ডেটার আকার, ফেক মডেল আর যে অবস্থা চালাননি তার নাম বলুন।
- সিক্রেট স্ক্যানে ভরসা করা। স্ক্যান আকার ধরে, ইতিহাস বা প্লেইন-টেক্সট পাসওয়ার্ড নয়। ম্যানুয়াল চেকলিস্ট রাখুন, আর যা কখনো ফাঁস হয়েছে তা বদলে ফেলুন।
- শিপ করা এনভায়রনমেন্টেই আপগ্রেড করা। তুলনা বা রোলব্যাক করা যায় না। স্ক্র্যাচ কপিতে টেস্ট করুন আর একই বদলে লক ফাইল হালনাগাদ করুন।
নিজে চেষ্টা করুন
- সহজ। প্যাকেজিং, কনফিগারেশন ও পুনরুৎপাদনযোগ্যতা আবার দেখুন। প্রজেক্টের একটা কপিতে
docs/LIMITATIONS.mdমুছে রিলিজ চেক চালান। আগে অনুমান করুন কোন চেক ব্যর্থ হবে, তারপর চালান। - মাঝারি। রুব্রিক, রিগ্রেশন ও রিলিজ গেট আবার দেখুন। গেট চালান
--min-score 0.35দিয়ে। কোন ব্যর্থতা থাকে, রায় কী, আর সেটা কেন ডিফল্ট বদলানোর জন্য যথেষ্ট নয়? - কঠিন। প্রোভাইডার-নিরপেক্ষ LLM ক্লায়েন্ট আবার দেখুন। এমন প্রোভাইডারের অ্যাডাপ্টার লিখুন যার রেসপন্স কয়েকটা অংশের তালিকা আর একটা শেষ হওয়ার কারণ, এবং কোনো ইউসেজ ফিল্ড নেই। দেখান যে কেটে যাওয়া উত্তর ব্যবহারকারীর কাছে কাটা-চিহ্নসহ পৌঁছায় এবং শেষ হওয়া খালি উত্তর চুক্তি-যাচাইয়ে ধরা পড়ে।
উত্তর
সহজ। একই কারণে দুটো চেক ব্যর্থ হয়: ফাইলটা আবশ্যক (ডকুমেন্টেশন) এবং তৈরি করা (না থাকা তৈরি ফাইল পুরনো গণ্য হয়)। ফল NOT READY আর এক্সিট কোড 1।
rm -rf ../copy && cp -r . ../copy && cd ../copy && rm docs/LIMITATIONS.md
python -m evidencedesk.release | grep -E 'FAIL|RESULT'[FAIL] documentation docs/LIMITATIONS.md missing or empty
[FAIL] generated docs current LIMITATIONS.md out of date
RESULT: NOT READY (2 failed, 1 skipped)মাঝারি। গেট GO ছাপে, আর lg03 ঠিক হয়; rc03, rc06, rc08 ও rc13 থেকে যায়। GO মানে কোনো মেট্রিক বেসলাইনের নিচে নামেনি আর কোনো জরুরি কেস ব্যর্থ হয়নি। এর মানে এই নয় যে 0.35 ঠিক: মানটা এই কেসগুলো দেখে বাছা, আর একটা বাংলা কেসই তার সমর্থন। আগে আরও বাংলা প্রত্যাখ্যান-কেস জোগাড় করুন।
python -m evidencedesk.gate --min-score 0.35 | sed -n '/failures/p;$p' failures: rc03, rc06, rc08, rc13
GATE: GOকঠিন। ইউসেজ ডিফল্টে শূন্য, মানে অজানা, আর অ্যাডাপ্টার সংখ্যা বানানোর বদলে মন্তব্যে সেটা বলে। length স্টপ রিজন আমাদের length হয়, তাই answer_question চিহ্নটা জুড়ে দেয়।
from evidencedesk.gate import contract_problems
from evidencedesk.llm import LLMRequest, LLMResponse, Message, Usage, answer_question
class PartsAdapter:
"""An imaginary provider: the reply is a list of parts and a done reason; it reports no usage at all."""
def __init__(self, transport):
self.transport = transport
def complete(self, request: LLMRequest) -> LLMResponse:
raw = self.transport({"text": request.messages[-1].content})
stop = {"complete": "end", "length": "length"}.get(raw["done_reason"], "other")
return LLMResponse("".join(raw["parts"]), "parts-model", Usage(), stop) # Usage() = zeros: unknown, and we say so
cut = PartsAdapter(lambda p: {"parts": ["Standard delivery ", "takes 4 to 7 days"], "done_reason": "length"})
print(contract_problems(cut.complete(LLMRequest((Message("user", "How long?"),)))), "|", answer_question(cut, "How long?"))
silent = PartsAdapter(lambda p: {"parts": [""], "done_reason": "complete"})
print(contract_problems(silent.complete(LLMRequest((Message("user", "How long?"),)))))[] | Standard delivery takes 4 to 7 days [cut off]
['a finished reply needs text']পাস চেকলিস্ট:
- সহজ: আপনি দুটো FAIL লাইন (ডকুমেন্টেশন ও তৈরি ডকুমেন্ট) আর NOT READY অনুমান করেছেন, এবং বলতে পারেন কেন দুটো।
- মাঝারি: চারটা ব্যর্থতা থেকে গিয়ে
GATE: GO, আর এমন কারণ যাতে একই কেস ও বাংলা ডেটার অভাবের কথা আছে। - কঠিন: অ্যাপ্লিকেশন কোড বদলায়নি; কাটা উত্তরে চুক্তি-যাচাই
[]দেয়, খালি উত্তরে একটা সমস্যা; কাটা-চিহ্ন দেখা যায়। - মিনি-ল্যাব: ক্রেডেনশিয়াল 0, লকের সঙ্গে মেলে, ডেমো মেলে, অফলাইন ডেমো ও গেট চলে, স্যুটসহ রিলিজ চেক READY।
সারসংক্ষেপ
- ডকুমেন্টেশন হলো প্রমাণ: আবার তৈরি করা যায় এমন ডেমো, টেস্ট দিয়ে প্রমাণিত ইন্টারফেস গাইড, ব্যর্থতাসহ মূল্যায়নের ফল, আর কী চালানো হয়েছে তার নামসহ সীমাবদ্ধতা।
- ডকুমেন্টের সংখ্যা কোনো রান থেকে তৈরি, আর ডকুমেন্ট পুরনো হলে টেস্ট ব্যর্থ হয়।
- সিক্রেট ও স্পর্শকাতর ফিক্সচার স্ক্যান করুন, প্রতিটা ফলাফল রিভিউ করুন, আর git ইতিহাসসহ ম্যানুয়াল চেকলিস্ট রাখুন।
- স্ক্র্যাচ কপিতে আপগ্রেড করুন, ফ্রিজ, স্যুট আর বেসলাইন মেলান, তারপর ইচ্ছা করে লক ফাইল হালনাগাদ করুন।
- ব্যর্থতা দেখে পরের ধাপ বাছুন, সবচেয়ে সস্তা ধারণা আগে বেশি ডেটায় পরীক্ষা করুন, আর প্রমাণ ছাড়া কোনো কম্পোনেন্ট যোগ করবেন না।
এরপর: প্র্যাকটিস ল্যাব পুরো প্রজেক্টকে কাজে লাগায় অফলাইন ফিক্সচার ও পরিষ্কার পাস-মানদণ্ডসহ পাঁচটা স্তরভিত্তিক চ্যালেঞ্জে।